web-dev-qa-db-ja.com

約束のリストを前提として、「raceToSuccess」ヘルパーをどのように実装しますか?

ES6 PromiseAPIの何かに戸惑っています。複数の非同期ジョブを同時に送信し、最初の成功で「解決」する明確なユースケースを見ることができます。これは、たとえば、複数の同等のサーバーが利用可能であるが、一部はダウンしている可能性があり、その他は負荷が高く低速である状況に対応するため、私の目標は、最初のサーバーから応答を取得して成功し、残りを無視することです(はい、これはクライアントがサーバーの観点から振る舞うための不快な方法であることを私は知っていますが、エンドユーザーにとっては素晴らしいことです;)

しかし、私が見る限り、私には「すべて」または「人種」のどちらかの行動があります。 「すべて」の動作は、すべてのリクエストが完了するまで待機しているようです。つまり、サーバーがすでに完了している場合でも、最も遅いものを待つ必要があります(実際、タイムアウトを待たなければならない可能性があり、災害が発生する可能性があります)ただし、「レース」の動作は、最初に完了するように思われます。これが失敗した場合は、災害でもあります。

「raceToSuccess」のような動作を許可するAPIに何かありますか、それとも手動で作成する必要がありますか。さらに言えば、どうやって手作業で作るのでしょうか?

ちなみに、同じパズルをJava 8 CompletableFutureで見つけました。これは、密接に並列したAPIのようです。それで、哲学的レベルで何かが欠けているのでしょうか。

22
Toby Eggitt

これは、ロジックを逆にするとはるかに明確になる典型的な例です。この場合のあなたの「競争」は、あなたがあなたの拒絶行動を実際に成功行動にしたいということです。

function oneSuccess(promises){
  return Promise.all(promises.map(p => {
    // If a request fails, count that as a resolution so it will keep
    // waiting for other possible successes. If a request succeeds,
    // treat it as a rejection so Promise.all immediately bails out.
    return p.then(
      val => Promise.reject(val),
      err => Promise.resolve(err)
    );
  })).then(
    // If '.all' resolved, we've just got an array of errors.
    errors => Promise.reject(errors),
    // If '.all' rejected, we've got the result we wanted.
    val => Promise.resolve(val)
  );
}
50
loganfsmyth

これは自分で簡単に書くことができます。

function raceToSuccess(promises) {
  return new Promise(
    resolve => 
      promises.forEach(
        promise => 
          promise.then(resolve)
      )
  );
}

これにより、すべての約束が開始され、いずれかが成功すると、新しい約束がその価値で解決されます。失敗した約束は無視されます。新しい約束はすでに解決されているので、その後の成功した約束は何も起こりません。入力promiseのいずれも解決されない場合、結果のPromiseは解決または拒否されないことに注意してください。

これは、すべての入力Promiseが拒否された場合に拒否されたPromiseを返す変更バージョンです。

function raceToSuccess(promises) {
  let numRejected = 0;

  return new Promise(
    (resolve, reject) => 
      promises.forEach(
        promise => 
          promise . 
            then(resolve) .
            catch(
              () => {
                if (++numRejected === promises.length) reject(); 
              }
           )
       )
  );
}

@loganfsmythのアプローチが好きです。概念を明確にするために、おそらく賛成する必要があります。これがそのバリ​​エーションです:

function invertPromise(promise) {
  return new Promise(
    (resolve, reject) => 
      promise.then(reject, resolve)
  );
}

function raceToSuccess(promises) {
  return invertPromise(
    Promise.all(
      promises.map(invertPromise)));
}

もう1つのアイデアは、失敗したプロミスを解決も拒否もしない(つまり、永続的に保留中の)プロミスに変換してから、Promise.raceを使用することです。

function pendingPromise()      { return new Promise(() => { }); }
function killRejected(promise) { return promise.catch(pendingPromise); }

function raceToSuccess(promises) {
  return Promise.race(promises.map(killRejected));
}

あなたはこれの振る舞いが好きかもしれないし嫌いかもしれません。返された約束は、入力された約束のいずれも満たされない場合、決して満たされないか拒否されません。また、永続的に保留中のプロミスがGCされない可能性もあります。または、一部のエンジンが最終的にそれらについて不平を言う可能性があります。

