HN 日本語サマリー

← 一覧へ戻る
プログラミング

TypeScript向けのPromise対応debounceおよびthrottleライブラリを作成しました

I made a Promise-aware debounce and throttle library for TypeScript (github.com)

7 pointsby slimy744 コメント

要約

この記事では、TypeScript向けの新しいタイミングおよび並行処理ユーティリティライブラリ「temporize」を紹介しています。このライブラリは、debouncing、throttling、batching、retries、idle schedulingなどの機能を提供し、すべてのスケジューリングされた呼び出しが実際の関数の結果に対するPromiseを返すことが特徴です。ランタイム依存がなく、ESMとCommonJSの両方で利用可能です。

全文翻訳

temporize モダンTypeScriptのための、小さく、型安全なタイミングおよび並行処理ユーティリティ。 スケジュールされた各呼び出しは、ラップされた関数の実際の結果に対する実際のPromiseを返します。 ランタイム依存はなく、パッケージはESMとCommonJSとして出荷されます。 インストール npm install @alsoftworks/temporize なぜtemporizeか? 機能 temporize lodash.debounce / lodash.throttle 推論された引数と解決された戻り値 はい はい 完全なジェネリック推論 はい はい 型は個別に利用可能 はい はい 呼び出しごとの戻り値 Promise<Awaited<R>> 最後の同期結果またはundefined 非同期エラー伝播 はい いいえ ネイティブPromiseの拒否 はい いいえ 呼び出し側が戻り値を管理 はい いいえ キャンセル .cancel() および AbortSignal .cancel() 最大debounce待機時間 はい はい Promiseキューレートリミッター throttlePromise いいえ 非同期オーバーラップポリシー debounceAsync いいえ アニメーションフレームのサーマル ブラウザAPIプラスNodeフォールバック いいえ マルチコール引数バッチ batch いいえ 指数バックオフリトライ retry いいえ アイドル期間スケジューリング idle Safari/Nodeフォールバック付き いいえ 同時実行Promise制限 concurrencyLimit FIFOキュー付き いいえ ランタイム依存 ゼロ メソッドごとのパッケージではゼロ モジュール ESMおよびCommonJS CommonJS(メソッドごとのパッケージ) スコープ temporizeは、作業がいつ、どのくらいの頻度で実行されるかを制御することに意図的に焦点を当てています。Debouncing、throttling、batching、retry timing、frame scheduling、idle schedulingはすべてそのファミリーに属します。Object、array、string、その他の一般的なユーティリティヘルパーは意図的にスコープ外です。この境界は設計上の決定であり、省略ではありません。 使用法 debounce import { debounce } from "@alsoftworks/temporize"; const search = debounce( async (query: string) => { const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`); return response.json() as Promise<{ total: number }>; }, 250, { maxWait: 1_000 }, ); const result = await search("type inference"); console.log(result.total); search.pending(); await search.flush(); search.cancel(); 複数の呼び出しが1つの呼び出しに集約され、それぞれがその呼び出しの結果で解決されるPromiseを受け取ります。Leading-only debounceは、ウィンドウ内で抑制された呼び出しをleading呼び出しの結果で解決します。 throttle import { throttle } from "@alsoftworks/temporize"; const savePosition = throttle( (x: number, y: number) => ({ x, y, savedAt: Date.now() }), 100, { leading: true, trailing: true }, ); const saved = await savePosition(120, 80); 通常のthrottlingは、余分な呼び出しを集約し、trailing呼び出しに最新の引数を使用します。 rafThrottle import { rafThrottle } from "@alsoftworks/temporize"; const updateLayout = rafThrottle((width: number) => { document.documentElement.style.setProperty("--viewport-width", `${width}px`); }); window.addEventListener("resize", () => updateLayout(window.innerWidth)); // 要求されたがまだ実行されていないフレームを破棄します。 updateLayout.cancel(); ブラウザでは、rafThrottleはrequestAnimationFrameを使用します。SSR中およびNodeでは、16msのsetTimeoutにフォールバックするため、アニメーションフレームのグローバルが存在しない場合でも、インポートして呼び出すことができます。 debounceAsync import { debounceAsync } from "@alsoftworks/temporize"; const loadUser = debounceAsync( async (id: string, signal: AbortSignal) => { const response = await fetch(`/api/users/${id}`, { signal }); return response.json() as Promise<{ id: string; name: string }>; }, 200, { overlap: "cancel-previous" }, ); // AbortSignalパラメータは内部的に供給され、この呼び出しからは省略されます。 const user = await loadUser("user_123"); オーバーラップポリシーは次のとおりです。 "queue" (デフォルト): 発火した各呼び出しを保持し、アクティブな作業が完了した後に開始します。 "drop": オーバーラップする呼び出しを開始しません。その呼び出し元はアクティブな呼び出しのPromiseを採用します。 "cancel-previous": アクティブな呼び出しの内部的に供給されたシグナルを中止し、新しい呼び出しを即座に開始します。 シグナルを宣言しないJavaScript関数は、追加の引数を安全に無視します。型付きシグナル注入の場合、ラップされた関数の最後のパラメータとして必須のAbortSignalを宣言します。debounceAsyncは、返される関数の呼び出しシグネクトからそのパラメータを削除します。キャンセルは協調的です。非同期操作は、インフライト作業を停止するためにシグナルを観測する必要があります。 throttlePromise import { throttlePromise } from "@alsoftworks/temporize"; const sendRequest = throttlePromise( async (path: string) => fetch(path).then((response) => response.status), 100, ); const requests = [ sendRequest("/api/one"), sendRequest("/api/two"), sendRequest("/api/three"), ]; console.log(sendRequest.queued()); console.log(await Promise.all(requests)); 各呼び出しはFIFOキューに入り、前の開始から少なくとも1つのウィンドウ後に開始されます。キューはディスパッチレートを制限しますが、同時実行性は制限しません。遅いPromiseは、次のウィンドウが開始されるときにアクティブなままである可能性があります。 batch import { batch } from "@alsoftworks/temporize"; const markNotificationsRead = batch( async (calls: Array<[notificationId: string]>) => { const ids = calls.map(([notificationId]) => notificationId); const response = await fetch("/api/notifications/read", { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({ ids }), }); if (!response.ok) throw new Error("Could not mark notifications as read"); return ids.length; }, 50, { maxSize: 100 }, ); const first = markNotificationsRead("notification_1"); const second = markNotificationsRead("notification_2"); // 両方のPromiseは2に解決されます。なぜなら、両方の呼び出しが1つのバッチ呼び出しを共有したからです。 console.log(await first, await second); debounceとは異なり、batchは各引数タプルを保持します。ラップされた関数は完全な配列で一度実行され、そのバッチ内のすべての呼び出し元は同じ結果またはエラーを受け取ります。 retry import { retry } from "@alsoftworks/temporize"; const controller = new AbortController(); const fetchJson = retry( async (url: string) => { const response = await fetch(url, { signal: controller.signal }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); return response.json() as Promise<unknown>; }, { attempts: 4, baseDelay: 250, maxDelay: 2_000, signal: controller.signal, }, ); const data = await fetchJson("/api/flaky-report"); リトライはデフォルトで指数バックオフとランダムなジッターを使用します。永続的なエラーがすぐに停止すべき場合は、shouldRetryを指定してください。 idle import { idle } from "@alsoftworks/temporize"; const recordAnalytics = idle( (eventName: string, properties: object) => { navigator.sendBeacon("/analytics", JSON.stringify({ eventName, properties })); }, { timeout: 2_000 }, ); recordAnalytics("dashboard_viewed", { source: "navigation" }); ラピッドな呼び出しは最新の引数を使用して集約され、ブラウザがアイドル状態になるまで延期されます。Safari、Node、およびrequestIdleCallbackを持たないその他の環境では、1msタイマーのフォールバックを使用します。 concurrencyLimit import { concurrencyLimit } from "@alsoftworks/temporize"; const upload = concurrencyLimit(async (file: File) => { const body = new FormData(); body.append("file", file); const response = await fetch("/api/uploads", { method: "POST", body, }); if (!response.ok) throw new Error(`Upload failed: ${file.name}`); return response.json(); }, 3); const uploaded = await Promise.all(files.map(upload)); console.log(upload.pending(), upload.queued()); 最大3つのアップロードが同時に開始されます。余分な呼び出しはFIFO順で待機します。 upload.cancel() はキューに入れられた呼び出しを拒否しますが、既にインフライト中のアップロードは完了させます。なぜなら、ラップされた関数はキャンセル可能ではない可能性があるからです。後続の呼び出しは通常通りキューに入れることができます。設定されたシグナルを中止すると、将来の呼び出しも拒否されます。 フレームワークアダプター ReactおよびVueアダプターはオプションのサブパスエクスポートです。 @alsoftworks/temporizeをインストールしても、どちらのフレームワークもインストールされず、コアパッケージをインポートしてもアダプターコードはロードされません。 アプリケーションが既に利用しているピアのみを追加してください。 React npm install @alsoftworks/temporize react @alsoftworks/temporize/reactからフックをインポートします: import { useEffect, useState } from "react"; import { useDebouncedValue } from "@alsoftworks/temporize/react"; export function Search(): JSX.Element { const [query, setQuery] = useState(""); const debouncedQuery = useDebounce