EDIT 2
According to your edit, you're looking for Promise.mapSeries
as provided by bluebird. You've given us a bit of a moving target, so this edit changes direction from my previous answer because the mapSeries
function works very differently than just executing a collection of Promises in serial order.
// mock bluebird's mapSeries function
// (Promise [a]) -> (a -> b) -> (Promise [b])
Promise.prototype.mapSeries = function mapSeries(f) {
return this.then(reducek (ys=> x=> k=> {
let value = f(x);
let next = x=> k([...ys, x]);
return value instanceof Promise ? value.then(next) : next(value);
}) ([]));
};
Just to get a top-level idea of how this would be used
// given: (Promise [a]) and (a -> b)
// return: (Promise [b])
somePromiseOfArray.mapSeries(x=> doSomething(x)); //=> somePromiseOfMappedArray
This relies on a small reducek
helper which operates like a normal reduce
except that the callback receives an additional continuation argument. The primary advantage here is that our reducing procedure has the option of being asynchronous now. The computation will only proceed when the continuation is applied. This is defined as a separately because it's a useful procedure all on its own; having this logic inside of mapSeries
would make it overly complicated.
// reduce continuation helper
// (a -> b -> (a -> a)) -> a-> [b] -> a
const reducek = f=> y=> ([x, ...xs])=> {
if (x === undefined)
return y;
else
return f (y) (x) (y => reducek (f) (y) (xs));
};
So you can get a basic understanding of how this helper works
// normal reduce
[1,2,3,4].reduce((x,y)=> x+y, 0); //=> 10
// reducek
reducek (x=> y=> next=> next(x+y)) (0) ([1,2,3,4]); //=> 10
Next we have two actions that we'll use in our demos. One that is completely synchronous and one that returns a Promise. This demonstrates that mapSeries
can also work on iterated values that are Promises themselves. This is the behaviour defined by bluebird.
// synchronous power
// Number -> Number -> Number
var power = x=> y=> Math.pow(y,x);
// asynchronous power
// Number -> Number -> (Promise Number)
var powerp = x=> y=>
new Promise((resolve, reject)=>
setTimeout(() => {
console.log("computing %d^%d...", y, x);
if (x < 10)
resolve(power(x)(y));
else
reject(Error("%d is just too big, sorry!", x));
}, 1000));
Lastly, a small helper used to facilitate logging in the demos
// log promise helper
const logp = p=>
p.then(
x=> console.log("Done:", x),
err=> console.log("Error:", err.message)
);
Demo time! Here I'm going to dogfood my own implementation of mapSeries
to run each demo in sequential order!.
Because mapSeries
excepts to be called on a Promise, I kick off each demo with Promise.resolve(someArrayOfValues)
// demos, map each demo to the log
Promise.resolve([
// fully synchronous actions map/resolve immediately
()=> Promise.resolve([power(1), power(2), power(3)]).mapSeries(pow=> pow(2)),
// asynchronous items will wait for resolve until mapping the next item
()=> Promise.resolve([powerp(1), powerp(2), powerp(3)]).mapSeries(pow=> pow(2)),
// errors bubble up nicely
()=> Promise.resolve([powerp(8), powerp(9), powerp(10)]).mapSeries(pow=> pow(2))
])
.mapSeries(demo=> logp(demo()));
Go ahead, run the demo now
// reduce continuation helper
// (a -> b -> (a -> a)) -> a-> [b] -> a
const reducek = f=> y=> ([x, ...xs])=> {
if (x === undefined)
return y;
else
return f (y) (x) (y => reducek (f) (y) (xs));
};
// mock bluebird's mapSeries function
// (Promise [a]) -> (a -> b) -> (Promise [b])
Promise.prototype.mapSeries = function mapSeries(f) {
return this.then(reducek (ys=> x=> k=>
(x=> next=>
x instanceof Promise ? x.then(next) : next(x)
) (f(x)) (x=> k([...ys, x]))
) ([]));
};
// synchronous power
// Number -> Number -> Number
var power = x=> y=> Math.pow(y,x);
// asynchronous power
// Number -> Number -> (Promise Number)
var powerp = x=> y=>
new Promise((resolve, reject)=>
setTimeout(() => {
console.log("computing %d^%d...", y, x);
if (x < 10)
resolve(power(x)(y));
else
reject(Error("%d is just too big, sorry!", x));
}, 1000));
// log promise helper
const logp = p=>
p.then(
x=> console.log("Done:", x),
err=> console.log("Error:", err.message)
);
// demos, map each demo to the log
Promise.resolve([
// fully synchronous actions map/resolve immediately
()=> Promise.resolve([power(1), power(2), power(3)]).mapSeries(pow=> pow(2)),
// asynchronous items will wait for resolve until mapping the next item
()=> Promise.resolve([powerp(1), powerp(2), powerp(3)]).mapSeries(pow=> pow(2)),
// errors bubble up nicely
()=> Promise.resolve([powerp(8), powerp(9), powerp(10)]).mapSeries(pow=> pow(2))
])
.mapSeries(f=> logp(f()));
EDIT
I'm reapproaching this problem as a series of promises should be considered like a chain or composition of promises. Each resolve promise will feed it's value to the next promise.
Per @Zhegan's remarks, it makes more sense for the series
function to take an array of promise creators, otherwise there's no way to guarantee the promises would run in serial. If you pass an array of Promises, each promise will immediately run its executor and start doing work. Thus, there's no way that the work of Promise 2 could depend on the completed work of Promise 1.
Per @Bergi's remarks, my previous answer was a little weird. I think this update makes things a little more consistent.
Promise series without error
// ([(a-> (Promise b)), (b-> (Promise c)]), ...]) -> a -> (Promise c)
Promise.series = function series(tasks) {
return x=>
tasks.reduce((a,b)=> a.then(b), Promise.resolve(x));
};
// a -> [a] -> (Promise [a])
var concatp = x=> xs=>
new Promise((resolve, reject)=>
setTimeout(() => {
console.log(xs, x);
if (xs.length < 3)
resolve(xs.concat([x]));
else
reject(Error('too many items'));
}, 250));
var done = (x)=> console.log('done:', x);
var err = (e)=> console.log('error:', e.message);
Promise.series([concatp(3), concatp(6), concatp(9)]) ([]) .then(done, err);
// [] 3
// [ 3 ] 6
// [ 3, 6 ] 9
// done: [ 3, 6, 9 ]
Promise series with an error
// ([(a-> (Promise b)), (b-> (Promise c)]), ...]) -> a -> (Promise c)
Promise.series = function series(tasks) {
return x=>
tasks.reduce((a,b)=> a.then(b), Promise.resolve(x));
};
// a -> [a] -> (Promise [a])
var concatp = x=> xs=>
new Promise((resolve, reject)=>
setTimeout(() => {
console.log(xs, x);
if (xs.length < 3)
resolve(xs.concat([x]));
else
reject(Error('too many items'));
}, 250));
var done = (x)=> console.log('done:', x);
var err = (e)=> console.log('error:', e.message);
Promise.series([concatp(3), concatp(6), concatp(9), concatp(12)]) ([]) .then(done, err);
// [] 3
// [ 3 ] 6
// [ 3, 6 ] 9
// [ 3, 6, 9 ] 12
// error: too many items