Add milliseconds delay to Array.map calls which returns Array of promises

匿名 (未验证) 提交于 2019-12-03 02:27:02

问题:

My need is simple. I would like to delay calls to sendEmail by 100 milliseconds. The email service provider permits at most sending 10 emails per second.

Note, however, though .map is synchronous, it immediately returns a Promise.

I have tried setTimeout to no avail, such as setTimeout(() => resolve(x), 100) and setTimeout(() => {return new Promise....}, 100).

Thoughts?

const promises = userEmailArray.map((userEmail) => {   return new Promise((resolve, reject) => {       ....       mailer.sendEmail(userEmail);       return resolve();     });   }); }); ... Promise.all(promises).then(() => resolve()).catch(error => reject(error)); 

回答1:

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);     }); }  const promises = userEmailArray.map(async function(userEmail) {     await sendEmail(userEmail).then(delay.bind(null, 100)); }); Promise.all(promises).then(() => {     // all done here }).catch(err => {     // process error here }); 

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.bind(null, data), t);     }); }  async function runSequence(array, delayT, fn) {     for (item of array) {         await fn(item).then(data => {             return delay(delayT, data);         });     } } 

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, sendEmail, 100).then(() => {     // all done here }).catch(err => {     // process error here }); 


回答2:

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.



回答3:

It's simpler to just do an async iteration of the array.

function send(i, arr, cb) {   if (i >= arr.length) return cb();    mailer.sendEmail(arr[i]);   setTimeout(send, 100, i+1, arr, cb); } send(0, userEmailArray, function() { console.log("all done") }); 


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!