NodeJS 等待承诺的一半解决

f87krz0w  于 2023-03-17  发布在  Node.js
关注(0)|答案(4)|浏览(120)

我想在NodeJS中并行处理多个异步调用。如果这些调用中的特定数量得到解决或者全部完成(一些得到解决,一些被拒绝),我可以继续。如何做到这一点?
Promise.All()在完成所有承诺时进行解析,Promise.Any()在解析任何一个承诺或拒绝所有承诺时进行解析。是否可以等待50%的承诺或n数量的承诺得到解析,而不是等待所有承诺得到解析。

ldxq2e6h

ldxq2e6h1#

您可以编写一个函数,为每个给定的Promise附加一个then回调,每次递增计数(或将结果推送到数组),并在计数达到某个值时进行解析。
一个基本示例:

function nResolve(promises, n) {
  if (n < 0) throw Error('Invalid n: ' + n);
  let results = [], rejected = 0;
  return new Promise((resolve, reject) => {
    if (!n) resolve([]);
    else promises.forEach(promise => promise.then(res => {
      results.push(res); // note: order is unpredictable
      if (results.length === n) resolve([...results]);
    }, err => {
      // reject only when there are not enough promises left for n to resolve
      if (++rejected > promises.length - n) reject();
    }));
  });
}
const delay = ms => new Promise(r => setTimeout(() => {
  console.log(ms);
  r(ms);
}, ms));
nResolve([delay(2000), delay(100), delay(500), delay(1000), delay(2000), 
  delay(3000), delay(5000), delay(10000)], 4)
  .then(results => console.log('4 Promises resolved', results));
pn9klfpd

pn9klfpd2#

首次尝试

很难想象函数的用例,但这是一个有趣的问题。firstN接受一个Array<Promise<T>>和一个数字n。它解析一个Map<number, T>,其中数字对应于输入数组中的promise索引。您可以在typescript操场上验证-

function firstN<T>(promises: Array<Promise<T>>, n: number): Promise<Map<number, T>> {
  let resolves = 0
  let failures = 0
  function loop(map: Map<number, Promise<readonly [number, T]>>): Promise<Map<number, T>> {
    return Promise
      .race(Array.from(map.values()))
      .then(
        ([id, v]) => {
          if (++resolves === n) return new Map().set(id, v)
          map.delete(id)
          return loop(map).then(rest => rest.set(id, v))
        },
        ([id, e]) => {
          if (++failures > promises.length - n) throw Error("too many failures")
          map.delete(id)
          return loop(map)
        }
      )
  }
  return n == 0
    ? Promise.resolve(new Map())
    : loop(new Map(promises.map((p, id) =>
        [id, p.then(v => [id, v] as const).catch(e => Promise.reject([id, e] as const))] as const
      )))
}

为了测试它,我们将模拟一个passfail函数,它们在ms毫秒之后解析-

const delay = (pass: boolean) => (ms:number) =>
  new Promise((res, rej) => setTimeout(pass ? res : rej, ms, ms))

const pass = delay(true)
const fail = delay(false)

现在做出承诺解决前三个问题-

const test1 = [
  pass(80), pass(10), fail(30), pass(90), pass(50),
  fail(20), pass(60), pass(40), fail(70), pass(100)
]

console.time("test1")
firstN(test1, 3)
  .then(console.log, console.error)
  .finally(() => console.timeEnd("test1"))

最先解决的是最短的10、40和50,键1、7和4对应于它们在输入中的位置,最重要的是,注意总运行时间等于一串中承诺时间最长的一个--

Map (3) {4 => 50, 7 => 40, 1 => 10} 
first 3: 53.608 ms

在第二个测试中,我们将确保当不充分的承诺解决时适当的失败。在这个测试中,10个承诺中有8个失败,使得不可能实现n=3-

const test2 = [
  pass(80), fail(10), fail(30), fail(90), fail(50),
  fail(20), fail(60), fail(40), fail(70), pass(100)
]

console.time("test2")
firstN(test2, 3)
  .then(console.log, console.error)
  .finally(() => console.timeEnd("test2"))

