var Promise = require('bluebird'); | |
var promiseWhile = function(condition, action) { | |
var resolver = Promise.defer(); | |
var loop = function() { | |
if (!condition()) return resolver.resolve(); | |
return Promise.cast(action()) | |
.then(loop) | |
.catch(resolver.reject); | |
}; | |
process.nextTick(loop); | |
return resolver.promise; | |
}; | |
// And below is a sample usage of this promiseWhile function | |
var sum = 0, | |
stop = 10; | |
promiseWhile(function() { | |
// Condition for stopping | |
return sum < stop; | |
}, function() { | |
// The function to run, should return a promise | |
return new Promise(function(resolve, reject) { | |
// Arbitrary 250ms async method to simulate async process | |
setTimeout(function() { | |
sum++; | |
// Print out the sum thus far to show progress | |
console.log(sum); | |
resolve(); | |
}, 250); | |
}); | |
}).then(function() { | |
// Notice we can chain it because it's a Promise, this will run after completion of the promiseWhile Promise! | |
console.log("Done"); | |
}); |
Cool. There is an operation in the functional programming lexicon known as "unfold", which is very similar. when.js provides an unfold
module that implements it--thought you might be interested to compare and contrast.
@briancavalier very cool, didn't know about that!
Perhaps this would be good as a pull request for the Bluebird library then...
Here is what I came up with a couple of days ago, using the awesomeness that are ES6 arrow functions and some of the other neat methods in Bluebird :D
Doing it without helpers can be acceptable too
(function loop(sum, stop) {
if (sum < stop) {
return Promise.delay(250).then(() => {
sum++;
console.log(sum);
return loop(sum, stop);
});
}
})(0, 10).then(() => console.log('Done'))
I mean considering some people are doing the same even with synchronous code just so that their code would be recursive, it's not pretty bad. And unlike in synchronous version the call stack doesn't keep growing here :P
It looks like maybe (at least with the current Bluebird) you want to bind the resolver.reject catch to the resolver, or on an error you'll get a promise error ("Illegal invocation, resolver resolve/reject must be called within a resolver context.") since you won't have the internal promise state?
looks like bluebird changed API , but they have docs how to get "old" defered()
Here's an updated example using the constructor instead of Promise.defer()
var promiseWhile = function(condition, action) {
return new Promise(function(resolve, reject) {
var loop = function() {
if (!condition()) return resolve();
return Promise.cast(action())
.then(loop)
.catch(function(e) {
reject(e);
});
};
process.nextTick(loop);
});
};
Do you know of a way to do this in plain js without using node.js (i.e. without using process.nextTick() )? Thanks.
There are process.nextTick polyfills or examples around, like here: https://gist.github.com/WebReflection/2953527
you could also just call loop() if your action is doing I/O
Updating @noam3127, if you want to return a promise of an array from your loop :
var promiseWhile = function(condition, action) {
return new Promise(function(resolve, reject) {
var loop = function(result) {
if (result === null || result === undefined) result = [];
if (!condition()) return resolve(result);
return Promise.join(action(), result, function(a,b){
return a.concat(b);
})
.then(loop)
.catch(function(e) {
reject(e);
});
};
process.nextTick(loop);
});
};
}
and, of course, you have to return it from your action() function :
setTimeout(function() {
...
resolve(*YOUR ARRAY*);
}, 250);
I was digging a bit about this and I found some interesting links:
petkaantonov/bluebird#553 (comment)
http://stackoverflow.com/a/29396005
http://stackoverflow.com/a/24660323
Simplifications:
function promiseWhile(predicate, action) {
function loop() {
if (!predicate()) return;
return Promise.resolve(action()).then(loop);
}
return Promise.resolve().then(loop);
}
Or
var promiseWhile = Promise.method(function(condition, action) {
if (!condition()) return;
return action().then(promiseWhile.bind(null, condition, action));
});
For anyone who doesn't want to use process.nextTick()
, I've written my own solution here. I'm using the Q framework, but it could be very easily reworked to use Bluebird. It returns a promise which will resolve to the final iteration's return value, so it works a lot like reduce
.
function promiseUntil(correct, action, last) {
if (last === undefined || last.then === undefined) { last = Q(last); }
return last.then(action).then(function (v) {
if (correct(v)) {
return Q(v);
} else {
return promiseUntil(correct, action, Q(v));
}
});
}
That is the promise-y equivalent of this function:
function loopUntil(correct, action, last) {
var value = action(last);
if (correct(value)) {
return value;
} else {
return loopUntil(correct, action, value);
}
}
Usage is straightforward:
function equalFour(v) {
return v === 4;
}
function increment(v) {
console.log(v);
return (v === undefined) ? 0 : v+1;
}
var p = promiseUntil(equalFour, increment);
// prints:
// undefined
// 0
// 1
// 2
// 3
p = promiseUntil(equalFour, increment, 0);
// prints:
// 0
// 1
// 2
// 3
console.log(p);
// { state: 'fulfilled', value: 4 }
@mkbitz, won't your solution run out of memory unless the JS engine supports tail call optimization?
@petkaantonov your version crashes in chrome with RangeError: Maximum call stack size exceeded.
Still looking for and elegant alternative for bluebird that does actually work...
ES6 native implementation
var promiseWhile = function(condition, action) {
var resolver = Promise.defer();
var loop = function() {
if (!condition()) return resolver.resolve();
return new Promise(action)
.then(loop)
.catch(resolver.reject);
};
process.nextTick(loop);
return resolver.promise;
};
var sum = 0, stop = 10;
promiseWhile(function() {
return sum < stop;
},function(resolve,reject){
setTimeout(function() {
sum++;
console.log(sum);
resolve();
}, 250);
}).then(function() {
console.log("Done");
});
For loop in an array
promiseFor = Promise.method(function(arr,action,steps) {
"use strict";
if(!steps) steps = 0;
if(arr.length<=steps) return;
return new Promise((resolve,reject)=>{
try {
action(steps);
resolve();
}
catch(e) {
reject(e);
}
}).then(Promise.for.bind(null,arr,action,steps+1));
});
Usage
asyncFunc()
.then(()=>{
var arr = [1,2,3]
return new promiseFor(arr,(i)=>{ console.log(i) });
})
.then(()=>{
console.log("done iterating");
});
Doing async function on each members of the array
global.Promise.asyncOnEach = Promise.method(function(arr,action,steps) {
if(!steps) steps = 0;
console.log("Currently working on index:",steps,"Value:",arr[steps]);
if(arr.length<=steps) {
console.log("Finished working on the array, now will return it");
console.log(arr);
return arr;
}
return new Promise((resolve,reject)=>{
action(arr[steps]).then((v)=>{
arr[steps] = v;
console.log(v);
resolve(arr);
}).catch((e)=>{
reject(e);
});
}).then(Promise.asyncOnEach.bind(null,arr,action,steps+1));
});
Usage:
Promise.asyncOnEach([3,6,9],asyncFunc)
.then((arr)=>{
console.log("The result is:",arr);
res.send("OK");
}).catch((e)=>{
console.log(e);
res.send("error");
});
Note, if you want to clone this and run this gist, you will want to run
npm install bluebird
in its directory after cloning it to ensure its only dependency is installed.Then you can run
node promise_while_loop.js
to see it do its rather boring task of printing the numbers 1 through 10 with a delay between each number.