breaker-box 4.2.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +189 -46
- package/dist/index.cjs +120 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +158 -50
- package/dist/index.d.mts +158 -50
- package/dist/index.mjs +120 -60
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -12
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../lib/util.ts","../lib/options.ts","../lib/helpers.ts","../lib/index.ts"],"sourcesContent":["export type AnyFn = (...args: any[]) => any\n\n/**\n * Asserts that the given value is truthy. If not, throws a `TypeError`.\n */\nexport function assert(value: unknown, message?: string): asserts value {\n\tif (!value) throw new TypeError(message)\n}\n\n/**\n * `[TypeScript]` For exhaustive checks in switch statements or if/else. Add\n * this check to `default` case or final `else` to ensure all possible values\n * have been handled. If a new value is added to the type, TypeScript will\n * throw an error and the editor will underline the `value`.\n */\n/* v8 ignore next 3 */\nexport const assertNever = (val: never, msg = \"Unexpected value\") => {\n\tthrow new TypeError(`${msg}: ${val}`)\n}\n\n/**\n * Returns a promise that resolves after the specified number of milliseconds.\n */\nexport const delayMs = (ms: number): Promise<void> =>\n\tnew Promise((next) => setTimeout(next, ms))\n\n/**\n * Rejects the given promise when the abort signal is triggered.\n */\nexport const rejectOnAbort = <T extends Promise<unknown> | undefined>(\n\tsignal: AbortSignal,\n\tpending: T,\n): Promise<Awaited<T>> => {\n\tlet teardown: () => void\n\treturn Promise.race([\n\t\tPromise.resolve(pending).finally(() =>\n\t\t\tsignal.removeEventListener(\"abort\", teardown),\n\t\t),\n\t\tnew Promise<never>((_, reject) => {\n\t\t\tteardown = () => reject(signal.reason)\n\t\t\tsignal.addEventListener(\"abort\", teardown)\n\t\t}),\n\t])\n}\n","import type { CircuitBreakerOptions } from \"./types.js\"\nimport { assert, type AnyFn } from \"./util.js\"\n\nexport function parseOptions<Fallback extends AnyFn>(\n\toptions: CircuitBreakerOptions<Fallback>,\n) {\n\tconst {\n\t\terrorIsFailure = () => false,\n\t\terrorThreshold = 0,\n\t\terrorWindow = 10_000,\n\t\tminimumCandidates = 6,\n\t\tonClose,\n\t\tonHalfOpen,\n\t\tonOpen,\n\t\tresetAfter = 30_000,\n\t\tretryDelay = () => undefined,\n\t} = options\n\n\t// errorIsFailure\n\tassert(\n\t\ttypeof errorIsFailure === \"function\",\n\t\t`\"errorIsFailure\" must be a function (received ${typeof errorIsFailure})`,\n\t)\n\n\t// errorThreshold\n\tassert(\n\t\terrorThreshold >= 0 && errorThreshold <= 1,\n\t\t`\"errorThreshold\" must be between 0 and 1 (received ${errorThreshold})`,\n\t)\n\n\t// errorWindow\n\tassert(\n\t\terrorWindow >= 1_000,\n\t\t`\"errorWindow\" must be milliseconds of at least 1 second (received ${errorWindow})`,\n\t)\n\n\t// minimumCandidates\n\tassert(\n\t\tminimumCandidates >= 1,\n\t\t`\"minimumCandidates\" must be greater than 0 (received ${minimumCandidates})`,\n\t)\n\n\t// (optional) onClose\n\tassert(\n\t\t!onClose || typeof onClose === \"function\",\n\t\t`\"onClose\" must be a function (received ${typeof onClose})`,\n\t)\n\n\t// (optional) onHalfOpen\n\tassert(\n\t\t!onHalfOpen || typeof onHalfOpen === \"function\",\n\t\t`\"onHalfOpen\" must be a function (received ${typeof onHalfOpen})`,\n\t)\n\n\t// (optional) onOpen\n\tassert(\n\t\t!onOpen || typeof onOpen === \"function\",\n\t\t`\"onOpen\" must be a function (received ${typeof onOpen})`,\n\t)\n\n\t// resetAfter\n\tassert(\n\t\tresetAfter >= 1_000,\n\t\t`\"resetAfter\" must be milliseconds of at least 1 second (received ${resetAfter})`,\n\t)\n\tassert(\n\t\tresetAfter >= errorWindow,\n\t\t`\"resetAfter\" must be greater than or equal to \"errorWindow\" (received ${resetAfter}, expected >= ${errorWindow})`,\n\t)\n\n\t// retryDelay\n\tassert(\n\t\ttypeof retryDelay === \"function\",\n\t\t`\"retryDelay\" must be a function (received ${typeof retryDelay})`,\n\t)\n\n\treturn {\n\t\terrorIsFailure,\n\t\terrorThreshold,\n\t\terrorWindow,\n\t\tminimumCandidates,\n\t\tonClose,\n\t\tonHalfOpen,\n\t\tonOpen,\n\t\tresetAfter,\n\t\tretryDelay,\n\t}\n}\n","import type { MainFn } from \"./types.js\"\nimport { delayMs } from \"./util.js\"\n\n/**\n * Returns a function which implements exponential backoff.\n *\n * @param maxSeconds - The maximum number of seconds to wait before retrying.\n * @returns A function which takes an `attempt` number and returns a promise\n * that resolves after the calculated delay.\n */\nexport function useExponentialBackoff(maxSeconds: number) {\n\treturn function exponentialBackoff(attempt: number) {\n\t\tconst num = Math.max(attempt - 2, 0)\n\t\tconst delay = Math.min(2 ** num, maxSeconds)\n\t\treturn delayMs(delay * 1_000)\n\t}\n}\n\nconst sqrt5 = /* @__PURE__ */ Math.sqrt(5)\n/**\n * @see https://en.wikipedia.org/wiki/Fibonacci_sequence#Closed-form_expression\n */\nconst binet = (n: number) =>\n\tMath.round(((1 + sqrt5) ** n - (1 - sqrt5) ** n) / (2 ** n * sqrt5))\n\n/**\n * Returns a function which implements Fibonacci backoff.\n *\n * @param maxSeconds - The maximum number of seconds to wait before retrying.\n * @returns A function which takes an `attempt` number and returns a promise\n * that resolves after the calculated delay.\n */\nexport function useFibonacciBackoff(maxSeconds: number) {\n\treturn function fibonacciBackoff(attempt: number) {\n\t\tconst delay = Math.min(binet(attempt), maxSeconds)\n\t\treturn delayMs(delay * 1_000)\n\t}\n}\n\n/**\n * Wrap a function with a timeout. If execution of `main` exceeds `timeoutMs`,\n * then the call is rejected with `new Error(timeoutMessage)`.\n */\nexport function withTimeout<Ret, Args extends unknown[]>(\n\tmain: MainFn<Ret, Args>,\n\ttimeoutMs: number,\n\ttimeoutMessage = \"ERR_CIRCUIT_BREAKER_TIMEOUT\",\n): MainFn<Ret, Args> {\n\tconst error = new Error(timeoutMessage)\n\n\treturn function withTimeoutFunction(...args) {\n\t\tlet timer: NodeJS.Timeout\n\t\treturn Promise.race([\n\t\t\tmain(...args).finally(() => clearTimeout(timer)),\n\t\t\tnew Promise<never>((_, reject) => {\n\t\t\t\ttimer = setTimeout(reject, timeoutMs, error)\n\t\t\t}),\n\t\t])\n\t}\n}\n","import { parseOptions } from \"./options.js\"\nimport type {\n\tHistoryEntry,\n\tHistoryMap,\n\tCircuitBreakerOptions,\n\tCircuitBreakerProtectedFn,\n\tCircuitState,\n\tMainFn,\n} from \"./types.js\"\nimport { assertNever, rejectOnAbort, type AnyFn } from \"./util.js\"\n\nexport * from \"./helpers.js\"\nexport { delayMs } from \"./util.js\"\n\nexport function createCircuitBreaker<\n\tRet,\n\tArgs extends unknown[],\n\tFallback extends AnyFn = MainFn<Ret, Args>,\n>(\n\tmain: MainFn<Ret, Args>,\n\toptions: CircuitBreakerOptions<Fallback> = {},\n): CircuitBreakerProtectedFn<Ret, Args> {\n\tconst {\n\t\terrorIsFailure,\n\t\terrorThreshold,\n\t\terrorWindow,\n\t\tminimumCandidates,\n\t\tonClose,\n\t\tonHalfOpen,\n\t\tonOpen,\n\t\tresetAfter,\n\t\tretryDelay,\n\t} = parseOptions(options)\n\tconst controller = new AbortController()\n\tconst history: HistoryMap = new Map()\n\tconst signal = controller.signal\n\tlet failureCause: unknown\n\tlet fallback = options.fallback || (() => Promise.reject(failureCause))\n\tlet halfOpenPending: Promise<unknown> | undefined\n\tlet resetTimer: NodeJS.Timeout\n\tlet state: CircuitState = \"closed\"\n\n\tfunction clearFailure() {\n\t\tfailureCause = undefined\n\t}\n\n\tfunction closeCircuit() {\n\t\tstate = \"closed\"\n\t\tclearFailure()\n\t\tclearTimeout(resetTimer)\n\t\tonClose?.()\n\t}\n\n\tfunction failureRate() {\n\t\tlet failures = 0\n\t\tlet total = 0\n\t\tfor (const { status } of history.values()) {\n\t\t\tif (status === \"rejected\") failures++\n\t\t\tif (status !== \"pending\") total++\n\t\t}\n\t\t// Don't calculate anything until we have enough data\n\t\tif (!total || total < minimumCandidates) return 0\n\t\treturn failures / total\n\t}\n\n\t/**\n\t * Break the circuit and wait for a reset\n\t */\n\tfunction openCircuit(cause: unknown) {\n\t\tfailureCause = cause\n\t\tstate = \"open\"\n\t\tclearTimeout(resetTimer)\n\t\tresetTimer = setTimeout(() => {\n\t\t\tstate = \"halfOpen\"\n\t\t\tonHalfOpen?.()\n\t\t}, resetAfter)\n\t\tonOpen?.(cause)\n\t}\n\n\tfunction createHistoryItem<T>(pending: Promise<T>) {\n\t\tconst entry: HistoryEntry = { status: \"pending\", timer: undefined }\n\t\tconst teardown = () => {\n\t\t\tclearTimeout(entry.timer)\n\t\t\thistory.delete(pending)\n\t\t\tsignal.removeEventListener(\"abort\", teardown)\n\t\t}\n\t\tsignal.addEventListener(\"abort\", teardown)\n\t\tconst settle = (value: \"resolved\" | \"rejected\") => {\n\t\t\tif (signal.aborted) return\n\t\t\tentry.status = value\n\t\t\t// Remove the entry from history when it falls outside of the error window\n\t\t\tentry.timer = setTimeout(teardown, errorWindow)\n\t\t}\n\t\thistory.set(pending, entry)\n\t\treturn { pending, settle, teardown }\n\t}\n\n\t/**\n\t * Wrap calls to `main` with circuit breaker logic\n\t */\n\tfunction execute(attempt: number, args: Args): Promise<Ret> {\n\t\t// Normal operation when circuit is closed. If an error occurs, keep track\n\t\t// of the failure count and open the circuit if it exceeds the threshold.\n\t\tif (state === \"closed\") {\n\t\t\tconst { pending, settle, teardown } = createHistoryItem(main(...args))\n\t\t\treturn pending.then(\n\t\t\t\t(result) => {\n\t\t\t\t\tsettle(\"resolved\")\n\t\t\t\t\treturn result\n\t\t\t\t},\n\t\t\t\tasync (cause: unknown) => {\n\t\t\t\t\t// Was the circuit disposed, or was this a non-retryable error?\n\t\t\t\t\tif (signal.aborted || errorIsFailure(cause)) {\n\t\t\t\t\t\tteardown()\n\t\t\t\t\t\tthrow cause\n\t\t\t\t\t}\n\n\t\t\t\t\t// Should this failure open the circuit?\n\t\t\t\t\tsettle(\"rejected\")\n\t\t\t\t\tif (failureRate() > errorThreshold) openCircuit(cause)\n\n\t\t\t\t\t// Retry the call after a delay.\n\t\t\t\t\tconst next = attempt + 1\n\t\t\t\t\tawait rejectOnAbort(signal, retryDelay(next, signal))\n\t\t\t\t\treturn execute(next, args)\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\n\t\t// Use the fallback while the circuit is open, or if a half-open trial\n\t\t// attempt was already made.\n\t\telse if (state === \"open\" || halfOpenPending) {\n\t\t\treturn fallback(...args)\n\t\t}\n\n\t\t// If the circuit is half-open, make one attempt. If it succeeds, close\n\t\t// the circuit and resume normal operation. If it fails, re-open the\n\t\t// circuit and run the fallback instead.\n\t\telse if (state === \"halfOpen\") {\n\t\t\treturn (halfOpenPending = main(...args))\n\t\t\t\t.finally(() => (halfOpenPending = undefined))\n\t\t\t\t.then(\n\t\t\t\t\t(result) => {\n\t\t\t\t\t\tif (signal.aborted) return result // disposed\n\t\t\t\t\t\tcloseCircuit()\n\t\t\t\t\t\treturn result\n\t\t\t\t\t},\n\t\t\t\t\tasync (cause: unknown) => {\n\t\t\t\t\t\t// Was the circuit disposed, or was this a non-retryable error?\n\t\t\t\t\t\tif (signal.aborted || errorIsFailure(cause)) throw cause\n\n\t\t\t\t\t\t// Open the circuit and try again later\n\t\t\t\t\t\topenCircuit(cause)\n\n\t\t\t\t\t\t// Retry the call after a delay.\n\t\t\t\t\t\tconst next = attempt + 1\n\t\t\t\t\t\tawait rejectOnAbort(signal, retryDelay(next, signal))\n\t\t\t\t\t\treturn execute(next, args)\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t/* v8 ignore next */\n\t\t}\n\n\t\t// exhaustive check\n\t\t/* v8 ignore next */\n\t\treturn assertNever(state)\n\t}\n\n\treturn Object.assign((...args: Args) => execute(1, args), {\n\t\tdispose: (disposeMessage = \"ERR_CIRCUIT_BREAKER_DISPOSED\") => {\n\t\t\tconst reason = new ReferenceError(disposeMessage)\n\t\t\tclearFailure()\n\t\t\tclearTimeout(resetTimer)\n\t\t\thistory.forEach((entry) => clearTimeout(entry.timer))\n\t\t\thistory.clear()\n\t\t\tfallback = () => Promise.reject(reason)\n\t\t\tstate = \"open\"\n\t\t\tcontroller.abort(reason)\n\t\t},\n\t\tgetLatestError: () => failureCause,\n\t\tgetState: () => state,\n\t})\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../lib/util.ts","../lib/options.ts","../lib/types.ts","../lib/helpers.ts","../lib/index.ts"],"sourcesContent":["export type AnyFn = (...args: never) => unknown\n\n/**\n * Asserts that the given value is truthy. If not, throws a `TypeError`.\n */\nexport function assert(value: unknown, message?: string): asserts value {\n\tif (!value) throw new TypeError(message)\n}\n\n/**\n * `[TypeScript]` For exhaustive checks in switch statements or if/else. Add\n * this check to `default` case or final `else` to ensure all possible values\n * have been handled. If a new value is added to the type, TypeScript will\n * throw an error and the editor will underline the `value`.\n */\n/* v8 ignore next -- @preserve */\nexport const assertNever = (val: never, msg = \"Unexpected value\"): never => {\n\tthrow new TypeError(`${msg}: ${val as string}`)\n}\n\n/**\n * Returns a promise that resolves after the specified number of milliseconds.\n */\nexport const delayMs = (ms: number, signal?: AbortSignal): Promise<void> => {\n\tif (!Number.isFinite(ms) || ms < 0) {\n\t\tthrow new RangeError(\n\t\t\t`\"ms\" must be a finite, non-negative number (received ${ms})`,\n\t\t)\n\t}\n\n\treturn signal\n\t\t? new Promise((resolve, reject) => {\n\t\t\t\tsignal.throwIfAborted()\n\n\t\t\t\tconst timer = setTimeout(() => {\n\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort)\n\t\t\t\t\tresolve()\n\t\t\t\t}, ms)\n\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\tclearTimeout(timer)\n\t\t\t\t\treject(signal.reason)\n\t\t\t\t}\n\n\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true })\n\t\t\t})\n\t\t: new Promise((next) => setTimeout(next, ms))\n}\n\n/**\n * Returns a promise which rejects when the abort signal is triggered or\n * resolves when the promise is fulfilled.\n */\nexport const abortable = <T>(\n\tsignal: AbortSignal,\n\tpending: PromiseLike<T>,\n): Promise<T> =>\n\tnew Promise((resolve, reject) => {\n\t\tsignal.throwIfAborted()\n\n\t\tconst onAbort = () => reject(signal.reason)\n\t\tsignal.addEventListener(\"abort\", onAbort, { once: true })\n\n\t\tPromise.resolve(pending)\n\t\t\t.then(resolve, reject)\n\t\t\t.finally(() => signal.removeEventListener(\"abort\", onAbort))\n\t})\n","import type { CircuitBreakerOptions } from \"./types.js\"\nimport { assert, type AnyFn } from \"./util.js\"\n\nexport function parseOptions<Fallback extends AnyFn>(\n\toptions: CircuitBreakerOptions<Fallback>,\n) {\n\tconst {\n\t\terrorIsFailure = () => false,\n\t\terrorThreshold = 0,\n\t\terrorWindow = 10_000,\n\t\tfallback,\n\t\tminimumCandidates = 1,\n\t\tonClose,\n\t\tonHalfOpen,\n\t\tonOpen,\n\t\tresetAfter = 30_000,\n\t} = options\n\n\t// errorIsFailure\n\tassert(\n\t\ttypeof errorIsFailure === \"function\",\n\t\t`\"errorIsFailure\" must be a function (received ${typeof errorIsFailure})`,\n\t)\n\n\t// errorThreshold\n\tassert(\n\t\terrorThreshold >= 0 && errorThreshold <= 1,\n\t\t`\"errorThreshold\" must be between 0 and 1 (received ${errorThreshold})`,\n\t)\n\n\t// errorWindow\n\tassert(\n\t\terrorWindow >= 1_000,\n\t\t`\"errorWindow\" must be milliseconds of at least 1 second (received ${errorWindow})`,\n\t)\n\n\t// (optional) fallback\n\tassert(\n\t\t!fallback || typeof fallback === \"function\",\n\t\t`\"fallback\" must be a function (received ${typeof fallback})`,\n\t)\n\n\t// minimumCandidates\n\tassert(\n\t\tminimumCandidates >= 1,\n\t\t`\"minimumCandidates\" must be greater than 0 (received ${minimumCandidates})`,\n\t)\n\n\t// (optional) onClose\n\tassert(\n\t\t!onClose || typeof onClose === \"function\",\n\t\t`\"onClose\" must be a function (received ${typeof onClose})`,\n\t)\n\n\t// (optional) onHalfOpen\n\tassert(\n\t\t!onHalfOpen || typeof onHalfOpen === \"function\",\n\t\t`\"onHalfOpen\" must be a function (received ${typeof onHalfOpen})`,\n\t)\n\n\t// (optional) onOpen\n\tassert(\n\t\t!onOpen || typeof onOpen === \"function\",\n\t\t`\"onOpen\" must be a function (received ${typeof onOpen})`,\n\t)\n\n\t// resetAfter\n\tassert(\n\t\tresetAfter >= 1_000,\n\t\t`\"resetAfter\" must be milliseconds of at least 1 second (received ${resetAfter})`,\n\t)\n\tassert(\n\t\tresetAfter >= errorWindow,\n\t\t`\"resetAfter\" must be greater than or equal to \"errorWindow\" (received ${resetAfter}, expected >= ${errorWindow})`,\n\t)\n\n\treturn {\n\t\terrorIsFailure,\n\t\terrorThreshold,\n\t\terrorWindow,\n\t\tminimumCandidates,\n\t\tonClose,\n\t\tonHalfOpen,\n\t\tonOpen,\n\t\tresetAfter,\n\t}\n}\n","import type { AnyFn } from \"./util\"\n\n/**\n * Symbol key for internal disposal protocol. Functions implementing this key\n * can be disposed by circuit breaker wrappers.\n */\nexport const disposeKey = Symbol(\"disposeKey\")\n\n/**\n * The three possible states of a circuit breaker.\n *\n * - `closed`: Normal operation, tracking failures\n * - `open`: Failing state, rejecting calls or using fallback\n * - `halfOpen`: Testing recovery with a single trial call\n */\nexport type CircuitState = \"closed\" | \"halfOpen\" | \"open\"\n\n/**\n * Configuration options for circuit breaker behavior.\n */\nexport interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {\n\t/**\n\t * Whether an error should be treated as non-retryable failure. When used and\n\t * when an error is considered a failure, the error will be thrown to the\n\t * caller.\n\t *\n\t * @default () => false // Errors are retryable by default\n\t */\n\terrorIsFailure?: (error: unknown) => boolean\n\n\t/**\n\t * The percentage of errors (as a number between 0 and 1) which must occur\n\t * within the error window before the circuit breaker opens.\n\t *\n\t * @default 0 // Any error opens the circuit\n\t */\n\terrorThreshold?: number\n\n\t/**\n\t * The sliding window of time in milliseconds over which errors are counted.\n\t *\n\t * @default 10_000 // 10 seconds\n\t */\n\terrorWindow?: number\n\n\t/**\n\t * If provided, then all rejected calls to `main` will be forwarded to\n\t * this function instead.\n\t *\n\t * @default undefined // No fallback, errors are propagated\n\t */\n\tfallback?: Fallback\n\n\t/**\n\t * The minimum number of calls that must be made before calculating the\n\t * error rate and determining whether the circuit breaker should open based on\n\t * the `errorThreshold`.\n\t *\n\t * @default 6\n\t */\n\tminimumCandidates?: number\n\n\t/**\n\t * Provide a function to be called when the circuit breaker is closed.\n\t */\n\tonClose?: () => void\n\n\t/**\n\t * Provide a function to be called when the circuit breaker transitions to\n\t * half-open state.\n\t */\n\tonHalfOpen?: () => void\n\n\t/**\n\t * Provide a function to be called when the circuit breaker is opened. It\n\t * receives the error as its only argument.\n\t */\n\tonOpen?: (cause: unknown) => void\n\n\t/**\n\t * The amount of time in milliseconds to wait before transitioning to a\n\t * half-open state.\n\t *\n\t * @default 30_000 // 30 seconds\n\t */\n\tresetAfter?: number\n}\n\n/**\n * A function wrapped with circuit breaker protection. Includes methods for\n * state inspection and resource cleanup.\n */\nexport interface CircuitBreakerProtectedFn<\n\tRet = unknown,\n\tArgs extends readonly unknown[] = readonly [],\n> {\n\t(...args: Args): Promise<Ret>\n\n\t/**\n\t * Free memory and stop timers. All future calls will be rejected with the\n\t * provided message.\n\t *\n\t * @default \"ERR_CIRCUIT_BREAKER_DISPOSED\"\n\t */\n\tdispose(this: void, disposeMessage?: string): void\n\n\t/** Get the current failure rate of the circuit breaker */\n\tgetFailureRate(this: void): number\n\n\t/** Get the last error which triggered the circuit breaker */\n\tgetLatestError(this: void): unknown\n\n\t/** Get the current state of the circuit breaker */\n\tgetState(this: void): CircuitState\n}\n\n/**\n * Tracks the status of a single call within the error window. Contains a timer\n * for automatic cleanup and the current resolution status.\n */\nexport interface HistoryEntry {\n\ttimer: NodeJS.Timeout | undefined\n\tstatus: \"pending\" | \"resolved\" | \"rejected\"\n}\n\n/**\n * Map tracking all in-flight and recent promises within the error window. Used\n * to calculate failure rates for circuit breaker decisions.\n */\nexport type HistoryMap<T = unknown> = Map<Promise<T>, HistoryEntry>\n\n/**\n * The main function signature that can be protected by a circuit breaker. May\n * optionally implement the disposal protocol via the `disposeKey` symbol.\n */\nexport interface MainFn<\n\tRet = unknown,\n\tArgs extends readonly unknown[] = never[],\n> {\n\t(...args: Args): Promise<Ret>\n\n\t[disposeKey]?: (disposeMessage?: string) => void\n}\n","import { disposeKey, type MainFn } from \"./types.js\"\nimport { assert, delayMs, abortable } from \"./util.js\"\n\n/**\n * Creates an exponential backoff strategy for retry delays.\n * Delay grows as 2^(attempt-2) seconds, capped at maxSeconds.\n *\n * The sequence is: 1s, 2s, 4s, 8s, 16s, 32s, etc.\n *\n * @param maxSeconds - Maximum delay in seconds before capping\n * @returns Function accepting attempt number and returning delay promise\n *\n * @example\n * ```ts\n * const backoff = useExponentialBackoff(30)\n * await backoff(2) // waits 1 second\n * await backoff(3) // waits 2 seconds\n * await backoff(10) // waits 30 seconds (capped)\n * ```\n */\nexport function useExponentialBackoff(maxSeconds: number) {\n\treturn function exponentialBackoff(attempt: number, signal?: AbortSignal) {\n\t\tconst num = Math.max(attempt - 2, 0)\n\t\tconst delay = Math.min(2 ** num, maxSeconds)\n\t\treturn delayMs(delay * 1_000, signal)\n\t}\n}\n\nconst sqrt5 = /* @__PURE__ */ Math.sqrt(5)\n/**\n * Binet's formula for calculating Fibonacci numbers in constant time.\n * @see https://en.wikipedia.org/wiki/Fibonacci_sequence#Closed-form_expression\n */\nconst binet = (n: number) =>\n\tMath.round(((1 + sqrt5) ** n - (1 - sqrt5) ** n) / (2 ** n * sqrt5))\n\n/**\n * Creates a Fibonacci backoff strategy for retry delays.\n * Delay follows the Fibonacci sequence: 1s, 2s, 3s, 5s, 8s, 13s, etc.\n *\n * More gradual than exponential backoff, useful for less aggressive retry patterns.\n *\n * @param maxSeconds - Maximum delay in seconds before capping\n * @returns Function accepting attempt number and returning delay promise\n *\n * @example\n * ```ts\n * const backoff = useFibonacciBackoff(60)\n * await backoff(2) // waits 1 second\n * await backoff(5) // waits 5 seconds\n * await backoff(10) // waits 55 seconds\n * ```\n */\nexport function useFibonacciBackoff(maxSeconds: number) {\n\treturn function fibonacciBackoff(attempt: number, signal?: AbortSignal) {\n\t\tconst delay = Math.min(binet(attempt), maxSeconds)\n\t\treturn delayMs(delay * 1_000, signal)\n\t}\n}\n\nexport interface RetryOptions {\n\t/**\n\t * Whether an error should be treated as non-retryable. When this returns\n\t * true, the error will be thrown immediately without retrying.\n\t *\n\t * @default () => false // All errors are retried\n\t */\n\tshouldRetry?: (error: unknown, attempt: number) => boolean\n\n\t/**\n\t * Maximum number of retries\n\t *\n\t * @default 3\n\t */\n\tmaxAttempts?: number\n\n\t/**\n\t * Function that returns a promise resolving when the next retry should occur.\n\t * Receives the attempt number (starting at 2) and an abort signal.\n\t *\n\t * @default () => Promise.resolve() // Immediate retry\n\t */\n\tretryDelay?: (attempt: number, signal: AbortSignal) => Promise<void>\n}\n\n/**\n * Wrap a function with retry logic. Errors will be retried according to the\n * provided options.\n *\n * @example\n * ```ts\n * // Compose with circuit breaker. Retry up to 3 times with no delay\n * const protectedA = createCircuitBreaker(\n * withRetry(unreliableApiCall, { maxAttempts: 3 })\n * )\n *\n * // Retry up to 5 times with exponential backoff\n * const protectedB = createCircuitBreaker(\n * withRetry(unreliableApiCall, {\n * maxAttempts: 5,\n * retryDelay: useExponentialBackoff(30),\n * })\n * )\n * ```\n */\nexport function withRetry<Ret, Args extends readonly unknown[]>(\n\tmain: MainFn<Ret, Args>,\n\toptions: Readonly<RetryOptions> = {},\n): MainFn<Ret, Args> {\n\tconst {\n\t\tshouldRetry = () => true,\n\t\tmaxAttempts = 3,\n\t\tretryDelay = () => Promise.resolve(),\n\t} = options\n\n\tassert(maxAttempts >= 1, \"maxAttempts must be a number greater than 0\")\n\n\tconst controller = new AbortController()\n\tconst { signal } = controller\n\n\tasync function withRetryFunction(...args: Args): Promise<Ret> {\n\t\tlet attempt = 1\n\t\twhile (true) {\n\t\t\ttry {\n\t\t\t\treturn await main(...args)\n\t\t\t} catch (cause) {\n\t\t\t\tif (attempt >= maxAttempts) {\n\t\t\t\t\tthrow new Error(`ERR_CIRCUIT_BREAKER_MAX_ATTEMPTS (${maxAttempts})`, {\n\t\t\t\t\t\tcause,\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tif (!shouldRetry(cause, attempt)) throw cause\n\t\t\t}\n\n\t\t\tattempt++\n\t\t\tawait abortable(signal, retryDelay(attempt, signal))\n\t\t}\n\t}\n\n\treturn Object.assign(withRetryFunction, {\n\t\t[disposeKey]: (disposeMessage = \"ERR_CIRCUIT_BREAKER_DISPOSED\") => {\n\t\t\tconst reason = new ReferenceError(disposeMessage)\n\t\t\tmain[disposeKey]?.(disposeMessage)\n\t\t\tcontroller.abort(reason)\n\t\t},\n\t})\n}\n\n/**\n * Wraps an async function with a timeout constraint. Rejects with an Error if\n * execution exceeds the specified timeout.\n *\n * @example\n * ```ts\n * const fetchWithTimeout = withTimeout(fetchData, 5000, \"Fetch timed out\")\n * try {\n * const data = await fetchWithTimeout(url)\n * } catch (error) {\n * console.error(error.message) // \"Fetch timed out\" after 5 seconds\n * }\n * ```\n */\nexport function withTimeout<Ret, Args extends readonly unknown[]>(\n\tmain: MainFn<Ret, Args>,\n\ttimeoutMs: number,\n\ttimeoutMessage = \"ERR_CIRCUIT_BREAKER_TIMEOUT\",\n): MainFn<Ret, Args> {\n\tconst error = new Error(timeoutMessage)\n\tconst controller = new AbortController()\n\tconst { signal } = controller\n\n\tfunction withTimeoutFunction(...args: Args): Promise<Ret> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst timer = setTimeout(reject, timeoutMs, error)\n\n\t\t\tabortable(signal, main(...args))\n\t\t\t\t.then(resolve, reject)\n\t\t\t\t.finally(() => clearTimeout(timer))\n\t\t})\n\t}\n\n\treturn Object.assign(withTimeoutFunction, {\n\t\t[disposeKey]: (disposeMessage = \"ERR_CIRCUIT_BREAKER_DISPOSED\") => {\n\t\t\tconst reason = new ReferenceError(disposeMessage)\n\t\t\tmain[disposeKey]?.(disposeMessage)\n\t\t\tcontroller.abort(reason)\n\t\t},\n\t})\n}\n","import { parseOptions } from \"./options.js\"\nimport {\n\tdisposeKey,\n\ttype CircuitBreakerOptions,\n\ttype CircuitBreakerProtectedFn,\n\ttype CircuitState,\n\ttype HistoryEntry,\n\ttype HistoryMap,\n\ttype MainFn,\n} from \"./types.js\"\nimport { assertNever } from \"./util.js\"\n\nexport * from \"./helpers.js\"\nexport {\n\ttype CircuitBreakerOptions,\n\ttype CircuitBreakerProtectedFn,\n\ttype CircuitState,\n\ttype MainFn,\n} from \"./types.js\"\nexport { delayMs } from \"./util.js\"\n\n/**\n * Creates a circuit breaker that wraps an async function with failure tracking\n * and automatic fallback behavior.\n *\n * The circuit breaker operates in three states:\n *\n * - `closed`: Normal operation, tracks failures in a sliding window\n * - `open`: Failed state, fallback is used until `resetAfter` milliseconds\n * - `halfOpen`: Testing recovery, allows one trial call\n *\n * When the failure rate exceeds `errorThreshold` within the `errorWindow`, the\n * circuit opens and rejects calls (using fallback if provided) for `resetAfter`\n * milliseconds. After this period, it transitions to half-open and allows one\n * trial call. Success closes the circuit; failure reopens it.\n *\n * @example\n * ```ts\n * const protectedFn = createCircuitBreaker(unreliableApiCall, {\n * errorThreshold: 0.5,\n * errorWindow: 10_000,\n * resetAfter: 30_000,\n * fallback: () => cachedResponse,\n * })\n *\n * try {\n * const result = await protectedFn(arg1, arg2)\n * } catch (error) {\n * console.error('Circuit breaker rejected call:', error)\n * }\n *\n * console.log(protectedFn.getState()) // 'closed' | 'open' | 'halfOpen'\n * protectedFn.dispose() // Clean up timers and resources\n * ```\n */\nexport function createCircuitBreaker<Ret, Args extends unknown[]>(\n\tmain: MainFn<Ret, Args>,\n\toptions: CircuitBreakerOptions<MainFn<Ret, Args>> = {},\n): CircuitBreakerProtectedFn<Ret, Args> {\n\tconst {\n\t\terrorIsFailure,\n\t\terrorThreshold,\n\t\terrorWindow,\n\t\tminimumCandidates,\n\t\tonClose,\n\t\tonHalfOpen,\n\t\tonOpen,\n\t\tresetAfter,\n\t} = parseOptions(options)\n\tconst controller = new AbortController()\n\tconst history: HistoryMap = new Map()\n\tconst signal = controller.signal\n\tlet failureCause: unknown\n\tlet failureRate = 0\n\tlet fallback: typeof main =\n\t\toptions.fallback || (() => Promise.reject(failureCause))\n\tlet halfOpenPending: Promise<unknown> | undefined\n\tlet resetTimer: NodeJS.Timeout | undefined\n\tlet state: CircuitState = \"closed\"\n\n\tfunction clearFailure() {\n\t\tfailureCause = undefined\n\t\tclearTimeout(resetTimer)\n\t\tresetTimer = undefined\n\t}\n\n\tfunction closeCircuit() {\n\t\tstate = \"closed\"\n\t\tclearFailure()\n\t\tif (onClose) setImmediate(onClose)\n\t}\n\n\tfunction calculateFailureRate(): number {\n\t\tlet failures = 0\n\t\tlet total = 0\n\t\tfor (const { status } of history.values()) {\n\t\t\tif (status === \"rejected\") failures++\n\t\t\tif (status !== \"pending\") total++\n\t\t}\n\t\t// Don't calculate anything until we have enough data\n\t\tif (!total || total < minimumCandidates) return 0\n\t\treturn failures / total\n\t}\n\n\tfunction openCircuit(cause: unknown) {\n\t\tstate = \"open\"\n\t\tfailureCause = cause\n\t\tclearTimeout(resetTimer)\n\t\tresetTimer = setTimeout(() => halfOpenCircuit(), resetAfter)\n\t\tif (onOpen) setImmediate(onOpen, cause)\n\t}\n\n\tfunction halfOpenCircuit() {\n\t\tstate = \"halfOpen\"\n\t\thalfOpenPending = undefined\n\t\tif (onHalfOpen) setImmediate(onHalfOpen)\n\t}\n\n\tfunction createHistoryItem<T>(pending: Promise<T>) {\n\t\tconst entry: HistoryEntry = { status: \"pending\", timer: undefined }\n\t\tconst teardown = (): void => {\n\t\t\tclearTimeout(entry.timer)\n\t\t\thistory.delete(pending)\n\t\t\tsignal.removeEventListener(\"abort\", teardown)\n\t\t}\n\t\tsignal.addEventListener(\"abort\", teardown, { once: true })\n\t\tconst settle = (value: \"resolved\" | \"rejected\"): number => {\n\t\t\tif (signal.aborted) return 0\n\t\t\tentry.status = value\n\t\t\t// Remove the entry from history when it falls outside of the error window\n\t\t\tentry.timer = setTimeout(teardown, errorWindow)\n\t\t\treturn (failureRate = calculateFailureRate())\n\t\t}\n\t\thistory.set(pending, entry)\n\t\treturn { pending, settle, teardown }\n\t}\n\n\t/**\n\t * Wrap calls to `main` with circuit breaker logic\n\t */\n\tfunction execute(...args: Args): Promise<Ret> {\n\t\t// Normal operation when circuit is closed. If an error occurs, keep track\n\t\t// of the failure count and open the circuit if it exceeds the threshold.\n\t\tif (state === \"closed\") {\n\t\t\tconst { pending, settle, teardown } = createHistoryItem(main(...args))\n\t\t\treturn pending.then(\n\t\t\t\t(result) => {\n\t\t\t\t\tsettle(\"resolved\")\n\t\t\t\t\treturn result\n\t\t\t\t},\n\t\t\t\t(cause: unknown) => {\n\t\t\t\t\t// Was the circuit disposed, or is this error considered a failure?\n\t\t\t\t\tif (signal.aborted || errorIsFailure(cause)) {\n\t\t\t\t\t\tteardown()\n\t\t\t\t\t\tthrow cause\n\t\t\t\t\t}\n\n\t\t\t\t\t// Should this error open the circuit?\n\t\t\t\t\tsettle(\"rejected\")\n\t\t\t\t\tif (failureRate > errorThreshold) openCircuit(cause)\n\n\t\t\t\t\treturn fallback(...args)\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\n\t\t// Use the fallback while the circuit is open, or if a half-open trial\n\t\t// attempt was already made.\n\t\telse if (state === \"open\" || halfOpenPending) {\n\t\t\treturn fallback(...args)\n\t\t}\n\n\t\t// If the circuit is half-open, make one attempt. If it succeeds, close\n\t\t// the circuit and resume normal operation. If it fails, re-open the\n\t\t// circuit and run the fallback instead.\n\t\telse if (state === \"halfOpen\") {\n\t\t\treturn (halfOpenPending = main(...args))\n\t\t\t\t.finally(() => (halfOpenPending = undefined))\n\t\t\t\t.then(\n\t\t\t\t\t(result) => {\n\t\t\t\t\t\tif (signal.aborted) return result // disposed\n\t\t\t\t\t\tcloseCircuit()\n\t\t\t\t\t\treturn result\n\t\t\t\t\t},\n\t\t\t\t\t(cause: unknown) => {\n\t\t\t\t\t\t// Was the circuit disposed, or was this a non-retryable error?\n\t\t\t\t\t\tif (signal.aborted || errorIsFailure(cause)) throw cause\n\n\t\t\t\t\t\t// Open the circuit and use fallback\n\t\t\t\t\t\topenCircuit(cause)\n\t\t\t\t\t\treturn fallback(...args)\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t}\n\n\t\t// exhaustive check\n\t\t/* v8 ignore next -- @preserve */\n\t\treturn assertNever(state)\n\t}\n\n\treturn Object.assign(execute, {\n\t\tdispose: (disposeMessage = \"ERR_CIRCUIT_BREAKER_DISPOSED\") => {\n\t\t\t/* v8 ignore if -- @preserve */\n\t\t\tif (signal.aborted) return\n\t\t\tconst reason = new ReferenceError(disposeMessage)\n\t\t\tmain[disposeKey]?.(disposeMessage)\n\t\t\tclearFailure()\n\t\t\thalfOpenPending = undefined\n\t\t\thistory.forEach((entry) => clearTimeout(entry.timer))\n\t\t\thistory.clear()\n\t\t\tfailureRate = 0\n\t\t\tfallback = () => Promise.reject(reason)\n\t\t\tstate = \"open\"\n\t\t\tcontroller.abort(reason)\n\t\t},\n\t\tgetFailureRate: () => failureRate,\n\t\tgetLatestError: () => failureCause,\n\t\tgetState: () => state,\n\t})\n}\n"],"names":[],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
type AnyFn = (...args:
|
|
1
|
+
type AnyFn = (...args: never) => unknown;
|
|
2
2
|
/**
|
|
3
3
|
* Returns a promise that resolves after the specified number of milliseconds.
|
|
4
4
|
*/
|
|
5
|
-
declare const delayMs: (ms: number) => Promise<void>;
|
|
5
|
+
declare const delayMs: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Symbol key for internal disposal protocol. Functions implementing this key
|
|
9
|
+
* can be disposed by circuit breaker wrappers.
|
|
10
|
+
*/
|
|
11
|
+
declare const disposeKey: unique symbol;
|
|
12
|
+
/**
|
|
13
|
+
* The three possible states of a circuit breaker.
|
|
14
|
+
*
|
|
15
|
+
* - `closed`: Normal operation, tracking failures
|
|
16
|
+
* - `open`: Failing state, rejecting calls or using fallback
|
|
17
|
+
* - `halfOpen`: Testing recovery with a single trial call
|
|
18
|
+
*/
|
|
7
19
|
type CircuitState = "closed" | "halfOpen" | "open";
|
|
20
|
+
/**
|
|
21
|
+
* Configuration options for circuit breaker behavior.
|
|
22
|
+
*/
|
|
8
23
|
interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
|
|
9
24
|
/**
|
|
10
25
|
* Whether an error should be treated as non-retryable failure. When used and
|
|
@@ -63,36 +78,12 @@ interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
|
|
|
63
78
|
* @default 30_000 // 30 seconds
|
|
64
79
|
*/
|
|
65
80
|
resetAfter?: number;
|
|
66
|
-
/**
|
|
67
|
-
* Provide a function which returns a promise that resolves when the next
|
|
68
|
-
* retry attempt should be made. Use this to implement custom retry logic,
|
|
69
|
-
* such as exponential backoff.
|
|
70
|
-
*
|
|
71
|
-
* Note that `attempt` always starts at 2, since first attempts are always
|
|
72
|
-
* made as soon as they come in.
|
|
73
|
-
*
|
|
74
|
-
* @default none // Retry errors immediately
|
|
75
|
-
* @example
|
|
76
|
-
* ```ts
|
|
77
|
-
* // Constant delay of 1 second for all retries
|
|
78
|
-
* const breaker = createCircuitBreaker(main, {
|
|
79
|
-
* retryDelay: () => delayMs(1_000),
|
|
80
|
-
* })
|
|
81
|
-
*
|
|
82
|
-
* // Double the previous delay each time, up to 30 seconds
|
|
83
|
-
* const breaker = createCircuitBreaker(main, {
|
|
84
|
-
* retryDelay: useExponentialBackoff(30),
|
|
85
|
-
* })
|
|
86
|
-
*
|
|
87
|
-
* // Use Fibonacci sequence for delay, up to 90 seconds
|
|
88
|
-
* const breaker = createCircuitBreaker(main, {
|
|
89
|
-
* retryDelay: useFibonacciBackoff(90),
|
|
90
|
-
* })
|
|
91
|
-
* ```
|
|
92
|
-
*/
|
|
93
|
-
retryDelay?: (attempt: number, signal: AbortSignal) => Promise<void>;
|
|
94
81
|
}
|
|
95
|
-
|
|
82
|
+
/**
|
|
83
|
+
* A function wrapped with circuit breaker protection. Includes methods for
|
|
84
|
+
* state inspection and resource cleanup.
|
|
85
|
+
*/
|
|
86
|
+
interface CircuitBreakerProtectedFn<Ret = unknown, Args extends readonly unknown[] = readonly []> {
|
|
96
87
|
(...args: Args): Promise<Ret>;
|
|
97
88
|
/**
|
|
98
89
|
* Free memory and stop timers. All future calls will be rejected with the
|
|
@@ -100,36 +91,153 @@ interface CircuitBreakerProtectedFn<Ret = unknown, Args extends unknown[] = neve
|
|
|
100
91
|
*
|
|
101
92
|
* @default "ERR_CIRCUIT_BREAKER_DISPOSED"
|
|
102
93
|
*/
|
|
103
|
-
dispose(disposeMessage?: string): void;
|
|
94
|
+
dispose(this: void, disposeMessage?: string): void;
|
|
95
|
+
/** Get the current failure rate of the circuit breaker */
|
|
96
|
+
getFailureRate(this: void): number;
|
|
104
97
|
/** Get the last error which triggered the circuit breaker */
|
|
105
|
-
getLatestError(): unknown
|
|
98
|
+
getLatestError(this: void): unknown;
|
|
106
99
|
/** Get the current state of the circuit breaker */
|
|
107
|
-
getState(): CircuitState;
|
|
100
|
+
getState(this: void): CircuitState;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The main function signature that can be protected by a circuit breaker. May
|
|
104
|
+
* optionally implement the disposal protocol via the `disposeKey` symbol.
|
|
105
|
+
*/
|
|
106
|
+
interface MainFn<Ret = unknown, Args extends readonly unknown[] = never[]> {
|
|
107
|
+
(...args: Args): Promise<Ret>;
|
|
108
|
+
[disposeKey]?: (disposeMessage?: string) => void;
|
|
108
109
|
}
|
|
109
|
-
type MainFn<Ret = unknown, Args extends unknown[] = never[]> = (...args: Args) => Promise<Ret>;
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
112
|
+
* Creates an exponential backoff strategy for retry delays.
|
|
113
|
+
* Delay grows as 2^(attempt-2) seconds, capped at maxSeconds.
|
|
114
|
+
*
|
|
115
|
+
* The sequence is: 1s, 2s, 4s, 8s, 16s, 32s, etc.
|
|
116
|
+
*
|
|
117
|
+
* @param maxSeconds - Maximum delay in seconds before capping
|
|
118
|
+
* @returns Function accepting attempt number and returning delay promise
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const backoff = useExponentialBackoff(30)
|
|
123
|
+
* await backoff(2) // waits 1 second
|
|
124
|
+
* await backoff(3) // waits 2 seconds
|
|
125
|
+
* await backoff(10) // waits 30 seconds (capped)
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
declare function useExponentialBackoff(maxSeconds: number): (attempt: number, signal?: AbortSignal) => Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Creates a Fibonacci backoff strategy for retry delays.
|
|
131
|
+
* Delay follows the Fibonacci sequence: 1s, 2s, 3s, 5s, 8s, 13s, etc.
|
|
132
|
+
*
|
|
133
|
+
* More gradual than exponential backoff, useful for less aggressive retry patterns.
|
|
134
|
+
*
|
|
135
|
+
* @param maxSeconds - Maximum delay in seconds before capping
|
|
136
|
+
* @returns Function accepting attempt number and returning delay promise
|
|
113
137
|
*
|
|
114
|
-
* @
|
|
115
|
-
*
|
|
116
|
-
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* const backoff = useFibonacciBackoff(60)
|
|
141
|
+
* await backoff(2) // waits 1 second
|
|
142
|
+
* await backoff(5) // waits 5 seconds
|
|
143
|
+
* await backoff(10) // waits 55 seconds
|
|
144
|
+
* ```
|
|
117
145
|
*/
|
|
118
|
-
declare function
|
|
146
|
+
declare function useFibonacciBackoff(maxSeconds: number): (attempt: number, signal?: AbortSignal) => Promise<void>;
|
|
147
|
+
interface RetryOptions {
|
|
148
|
+
/**
|
|
149
|
+
* Whether an error should be treated as non-retryable. When this returns
|
|
150
|
+
* true, the error will be thrown immediately without retrying.
|
|
151
|
+
*
|
|
152
|
+
* @default () => false // All errors are retried
|
|
153
|
+
*/
|
|
154
|
+
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Maximum number of retries
|
|
157
|
+
*
|
|
158
|
+
* @default 3
|
|
159
|
+
*/
|
|
160
|
+
maxAttempts?: number;
|
|
161
|
+
/**
|
|
162
|
+
* Function that returns a promise resolving when the next retry should occur.
|
|
163
|
+
* Receives the attempt number (starting at 2) and an abort signal.
|
|
164
|
+
*
|
|
165
|
+
* @default () => Promise.resolve() // Immediate retry
|
|
166
|
+
*/
|
|
167
|
+
retryDelay?: (attempt: number, signal: AbortSignal) => Promise<void>;
|
|
168
|
+
}
|
|
119
169
|
/**
|
|
120
|
-
*
|
|
170
|
+
* Wrap a function with retry logic. Errors will be retried according to the
|
|
171
|
+
* provided options.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* // Compose with circuit breaker. Retry up to 3 times with no delay
|
|
176
|
+
* const protectedA = createCircuitBreaker(
|
|
177
|
+
* withRetry(unreliableApiCall, { maxAttempts: 3 })
|
|
178
|
+
* )
|
|
121
179
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
180
|
+
* // Retry up to 5 times with exponential backoff
|
|
181
|
+
* const protectedB = createCircuitBreaker(
|
|
182
|
+
* withRetry(unreliableApiCall, {
|
|
183
|
+
* maxAttempts: 5,
|
|
184
|
+
* retryDelay: useExponentialBackoff(30),
|
|
185
|
+
* })
|
|
186
|
+
* )
|
|
187
|
+
* ```
|
|
125
188
|
*/
|
|
126
|
-
declare function
|
|
189
|
+
declare function withRetry<Ret, Args extends readonly unknown[]>(main: MainFn<Ret, Args>, options?: Readonly<RetryOptions>): MainFn<Ret, Args>;
|
|
127
190
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
191
|
+
* Wraps an async function with a timeout constraint. Rejects with an Error if
|
|
192
|
+
* execution exceeds the specified timeout.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* const fetchWithTimeout = withTimeout(fetchData, 5000, "Fetch timed out")
|
|
197
|
+
* try {
|
|
198
|
+
* const data = await fetchWithTimeout(url)
|
|
199
|
+
* } catch (error) {
|
|
200
|
+
* console.error(error.message) // "Fetch timed out" after 5 seconds
|
|
201
|
+
* }
|
|
202
|
+
* ```
|
|
130
203
|
*/
|
|
131
|
-
declare function withTimeout<Ret, Args extends unknown[]>(main: MainFn<Ret, Args>, timeoutMs: number, timeoutMessage?: string): MainFn<Ret, Args>;
|
|
204
|
+
declare function withTimeout<Ret, Args extends readonly unknown[]>(main: MainFn<Ret, Args>, timeoutMs: number, timeoutMessage?: string): MainFn<Ret, Args>;
|
|
132
205
|
|
|
133
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Creates a circuit breaker that wraps an async function with failure tracking
|
|
208
|
+
* and automatic fallback behavior.
|
|
209
|
+
*
|
|
210
|
+
* The circuit breaker operates in three states:
|
|
211
|
+
*
|
|
212
|
+
* - `closed`: Normal operation, tracks failures in a sliding window
|
|
213
|
+
* - `open`: Failed state, fallback is used until `resetAfter` milliseconds
|
|
214
|
+
* - `halfOpen`: Testing recovery, allows one trial call
|
|
215
|
+
*
|
|
216
|
+
* When the failure rate exceeds `errorThreshold` within the `errorWindow`, the
|
|
217
|
+
* circuit opens and rejects calls (using fallback if provided) for `resetAfter`
|
|
218
|
+
* milliseconds. After this period, it transitions to half-open and allows one
|
|
219
|
+
* trial call. Success closes the circuit; failure reopens it.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* const protectedFn = createCircuitBreaker(unreliableApiCall, {
|
|
224
|
+
* errorThreshold: 0.5,
|
|
225
|
+
* errorWindow: 10_000,
|
|
226
|
+
* resetAfter: 30_000,
|
|
227
|
+
* fallback: () => cachedResponse,
|
|
228
|
+
* })
|
|
229
|
+
*
|
|
230
|
+
* try {
|
|
231
|
+
* const result = await protectedFn(arg1, arg2)
|
|
232
|
+
* } catch (error) {
|
|
233
|
+
* console.error('Circuit breaker rejected call:', error)
|
|
234
|
+
* }
|
|
235
|
+
*
|
|
236
|
+
* console.log(protectedFn.getState()) // 'closed' | 'open' | 'halfOpen'
|
|
237
|
+
* protectedFn.dispose() // Clean up timers and resources
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
declare function createCircuitBreaker<Ret, Args extends unknown[]>(main: MainFn<Ret, Args>, options?: CircuitBreakerOptions<MainFn<Ret, Args>>): CircuitBreakerProtectedFn<Ret, Args>;
|
|
134
241
|
|
|
135
|
-
export { createCircuitBreaker, delayMs, useExponentialBackoff, useFibonacciBackoff, withTimeout };
|
|
242
|
+
export { createCircuitBreaker, delayMs, useExponentialBackoff, useFibonacciBackoff, withRetry, withTimeout };
|
|
243
|
+
export type { CircuitBreakerOptions, CircuitBreakerProtectedFn, CircuitState, MainFn, RetryOptions };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
type AnyFn = (...args:
|
|
1
|
+
type AnyFn = (...args: never) => unknown;
|
|
2
2
|
/**
|
|
3
3
|
* Returns a promise that resolves after the specified number of milliseconds.
|
|
4
4
|
*/
|
|
5
|
-
declare const delayMs: (ms: number) => Promise<void>;
|
|
5
|
+
declare const delayMs: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Symbol key for internal disposal protocol. Functions implementing this key
|
|
9
|
+
* can be disposed by circuit breaker wrappers.
|
|
10
|
+
*/
|
|
11
|
+
declare const disposeKey: unique symbol;
|
|
12
|
+
/**
|
|
13
|
+
* The three possible states of a circuit breaker.
|
|
14
|
+
*
|
|
15
|
+
* - `closed`: Normal operation, tracking failures
|
|
16
|
+
* - `open`: Failing state, rejecting calls or using fallback
|
|
17
|
+
* - `halfOpen`: Testing recovery with a single trial call
|
|
18
|
+
*/
|
|
7
19
|
type CircuitState = "closed" | "halfOpen" | "open";
|
|
20
|
+
/**
|
|
21
|
+
* Configuration options for circuit breaker behavior.
|
|
22
|
+
*/
|
|
8
23
|
interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
|
|
9
24
|
/**
|
|
10
25
|
* Whether an error should be treated as non-retryable failure. When used and
|
|
@@ -63,36 +78,12 @@ interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
|
|
|
63
78
|
* @default 30_000 // 30 seconds
|
|
64
79
|
*/
|
|
65
80
|
resetAfter?: number;
|
|
66
|
-
/**
|
|
67
|
-
* Provide a function which returns a promise that resolves when the next
|
|
68
|
-
* retry attempt should be made. Use this to implement custom retry logic,
|
|
69
|
-
* such as exponential backoff.
|
|
70
|
-
*
|
|
71
|
-
* Note that `attempt` always starts at 2, since first attempts are always
|
|
72
|
-
* made as soon as they come in.
|
|
73
|
-
*
|
|
74
|
-
* @default none // Retry errors immediately
|
|
75
|
-
* @example
|
|
76
|
-
* ```ts
|
|
77
|
-
* // Constant delay of 1 second for all retries
|
|
78
|
-
* const breaker = createCircuitBreaker(main, {
|
|
79
|
-
* retryDelay: () => delayMs(1_000),
|
|
80
|
-
* })
|
|
81
|
-
*
|
|
82
|
-
* // Double the previous delay each time, up to 30 seconds
|
|
83
|
-
* const breaker = createCircuitBreaker(main, {
|
|
84
|
-
* retryDelay: useExponentialBackoff(30),
|
|
85
|
-
* })
|
|
86
|
-
*
|
|
87
|
-
* // Use Fibonacci sequence for delay, up to 90 seconds
|
|
88
|
-
* const breaker = createCircuitBreaker(main, {
|
|
89
|
-
* retryDelay: useFibonacciBackoff(90),
|
|
90
|
-
* })
|
|
91
|
-
* ```
|
|
92
|
-
*/
|
|
93
|
-
retryDelay?: (attempt: number, signal: AbortSignal) => Promise<void>;
|
|
94
81
|
}
|
|
95
|
-
|
|
82
|
+
/**
|
|
83
|
+
* A function wrapped with circuit breaker protection. Includes methods for
|
|
84
|
+
* state inspection and resource cleanup.
|
|
85
|
+
*/
|
|
86
|
+
interface CircuitBreakerProtectedFn<Ret = unknown, Args extends readonly unknown[] = readonly []> {
|
|
96
87
|
(...args: Args): Promise<Ret>;
|
|
97
88
|
/**
|
|
98
89
|
* Free memory and stop timers. All future calls will be rejected with the
|
|
@@ -100,36 +91,153 @@ interface CircuitBreakerProtectedFn<Ret = unknown, Args extends unknown[] = neve
|
|
|
100
91
|
*
|
|
101
92
|
* @default "ERR_CIRCUIT_BREAKER_DISPOSED"
|
|
102
93
|
*/
|
|
103
|
-
dispose(disposeMessage?: string): void;
|
|
94
|
+
dispose(this: void, disposeMessage?: string): void;
|
|
95
|
+
/** Get the current failure rate of the circuit breaker */
|
|
96
|
+
getFailureRate(this: void): number;
|
|
104
97
|
/** Get the last error which triggered the circuit breaker */
|
|
105
|
-
getLatestError(): unknown
|
|
98
|
+
getLatestError(this: void): unknown;
|
|
106
99
|
/** Get the current state of the circuit breaker */
|
|
107
|
-
getState(): CircuitState;
|
|
100
|
+
getState(this: void): CircuitState;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The main function signature that can be protected by a circuit breaker. May
|
|
104
|
+
* optionally implement the disposal protocol via the `disposeKey` symbol.
|
|
105
|
+
*/
|
|
106
|
+
interface MainFn<Ret = unknown, Args extends readonly unknown[] = never[]> {
|
|
107
|
+
(...args: Args): Promise<Ret>;
|
|
108
|
+
[disposeKey]?: (disposeMessage?: string) => void;
|
|
108
109
|
}
|
|
109
|
-
type MainFn<Ret = unknown, Args extends unknown[] = never[]> = (...args: Args) => Promise<Ret>;
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
112
|
+
* Creates an exponential backoff strategy for retry delays.
|
|
113
|
+
* Delay grows as 2^(attempt-2) seconds, capped at maxSeconds.
|
|
114
|
+
*
|
|
115
|
+
* The sequence is: 1s, 2s, 4s, 8s, 16s, 32s, etc.
|
|
116
|
+
*
|
|
117
|
+
* @param maxSeconds - Maximum delay in seconds before capping
|
|
118
|
+
* @returns Function accepting attempt number and returning delay promise
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const backoff = useExponentialBackoff(30)
|
|
123
|
+
* await backoff(2) // waits 1 second
|
|
124
|
+
* await backoff(3) // waits 2 seconds
|
|
125
|
+
* await backoff(10) // waits 30 seconds (capped)
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
declare function useExponentialBackoff(maxSeconds: number): (attempt: number, signal?: AbortSignal) => Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Creates a Fibonacci backoff strategy for retry delays.
|
|
131
|
+
* Delay follows the Fibonacci sequence: 1s, 2s, 3s, 5s, 8s, 13s, etc.
|
|
132
|
+
*
|
|
133
|
+
* More gradual than exponential backoff, useful for less aggressive retry patterns.
|
|
134
|
+
*
|
|
135
|
+
* @param maxSeconds - Maximum delay in seconds before capping
|
|
136
|
+
* @returns Function accepting attempt number and returning delay promise
|
|
113
137
|
*
|
|
114
|
-
* @
|
|
115
|
-
*
|
|
116
|
-
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* const backoff = useFibonacciBackoff(60)
|
|
141
|
+
* await backoff(2) // waits 1 second
|
|
142
|
+
* await backoff(5) // waits 5 seconds
|
|
143
|
+
* await backoff(10) // waits 55 seconds
|
|
144
|
+
* ```
|
|
117
145
|
*/
|
|
118
|
-
declare function
|
|
146
|
+
declare function useFibonacciBackoff(maxSeconds: number): (attempt: number, signal?: AbortSignal) => Promise<void>;
|
|
147
|
+
interface RetryOptions {
|
|
148
|
+
/**
|
|
149
|
+
* Whether an error should be treated as non-retryable. When this returns
|
|
150
|
+
* true, the error will be thrown immediately without retrying.
|
|
151
|
+
*
|
|
152
|
+
* @default () => false // All errors are retried
|
|
153
|
+
*/
|
|
154
|
+
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Maximum number of retries
|
|
157
|
+
*
|
|
158
|
+
* @default 3
|
|
159
|
+
*/
|
|
160
|
+
maxAttempts?: number;
|
|
161
|
+
/**
|
|
162
|
+
* Function that returns a promise resolving when the next retry should occur.
|
|
163
|
+
* Receives the attempt number (starting at 2) and an abort signal.
|
|
164
|
+
*
|
|
165
|
+
* @default () => Promise.resolve() // Immediate retry
|
|
166
|
+
*/
|
|
167
|
+
retryDelay?: (attempt: number, signal: AbortSignal) => Promise<void>;
|
|
168
|
+
}
|
|
119
169
|
/**
|
|
120
|
-
*
|
|
170
|
+
* Wrap a function with retry logic. Errors will be retried according to the
|
|
171
|
+
* provided options.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* // Compose with circuit breaker. Retry up to 3 times with no delay
|
|
176
|
+
* const protectedA = createCircuitBreaker(
|
|
177
|
+
* withRetry(unreliableApiCall, { maxAttempts: 3 })
|
|
178
|
+
* )
|
|
121
179
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
180
|
+
* // Retry up to 5 times with exponential backoff
|
|
181
|
+
* const protectedB = createCircuitBreaker(
|
|
182
|
+
* withRetry(unreliableApiCall, {
|
|
183
|
+
* maxAttempts: 5,
|
|
184
|
+
* retryDelay: useExponentialBackoff(30),
|
|
185
|
+
* })
|
|
186
|
+
* )
|
|
187
|
+
* ```
|
|
125
188
|
*/
|
|
126
|
-
declare function
|
|
189
|
+
declare function withRetry<Ret, Args extends readonly unknown[]>(main: MainFn<Ret, Args>, options?: Readonly<RetryOptions>): MainFn<Ret, Args>;
|
|
127
190
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
191
|
+
* Wraps an async function with a timeout constraint. Rejects with an Error if
|
|
192
|
+
* execution exceeds the specified timeout.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* const fetchWithTimeout = withTimeout(fetchData, 5000, "Fetch timed out")
|
|
197
|
+
* try {
|
|
198
|
+
* const data = await fetchWithTimeout(url)
|
|
199
|
+
* } catch (error) {
|
|
200
|
+
* console.error(error.message) // "Fetch timed out" after 5 seconds
|
|
201
|
+
* }
|
|
202
|
+
* ```
|
|
130
203
|
*/
|
|
131
|
-
declare function withTimeout<Ret, Args extends unknown[]>(main: MainFn<Ret, Args>, timeoutMs: number, timeoutMessage?: string): MainFn<Ret, Args>;
|
|
204
|
+
declare function withTimeout<Ret, Args extends readonly unknown[]>(main: MainFn<Ret, Args>, timeoutMs: number, timeoutMessage?: string): MainFn<Ret, Args>;
|
|
132
205
|
|
|
133
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Creates a circuit breaker that wraps an async function with failure tracking
|
|
208
|
+
* and automatic fallback behavior.
|
|
209
|
+
*
|
|
210
|
+
* The circuit breaker operates in three states:
|
|
211
|
+
*
|
|
212
|
+
* - `closed`: Normal operation, tracks failures in a sliding window
|
|
213
|
+
* - `open`: Failed state, fallback is used until `resetAfter` milliseconds
|
|
214
|
+
* - `halfOpen`: Testing recovery, allows one trial call
|
|
215
|
+
*
|
|
216
|
+
* When the failure rate exceeds `errorThreshold` within the `errorWindow`, the
|
|
217
|
+
* circuit opens and rejects calls (using fallback if provided) for `resetAfter`
|
|
218
|
+
* milliseconds. After this period, it transitions to half-open and allows one
|
|
219
|
+
* trial call. Success closes the circuit; failure reopens it.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* const protectedFn = createCircuitBreaker(unreliableApiCall, {
|
|
224
|
+
* errorThreshold: 0.5,
|
|
225
|
+
* errorWindow: 10_000,
|
|
226
|
+
* resetAfter: 30_000,
|
|
227
|
+
* fallback: () => cachedResponse,
|
|
228
|
+
* })
|
|
229
|
+
*
|
|
230
|
+
* try {
|
|
231
|
+
* const result = await protectedFn(arg1, arg2)
|
|
232
|
+
* } catch (error) {
|
|
233
|
+
* console.error('Circuit breaker rejected call:', error)
|
|
234
|
+
* }
|
|
235
|
+
*
|
|
236
|
+
* console.log(protectedFn.getState()) // 'closed' | 'open' | 'halfOpen'
|
|
237
|
+
* protectedFn.dispose() // Clean up timers and resources
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
declare function createCircuitBreaker<Ret, Args extends unknown[]>(main: MainFn<Ret, Args>, options?: CircuitBreakerOptions<MainFn<Ret, Args>>): CircuitBreakerProtectedFn<Ret, Args>;
|
|
134
241
|
|
|
135
|
-
export { createCircuitBreaker, delayMs, useExponentialBackoff, useFibonacciBackoff, withTimeout };
|
|
242
|
+
export { createCircuitBreaker, delayMs, useExponentialBackoff, useFibonacciBackoff, withRetry, withTimeout };
|
|
243
|
+
export type { CircuitBreakerOptions, CircuitBreakerProtectedFn, CircuitState, MainFn, RetryOptions };
|