只有80和100通过,一旦90失败,我们的职能有足够的信息知道不可能成功,并提供早期拒绝-

[ERR]: too many failures 
test2: 92.656ms

在我们的第三个测试中,我们验证n=0是否立即退出以及空Map -是否正确返回

const test3 = [pass(80), pass(90), pass(30)]

console.time("test3")
firstN(test3, 0)
  .then(console.log, console.error)
  .finally(() => console.timeEnd("test3"))
Map (0) {} 
test3: 1.413ms

最后,我们测试n超过提供的承诺数的场景,我们看到立即拒绝并显示适当的消息-

console.time("test4")
firstN(test4, 3)
  .then(console.log, console.error)
  .finally(() => console.timeEnd("test4"))
[ERR]: n is too great
test4: 1.728ms

第二次尝试

最初,我考虑将Set和Map作为在循环中利用Promise.race的方法。在完成程序的初稿后,我进行了重构,并通过了上述测试,降低了实现复杂性-

function firstN<T>(promises: Array<Promise<T>>, n: number): Promise<Map<number, T>> {
  const result = new Map<number, T>()
  let failures = 0
  if (n == 0) return Promise.resolve(new Map())
  if (n > promises.length) return Promise.reject(Error("n is too great"))
  return new Promise((resolve, reject) => {
    for (const [id, p] of promises.entries()) {
      p.then(
        v => {
          if (result.size < n) result.set(id, v)
          if (result.size == n) resolve(result)
        },
        e => {
          if (++failures > promises.length - n)
            reject(Error("too many failures"))
        }
      )
    }
  })
}
r7knjye2

r7knjye23#

下面是另一个例子:

function mkProm(name){
 return new Promise((res,rej)=> // 50% of all promises are expected to be rejected
  setTimeout(()=>Math.random()>.5?res(name):rej(name+" rejected!"), Math.random()*2000+200) );
}
function firstn(parr,n){
 return new Promise((res,rej)=>{
  let rp={},m=parr.length-n+1,rmsg="*** firstn rejected. ***";
  if(n<1) res(rp); // nothing to do ...
  if(m<1) rej(rmsg); // impossible to achieve ...
  parr.forEach((p,i)=>p.then(r=>{
   console.log(r); // for demo and debugging purposes only ...
   rp[i]=r;
   if(!--n) res(rp);
  }, err=>{
   console.log(m-1,err); // for demo only ...
   if(!--m) rej(rmsg)
  }) )
 })
}

firstn([mkProm("one"),mkProm("four"),mkProm("two"),mkProm("three"),mkProm("seven"),mkProm("five"),mkProm("six")],3)
.then(console.log,console.log)

firstn promise的行为有点像Promise.all(),一旦第一个n promise被解析,它就会被解析,并提供一个对象:

  • 它的关键是实现承诺的指标
  • 它的值是它们各自解析的数据。

对于所有非零值,表达式!--n递减(本地)计数器n并返回false

更新:在@Bergi的评论之后,我现在也加入了一个适当的拒绝处理。如果一开始要求太多的解析(n>parr.length),那么firstn承诺将立即拒绝。否则m=parr.length-n+1将在每个单独的承诺被拒绝后递减,并且一旦m==0主(firstn)承诺也将被拒绝。

@Wyck建议使用确定性函数而不是Math.random()来进行单元测试。好的,下面是一个带有简单伪随机数生成器的版本:
x一个一个一个一个x一个一个二个x

意见:

我用"1-""2-"作为这两个运行的承诺名称的前缀。由于第二个运行在第一个firstN-promise被解决后就已经开始了,所以第一个运行的其余单个承诺将在第二个运行期间继续被解决/拒绝。

y0u0uwnf

y0u0uwnf4#

Promise.allSettled()接受一个异步调用数组。您不能先添加50%的调用,然后再添加其他50%。
它将返回一个数组,其中包含结果和状态[已拒绝或已解决]

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
const promises = [promise1, promise2];

Promise.allSettled(promises).
  then((results) => results.forEach((result) => console.log(result.status)));

// Expected output:
// "fulfilled"
// "rejected"

相关问题