skip to Main Content

So I’ve been trying to wrap my head around a possible solution but my attempts to stub out a simple example have been unsuccessful.

So I have an array of data that looks something like this:

const arr = [{
  "content": "FIRST_CONTENT_HERE",
  "callback": function callback () {}
},
{
  "content": "SECOND_CONTENT_HERE",
  "callback": function callback () {}
},
{
  "content": "THIRD_CONTENT_HERE",
  "callback": function callback () {}
},
{
  "content": "THIRD_CONTENT_HERE",
  "callback": function callback () {}
}
];

I want to create a generator to iterate through that array but not return (yield?) control back to the next iteration until that callback function has been executed. I tried wrapping that callback in a promise and made it so that the promise wouldn’t resolve until that callback was executed. I tried making sure that the callback was an async function to wrap that promise instead. I tried a few things but I just wasn’t able to get them to work. After I called the generator function and iterated through it, it just blew through each element of the array. No matter how I set up the loop, it never waited for the callback to execute before it moved on to the next iteration.

I searched around on but most examples that I found of generators and how they interacted with promises (or async functions), the examples only ever showed something like this yield Promise.resolve(x) which isn’t what I want. I don’t want it to resolve right away — I need it to wait until the callback is executed. And the callback would be set up as an onclick handler (to close out a dialog) and would be executed only when that event occurs. So basically, the next dialog shouldn’t instantiate until the previous has been closed. So for each loop of the array, a few things happen:

  • Instantiates a dialog
  • Sets up an onclick handler for that dialog. Ultimately, callback() is to be executed somewhere therein.
  • I can wrap the callback in a function that returns a Promise (or something along these lines) where the promise doesn’t resolve until that event fires. So when it fires, it both resolves the promise and executes the callback.
  • When that happens, the iterator/generator moves on to the next iteration and instantiates the next dialog. And then the next until the original array has been looped

I’m not saying that my approach is correct. I could be totally and completely off base. But there is something niggling in the back of my head that generators and Promises are the way to go.

How would I best implement the functionality I’m trying to achieve? As I said, I just can’t wrap my head around this…

Here’s an example of some code that I wrote. This has gone through several changes as I tried different things. I’m using fetch() here because I know that it returns a Promise. I’m convinced that Promises need to feature somewhere in my solution to make sure that my non-async callback() is executed before control moves on. It’s fugly but I was just trying different proofs of concept.

      async function runRequest(url) {
        const response = await fetch(url);

        return response;
      }

      async function* setupGenerator(arr) {
        for (let i = 0; i < arr.length; i++) {
          const response = await runRequest(arr[i]);
          yield response;
        }
      }

      const urls = [
        '/activityViewer/index.html',
        '/message/index.html',
        '/users/index.html',
      ];
debugger;
      const generator = setupGenerator(urls);
      for (let response of generator) {
          console.log('In FOR loop; response; ', response);
      }

thnx,
Christoph

3

