Add milliseconds delay to Array.map calls which returns Array of promises
There are a bunch of different ways to approach this. I'd probably just use a recursive chained promise myself and then you can more precisely use a timer based on the finish from the previous call and you can use promises for calling it and handling propagation of errors.
I've assumed here that your mailer.sendEmail()
follows the node.js callback calling convention so we need to "promisify" it. If it already returns a promise, then you can use it directly instead of the sendEmail()
function that I created.
Anyways, here are a bunch of different approaches.
Recall Same Function After Delay (delayed recursion)
// make promisified version - assumes it follows node.js async calling convention
let sendEmail = util.promisify(mailer.sendEmail);
function delay(t, data) {
return new Promise(resolve => {
setTimeout(resolve.bind(null, data), t);
});
}
function sendAll(array) {
let index = 0;
function next() {
if (index < array.length) {
return sendEmail(array[index++]).then(function() {
return delay(100).then(next);
});
}
}
return Promise.resolve().then(next);
}
// usage
sendAll(userEmailArray).then(() => {
// all done here
}).catch(err => {
// process error here
});
Use setInterval to Control Pace
You could also just use a setInterval to just launch a new request every 100ms until the array was empty:
// promisify
let sendEmail = util.promisify(mailer.sendEmail);
function sendAll(array) {
return new Promise((resolve, reject) => {
let index = 0;
let timer = setInterval(function() {
if (index < array.length) {
sendEmail(array[index++]).catch(() => {
clearInterval(timer);
reject();
});
} else {
clearInterval(timer);
resolve();
}
}, 100);
})
}
Use await to Pause Loop
And, you could use await
in ES6 to "pause" the loop:
// make promisified version - assumes it follows node.js async calling convention
let sendEmail = util.promisify(mailer.sendEmail);
function delay(t, data) {
return new Promise(resolve => {
setTimeout(resolve.bind(null, data), t);
});
}
// assume this is inside an async function
for (let userEmail of userEmailArray) {
await sendEmail(userEmail).then(delay.bind(null, 100));
}
Use .reduce()
with Promises to Sequence Access to Array
If you aren't trying to accumulate an array of results, but just want to sequence, then a canonical ways to do that is using a promise chain driven by .reduce()
:
// make promisified version - assumes it follows node.js async calling convention
let sendEmail = util.promisify(mailer.sendEmail);
function delay(t, data) {
return new Promise(resolve => {
setTimeout(resolve.bind(null, data), t);
});
}
userEmailArray.reduce(function(p, userEmail) {
return p.then(() => {
return sendEmail(userEmail).then(delay.bind(null, 100));
});
}, Promise.resolve()).then(() => {
// all done here
}).catch(err => {
// process error here
});
Using Bluebird Features for both Concurrency Control and Delay
The Bluebird promise library has a couple useful features built in that help here:
const Promise = require('Bluebird');
// make promisified version - assumes it follows node.js async calling convention
let sendEmail = Promise.promisify(mailer.sendEmail);
Promise.map(userEmailArray, userEmail => {
return sendEmail(userEmail).delay(100);
}, {concurrency: 1}).then(() => {
// all done here
}).catch(err => {
// process error here
});
Note the use of both the {concurrency: 1}
feature to control how many requests are in-flight at the same time and the built-in .delay(100)
promise method.
Create Sequence Helper That Can Be Used Generally
And, it might be useful to just create a little helper function for sequencing an array with a delay between iterations:
function delay(t, data) {
return new Promise(resolve => {
setTimeout(resolve, t, data);
});
}
async function runSequence(array, delayT, fn) {
let results = [];
for (let item of array) {
let data = await fn(item);
results.push(data);
await delay(delayT);
}
return results;
}
Then, you can just use that helper whenever you need it:
// make promisified version - assumes it follows node.js async calling convention
let sendEmail = util.promisify(mailer.sendEmail);
runSequence(userEmailArray, 100, sendEmail).then(() => {
// all done here
}).catch(err => {
// process error here
});
You already have a 'queue' of sorts: a list of addresses to send to. All you really need to do now is pause before sending each one. However, you don't want to pause for the same length of time prior to each send. That'll result in a single pause of n ms, then a whole raft of messages being sent within a few ms of each other. Try running this and you'll see what I mean:
const userEmailArray = [ 'one', 'two', 'three' ]
const promises = userEmailArray.map(userEmail =>
new Promise(resolve =>
setTimeout(() => {
console.log(userEmail)
resolve()
}, 1000)
)
)
Promise.all(promises).then(() => console.log('done'))
Hopefully you saw a pause of about a second, then a bunch of messages appearing at once! Not really what we're after.
Ideally, you'd delegate this to a worker process in the background so as not to block. However, assuming you're not going to do that for now, one trick is to have each call delayed by a different amount of time. (Note that this does not solve the problem of multiple users trying to process large lists at once, which presumably is going to trigger the same API restrictions).
const userEmailArray = [ 'one', 'two', 'three' ]
const promises = userEmailArray.map((userEmail, i) =>
new Promise(resolve =>
setTimeout(() => {
console.log(userEmail)
resolve()
}, 1000 * userEmailArray.length - 1000 * i)
)
)
Promise.all(promises).then(() => console.log('done'))
Here, you should see each array element be processed in roughly staggered fashion. Again, this is not a scaleable solution but hopefully it demonstrates a bit about timing and promises.