Skip to content

Instantly share code, notes, and snippets.

@atengberg
Last active May 3, 2024 10:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save atengberg/d29740421bf882d7e0b01cb55a670290 to your computer and use it in GitHub Desktop.
Save atengberg/d29740421bf882d7e0b01cb55a670290 to your computer and use it in GitHub Desktop.
Of all the eager settled promises.
/*
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
*/
@kedicesur
Copy link

Hi, regarding my SO answer the for await of loop abstracts out the difference between the async *[Symbol.asyncIterator]() and *[Symbol.iterator](). Things to remember.

  • *[Symbol.iterator](): Yields an object holding a promise in the value property. However the for await of loop abstracts this out and directly deals with the promise at the value 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 with done : 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.
class RandomPromises{
    constructor(n){
        this.ps = Array.from({length:n}, (_,i) => new Promise((v,x) => i === 2 ? setTimeout(x,i*1000+1000,`Error at ${i}`)
                                                                               : setTimeout(v,i*1000+1000,i)))
    }
    async *[Symbol.asyncIterator](){
        while(this.ps.length){
            yield this.ps.shift();
        }
    }
}
var it = new RandomPromises(5)[Symbol.asyncIterator]();
function doWhile(it){
    it.next()
      .then(o => ( console.log(o)
                 , !o.done && doWhile(it)
                 ))
      .catch(e => ( console.log(e)
                  , doWhile(it) // just to show it's done: true
                  ))
}
doWhile(it);
// logs
undefined
nnn:29 {value: 0, done: false}
nnn:29 {value: 1, done: false}
nnn:32 Error at 2
nnn:29 {value: undefined, done: true} // comes from last recursion after catch

Now this behaviour of *[Symbol.asyncIterator]() doesn't manifest itself when consumed with for await of loop. Regardless of the rejection the async iterator continues to yield. See the following

class RandomPromises{
    constructor(n){
        this.ps = Array.from({length:n}, (_,i) => new Promise((v,x) => i === 2 ? setTimeout(x,i*1000+1000,`Error at ${i}`)
                                                                               : setTimeout(v,i*1000+1000,i)))
    }
    async *[Symbol.asyncIterator](){
        while(this.ps.length){
            yield this.ps.shift();
        }
    }
}

var ps = new RandomPromises(5)
async function sink(){
    try { for await (p of ps){
              console.log(p);
          }
        }
    catch(e){
        console.log("Downstream error",e)
        sink();
    }
}
sink();
// logs
nnn:16 0
nnn:16 1
nnn:20 Downstream error Error at 2
nnn:16 3
nnn:16 4

In order to be able consume the whole array with .next() we have to take precaution inside the asyncIterator by using a .catch(). So like

class RandomPromises{
    constructor(n){
        this.ps = Array.from({length:n}, (_,i) => new Promise((v,x) => i === 2 ? setTimeout(x,i*1000+1000,`Error at ${i}`)
                                                                               : setTimeout(v,i*1000+1000,i)))
    }
    async *[Symbol.asyncIterator](){
        while(this.ps.length){
            yield this.ps.shift().catch(e => e); // HERE
        }
    }
}
var it = new RandomPromises(5)[Symbol.asyncIterator]();
function doWhile(it){
    it.next()
      .then(o => ( console.log(o)
                 , !o.done && doWhile(it)
                 ))
      .catch(e => ( console.log(e)
                  , doWhile(it) // just to show it's done: true
                  ))
}
doWhile(it);
// logs
nnn:29 {value: 0, done: false}
nnn:29 {value: 1, done: false}
nnn:29 {value: 'Error at 2', done: false}
nnn:29 {value: 3, done: false}
nnn:29 {value: 4, done: false}
nnn:29 {value: undefined, done: true}

It 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. First while(true)

class SortedPromisesArray extends Array {
  #RESOLVE;
  #REJECT;
  #COUNT;
  constructor(...args){
    super(...args.filter(p => Object(p).constructor === Promise));
    this.#COUNT = this.length;
    this.forEach(p => p.then(v => this.#RESOLVE(v), e => this.#REJECT(e)));
    this[Symbol.iterator] = this[Symbol.asyncIterator];
  };
  async *[Symbol.asyncIterator]() {
    while(true) {
      yield new Promise((resolve,reject) => [this.#RESOLVE, this.#REJECT] = [resolve, reject]).catch(e => ( console.log("Error received upstream",e)
                                                                                                          , "Error received upstream" // return error in value
                                                                                                          ));
    };
  };
};

var promise  = (val, delay, resolves) => new Promise((v,x) => setTimeout(() => resolves ? v(val) : x(val), delay));
var promises = [ 
  promise("⎌1", 1000, true) , 
  promise("⎌2", 2000, true), 
  promise("⎆3", 1500, false),
  promise("⎆4", 3000, true) 
];
var sortedPromiseArray = new SortedPromisesArray(...promises);
var it = sortedPromiseArray[Symbol.asyncIterator]()
function doWhile(it){
  it.next()
    .then(o => ( console.log(o)
               , !o.done && doWhile(it)
               ))
    .catch(e => ( console.log("Error received downstream:",e)
                , doWhile(it)
               ))
}
doWhile(it);
//logs
Script snippet #19:31 {value: '⎌1', done: false}
Script snippet #19:13 Error received upstream ⎆3
Script snippet #19:31 {value: 'Error received upstream', done: false}
Script snippet #19:31 {value: '⎌2', done: false}
Script snippet #19:31 {value: '⎆4', done: false}

Now replace while(true) with while(this.#COUNT--) and it will log;

Script snippet #19:28 {value: '⎌1', done: false}
Script snippet #19:13 Error received upstream ⎆3
Script snippet #19:31 {value: 'Error received upstream', done: false}
Script snippet #19:28 {value: '⎌2', done: false}
Script snippet #19:28 {value: '⎆4', done: false}
Script snippet #19:28 {value: undefined, done: true}

Which means we have no indefinitelly hanging promises left.

@atengberg
Copy link
Author

atengberg commented Apr 12, 2024

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.

@kedicesur
Copy link

kedicesur commented May 3, 2024

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