Last active
February 8, 2017 09:53
-
-
Save pchampin/6751c974561b3a7ae9ac to your computer and use it in GitHub Desktop.
An extension of JS Promises with a forEach method.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* global Promise */ | |
/** | |
* An IterablePromise is used to combine the ease of use of loops, | |
* with the power of Promises. | |
* | |
* Assume you want to apply an asynchronous function process(), | |
* returning a Promise, to each item of an array ``a``, | |
* but wait for each element to be processed before processing the next one. | |
* You would do it like this: | |
* | |
* new IterablePromise(a).forEach(function (value) { | |
* process(value); // may throw an exception | |
* }.then(function(allResults) { | |
* // do something when all the results that have been processed | |
* }).catch(function(err) { | |
* // something bad happend | |
* }); | |
* | |
* ItarablePromise also accept a preprocess function as its second argument. | |
* This function may be asynchronous (i.e. return a promise). | |
* | |
* IterablePromise guarantees that | |
* - the values of ``a`` will all be preprocessed ASAP; | |
* - then they will be processed ASAP but in the correct order, | |
* i.e. waiting each process() to resolve before processing the next value, | |
* but not waiting for the preprocessing of subsequent values to start; | |
* - an exception thrown by preprocess() or process() will stop the processing, | |
* (just like in a standard synchronous loop), | |
* preventing all further elements of ``a`` to be processed | |
* (but not preprocessed); | |
* - as a bonus, if the string 'break' is thrown, | |
* this will stop the iteration (as any other exception), | |
* but will *not* be considered as a rejection of the promise. | |
* | |
* In other words, in the callback of the forEach() method: | |
* - ``return`` acts as a ``continue`` in a standard loop, | |
* - ``throw 'break'`` acts as a break in a standard loop, | |
* - any other exception acts as an exception in a standard loop. | |
* | |
* Note that ``new IterablePromise(a, preprocess)`` can also be used | |
* as a standard promise (i.e. without calling forEach), | |
* in which case it is equivalent to: | |
* | |
* Promise.all(a.map(preprocess)) | |
* | |
* See testIterablePromise below for examples of use. | |
*/ | |
function IterablePromise(arrayLike, preprocess) { | |
if (preprocess !== undefined) { | |
arrayLike = Array.prototype.map.bind(arrayLike)(preprocess); | |
} | |
var that = this; | |
var lst = []; | |
var i = 0; | |
that.forEach = function(callback) { | |
if (i >= arrayLike.length) { | |
return Promise.resolve(lst); | |
} else { | |
return Promise.resolve(arrayLike[i++]) | |
.then(callback) | |
.then(function(x) { lst.push(x); }) | |
.then(that.forEach.bind(that, callback)) | |
.catch(function(err) {if(err !== 'break') throw err; else return lst; }) | |
; | |
} | |
}; | |
that.then = function(onFullfilled, onRejected) { | |
return that.forEach(function(x){return x}).then(onFullfilled, onRejected); | |
}; | |
that.catch = function(onRejected) { | |
return that.forEach(function(x){return x}).catch(onRejected); | |
}; | |
} | |
/** | |
* Minimal test suite | |
*/ | |
function testIterablePromise() { | |
// simple handling of elements | |
function test1() { | |
return new Promise(function(resolve, reject) { | |
console.log('--- test1'); | |
var a = [1,2,3]; | |
new IterablePromise(a) | |
.forEach(function(value) { | |
return process(value); | |
}).then(function(allResults) { | |
console.log(allResults); | |
resolve(); | |
}).catch(function(err) { | |
reject("test1: " + err); | |
}); | |
}); | |
} | |
// simple handling of elements with preprocessing | |
function test2() { | |
return new Promise(function(resolve, reject) { | |
console.log('--- test2'); | |
var a = [1,2,3]; | |
new IterablePromise(a, preprocess) | |
.forEach(function(value) { | |
return process(value); | |
}).then(function(allResults) { | |
console.log(allResults); | |
resolve(); | |
}).catch(function(err) { | |
reject("test2: " + err); | |
}); | |
}); | |
} | |
// use IterablePromise as a simple Promise | |
function test3() { | |
return new Promise(function(resolve, reject) { | |
console.log('--- test3'); | |
var a = [1,2,3]; | |
new IterablePromise(a, preprocess) | |
.then(function(allResults) { | |
console.log(allResults); | |
resolve(); | |
}).catch(function(err) { | |
reject("test3: " + err); | |
}); | |
}); | |
} | |
// exception thrown by preprocess (on 0) | |
function test4() { | |
return new Promise(function(resolve, reject) { | |
console.log('--- test4'); | |
var a = [1,2,3,0,4]; | |
new IterablePromise(a, preprocess) | |
.forEach(function(value) { | |
return process(value); | |
}).then(function(allResults) { | |
reject("test4: should have caught an error"); | |
}).catch(function(err) { | |
resolve(); | |
}); | |
}); | |
} | |
// exception thrown by process | |
function test5() { | |
return new Promise(function(resolve, reject) { | |
console.log('--- test4'); | |
var a = [1,2,3,4,5]; | |
new IterablePromise(a, preprocess) | |
.forEach(function(value) { | |
if (value === 4) throw "error"; | |
return process(value); | |
}).then(function(allResults) { | |
reject("test4: should have caught an error"); | |
}).catch(function(err) { | |
resolve(); | |
}); | |
}); | |
} | |
// throw "break" | |
function test6() { | |
return new Promise(function(resolve, reject) { | |
console.log('--- test5'); | |
var a = [1,2,3,4,5]; | |
new IterablePromise(a, preprocess) | |
.forEach(function(value) { | |
if (value === 4) throw 'break'; | |
return process(value); | |
}).then(function(allResults) { | |
console.log(allResults); | |
resolve(); | |
}).catch(function(err) { | |
reject("test5: throw 'break' should not be rejected"); | |
}); | |
}); | |
} | |
// utility functions | |
function makeTimeoutPromise(f, delay) { | |
if (delay === undefined) delay = 200; | |
return new Promise(function (resolve, reject) { | |
setTimeout(function() { | |
try { resolve(f()); } | |
catch (err) { reject(err); } | |
}, delay); | |
}); | |
} | |
function preprocess(x) { | |
var delay = 200; | |
if (x == 2) delay = 600; | |
return makeTimeoutPromise(function() { | |
console.log("preprocess " + x); | |
if (x === 0) throw "preprocess failed"; | |
return x; | |
}, delay); | |
} | |
function process(x) { | |
return makeTimeoutPromise(function() { | |
console.log("process " + x); | |
return x*10; | |
}, 500 - 300*(x%2)); | |
} | |
Promise.resolve() | |
.then(test1) | |
.then(test2) | |
.then(test3) | |
.then(test4) | |
.then(test5) | |
.then(test6) | |
.catch(function(err) { | |
console.error(err); | |
}); | |
} | |
testIterablePromise(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment