Search code examples
javascriptasync-awaititerator

Use async iterator triggered by a custom function in main scope


What I want to do is to create an iterator, which is only triggered when an external function is called, say an external event.

An iterator that simply waits for custom events.

function createIteratorWithFunction() {
  var thingThatResolves;
  var asyncIterable = {
    thingThatResolves,
    [Symbol.asyncIterator]() {
      return {
        next() {
          return (new Promise((resolve, reject) => asyncIterable.thingThatResolves = (resolve))).then(_ => ({
            value: _,
            done: false
          }));

        },
        return () {
          return {
            done: true
          }
        }
      };
    }
  };
  return asyncIterable;

}

iter = createIteratorWithFunction();
(async function() {
  for await (let val of iter) {
    console.log(val);
  }
})()
<button onclick="iter.thingThatResolves('execute');iter.thingThatResolves(3)">execute next!</button>

As you can see, it only resolves 'execute', but not 3, of course because promises can't be resolved more than once, and it only is updated asynchronously, I understand this, but since the iterator is async, how would I create a queue, so that any values that could've synchronously been triggered are retrieved by next(), as well?


Solution

  • I have this feeling that there's a more elegant solution involving promise chains, but it's escaping me at the moment. :-) See inline comments:

    function createIteratorWithFunction() {
        // Our pending promise, if any
        let promise = null;
        // The `resolve` function for our `pending` promise
        let resolve = null;
        // The values in the queue
        const values = [];
        // The async iterable
        const asyncIterable = {
            add(value) {
                // Add a value to the queue; if there's a pending promise, fulfill it
                values.push(value);
                const r = resolve;
                resolve = pending = null;
                r?.();
            },
            [Symbol.asyncIterator]() {
                return {
                    async next() {
                        // If we don't have a value...
                        while (!values.length) {
                            // ...we need to wait for one; make sure we have something
                            // to wait for
                            if (!resolve) {
                                pending = new Promise(r => { resolve = r; });
                            }
                            await pending;
                        }
                        // Get the value we waited for and return it
                        const value = values.shift();
                        return {
                            value,
                            done: false,
                        };
                    },
                    return() {
                        return {
                            done: true,
                        };
                    }
                };
            }
        };
        return asyncIterable;
    }
    
    const iter = createIteratorWithFunction();
    (async function() {
        for await (let val of iter) {
            console.log(val);
        }
    })();
    
    document.getElementById("execute").addEventListener("click", () => {
        iter.add("execute");
        iter.add(3);
    });
    <button id="execute">execute next!</button>

    One of the key things here is that an async iterable can have overlapping iterations, and it has to not get confused by that. This implementation avoids that by creating the promise it'll wait on synchronously if it needs one.

    function createIteratorWithFunction() {
        // Our pending promise, if any
        let promise = null;
        // The `resolve` function for our `pending` promise
        let resolve = null;
        // The values in the queue
        const values = [];
        // The async iterable
        const asyncIterable = {
            add(value) {
                // Add a value to the queue; if there's a pending promise, fulfill it
                values.push(value);
                const r = resolve;
                resolve = pending = null;
                r?.();
            },
            [Symbol.asyncIterator]() {
                return {
                    async next() {
                        // If we don't have a value...
                        while (!values.length) {
                            // ...we need to wait for one; make sure we have something
                            // to wait for
                            if (!resolve) {
                                pending = new Promise(r => { resolve = r; });
                            }
                            await pending;
                        }
                        // Get the value we waited for and return it
                        const value = values.shift();
                        return {
                            value,
                            done: false,
                        };
                    },
                    return() {
                        return {
                            done: true,
                        };
                    }
                };
            }
        };
        return asyncIterable;
    }
    
    const iter = createIteratorWithFunction();
    (async function() {
        for await (let val of iter) {
            console.log("first:", val);
        }
    })();
    (async function() {
        for await (let val of iter) {
            console.log("second:", val);
        }
    })();
    
    document.getElementById("execute").addEventListener("click", () => {
        iter.add("execute");
        iter.add(3);
    });
    <button id="execute">execute next!</button>

    I'm never happy when I have to make the promise's resolve function accessible outside the promise executor function (the function you pass new Promise), but as I say, the elegant solution with promise chains is escaping me. I sense strongly that it's there...somewhere... :-)