Last active
May 3, 2024 10:05
-
-
Save atengberg/d29740421bf882d7e0b01cb55a670290 to your computer and use it in GitHub Desktop.
Of all the eager settled promises.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Always interesting when while working on a specific problem resolution, | |
coincidentally the circle gets squared. In this case, while building one thing, | |
it lead down the rabbit hole of async generators which surveyed everything from | |
https://www.dreamsongs.com/WorseIsBetter.html to https://okmij.org/ftp/Streams.html | |
and much, much more until it led back to an SO post that had a related SO post that | |
was quite literally almost the same thing I had originally set out to build (!). | |
I should probably compile all the bookmarks for any potential shared future benefit, | |
but note this particular generator function is almost *entirely* modeled on the work of Redu from: | |
https://stackoverflow.com/a/66751675/ | |
where more background can be found: | |
https://betterprogramming.pub/why-would-you-use-async-generators-eabbd24c7ae6 */ | |
/** Returns the resolved or rejected ("settled") value of each given promise in order of quickest to complete. */ | |
function* EagerPromiseSequencer(promises) { | |
// These two are a ~JS "Jensen device" used to redirect invocation | |
// of each promise's settlement to caller's consumption. | |
let _RESOLVE, _REJECT; | |
// Expand each promise, linking the `resolve` and `reject` handlers to the generator's while-loop 'iteration'. | |
promises.forEach(p => p.then(v => _RESOLVE(v), e => _REJECT(e))); | |
// Let the generator yield a new promise for each of the given promises to pass the settled values to the caller. | |
while(true) yield new Promise((r,j) => [_RESOLVE, _REJECT] = [r,j]); | |
} | |
// Usage: (again, mostly directly copied from Redu's original code) | |
const promise = (val, delay, resolves) => new Promise((v,x) => setTimeout(() => resolves ? v(val) : x(val), delay)); | |
const promises = [ | |
promise("⎌1", 1000, true) , | |
promise("⎌2", 2000, true), | |
promise("⎆3", 1500, false), | |
promise("⎆4", 3000, true) | |
]; | |
const genEPS = EagerPromiseSequencer(promises); | |
async function sink() { | |
try { | |
for await (const value of genEPS) console.log(`Got: ${value}`); | |
} catch(err) { | |
console.log(`caught at endpoint --> exception ${err}`); | |
sink(); | |
} | |
} | |
// note caller's for await should be wrapped as a function to recursively | |
// call when rejected error is pulled to continue iteration to completion. (see original SO) | |
sink(); | |
/*> | |
Got: ⎌1 | |
caught at endpoint --> exception ⎆3 | |
Got: ⎌2 | |
Got: ⎆4 | |
*/ |
But can we do without any loop statements, at all?
I'm joking--but that's an awesome answer explaining the point of difference well: thank you. I would have more to say, but I need to get back to resolving generators, streams and worker threads. Look forward to any more posts you might do in the future!
- The effection library looks interesting.
I just noticed EcmaScript finally standardized exposing of resolve and reject by Promise.withResolvers() static method which is an important upgrade to the Promises.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi, regarding my SO answer the
for await of
loop abstracts out the difference between theasync *[Symbol.asyncIterator]()
and*[Symbol.iterator]()
. Things to remember.*[Symbol.iterator]()
: Yields an object holding a promise in thevalue
property. However thefor await of loop
abstracts this out and directly deals with the promise at thevalue
property. Accordingly you can catch it in the loop if it throws;*[Symbol.asyncIterator]()
: Yields a promise holding an object like{value: any, done: bool}
. This means it yields a new promise and wraps the value to be yielded into this promise. If this yielded promise is to contain a promise as the value (like in our case) instead of placing the value it implicitly waits for the settlement of the promise before yielding and places the resolving value to the value property of the promise to be yieleded. Accordinly if our promise rejects the async iteration terminates withdone : true
. This is very obvious if we consume the async iterator with.next()
. Remember that.next()
is a synchronous function and just gets the yielded promise. See this simplified example where we have 5 promises each should resolve 1 sec after the other except for #3 that rejects at 3rd second.Now this behaviour of
*[Symbol.asyncIterator]()
doesn't manifest itself when consumed withfor await of
loop. Regardless of the rejection the async iterator continues to yield. See the followingIn order to be able consume the whole array with
.next()
we have to take precaution inside the asyncIterator by using a.catch()
. So likeIt returns
done: true
becuse the generator's while loop finalizes. Now going back to our case but this time lets consume it with next() so that we can analyze the done case. Firstwhile(true)
Now replace
while(true)
withwhile(this.#COUNT--)
and it will log;Which means we have no indefinitelly hanging promises left.