Call an array of promises in parallel, but resolve them in order without waiting for other promises to resolve

依然范特西╮ 提交于 2019-12-08 11:12:48

问题


I have an array of promises that I would like to call in parallel, but resolve synchronously.

I made this bit of code to do the required task, however, I needed to create my own object QueryablePromise to wrap the native Promise that I can synchronously check to see it's resolved status.

Is there any better way to achieve this task that doesn't require a special object?

Please note. I do not want to use Promise.all as I don't want to have to wait for all promises to resolve before processing the effects of the promises. And I cannot use async functions in my code base.

const PROMISE = Symbol('PROMISE')
const tap = fn => x => (fn(x), x)

class QueryablePromise {
  resolved = false
  rejected = false
  fulfilled = false
  constructor(fn) {
    this[PROMISE] = new Promise(fn)
      .then(tap(() => {
        this.fulfilled = true
        this.resolved = true
      }))
      .catch(x => {
        this.fulfilled = true
        this.rejected = true
        throw x
      })
  }
  then(fn) {
    this[PROMISE].then(fn)
    return this
  }
  catch(fn) {
    this[PROMISE].catch(fn)
    return this
  }
  static resolve(x) {
    return new QueryablePromise((res) => res(x))
  }
  static reject(x) {
    return new QueryablePromise((_, rej) => rej(x))
  }
}

/**
 * parallelPromiseSynchronousResolve
 *
 * Call array of promises in parallel but resolve them in order
 *
 * @param  {Array<QueryablePromise>}  promises
 * @praram {Array<fn>|fn}  array of resolver function or single resolve function
 */
function parallelPromiseSynchronousResolve(promises, resolver) {
  let lastResolvedIndex = 0
  const resolvePromises = (promise, i) => {
    promise.then(tap(x => {
      // loop through all the promises starting at the lastResolvedIndex
      for (; lastResolvedIndex < promises.length; lastResolvedIndex++) {
        // if promise at the current index isn't resolved break the loop
        if (!promises[lastResolvedIndex].resolved) {
          break
        }
        // resolve the promise with the correct resolve function
        promises[lastResolvedIndex].then(
          Array.isArray(resolver)
            ? resolver[lastResolvedIndex]
            : resolver
        )
      }
    }))
  }
  
  promises.forEach(resolvePromises)
}

const timedPromise = (delay, label) => 
  new QueryablePromise(res => 
    setTimeout(() => {
      console.log(label)
      res(label)
    }, delay)
  )

parallelPromiseSynchronousResolve([
  timedPromise(20, 'called first promise'),
  timedPromise(60, 'called second promise'),
  timedPromise(40, 'called third promise'),
], [
  x => console.log('resolved first promise'),
  x => console.log('resolved second promise'),
  x => console.log('resolved third promise'),
])
<script src="https://codepen.io/synthet1c/pen/KyQQmL.js"></script>

Cheers for any help.


回答1:


Using a for await...of loop, you can do this quite nicely if you already have the array of promises:

const delay = ms => new Promise(resolve => { setTimeout(resolve, ms); });
const range = (length, mapFn) => Array.from({ length }, (_, index) => mapFn(index));

(async () => {
  const promises = range(5, index => {
    const ms = Math.round(Math.random() * 5000);
    return delay(ms).then(() => ({ ms, index }));
  });

  const start = Date.now();

  for await (const { ms, index } of promises) {
    console.log(`index ${index} resolved at ${ms}, consumed at ${Date.now() - start}`);
  }
})();

Since you can't use asynchronous functions, you can mimic the effect of for await...of by chaining the promises together using Array.prototype.reduce(), and synchronously scheduling a callback for each chain:

const delay = ms => new Promise(resolve => { setTimeout(resolve, ms); });
const range = (length, mapFn) => Array.from({ length }, (_, index) => mapFn(index));

const asyncForEach = (array, cb) => array.reduce(
  (chain, promise, index) => chain.then(
    () => promise
  ).then(
    value => cb(value, index)
  ),
  Promise.resolve()
);

const promises = range(5, index => {
  const ms = Math.round(Math.random() * 5000);
  return delay(ms).then(() => ms);
});

const start = Date.now();

asyncForEach(promises, (ms, index) => {
  console.log(`index ${index} resolved at ${ms}, consumed at ${Date.now() - start}`);
});

Error Handling

Since the promises were stated to be instantiated in parallel, I'll assume that errors on any individual promise will not propagate to other promises except through any potentially brittle chains constructed via asyncForEach() (like above).

But we also want to avoid cross-propagating errors between promises when chaining them together in asyncForEach(). Here's a way to schedule error callbacks robustly where errors can only propagate from the original promises:

const delay = ms => new Promise(resolve => { setTimeout(resolve, ms); });
const maybe = p => p.then(v => Math.random() < 0.5 ? Promise.reject(v) : v);
const range = (length, mapFn) => Array.from({ length }, (_, index) => mapFn(index));

const asyncForEach = (array, fulfilled, rejected = () => {}) => array.reduce(
  (chain, promise, index) => {
    promise.catch(() => {}); // catch early rejection until handled below by chain
    return chain.then(
      () => promise,
      () => promise // catch rejected chain and settle with promise at index
    ).then(
      value => fulfilled(value, index),
      error => rejected(error, index)
    );
  },
  Promise.resolve()
);

const promises = range(5, index => {
  const ms = Math.round(Math.random() * 5000);
  return maybe(delay(ms).then(() => ms)); // promises can fulfill or reject
});

const start = Date.now();

const settled = state => (ms, index) => {
  console.log(`index ${index} ${state}ed at ${ms}, consumed at ${Date.now() - start}`);
};

asyncForEach(
  promises,
  settled('fulfill'),
  settled('reject') // indexed callback for rejected state
);

The only caveat to note here is that any errors thrown in the callbacks passed to asyncForEach() will get swallowed by the error handling in the chain except for errors thrown within the callbacks on the last index of the array.




回答2:


I would recommend to indeed use Promise.all - but not on all promises at once, rather all the promises that you want to have fulfilled for each step. You can create this "tree list" of promises with reduce:

function parallelPromisesSequentialReduce(promises, reducer, initial) {
  return promises.reduce((acc, promise, i) => {
    return Promise.all([acc, promise]).then(([prev, res]) => reducer(prev, res, i));
  }, Promise.resolve(initial));
}

const timedPromise = (delay, label) => new Promise(resolve =>
  setTimeout(() => {
    console.log('fulfilled ' + label + ' promise');
    resolve(label);
  }, delay)
);

parallelPromisesSequentialReduce([
  timedPromise(20, 'first'),
  timedPromise(60, 'second'),
  timedPromise(40, 'third'),
], (acc, res) => {
  console.log('combining ' + res + ' promise with previous result (' + acc + ')');
  acc.push(res);
  return acc;
}, []).then(res => {
  console.log('final result', res);
}, console.error);


来源:https://stackoverflow.com/questions/54620494/call-an-array-of-promises-in-parallel-but-resolve-them-in-order-without-waiting

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!