简体   繁体   中英

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

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.

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.

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); 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM