速度を向上させるためのトリックは、低速API


TLDR; I created a small npm package that acts as a wrapper around node-fetch, and returns the same promise for the same request, until it resolves. You can visit the repo of this package here. Below, I explain my motivation, and how I tackled the issue.


だからここにシナリオがあります.
あなたは本当に遅いサードパーティ製APIとのインターフェイスを持つシステムを持っています.ユーザBobはいくつかのデータを必要とするので、システムはサードパーティAPIへのリクエストを行い、応答を待ちます.一方、ユーザーアリスは同じ日付を必要とします、そして、システムは彼女に代わってAPIに同じ要求を実行します.両方のユーザーは現在、2つのリクエストを待っている唯一の違いは、実行時間です.
このAPIへの要求が1秒の平均応答時間を持つならば、両方のユーザーは1秒を待ちます.また、1秒以上のためにあなたのシステムとサードパーティのAPIのリソースを占有する必要がありますし、2秒で最大!

解決策
あなたが両方のユーザー、ボブとアリスを持つことができたならば、同じ要求を待ちますか?その後、ボブはまだ1秒の要求を待つが、アリスはボブの要求を使用し、応答のためのより少ない時間を待ちます.
そのためには、約束キャッシュサブシステムが必要です.このサブシステムは、我々の要求の約束を保存するデータ構造とそれらを必要としないときにそれらを取り出す/削除する方法で構成されます.

データ構造
我々の約束を内部に格納するデータ構造が必要だ.このデータ構造は、1つの操作(O(1))で新しい約束を格納して、取り出すことができる必要があります.だから、最良の選択はキー/バリューストアになります.JavaScriptは、2つのような構造を提供していますbasic objectMap() インスタンス.The most preferrable data structure for our use-case among the two is the Map() .
それで、それをつくりましょう.
const promiseCache: Map<string, Promise<Response>> = new Map();

検索/記憶
さて、リクエスト関数をラップし、同じリクエストに対して同じ約束を取り戻す関数を作成しましょう.そうでなければ、新しいリクエストを実行し、キャッシュに格納します.
function memoizedRequest(url: string) {
    const key = url;
    if (promiseCache.has(key)) {
        return promiseCache.get(key);
    }

    const promise = request(url);
    promiseCache.set(key, promise);

    return promise;
}
これにより,我々の約束キャッシュサブシステムの基本機能を達成した.システムを使用してリクエストを実行するとmemoizedRequest 関数が既に要求されている場合、同じプロミスを返します.
しかし、我々はまだ、約束が解決するとき(要求が結果を返すとき)、キャッシュから見込みの削除のためにメカニズムをまだ実装しませんでした

削除-キャッシュ無効化
このために、我々は約束を約束して、約束をキャッシュから削除するのを待ちます.
async function promiseInvalidator(key: string, promise: Promise<any>) {
    await promise;
    promiseCache.delete(key);

    return promise;
}
そして、この無効関数を含めるためにmemoizedRequest関数を変更します.
function memoizedRequest(url: string) {
    const key = url;
    if (promiseCache.has(key)) {
        return promiseCache.get(key);
    }

    const promise = promiseInvalidator(key, request(url));
    promiseCache.set(key, promise);

    return promise;
}

しかし、より複雑な要求で何が起こりますか?
すべてのリクエストは、彼らが実行されているURLだけで区別することはできません.他の多くのパラメータが要求されます.
そのためには、我々の約束キャッシュのキーを絞り込み、オプションオブジェクトを関数に追加する必要があります.
function memoizedRequest(url: string, options: RequestOptions) {
    const key = url + JSON.stringify(options);
    if (promiseCache.has(key)) {
        return promiseCache.get(key);
    }

    const promise = promiseInvalidator(key, request(url));
    promiseCache.set(key, promise);

    return promise;
}
今、同じオプションを使用するリクエストだけが解決するまで同じ約束を返します.
これにより、パッケージの基本機能をすべて実装しました.しかし、我々は要求失敗の可能性を考慮に入れませんでした.これをコード化しましょうpromiseInvalidator 関数は、常に解決するとき、または拒否するときにキャッシュからの約束を削除します.
async function promiseInvalidator(key: string, promise: Promise<any>) {
    try {
        await promise;
    } finally {
        promiseCache.delete(key);
    }

    return promise;
}

より多くの改善
この実装は、生産システム上で重大であると証明できる小さな欠点を有する.すべてのリクエストのデータは、我々のデータストアのキーの中に格納され、特に我々の要求が多くのデータを含むとき、我々のアプリケーションのメモリ要件を非常に増やします.これを解決するにはhash function 私たちのキーでは、リクエストの実際のすべてを含める必要がなく、それぞれの異なるリクエストに一意の値を割り当てます.
const key = hasher(url + JSON.stringify(options));

警告
この解決策はどんな状況にも適用できません.この解決策を使用するには、あなたがインタフェースをとっているAPIを確実にする必要があります、解決するそれらの要求にかかる時間の量の2つの異なる要求のために異なる応答を提供していません.

パッケージ
これを自分でコード化したくないなら、私は単純なものを作りましたnpm package これは上記の全てをラッパーとして行うnode-fetch (あるいは他のフェッチ機能を選択します).
import memoizedNodeFetch from 'memoized-node-fetch';

const fetch = memoizedNodeFetch();

(async () => {
    const fetch1 = fetch('https://jsonplaceholder.typicode.com/todos/1');
    const fetch2 = fetch('https://jsonplaceholder.typicode.com/todos/1');

    // This should return true because both requests return the same promise.
    console.log(fetch1 === fetch2);

    const res1 = await fetch1;
    const res2 = await fetch2;

    console.log(await res1.json());
    console.log(await res2.json());
})();
上記の作業のすべてを見ることができます.
https://github.com/chrispanag/memoized-node-fetch
PS : 1フロントエンドで使用することができますが、私は非常に有用なユースケースを見つけることができません.特に、クエリ/SWRのような他のパッケージを持っているときには、上記と異なる機能を実行しますが、時々必要性を取り除くことができます.
PS 2 :このリポジトリの他の2つの貢献者への特別な感謝ferrybig and Bonjur 彼らの貴重な入力と提案のために!