7
user663031

私はPromise.race()に基づく関数を使用していますが、ひねりを加えています。指定されたすべてのpromiseが拒否しない限り、拒否を無視します。

// ignores any rejects except if all promises rejects
Promise.firstResolve = function (promises) {
    return new Promise(function (fulfil, reject) {
        var rejectCount = 0;
        promises.forEach(function (promise) {
            promise.then(fulfil, () => {
                rejectCount++;
                if(rejectCount == promises.length) {
                    reject('All promises were rejected');
                } 
            });
        });
    });
};

これは、RichHarrisのPromiseポリフィルレース方式に基づいています。ループプロミスを条件付きで拒否しました。指定されたすべてのプロミスが失敗した場合にのみメインプロミスを拒否します。それ以外の場合は、拒否を無視して最初の成功を解決します。

使用法:

// fastest promise to end, but is a reject (gets ignored)
var promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("foo")
    }, 100);
})

// fastest promise to resolve (wins the race)
var promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("bar")
    }, 200);
})

// Another, slower resolve (gets ignored)
var promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("baz")
    }, 300);
})

Promise.firstResolve([promise1, promise2, promise3])
    .then((res) => {
        console.log(res) // "bar"
    })
    .catch(err => {
        console.log(err) // "All promises were rejected" (if all promises were to fail)
    })

私がpromise反転アプローチの代わりにこれを使用する理由は、私の意見ではこれがより読みやすいからです。

質問を最も厳密に喜ばせるために、最初に成功した約束を解決するバージョンがありますが、与えられたすべての約束が失敗した場合は何もしません。

// ignores any and all rejects
Promise.firstResolve = function (promises) {
    return new Promise(function (fulfil) {
        promises.forEach(function (promise) {
            promise.then(fulfil, () => {});
        });
    });
};

(上記と同じ使用法)

編集:これは実際には@ user663031の提案と同じです。今まで気づかなかった。

3
Adam Karacsony

「raceToSuccess」のような動作を許可するAPIに何かがありますか

すぐに、ほぼ確実にあります。ステージ3 proposal for _Promise.any_ :があります。

Promise.any()は、反復可能なPromiseオブジェクトを受け取り、反復可能なpromiseの1つが満たされるとすぐに、そのpromiseの値で解決される単一のpromiseを返します。

したがって、次の構文が有効になります。

_// assume getApi returns a Promise

const promises = [
  getApi('url1'),
  getApi('url2'),
  getApi('url3'),
  getApi('url4'),
];
Promise.any(promises)
  .then((result) => {
    // result will contain the resolve value of the first Promise to resolve
  })
  .catch((err) => {
    // Every Promise rejected
  });
_

_Promise.any_は Spidermonkeyで実装 されており、 somepolyfills が利用可能です。

1

古いトピックですが、これが私のエントリです。これは本質的に@loganfsmythのソリューションですが、 Promise.all() によって確立された規則に準拠するためのチェックがさらにいくつかあります。

  • 入力としての空の配列は、すでに解決されたpromiseを(同期的に)返します
  • 配列内のpromise以外のエントリは、解決された値として使用される最初のそのようなエントリになります
Promise.any = a => {
  return !a.length ?
    Promise.resolve() :
    Promise.all(a.map(
      e => (typeof e.then !== 'function') ?
        Promise.reject(e) :
        e.then(
          result => Promise.reject(result),
          failure => Promise.resolve(failure)
        )
    )).then(
      allRejected => Promise.reject(allRejected),
      firstResolved => Promise.resolve(firstResolved)
    );
};

// Testing...

function delayed(timeout, result, rejected) {
  return new Promise((resolve, reject) => {
    setTimeout(
      () => rejected ? reject(result) : resolve(result),
      timeout);
  });
}

Promise.any([
  delayed(800, 'a'),
  delayed(500, 'b'),
  delayed(250, 'c', true)
]).then(e => {
  console.log('First resolved (expecting b):', e);
});

Promise.any([
  delayed(800, 'a', true),
  delayed(500, 'b', true),
  delayed(250, 'c', true)
]).then(null, e => {
  console.log('All rejected (expecting array of failures):', e);
});

