Lucas

問題

TypeScript 生態系中的大多數韌性函式庫都綁定在 HTTP 用戶端(axios-retry、p-retry、Polly.js),或是要求你把函式包裝在類別中並使用 .execute() 的繁瑣方式。如果你只是想把重試邏輯、逾時與斷路器套用到 任何 async 函式——資料庫查詢、內部服務呼叫、檔案操作——而不引入沉重的依賴,該怎麼辦?

在此隆重介紹 houhou

什麼是 houhou?

Houhou 是一個零依賴的 TypeScript 函式庫(約 500 行程式碼),可透過可組合的韌性策略包裝任何 async 函式。包裝後的函式保留 完全相同的簽章——你可以像呼叫原始函式一樣呼叫它。

import { task } from 'houhou'

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

await charge(account, amount)

Enter fullscreen mode Exit fullscreen mode

策略一覽

重試

使用固定或指數退避重新執行失敗的動作:

task(fetchUser).retry(3) // 簡寫

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

Enter fullscreen mode Exit fullscreen mode

逾時

若函式未在指定時間內完成則拒絕:

task(fetchUser).timeout(5000)

Enter fullscreen mode Exit fullscreen mode

備援

在失敗時執行替代函式:

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

Enter fullscreen mode Exit fullscreen mode

斷路器

避免重複呼叫不健康的服務:

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

Enter fullscreen mode Exit fullscreen mode

延遲

在執行前等待:

task(syncData).delay(1000)

Enter fullscreen mode Exit fullscreen mode

策略順序很重要

策略是 巢狀包裝 的:最後呼叫的方法會包裝前面的方法。執行順序與宣告順序相反。

task(fn).retry(3).timeout(1000)
// → timeout 包裝 retry
// → 函式執行 → 失敗時重試(最多 3 次)→ 總逾時 1 秒
// → 若逾時觸發,就不會再有重試

Enter fullscreen mode Exit fullscreen mode

task(fn).timeout(1000).retry(3)
// → retry 包裝 timeout
// → 函式執行 → 1 秒逾時 → 若逾時發生,retry 會捕獲它
// → 整個循環最多重複 3 次

Enter fullscreen mode Exit fullscreen mode

型別安全 — 策略鎖定

每個方法都是 可鎖定 的。TypeScript 會在編譯時期防止重複設定同一策略,執行時則由 Set 防護機制強制執行:

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

// @ts-expect-error — 'retry' 已被鎖定
t.retry(2) // 執行時也會拋出錯誤

Enter fullscreen mode Exit fullscreen mode

使用 AbortSignal 取消

Houhou 使用 AbortController 在逾時觸發或提供外部訊號時取消操作。AbortSignal 會作為函式的最後一個參數傳入:

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

task(fn).timeout(5000)('https://api.example.com')
// → 逾時觸發 → controller.abort() → fetch 被取消

Enter fullscreen mode Exit fullscreen mode

你也可以傳入自己的外部訊號:

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

controller.abort() // 取消所有操作

Enter fullscreen mode Exit fullscreen mode

組合範例

策略可以以任意順序流暢地串接:

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

Enter fullscreen mode Exit fullscreen mode

名稱由來

「houhou」在日文中意指「方法」或「做法」——非常適合用來描述如何執行你的函式。

立即試用

npm install houhou
# 或
pnpm add houhou
yarn add houhou

Enter fullscreen mode Exit fullscreen mode

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