-
-
Notifications
You must be signed in to change notification settings - Fork 3.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Promise preloads, loadPromise and loadPromiseAsync #3507
Conversation
i like. is the goal to rewrite the legacy preload functions as Promise-based ones? and/or handling |
@Spongman Yeah, that would be my general plan at least, get all the internal ones moved over and then deprecate the old preload API would be the plan so that external libraries don't have to call I would still love to get promise-returning lifecycle functions in and would like to have #3000 re-opened and reconsidered, especially as currently there's weird behaviour like the following: let value;
function preload() {
loadPromiseAsync(promise)
.then(function(data) {
// This code actually runs after setup, weirdly
value = data;
});
}
function setup() {
console.log(value); // undefined
} Of course if we had a promise-accepting |
I'm having a bit of a hard time understanding how the changes here will be used, maybe because I have a lot going on now that I can't focus enough on this, but nonetheless I am very excited about introducing promises to loading functions in p5.js. A few things I would like some clarification on if you don't mind:
Personally, if #3000 would make this PR obsolete I would prefer to pursue that as the API exposed in that PR seems more straightforward, the only thing I don't know is that whether it breaks existing code or not. |
These are all great questions.
async function preload() {
await otherLibrary.loadX();
await otherLibrary.loadY();
} vs. function preload() {
loadPromiseAsync(otherLibrary.loadX());
loadPromiseAsync(otherLibrary.loadY());
} Where in the second example, both promises are loaded in parallel because there's no synchronization between them. I really don't know what's the better way to go though, because if you have to use the return value it gets a lot harder: // Sequential loading
let a, b;
async function preload() {
// Fairly simple variable assignment
a = await otherLibrary.loadX();
b = await otherLibrary.loadY();
}
// Or in parallel, but very hard to read:
let a, b;
async function preload() {
[
a,
b,
] = await Promise.all([
otherLibrary.loadX(),
otherLibrary.loadY(),
]);
} vs. let a, b, c, d;
function preload() {
// Forced to use one of:
// callback
loadPromiseAsync(otherLibrary.loadX(), function(data) {
a = data;
});
// Internal-then
loadPromiseAsync(otherLibrary.loadY().then(function(data) {
b = data;
}));
// return-value-then (Which WILL be called AFTER setup, which is confusing to users)
loadPromiseAsync(otherLibrary.loadZ()).then(function(data) {
c = data;
});
// Or, with problematic side-effects, loadPromise
d = loadPromise(otherLibrary.loadA());
} The internal-then style becomes problematic when you try to add catch in too: function preload() {
// callback
loadPromiseAsync(otherLibrary.loadX(), function() {}, function(err) {
// Error handler here works correctly
});
// Internal-then
loadPromiseAsync(otherLibrary.loadY().catch(function(error) {
// Error handler here will make setup continue normally unless the error is re-thrown
}));
} And hopefully that explains why I made the api decisions I did here - it only makes sense from a reliability standpoint to use #3000 gives some other benefits as well that aren't just in preload, so this PR does not replace that mechanism entirely either. I don't think the code there would break anything. There's also a use case for having both mechanisms working together simultaneously: async function loadAsset(url) {
// do something
}
async function preload() {
const assetList = await loadAsset('list.json');
for (let asset of assetList) {
loadPromiseAsync(loadAsset(asset));
}
} vs. either with no async preload: async function loadAsset(url) {
// do something
}
function preload() {
loadPromiseAsync(loadAsset('list.json'), function(assetList) {
for (let asset of assetList) {
// This preload-function in a handler is a bit weird to look at but should work fine
loadPromiseAsync(loadAsset(asset));
}
});
// OR
loadPromiseAsync((async function() {
const assetList = loadAsset('list.json');
for (let asset of assetList) {
loadPromiseAsync(loadAsset(asset));
}
})());
} or with no async function loadAsset(url) {
// do something
}
async function preload() {
const assetList = loadAsset('list.json');
await Promise.all(assetList.map(loadAsset));
} In either case, I am not against dropping |
for the naming, it's pretty common practice for systems with methods that return a Promise-like object that requires async function myMethodAsync(foo) {
await foo.methodAsync();
} |
@Spongman Maybe there are some other places with this kind of convention but the one I'm familiar with is the Bluebird Promisify way of affixing @meiamsome That clarified it a lot, thanks! I see that there are some modification to Continuing on // Sequential loading
let a, b;
async function preload() {
// Fairly simple variable assignment
a = await otherLibrary.loadX();
b = await otherLibrary.loadY();
}
// Or in parallel, but very hard to read:
let a, b;
async function preload() {
[
a,
b,
] = await Promise.all([
otherLibrary.loadX(),
otherLibrary.loadY(),
]);
} I don't necessary think this pattern is that bad. For sequential loading it is true that it is rather nonsensical to call these functions sequentially in Javascript but for most beginners that should be pretty straightforward to understand (one command execute after the other), and it can be moved over to an For parallel loading, because of using destructuring as you have here, I don't think it is that hard to read, especially given that those who would be using this pattern would be people that wanted to optimize their code beyond what sequential loading can offer in terms of load speed and thus I assume would be reasonably comfortable dealing with arrays. I would like to see others opinion on this though. |
@limzykenneth let data = loadPromise(promise, callback, errorCallback);
let dataPromise = loadPromiseAsync(promise, callback, errorCallback); So either one will support using callbacks. I'm just prefering to use In terms of let a, b;
async function preload() {
[
a,
b,
] = await Promise.all([
otherLibrary.loadX(),
otherLibrary.loadY(),
]);
} I agree that it is ok in terms of readability really, it's just when you get to like 10 preloads then you'd have a nice time deleting the matching lines. But there's not really a way around that, and someone can always do this if they so wanted: let a, b;
async function preload() {
await Promise.all([
otherLibrary.loadX().then(data => a = data),
otherLibrary.loadY().then(data => b = data),
]);
} As always, having input from many people would be greatly appreciated, I agree. |
Hi @meiamsome sorry it took so long to get to this one. I'm a little confused based on the issue this is addressing. I thought from your original comment on #2698 that the goal was to update the current |
@lmccart you are right that this is the stated goal. This PR is most of the work I should think, as it is mostly the behind-the-scenes work required for hooking into promises. This PR is split into two separate commits:
All other preloads would end up relying on no. 1, so I thought it would be a bad idea to not include any form of testing around that - hopefully that explains my thought process in including That said, I can understand your reasoning to want to separate them, as only no. 1 is relevant to #2698. If you can confirm for me that you would be happy with a PR just containing 9908aba without any form of testing then I will remove the tip commit on this branch - if you would want testing then I am happy to oblige, but it may take a short time for me to look into the best way to do that. |
@meiamsome got it, ok. it would be really helpful to separate these two commits into different pull requests. we will need testing for @9908aba in the form of unit tests. if you're able to do this, that would be fantastic. @ihsavru is also working on unit tests right now, maybe they are able to help? no worries if this takes a little longer, I think it's important that we merge the tests and loadX changes together. thanks! |
Main mechanism moved to #3905. |
This is a big PR that adds the following:
A preload system that works similarly to the current one, but also does the following:
this._decrementPreload
automatically, which could be problematic for other libraries to do reliably (They might not have access to thethis
instance)callback
anderrorCallback
in p5 flavour to the end of the arguments automatically so that they can be handled reliably & in one place which will hopefully reduce the verbosity of the current preloadsloadXAsync
the returns a promise it can generate aloadX
that will return an object. This has the same set of problems as theloadJSON
does currently, in that it won't work correctly with arrays and other return types necessarily. It should be sufficient for most use cases, though.An example
loadPromiseAsync
which takes in a promise and makes p5 wait for the promise before starting the sketch. AloadPromise
which is generated using the preload system. Both of these have a reasonable test suite modeled on the other preload functions. These will be useful to users of other libraries that load with promises (I'm thinking ml5, tfjs, etc.)I have also spun out this preload system into its own file so that it is easier to see which bits are related to the preload system, as well as implementing
loadPromise
in its own file for a similar reason (It definitely doesn't belong infiles.js
in my opinion.)This is the majority of #2698, I should think.