Promise.any([
  delayed(800, 'a'),
  delayed(500, 'b'),
  delayed(250, 'c', true),
  'd',
  'e'
]).then(e => {
  console.log('First non-promise (expecting d):', e);
});

// Because this is the only case to resolve synchronously,
// its output should appear before the others
Promise.any([]).then(e => {
  console.log('Empty input (expecting undefined):', e);
});
1
tavnab

@loganfsmythアプローチをタイムアウトで拡張し、次のような小さな関数を作成しました。

  • すべての約束を実行し、
  • 約束が成功するのを指定された時間(options.timeOutMs)以内で待ちます。
  • 成功した最初のものを返します。

次のスニペットでは、それをテストできます。

const firstThatCompleteSuccessfullyES6 = (options) => {

    // return the first promise that resolve
    const oneSuccess = (promises) => Promise.all(promises.map(p => {
                    // If a request fails, count that as a resolution so it will keep
                    // waiting for other possible successes. If a request succeeds,
                    // treat it as a rejection so Promise.all immediately bails out.
                    return p.then(
                        (val) => { return Promise.reject(val); },
                        (err) => { return Promise.resolve(err); }
                    );
            })
            ).then(
                // If '.all' resolved, we've just got an array of errors.
                (errors) => { return Promise.reject(errors); },

                // If '.all' rejected, we've got the result we wanted.
                (val) => { return Promise.resolve(val); }
            );
    

    // return the promise or reect it if timeout occur first
    const timeoutPromise = (ms, promise) => new Promise(function(resolve, reject) {
            setTimeout(() => reject(new Error('timeout')), ms);
            promise.then(resolve, reject);
        });
    

    if (options.subsystems.length < 1) {
        return Promise.reject('Parameters error, no subSystems specified');
    }

    const timedOutSubsystems = options.subsystems.map(function(subsystem){
        return timeoutPromise(options.timeOutMs, subsystem(options));
    });

    const startDate = Date.now();

    return oneSuccess(
        timedOutSubsystems
    )
    .then((result) => {
        const elapsedTime = Math.abs((startDate - Date.now()) / 1000);
        console.log('firstThatCompleteSuccessfully() done, after s: ' + elapsedTime + ': '+ result);
        return result;
    })
    .catch((error) => {
        const elapsedTime = Math.abs((startDate - Date.now()) / 1000);
        console.error('firstThatCompleteSuccessfully() error/nodata: ' + error);
    });

}



// example of use with two promises (subsystem1 & subsystem2) that resolves after a fixed amount of time

const subsystem1 = (options) => new Promise(function(resolve, reject) {
        setTimeout(function(){
            console.log('subsystem1 finished');
            resolve('subsystem 1 OK');
        }, 1000);
    });



const subsystem2 = (options) => new Promise(function(resolve, reject) {
        setTimeout(function(){
            console.log('subsystem2 finished');
            resolve('subsystem 2 OK');
        }, 2000);
    });


firstThatCompleteSuccessfullyES6({
    subsystems: [subsystem1, subsystem2],
    timeOutMs: 2000
})
.then((result) => console.log("Finished: "+result));
0
mircoc

この問題を解決するために、私はPromise.ricePromise.allSettledを使用しました。

次のコードは、Promise.rice成功値で待機します。しかし、干し草がなければ成功の結果です。すべてのエラーを含む配列を返します。

const PromiseRiceSuccess = <T = unknown>(promises: Promise<T>[]) => {
  let done: (reason?: T) => void;
  const waitEndAllPromises = new Promise((resolve, reject) => done = reject);
  const waitCatchs = promise => Promise.resolve(promise).catch(() => waitEndAllPromises);

  Promise.allSettled(promises).then(r => done(r));

  return Promise.race(promises.map(waitCatchs));
};

例:

PromiseRiceSuccess([
  Promise.reject(1),
  new Promise((r) => setTimeout(() => r(2), 4000)),
]);
// 2

PromiseRiceSuccess([
  Promise.reject(1),
  new Promise((resolve, reject) => setTimeout(() => reject(2), 4000)),
]);
// Uncaught (in promise) (2) [{…}, {…}]
0
JonDotsoy