Lucas

問題

TypeScriptエコシステムの耐障害性ライブラリの多くはHTTPクライアント(axios-retry、p-retry、Polly.js)に依存するか、関数を.execute()という儀式を伴うクラスでラップする必要があります。データベースクエリ、内部サービス呼び出し、ファイル操作など任意の非同期関数に、再試行ロジック、タイムアウト、サーキットブレーカーを、重い依存関係を導入せずにラップしたい場合はどうでしょうか?

そこで登場するのがhouhouです。

houhouとは?

Houhouは、任意の非同期関数を合成可能な耐障害性ポリシーでラップする、依存関係ゼロのTypeScriptライブラリ(約500 LOC)です。ラップされた関数は完全に同じシグネチャを保持するため、元の関数と同じように呼び出せます。

import { task } from 'houhou'

const charge = task(chargeCard)
  .retry(3)
  .timeout(10_000)
  .fallback(() => ({ status: 'pending' }))

await charge(account, amount)

全画面表示にする 全画面表示を終了

ポリシーの概要

Retry

固定または指数バックオフで失敗時に再実行します:

task(fetchUser).retry(3) // 短縮形

task(fetchUser).retry({
  attempts: 5,
  backoff: 'exponential',
  jitter: true,
  delay: 500
})

全画面表示にする 全画面表示を終了

Timeout

指定時間内に完了しなかった場合に拒否します:

task(fetchUser).timeout(5000)

全画面表示にする 全画面表示を終了

Fallback

失敗時に代替関数を実行します:

task(fetchUser).fallback(() => loadFromCache(id))

全画面表示にする 全画面表示を終了

Circuit Breaker

不健全なサービスへの繰り返しの呼び出しを防ぎます:

task(queryDb).circuitBreaker({
  failureThreshold: 5,
  successThreshold: 2,
  resetTimeout: 30_000
})

全画面表示にする 全画面表示を終了

Delay

実行前に待機します:

task(syncData).delay(1000)

全画面表示にする 全画面表示を終了

ポリシーの順序が重要

ポリシーはネストされます。最後に呼び出されたメソッドが前のメソッドをラップします。実行順序は宣言順序の逆になります。

task(fn).retry(3).timeout(1000)
// → timeout が retry をラップ
// → 関数が実行 → 失敗時に再試行(最大3回) → 合計1秒のタイムアウト
// → タイムアウトが発生した場合、それ以上の再試行は行われない

全画面表示にする 全画面表示を終了

task(fn).timeout(1000).retry(3)
// → retry が timeout をラップ
// → 関数が実行 → 1秒のタイムアウト → タイムアウトが発生した場合、retry がキャッチ
// → サイクル全体が最大3回繰り返される

全画面表示にする 全画面表示を終了

型安全性 — ポリシーロック

各メソッドはロック可能です。TypeScriptはコンパイル時に同じポリシーを2回設定することを防ぎ、ランタイムのSetガードが実行時にもそれを強制します:

const t = task(fn).retry(3)

// @ts-expect-error — 'retry' はすでにロックされています
t.retry(2) // 実行時にも例外が発生

全画面表示にする 全画面表示を終了

AbortSignalによるキャンセル

HouhouはAbortControllerを使用して、タイムアウトが発生したときや外部シグナルが提供されたときに操作をキャンセルします。AbortSignalは関数の最後の引数として渡されます:

const fn = (url: string, signal?: AbortSignal) => fetch(url, { signal })

task(fn).timeout(5000)('https://api.example.com')
// → タイムアウトが発生 → controller.abort() → fetchがキャンセルされる

全画面表示にする 全画面表示を終了

独自の外部シグナルを渡すこともできます:

const controller = new AbortController()
const promise = task(fn).timeout(5000).retry(3)('url', controller.signal)

controller.abort() // すべてをキャンセル

全画面表示にする 全画面表示を終了

合成の例

ポリシーは任意の順序で流れるように連結できます:

const resilient = task(callApi)
  .retry({ attempts: 3, backoff: 'exponential' })
  .timeout(5000)
  .fallback(loadFromCache)
  .circuitBreaker({ failureThreshold: 5, successThreshold: 2, resetTimeout: 30_000 })
  .delay(100)

全画面表示にする 全画面表示を終了

名前について

「houhou」は日本語で「方法」や「やり方」を意味します。これは、関数の実行方法に関するライブラリにふさわしい名前です。

試してみる

npm install houhou
# または
pnpm add houhou
yarn add houhou

全画面表示にする 全画面表示を終了

GitHub: https://github.com/smokeeaasd/houhou
npm: https://npmjs.com/package/houhou