Answers


  1. You can use async iterarors:

    const arr = [{
      "content": "FIRST_CONTENT_HERE",
      "callback": function callback (_) { return _; }
    },
    {
      "content": "SECOND_CONTENT_HERE",
      "callback": function callback (_) { return _; }
    },
    {
      "content": "THIRD_CONTENT_HERE",
      "callback": function callback (_) { return Promise.resolve(_); }
    },
    {
      "content": "FOUR_CONTENT_HERE",
      "callback": function callback (_) { return _; }
    }
    ];
    
    async function* asyncGenerator(tasks) {
      for (const task of tasks) {
        yield task.callback(task.content);
      }
    }
    
    ;(async function() {
      for await (const txt of asyncGenerator(arr)) {
        console.log('->', txt);
      }
    })();
    
    
    Login or Signup to reply.
  2. Generators and Promises have different goals

    I want to create a generator to iterate through that array but not return (yield?) control back to the next iteration until that callback function has been executed.

    The problem is that generators and Promises have fundamentally different assumptions. A Promise is essentially "I’ll work on this ’til I’m done, and call you back when it’s finished" while a generator is essentially "Let me know when you are ready for the next one".

    If you are using a generator, then you must return a value the next time the function is called… you can’t wait. So the question is, how important is it to you to wait until the first call is done before beginning the second call?

    If you can process all of the callouts at the same time, then your generator can just return Promises and your calling code can take responsibility for awaiting them.

    If, however, it is important that that callouts be processed one at a time in sequence, then you can’t use a generator. You can, however, use a callback.

    Solution 1: Maintain control by using a callback argument

    This probably isn’t exactly what you were hoping for, but if you must ensure that the callouts are called one at a time in sequence then this is a simple solution.

    const arr = [{
      "content": "FIRST_CONTENT_HERE",
      "callback": function callback () {}
    },
    {
      "content": "SECOND_CONTENT_HERE",
      "callback": function callback () {}
    },
    {
      "content": "THIRD_CONTENT_HERE",
      "callback": function callback () {}
    },
    {
      "content": "THIRD_CONTENT_HERE",
      "callback": function callback () {}
    }
    ];
    
    async function processList(itemCallback) {
      for (const item of arr) {
        await item.callback();
        itemCallback(item);
      }
    }
    
    async function callingCode() {
      await processList(item => {
        // Do something with item...
      });
    }
    

    Solution 2: Let the caller have control by returning Promises

    If the generator is more about convenience and syntactic beauty than performance or technical requirements, then you can just return Promises and let the calling code handle them:

    const arr = [{
      "content": "FIRST_CONTENT_HERE",
      "callback": async function callback () {}
    },
    {
      "content": "SECOND_CONTENT_HERE",
      "callback": async function callback () {}
    },
    {
      "content": "THIRD_CONTENT_HERE",
      "callback": async function callback () {}
    },
    {
      "content": "THIRD_CONTENT_HERE",
      "callback": async function callback () {}
    }
    ];
    
    async function* getList() {
      for (const item of arr) {
        yield item.callback().then(() => item);
      }
    }
    
    async function callingCode() {
      for await (const item of getList()) {
        // Do something with item
      }
    }
    
    Login or Signup to reply.
  3. You could let the callback be the function that opens the dialog and returns a promise. This promise should resolve when the click handler is called.

    Here is an example:

    promiseDialog is such a callback function: it takes a title to display, and the dialog allows the user to enter some input and close the dialog.

    You can have a driver function iterate the array you have given, but each time with promiseDialog as callback, and yielding the promise that this callback returns.

    Finally, you could consume these yielded promises with a for await loop:

    (NB: the dialog is not really a modal dialog, but it is irrelevant for the demo)

    function dialogPromise(title) {
        const dialog = document.querySelector("#dialog");
        dialog.querySelector("p").textContent = title;
        dialog.style.display = "inline-block";
        const button = dialog.querySelector("button");
        const input = dialog.querySelector("input");
        input.value = "";
        input.focus();
        return new Promise(function(resolve) { 
            button.addEventListener("click", function (e) {
                e.preventDefault();
                dialog.style.display = "none";
                // Resolve when paint cycle has happened (to hide dialog)
                requestAnimationFrame(() => requestAnimationFrame(() =>
                    resolve(input.value)
                ));
            }, { once: true });
        });
    }
    
    async function* chainDialogs(tasks) {
        for (const task of tasks) {
            yield task.callback(task.content);
        }
    }
    
    (async function() {
        const arr = [
            { "content": "FIRST_CONTENT_HERE",  "callback": dialogPromise }, 
            { "content": "SECOND_CONTENT_HERE", "callback": dialogPromise },
            { "content": "THIRD_CONTENT_HERE",  "callback": dialogPromise },
            { "content": "FOUR_CONTENT_HERE",   "callback": dialogPromise }
        ];
        for await (const result of chainDialogs(arr)) {
            console.log(result);
        }
    })();
    #dialog { position: absolute; display: none; margin: 5vw ; padding: 5px; border: 1px solid; background: #ee8 }
    <form id="dialog">
        <p>Test dialog</p>
        Your answer: <input>
        <button>OK</button>
    </form>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search