Search code examples
javascriptecmascript-6promisees6-promise

How to sequence a series of dynamically generated asynch operations using promises?


In the following example, I am simulating a scenario where an user can trigger some actions, every action is a asynchronous operation (wrapped in a promise) which can be resolved with a random timing.

The list of actions is also dynamic, an user can trigger only one actions or many in a span of time.

I would need:

  • Keep the promise resolution sequential, keeping in consideration that a full list of actions is not known in advance.

I can use ES6 and Browser native Promises

To test the example, click several times (with variable frequency) the button.

  (function (window) {
            document.addEventListener('DOMContentLoaded', e => {
                let logActionsElm = document.getElementById('logActions');
                let logOperationsElm = document.getElementById('logOperations');
                let logCounterElm = document.getElementById('logCounter');
                let btnActionElm = document.getElementById('btnAction');

                let actionCounter = 0;

                let operationDurations = [1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000];
                let getRandomNumber = (min, max) => Math.floor(Math.random() * (max - 0 + min)) + min;
                let getRandomOperationDuration = x => operationDurations[getRandomNumber(0, 8)];

                let promises = [];

                let createAction = (id, duration) => {

                    logActionsElm.innerHTML += `${id} action_start_duration: ${duration} --------- ${id}<br>`;

                    let promiseAction = new Promise((resolve, reject) => {
                        setTimeout(e => {
                            logActionsElm.innerHTML += `${id} action_end___duration: ${duration} --------- ${id}<br>`;
                        }, duration);
                    });
                };

                let createOperation = x => {
                    actionCounter++;
                    let duration = getRandomOperationDuration() / 10;
                    createAction(actionCounter, duration);
                    //logActionsElm.innerHTML += `action ${actionCounter} created will resolve after ${duration}<br>`;
                };

                btnActionElm.addEventListener('click', e => {
                    createOperation();
                });

                var counter = 0;
                setInterval(x => {
                    if (counter >= 20) {
                        return;
                    }
                    counter++;
                    logCounterElm.innerHTML += `${counter} second <br>`;
                }, 1000);
            });
        })(window);
        body {
            font-size: 1.5em;
            font-family: 'Courier New';
        }

        #logCounter {
            position: fixed;
            top: 0;
            right: 0;
            margin: 2em;
        }
    <button id="btnAction">Triger and action</button>
    <div id="logActions"></div>
    <div id="logOperations"></div>
    <div id="logCounter"></div>


Solution

  • You could append new action functions to a single promise to ensure actions aren't evaluated until after all previous actions have resolved:

    let queue = Promise.resolve();
    let userActionIdCounter = 0;
    
    function queueAction(fn) {
      queue = queue.then(fn);
      return queue;
    }
    
    function userAction() {
      return new Promise((resolve) => {
        const seconds = Math.ceil(Math.random() * 5);
        const actionId = userActionIdCounter;
        userActionIdCounter += 1;
        console.log('User action', userActionIdCounter, 'started');
        setTimeout(() => {
          console.log('User action', userActionIdCounter, 'complete');
          resolve();
        }, seconds * 1000);
      });
    }
    
    document.getElementById('action').addEventListener('click', () => {
      queueAction(userAction);
    });
    <button id='action'>Trigger action</button>