breaker-box 2.0.0 → 4.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 CHANGED
@@ -27,10 +27,11 @@ async function unreliableApiCall(data: string) {
27
27
  }
28
28
 
29
29
  const protectedApiCall = createCircuitBreaker(unreliableApiCall, {
30
- errorIsFailure: () => true, // Any error is considered a failure
31
- failureThreshold: 1, // Open circuit after first failure
32
- fallback: undefined, // No fallback, errors are propagated
33
- resetAfter: 30_000, // Try again after 30 seconds
30
+ errorIsFailure: (error) => error.message.includes("404"), // Don't retry 404s
31
+ errorThreshold: 0.5, // Open circuit when 50% of calls fail
32
+ errorWindow: 10_000, // Track errors over 10 second window
33
+ minimumCandidates: 6, // Need at least 6 calls before calculating error rate
34
+ resetAfter: 30_000, // Try again after 30 seconds
34
35
  })
35
36
 
36
37
  try {
@@ -41,6 +42,41 @@ try {
41
42
  }
42
43
  ```
43
44
 
45
+ ### Retry Strategies
46
+
47
+ ```typescript
48
+ import {
49
+ createCircuitBreaker,
50
+ useExponentialBackoff,
51
+ useFibonacciBackoff,
52
+ } from "breaker-box"
53
+
54
+ // Exponential backoff: 1s, 2s, 4s, 8s, up to 30s max
55
+ const protectedWithExponential = createCircuitBreaker(unreliableApiCall, {
56
+ retryDelay: useExponentialBackoff(30),
57
+ })
58
+
59
+ // Fibonacci backoff: 1s, 2s, 3s, 5s, 8s, up to 60s max
60
+ const protectedWithFibonacci = createCircuitBreaker(unreliableApiCall, {
61
+ retryDelay: useFibonacciBackoff(60),
62
+ })
63
+ ```
64
+
65
+ ### Timeout Protection
66
+
67
+ ```typescript
68
+ import { createCircuitBreaker, withTimeout } from "breaker-box"
69
+
70
+ // Wrap function with 5-second timeout
71
+ const timeoutProtectedCall = withTimeout(
72
+ unreliableApiCall,
73
+ 5_000,
74
+ "Request timed out",
75
+ )
76
+
77
+ const protectedApiCall = createCircuitBreaker(timeoutProtectedCall, {})
78
+ ```
79
+
44
80
  ### Event Monitoring
45
81
 
46
82
  ```typescript
@@ -55,13 +91,13 @@ const protectedFunction = createCircuitBreaker(unreliableApiCall, {
55
91
 
56
92
  // Check current state
57
93
  console.log("Current state:", protectedFunction.getState())
58
- // Possible states: 'closed', 'open', 'halfOpen', 'disposed'
94
+ // Possible states: 'closed', 'open', 'halfOpen'
59
95
  ```
60
96
 
61
97
  ### Cleanup
62
98
 
63
99
  ```typescript
64
- // Clean up resources when done
100
+ // Clean up resources when shutting down
65
101
  protectedFunction.dispose()
66
102
  ```
67
103
 
@@ -75,20 +111,37 @@ Creates a circuit breaker around the provided async function.
75
111
 
76
112
  - `fn`: The async function to protect
77
113
  - `options`: Configuration object (optional)
78
- - `errorIsFailure`: Function to determine if an error counts as failure (default: all errors)
79
- - `failureThreshold`: Number of failures before opening circuit (default: 1)
80
- - `fallback`: Function to call when circuit is open (default: undefined)
81
- - `onClose`: Function to call when circuit is closed (default: undefined)
82
- - `onOpen`: Function to call when circuit is opened (default: undefined)
83
- - `resetAfter`: Milliseconds to wait before trying again (default: 30000)
114
+ - `errorIsFailure`: Function to determine if an error is non-retryable (default: `() => false`)
115
+ - `errorThreshold`: Percentage (0-1) of errors that triggers circuit opening (default: `0`)
116
+ - `errorWindow`: Time window in ms for tracking errors (default: `10_000`)
117
+ - `fallback`: Function to call when circuit is open (default: undefined)
118
+ - `minimumCandidates`: Minimum calls before calculating error rate (default: `6`)
119
+ - `onClose`: Function called when circuit closes (default: undefined)
120
+ - `onOpen`: Function called when circuit opens (default: undefined)
121
+ - `resetAfter`: Milliseconds to wait before trying half-open (default: `30_000`)
122
+ - `retryDelay`: Function returning promise for retry delays (default: immediate retry)
84
123
 
85
124
  #### Returns
86
125
 
87
126
  A function with the same signature as `fn` and additional methods:
88
127
 
89
- - `.dispose()`: Clean up resources
128
+ - `.dispose(message?)`: Clean up resources and reject future calls
90
129
  - `.getLatestError()`: Returns the error which triggered the circuit breaker
91
- - `.getState()`: Returns current circuit state
130
+ - `.getState()`: Returns current circuit state (`'closed'`, `'open'`, `'halfOpen'`)
131
+
132
+ ### Helper Functions
133
+
134
+ #### `useExponentialBackoff(maxSeconds)`
135
+
136
+ Returns a retry delay function that implements exponential backoff (2^n seconds, capped at maxSeconds).
137
+
138
+ #### `useFibonacciBackoff(maxSeconds)`
139
+
140
+ Returns a retry delay function that implements Fibonacci backoff (Fibonacci sequence in seconds, capped at maxSeconds).
141
+
142
+ #### `withTimeout(fn, timeoutMs, message?)`
143
+
144
+ Wraps a function with a timeout. Rejects with `Error(message)` if execution exceeds `timeoutMs`.
92
145
 
93
146
  ### Development
94
147
 
package/dist/index.cjs CHANGED
@@ -1,71 +1,171 @@
1
1
  'use strict';
2
2
 
3
+ const assertNever = (val, msg = "Unexpected value") => {
4
+ throw new TypeError(`${msg}: ${val}`);
5
+ };
6
+ const delayMs = (ms) => new Promise((next) => setTimeout(next, ms));
7
+ const rejectOnAbort = (signal, pending) => {
8
+ let reject;
9
+ return Promise.race([
10
+ Promise.resolve(pending).finally(
11
+ () => signal.removeEventListener("abort", reject)
12
+ ),
13
+ new Promise((_, reject_) => {
14
+ reject = reject_;
15
+ signal.addEventListener("abort", () => reject(signal.reason));
16
+ })
17
+ ]);
18
+ };
19
+
20
+ function useExponentialBackoff(maxSeconds) {
21
+ return function exponentialBackoff(attempt) {
22
+ const num = Math.max(attempt - 2, 0);
23
+ const delay = Math.min(2 ** num, maxSeconds);
24
+ return delayMs(delay * 1e3);
25
+ };
26
+ }
27
+ const sqrt5 = /* @__PURE__ */ Math.sqrt(5);
28
+ const binet = (n) => Math.round(((1 + sqrt5) ** n - (1 - sqrt5) ** n) / (2 ** n * sqrt5));
29
+ function useFibonacciBackoff(maxSeconds) {
30
+ return function fibonacciBackoff(attempt) {
31
+ const delay = Math.min(binet(attempt), maxSeconds);
32
+ return delayMs(delay * 1e3);
33
+ };
34
+ }
35
+ function withTimeout(main, timeoutMs, timeoutMessage = "ERR_CIRCUIT_BREAKER_TIMEOUT") {
36
+ const error = new Error(timeoutMessage);
37
+ return function withTimeoutFunction(...args) {
38
+ let timer;
39
+ return Promise.race([
40
+ main(...args).finally(() => clearTimeout(timer)),
41
+ new Promise((_, reject) => {
42
+ timer = setTimeout(reject, timeoutMs, error);
43
+ })
44
+ ]);
45
+ };
46
+ }
47
+
3
48
  function createCircuitBreaker(main, options = {}) {
4
49
  const {
5
- errorIsFailure = () => true,
6
- failureThreshold = 1,
7
- fallback = () => Promise.reject(failureCause),
50
+ errorIsFailure = () => false,
51
+ errorThreshold = 0,
52
+ errorWindow = 1e4,
53
+ minimumCandidates = 6,
8
54
  onClose,
9
55
  onOpen,
10
- resetAfter = 3e4
56
+ resetAfter = 3e4,
57
+ retryDelay = () => {
58
+ }
11
59
  } = options;
60
+ const controller = new AbortController();
61
+ const history = /* @__PURE__ */ new Map();
62
+ const signal = controller.signal;
63
+ let failureCause;
64
+ let fallback = options.fallback || (() => Promise.reject(failureCause));
12
65
  let halfOpenPending;
66
+ let resetTimer;
13
67
  let state = "closed";
14
- let failureCause = void 0;
15
- let failureCount = 0;
16
- let resetTimer = void 0;
68
+ function clearFailure() {
69
+ failureCause = void 0;
70
+ }
71
+ function closeCircuit() {
72
+ state = "closed";
73
+ clearFailure();
74
+ clearTimeout(resetTimer);
75
+ onClose?.();
76
+ }
77
+ function failureRate() {
78
+ let failures = 0;
79
+ let total = 0;
80
+ for (const { status } of history.values()) {
81
+ if (status === "rejected") failures++;
82
+ if (status !== "pending") total++;
83
+ }
84
+ if (!total || total < minimumCandidates) return 0;
85
+ return failures / total;
86
+ }
17
87
  function openCircuit(cause) {
88
+ failureCause = cause;
18
89
  state = "open";
19
- onOpen?.(cause);
20
90
  clearTimeout(resetTimer);
21
91
  resetTimer = setTimeout(() => state = "halfOpen", resetAfter);
92
+ onOpen?.(cause);
22
93
  }
23
- function closeCircuit() {
24
- state = "closed";
25
- failureCause = void 0;
26
- failureCount = 0;
27
- clearTimeout(resetTimer);
94
+ function createHistoryItem(pending) {
95
+ const entry = { status: "pending", timer: void 0 };
96
+ const teardown = () => {
97
+ clearTimeout(entry.timer);
98
+ history.delete(pending);
99
+ signal.removeEventListener("abort", teardown);
100
+ };
101
+ signal.addEventListener("abort", teardown);
102
+ const settle = (value) => {
103
+ if (signal.aborted) return;
104
+ entry.status = value;
105
+ entry.timer = setTimeout(teardown, errorWindow);
106
+ };
107
+ history.set(pending, entry);
108
+ return { pending, settle, teardown };
28
109
  }
29
- async function protectedFunction(...args) {
110
+ function execute(attempt, args) {
30
111
  if (state === "closed") {
31
- return main(...args).catch((cause) => {
32
- if (state === "disposed") throw cause;
33
- failureCause = cause;
34
- failureCount += errorIsFailure(cause) ? 1 : 0;
35
- if (failureCount >= failureThreshold) openCircuit(cause);
36
- return protectedFunction(...args);
37
- });
112
+ const { pending, settle, teardown } = createHistoryItem(main(...args));
113
+ return pending.then(
114
+ (result) => {
115
+ settle("resolved");
116
+ return result;
117
+ },
118
+ async (cause) => {
119
+ if (signal.aborted || errorIsFailure(cause)) {
120
+ teardown();
121
+ throw cause;
122
+ }
123
+ settle("rejected");
124
+ if (failureRate() > errorThreshold) openCircuit(cause);
125
+ const next = attempt + 1;
126
+ await rejectOnAbort(signal, retryDelay(next, signal));
127
+ return execute(next, args);
128
+ }
129
+ );
38
130
  } else if (state === "open" || halfOpenPending) {
39
131
  return fallback(...args);
40
132
  } else if (state === "halfOpen") {
41
133
  return (halfOpenPending = main(...args)).finally(() => halfOpenPending = void 0).then(
42
134
  (result) => {
43
- if (state !== "disposed") {
44
- closeCircuit();
45
- onClose?.();
46
- }
135
+ if (signal.aborted) return result;
136
+ closeCircuit();
47
137
  return result;
48
138
  },
49
- (cause) => {
50
- if (state === "disposed") throw cause;
139
+ async (cause) => {
140
+ if (signal.aborted || errorIsFailure(cause)) throw cause;
51
141
  openCircuit(cause);
52
- return fallback(...args);
142
+ const next = attempt + 1;
143
+ await rejectOnAbort(signal, retryDelay(next, signal));
144
+ return execute(next, args);
53
145
  }
54
146
  );
55
- } else if (state === "disposed") {
56
- throw new Error("Circuit breaker has been disposed");
57
- } else {
58
- throw void 0;
59
147
  }
148
+ return assertNever(state);
60
149
  }
61
- protectedFunction.dispose = () => {
62
- closeCircuit();
63
- state = "disposed";
64
- };
65
- protectedFunction.getLatestError = () => failureCause;
66
- protectedFunction.getState = () => state;
67
- return protectedFunction;
150
+ return Object.assign((...args) => execute(1, args), {
151
+ dispose: (disposeMessage = "ERR_CIRCUIT_BREAKER_DISPOSED") => {
152
+ const reason = new ReferenceError(disposeMessage);
153
+ clearFailure();
154
+ clearTimeout(resetTimer);
155
+ history.forEach((entry) => clearTimeout(entry.timer));
156
+ history.clear();
157
+ fallback = () => Promise.reject(reason);
158
+ state = "open";
159
+ controller.abort(reason);
160
+ },
161
+ getLatestError: () => failureCause,
162
+ getState: () => state
163
+ });
68
164
  }
69
165
 
70
166
  exports.createCircuitBreaker = createCircuitBreaker;
167
+ exports.delayMs = delayMs;
168
+ exports.useExponentialBackoff = useExponentialBackoff;
169
+ exports.useFibonacciBackoff = useFibonacciBackoff;
170
+ exports.withTimeout = withTimeout;
71
171
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":["../lib/index.ts"],"sourcesContent":["import { type AnyFn, assertNever } from \"./util.js\"\n\nexport type CircuitState = \"closed\" | \"disposed\" | \"halfOpen\" | \"open\"\n\nexport interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {\n\t/**\n\t * Whether an error should be considered a failure that could trigger\n\t * the circuit breaker. Use this to prevent certain errors from\n\t * incrementing the failure count.\n\t *\n\t * @default () => true // Every error is considered a failure\n\t */\n\terrorIsFailure?: (error: unknown) => boolean\n\n\t/**\n\t * The number of failures before the circuit breaker opens.\n\t *\n\t * @default 1 // The first error opens the circuit\n\t */\n\tfailureThreshold?: 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/** Called when the circuit breaker is closed */\n\tonClose?: () => void\n\n\t/** Called when the circuit breaker is opened */\n\tonOpen?: (cause: unknown) => void\n\n\t/**\n\t * The amount of time to wait before allowing a half-open state.\n\t *\n\t * @default 30_000 // 30 seconds\n\t */\n\tresetAfter?: number\n}\n\nexport interface CircuitBreakerProtectedFn<\n\tRet = unknown,\n\tArgs extends unknown[] = never[]\n> {\n\t(...args: Args): Promise<Ret>\n\n\t/** Free memory and stop timers */\n\tdispose(): void\n\n\t/** Get the last error which triggered the circuit breaker */\n\tgetLatestError(): unknown | undefined\n\n\t/** Get the current state of the circuit breaker */\n\tgetState(): CircuitState\n}\n\nexport function createCircuitBreaker<\n\tRet,\n\tArgs extends unknown[],\n\tFallback extends AnyFn = (...args: Args) => Promise<Ret>\n>(\n\tmain: (...args: Args) => Promise<Ret>,\n\toptions: CircuitBreakerOptions<Fallback> = {}\n): CircuitBreakerProtectedFn<Ret, Args> {\n\tconst {\n\t\terrorIsFailure = () => true,\n\t\tfailureThreshold = 1,\n\t\tfallback = () => Promise.reject(failureCause),\n\t\tonClose,\n\t\tonOpen,\n\t\tresetAfter = 30_000,\n\t} = options\n\tlet halfOpenPending: Promise<unknown> | undefined\n\tlet state: CircuitState = \"closed\"\n\tlet failureCause: unknown | undefined = undefined\n\tlet failureCount = 0\n\tlet resetTimer: NodeJS.Timeout | undefined = undefined\n\n\t/**\n\t * Break the circuit and wait for a reset\n\t */\n\tfunction openCircuit(cause: unknown) {\n\t\tstate = \"open\"\n\t\tonOpen?.(cause)\n\t\tclearTimeout(resetTimer)\n\t\tresetTimer = setTimeout(() => (state = \"halfOpen\"), resetAfter)\n\t}\n\n\t/**\n\t * Reset the circuit and resume normal operation\n\t */\n\tfunction closeCircuit() {\n\t\tstate = \"closed\"\n\t\tfailureCause = undefined\n\t\tfailureCount = 0\n\t\tclearTimeout(resetTimer)\n\t}\n\n\t/**\n\t * Wrap calls to `main` with circuit breaker logic\n\t */\n\tasync function protectedFunction(...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\treturn main(...args).catch((cause) => {\n\t\t\t\tif (state === \"disposed\") throw cause\n\t\t\t\tfailureCause = cause\n\t\t\t\tfailureCount += errorIsFailure(cause) ? 1 : 0\n\t\t\t\tif (failureCount >= failureThreshold) openCircuit(cause)\n\t\t\t\treturn protectedFunction(...args)\n\t\t\t})\n\t\t}\n\n\t\t// Use the fallback while the circuit is open\n\t\telse if (state === \"open\" || halfOpenPending) {\n\t\t\treturn fallback(...args)\n\t\t}\n\n\t\t// While the circuit is half-open, try the main function once. If it\n\t\t// succeeds, close the circuit and resume normal operation. If it fails,\n\t\t// re-open the 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 (state !== \"disposed\") {\n\t\t\t\t\t\t\tcloseCircuit()\n\t\t\t\t\t\t\tonClose?.()\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn result\n\t\t\t\t\t},\n\t\t\t\t\t(cause) => {\n\t\t\t\t\t\tif (state === \"disposed\") throw cause\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// Shutting down...\n\t\telse if (state === \"disposed\") {\n\t\t\tthrow new Error(\"Circuit breaker has been disposed\")\n\t\t\t/* v8 ignore next */\n\t\t}\n\n\t\t// exhaustive check\n\t\t/* v8 ignore next 5 */\n\t\telse {\n\t\t\tthrow process.env.NODE_ENV !== \"production\"\n\t\t\t\t? assertNever(state)\n\t\t\t\t: undefined\n\t\t}\n\t}\n\n\tprotectedFunction.dispose = () => {\n\t\tcloseCircuit()\n\t\tstate = \"disposed\"\n\t}\n\n\tprotectedFunction.getLatestError = () => failureCause\n\n\tprotectedFunction.getState = () => state\n\n\treturn protectedFunction\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","sources":["../lib/util.ts","../lib/helpers.ts","../lib/index.ts"],"sourcesContent":["export type AnyFn = (...args: any[]) => any\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\nexport const rejectOnAbort = <T>(signal: AbortSignal, pending: T) => {\n\tlet reject: (reason?: unknown) => void\n\treturn Promise.race([\n\t\tPromise.resolve(pending).finally(() =>\n\t\t\tsignal.removeEventListener(\"abort\", reject),\n\t\t),\n\t\tnew Promise<never>((_, reject_) => {\n\t\t\treject = reject_\n\t\t\tsignal.addEventListener(\"abort\", () => reject(signal.reason))\n\t\t}),\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 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 = () => false,\n\t\terrorThreshold = 0,\n\t\terrorWindow = 10_000,\n\t\tminimumCandidates = 6,\n\t\tonClose,\n\t\tonOpen,\n\t\tresetAfter = 30_000,\n\t\tretryDelay = () => {},\n\t} = 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(() => (state = \"halfOpen\"), 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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"}
package/dist/index.d.cts CHANGED
@@ -1,21 +1,32 @@
1
1
  type AnyFn = (...args: any[]) => any;
2
+ /**
3
+ * Returns a promise that resolves after the specified number of milliseconds.
4
+ */
5
+ declare const delayMs: (ms: number) => Promise<void>;
2
6
 
3
- type CircuitState = "closed" | "disposed" | "halfOpen" | "open";
7
+ type CircuitState = "closed" | "halfOpen" | "open";
4
8
  interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
5
9
  /**
6
- * Whether an error should be considered a failure that could trigger
7
- * the circuit breaker. Use this to prevent certain errors from
8
- * incrementing the failure count.
10
+ * Whether an error should be treated as non-retryable failure. When used and
11
+ * when an error is considered a failure, the error will be thrown to the
12
+ * caller.
9
13
  *
10
- * @default () => true // Every error is considered a failure
14
+ * @default () => false // Errors are retryable by default
11
15
  */
12
16
  errorIsFailure?: (error: unknown) => boolean;
13
17
  /**
14
- * The number of failures before the circuit breaker opens.
18
+ * The percentage of errors (as a number between 0 and 1) which must occur
19
+ * within the error window before the circuit breaker opens.
15
20
  *
16
- * @default 1 // The first error opens the circuit
21
+ * @default 0 // Any error opens the circuit
17
22
  */
18
- failureThreshold?: number;
23
+ errorThreshold?: number;
24
+ /**
25
+ * The sliding window of time in milliseconds over which errors are counted.
26
+ *
27
+ * @default 10_000 // 10 seconds
28
+ */
29
+ errorWindow?: number;
19
30
  /**
20
31
  * If provided, then all rejected calls to `main` will be forwarded to
21
32
  * this function instead.
@@ -23,27 +34,97 @@ interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
23
34
  * @default undefined // No fallback, errors are propagated
24
35
  */
25
36
  fallback?: Fallback;
26
- /** Called when the circuit breaker is closed */
37
+ /**
38
+ * The minimum number of calls that must be made before calculating the
39
+ * error rate and determining whether the circuit breaker should open based on
40
+ * the `errorThreshold`.
41
+ *
42
+ * @default 6
43
+ */
44
+ minimumCandidates?: number;
45
+ /**
46
+ * Provide a function to be called when the circuit breaker is closed.
47
+ */
27
48
  onClose?: () => void;
28
- /** Called when the circuit breaker is opened */
49
+ /**
50
+ * Provide a function to be called when the circuit breaker is opened. It
51
+ * receives the error as its only argument.
52
+ */
29
53
  onOpen?: (cause: unknown) => void;
30
54
  /**
31
- * The amount of time to wait before allowing a half-open state.
55
+ * The amount of time in milliseconds to wait before transitioning to a
56
+ * half-open state.
32
57
  *
33
58
  * @default 30_000 // 30 seconds
34
59
  */
35
60
  resetAfter?: number;
61
+ /**
62
+ * Provide a function which returns a promise that resolves when the next
63
+ * retry attempt should be made. Use this to implement custom retry logic,
64
+ * such as exponential backoff.
65
+ *
66
+ * Note that `attempt` always starts at 2, since first attempts are always
67
+ * made as soon as they come in.
68
+ *
69
+ * @default none // Retry errors immediately
70
+ * @example
71
+ * ```ts
72
+ * // Constant delay of 1 second for all retries
73
+ * const breaker = createCircuitBreaker(main, {
74
+ * retryDelay: () => delayMs(1_000),
75
+ * })
76
+ *
77
+ * // Double the previous delay each time, up to 30 seconds
78
+ * const breaker = createCircuitBreaker(main, {
79
+ * retryDelay: useExponentialBackoff(30),
80
+ * })
81
+ *
82
+ * // Use Fibonacci sequence for delay, up to 90 seconds
83
+ * const breaker = createCircuitBreaker(main, {
84
+ * retryDelay: useFibonacciBackoff(90),
85
+ * })
86
+ * ```
87
+ */
88
+ retryDelay?: (attempt: number, signal: AbortSignal) => Promise<void>;
36
89
  }
37
90
  interface CircuitBreakerProtectedFn<Ret = unknown, Args extends unknown[] = never[]> {
38
91
  (...args: Args): Promise<Ret>;
39
- /** Free memory and stop timers */
40
- dispose(): void;
92
+ /**
93
+ * Free memory and stop timers. All future calls will be rejected with the
94
+ * provided message.
95
+ *
96
+ * @default "ERR_CIRCUIT_BREAKER_DISPOSED"
97
+ */
98
+ dispose(disposeMessage?: string): void;
41
99
  /** Get the last error which triggered the circuit breaker */
42
100
  getLatestError(): unknown | undefined;
43
101
  /** Get the current state of the circuit breaker */
44
102
  getState(): CircuitState;
45
103
  }
46
- declare function createCircuitBreaker<Ret, Args extends unknown[], Fallback extends AnyFn = (...args: Args) => Promise<Ret>>(main: (...args: Args) => Promise<Ret>, options?: CircuitBreakerOptions<Fallback>): CircuitBreakerProtectedFn<Ret, Args>;
104
+ type MainFn<Ret = unknown, Args extends unknown[] = never[]> = (...args: Args) => Promise<Ret>;
105
+
106
+ /**
107
+ * Returns a function which implements exponential backoff.
108
+ *
109
+ * @param maxSeconds - The maximum number of seconds to wait before retrying.
110
+ * @returns A function which takes an `attempt` number and returns a promise
111
+ * that resolves after the calculated delay.
112
+ */
113
+ declare function useExponentialBackoff(maxSeconds: number): (attempt: number) => Promise<void>;
114
+ /**
115
+ * Returns a function which implements Fibonacci backoff.
116
+ *
117
+ * @param maxSeconds - The maximum number of seconds to wait before retrying.
118
+ * @returns A function which takes an `attempt` number and returns a promise
119
+ * that resolves after the calculated delay.
120
+ */
121
+ declare function useFibonacciBackoff(maxSeconds: number): (attempt: number) => Promise<void>;
122
+ /**
123
+ * Wrap a function with a timeout. If execution of `main` exceeds `timeoutMs`,
124
+ * then the call is rejected with `new Error(timeoutMessage)`.
125
+ */
126
+ declare function withTimeout<Ret, Args extends unknown[]>(main: MainFn<Ret, Args>, timeoutMs: number, timeoutMessage?: string): MainFn<Ret, Args>;
127
+
128
+ declare function createCircuitBreaker<Ret, Args extends unknown[], Fallback extends AnyFn = MainFn<Ret, Args>>(main: MainFn<Ret, Args>, options?: CircuitBreakerOptions<Fallback>): CircuitBreakerProtectedFn<Ret, Args>;
47
129
 
48
- export { createCircuitBreaker };
49
- export type { CircuitBreakerOptions, CircuitBreakerProtectedFn, CircuitState };
130
+ export { createCircuitBreaker, delayMs, useExponentialBackoff, useFibonacciBackoff, withTimeout };
package/dist/index.d.mts CHANGED
@@ -1,21 +1,32 @@
1
1
  type AnyFn = (...args: any[]) => any;
2
+ /**
3
+ * Returns a promise that resolves after the specified number of milliseconds.
4
+ */
5
+ declare const delayMs: (ms: number) => Promise<void>;
2
6
 
3
- type CircuitState = "closed" | "disposed" | "halfOpen" | "open";
7
+ type CircuitState = "closed" | "halfOpen" | "open";
4
8
  interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
5
9
  /**
6
- * Whether an error should be considered a failure that could trigger
7
- * the circuit breaker. Use this to prevent certain errors from
8
- * incrementing the failure count.
10
+ * Whether an error should be treated as non-retryable failure. When used and
11
+ * when an error is considered a failure, the error will be thrown to the
12
+ * caller.
9
13
  *
10
- * @default () => true // Every error is considered a failure
14
+ * @default () => false // Errors are retryable by default
11
15
  */
12
16
  errorIsFailure?: (error: unknown) => boolean;
13
17
  /**
14
- * The number of failures before the circuit breaker opens.
18
+ * The percentage of errors (as a number between 0 and 1) which must occur
19
+ * within the error window before the circuit breaker opens.
15
20
  *
16
- * @default 1 // The first error opens the circuit
21
+ * @default 0 // Any error opens the circuit
17
22
  */
18
- failureThreshold?: number;
23
+ errorThreshold?: number;
24
+ /**
25
+ * The sliding window of time in milliseconds over which errors are counted.
26
+ *
27
+ * @default 10_000 // 10 seconds
28
+ */
29
+ errorWindow?: number;
19
30
  /**
20
31
  * If provided, then all rejected calls to `main` will be forwarded to
21
32
  * this function instead.
@@ -23,27 +34,97 @@ interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
23
34
  * @default undefined // No fallback, errors are propagated
24
35
  */
25
36
  fallback?: Fallback;
26
- /** Called when the circuit breaker is closed */
37
+ /**
38
+ * The minimum number of calls that must be made before calculating the
39
+ * error rate and determining whether the circuit breaker should open based on
40
+ * the `errorThreshold`.
41
+ *
42
+ * @default 6
43
+ */
44
+ minimumCandidates?: number;
45
+ /**
46
+ * Provide a function to be called when the circuit breaker is closed.
47
+ */
27
48
  onClose?: () => void;
28
- /** Called when the circuit breaker is opened */
49
+ /**
50
+ * Provide a function to be called when the circuit breaker is opened. It
51
+ * receives the error as its only argument.
52
+ */
29
53
  onOpen?: (cause: unknown) => void;
30
54
  /**
31
- * The amount of time to wait before allowing a half-open state.
55
+ * The amount of time in milliseconds to wait before transitioning to a
56
+ * half-open state.
32
57
  *
33
58
  * @default 30_000 // 30 seconds
34
59
  */
35
60
  resetAfter?: number;
61
+ /**
62
+ * Provide a function which returns a promise that resolves when the next
63
+ * retry attempt should be made. Use this to implement custom retry logic,
64
+ * such as exponential backoff.
65
+ *
66
+ * Note that `attempt` always starts at 2, since first attempts are always
67
+ * made as soon as they come in.
68
+ *
69
+ * @default none // Retry errors immediately
70
+ * @example
71
+ * ```ts
72
+ * // Constant delay of 1 second for all retries
73
+ * const breaker = createCircuitBreaker(main, {
74
+ * retryDelay: () => delayMs(1_000),
75
+ * })
76
+ *
77
+ * // Double the previous delay each time, up to 30 seconds
78
+ * const breaker = createCircuitBreaker(main, {
79
+ * retryDelay: useExponentialBackoff(30),
80
+ * })
81
+ *
82
+ * // Use Fibonacci sequence for delay, up to 90 seconds
83
+ * const breaker = createCircuitBreaker(main, {
84
+ * retryDelay: useFibonacciBackoff(90),
85
+ * })
86
+ * ```
87
+ */
88
+ retryDelay?: (attempt: number, signal: AbortSignal) => Promise<void>;
36
89
  }
37
90
  interface CircuitBreakerProtectedFn<Ret = unknown, Args extends unknown[] = never[]> {
38
91
  (...args: Args): Promise<Ret>;
39
- /** Free memory and stop timers */
40
- dispose(): void;
92
+ /**
93
+ * Free memory and stop timers. All future calls will be rejected with the
94
+ * provided message.
95
+ *
96
+ * @default "ERR_CIRCUIT_BREAKER_DISPOSED"
97
+ */
98
+ dispose(disposeMessage?: string): void;
41
99
  /** Get the last error which triggered the circuit breaker */
42
100
  getLatestError(): unknown | undefined;
43
101
  /** Get the current state of the circuit breaker */
44
102
  getState(): CircuitState;
45
103
  }
46
- declare function createCircuitBreaker<Ret, Args extends unknown[], Fallback extends AnyFn = (...args: Args) => Promise<Ret>>(main: (...args: Args) => Promise<Ret>, options?: CircuitBreakerOptions<Fallback>): CircuitBreakerProtectedFn<Ret, Args>;
104
+ type MainFn<Ret = unknown, Args extends unknown[] = never[]> = (...args: Args) => Promise<Ret>;
105
+
106
+ /**
107
+ * Returns a function which implements exponential backoff.
108
+ *
109
+ * @param maxSeconds - The maximum number of seconds to wait before retrying.
110
+ * @returns A function which takes an `attempt` number and returns a promise
111
+ * that resolves after the calculated delay.
112
+ */
113
+ declare function useExponentialBackoff(maxSeconds: number): (attempt: number) => Promise<void>;
114
+ /**
115
+ * Returns a function which implements Fibonacci backoff.
116
+ *
117
+ * @param maxSeconds - The maximum number of seconds to wait before retrying.
118
+ * @returns A function which takes an `attempt` number and returns a promise
119
+ * that resolves after the calculated delay.
120
+ */
121
+ declare function useFibonacciBackoff(maxSeconds: number): (attempt: number) => Promise<void>;
122
+ /**
123
+ * Wrap a function with a timeout. If execution of `main` exceeds `timeoutMs`,
124
+ * then the call is rejected with `new Error(timeoutMessage)`.
125
+ */
126
+ declare function withTimeout<Ret, Args extends unknown[]>(main: MainFn<Ret, Args>, timeoutMs: number, timeoutMessage?: string): MainFn<Ret, Args>;
127
+
128
+ declare function createCircuitBreaker<Ret, Args extends unknown[], Fallback extends AnyFn = MainFn<Ret, Args>>(main: MainFn<Ret, Args>, options?: CircuitBreakerOptions<Fallback>): CircuitBreakerProtectedFn<Ret, Args>;
47
129
 
48
- export { createCircuitBreaker };
49
- export type { CircuitBreakerOptions, CircuitBreakerProtectedFn, CircuitState };
130
+ export { createCircuitBreaker, delayMs, useExponentialBackoff, useFibonacciBackoff, withTimeout };
package/dist/index.mjs CHANGED
@@ -1,69 +1,165 @@
1
+ const assertNever = (val, msg = "Unexpected value") => {
2
+ throw new TypeError(`${msg}: ${val}`);
3
+ };
4
+ const delayMs = (ms) => new Promise((next) => setTimeout(next, ms));
5
+ const rejectOnAbort = (signal, pending) => {
6
+ let reject;
7
+ return Promise.race([
8
+ Promise.resolve(pending).finally(
9
+ () => signal.removeEventListener("abort", reject)
10
+ ),
11
+ new Promise((_, reject_) => {
12
+ reject = reject_;
13
+ signal.addEventListener("abort", () => reject(signal.reason));
14
+ })
15
+ ]);
16
+ };
17
+
18
+ function useExponentialBackoff(maxSeconds) {
19
+ return function exponentialBackoff(attempt) {
20
+ const num = Math.max(attempt - 2, 0);
21
+ const delay = Math.min(2 ** num, maxSeconds);
22
+ return delayMs(delay * 1e3);
23
+ };
24
+ }
25
+ const sqrt5 = /* @__PURE__ */ Math.sqrt(5);
26
+ const binet = (n) => Math.round(((1 + sqrt5) ** n - (1 - sqrt5) ** n) / (2 ** n * sqrt5));
27
+ function useFibonacciBackoff(maxSeconds) {
28
+ return function fibonacciBackoff(attempt) {
29
+ const delay = Math.min(binet(attempt), maxSeconds);
30
+ return delayMs(delay * 1e3);
31
+ };
32
+ }
33
+ function withTimeout(main, timeoutMs, timeoutMessage = "ERR_CIRCUIT_BREAKER_TIMEOUT") {
34
+ const error = new Error(timeoutMessage);
35
+ return function withTimeoutFunction(...args) {
36
+ let timer;
37
+ return Promise.race([
38
+ main(...args).finally(() => clearTimeout(timer)),
39
+ new Promise((_, reject) => {
40
+ timer = setTimeout(reject, timeoutMs, error);
41
+ })
42
+ ]);
43
+ };
44
+ }
45
+
1
46
  function createCircuitBreaker(main, options = {}) {
2
47
  const {
3
- errorIsFailure = () => true,
4
- failureThreshold = 1,
5
- fallback = () => Promise.reject(failureCause),
48
+ errorIsFailure = () => false,
49
+ errorThreshold = 0,
50
+ errorWindow = 1e4,
51
+ minimumCandidates = 6,
6
52
  onClose,
7
53
  onOpen,
8
- resetAfter = 3e4
54
+ resetAfter = 3e4,
55
+ retryDelay = () => {
56
+ }
9
57
  } = options;
58
+ const controller = new AbortController();
59
+ const history = /* @__PURE__ */ new Map();
60
+ const signal = controller.signal;
61
+ let failureCause;
62
+ let fallback = options.fallback || (() => Promise.reject(failureCause));
10
63
  let halfOpenPending;
64
+ let resetTimer;
11
65
  let state = "closed";
12
- let failureCause = void 0;
13
- let failureCount = 0;
14
- let resetTimer = void 0;
66
+ function clearFailure() {
67
+ failureCause = void 0;
68
+ }
69
+ function closeCircuit() {
70
+ state = "closed";
71
+ clearFailure();
72
+ clearTimeout(resetTimer);
73
+ onClose?.();
74
+ }
75
+ function failureRate() {
76
+ let failures = 0;
77
+ let total = 0;
78
+ for (const { status } of history.values()) {
79
+ if (status === "rejected") failures++;
80
+ if (status !== "pending") total++;
81
+ }
82
+ if (!total || total < minimumCandidates) return 0;
83
+ return failures / total;
84
+ }
15
85
  function openCircuit(cause) {
86
+ failureCause = cause;
16
87
  state = "open";
17
- onOpen?.(cause);
18
88
  clearTimeout(resetTimer);
19
89
  resetTimer = setTimeout(() => state = "halfOpen", resetAfter);
90
+ onOpen?.(cause);
20
91
  }
21
- function closeCircuit() {
22
- state = "closed";
23
- failureCause = void 0;
24
- failureCount = 0;
25
- clearTimeout(resetTimer);
92
+ function createHistoryItem(pending) {
93
+ const entry = { status: "pending", timer: void 0 };
94
+ const teardown = () => {
95
+ clearTimeout(entry.timer);
96
+ history.delete(pending);
97
+ signal.removeEventListener("abort", teardown);
98
+ };
99
+ signal.addEventListener("abort", teardown);
100
+ const settle = (value) => {
101
+ if (signal.aborted) return;
102
+ entry.status = value;
103
+ entry.timer = setTimeout(teardown, errorWindow);
104
+ };
105
+ history.set(pending, entry);
106
+ return { pending, settle, teardown };
26
107
  }
27
- async function protectedFunction(...args) {
108
+ function execute(attempt, args) {
28
109
  if (state === "closed") {
29
- return main(...args).catch((cause) => {
30
- if (state === "disposed") throw cause;
31
- failureCause = cause;
32
- failureCount += errorIsFailure(cause) ? 1 : 0;
33
- if (failureCount >= failureThreshold) openCircuit(cause);
34
- return protectedFunction(...args);
35
- });
110
+ const { pending, settle, teardown } = createHistoryItem(main(...args));
111
+ return pending.then(
112
+ (result) => {
113
+ settle("resolved");
114
+ return result;
115
+ },
116
+ async (cause) => {
117
+ if (signal.aborted || errorIsFailure(cause)) {
118
+ teardown();
119
+ throw cause;
120
+ }
121
+ settle("rejected");
122
+ if (failureRate() > errorThreshold) openCircuit(cause);
123
+ const next = attempt + 1;
124
+ await rejectOnAbort(signal, retryDelay(next, signal));
125
+ return execute(next, args);
126
+ }
127
+ );
36
128
  } else if (state === "open" || halfOpenPending) {
37
129
  return fallback(...args);
38
130
  } else if (state === "halfOpen") {
39
131
  return (halfOpenPending = main(...args)).finally(() => halfOpenPending = void 0).then(
40
132
  (result) => {
41
- if (state !== "disposed") {
42
- closeCircuit();
43
- onClose?.();
44
- }
133
+ if (signal.aborted) return result;
134
+ closeCircuit();
45
135
  return result;
46
136
  },
47
- (cause) => {
48
- if (state === "disposed") throw cause;
137
+ async (cause) => {
138
+ if (signal.aborted || errorIsFailure(cause)) throw cause;
49
139
  openCircuit(cause);
50
- return fallback(...args);
140
+ const next = attempt + 1;
141
+ await rejectOnAbort(signal, retryDelay(next, signal));
142
+ return execute(next, args);
51
143
  }
52
144
  );
53
- } else if (state === "disposed") {
54
- throw new Error("Circuit breaker has been disposed");
55
- } else {
56
- throw void 0;
57
145
  }
146
+ return assertNever(state);
58
147
  }
59
- protectedFunction.dispose = () => {
60
- closeCircuit();
61
- state = "disposed";
62
- };
63
- protectedFunction.getLatestError = () => failureCause;
64
- protectedFunction.getState = () => state;
65
- return protectedFunction;
148
+ return Object.assign((...args) => execute(1, args), {
149
+ dispose: (disposeMessage = "ERR_CIRCUIT_BREAKER_DISPOSED") => {
150
+ const reason = new ReferenceError(disposeMessage);
151
+ clearFailure();
152
+ clearTimeout(resetTimer);
153
+ history.forEach((entry) => clearTimeout(entry.timer));
154
+ history.clear();
155
+ fallback = () => Promise.reject(reason);
156
+ state = "open";
157
+ controller.abort(reason);
158
+ },
159
+ getLatestError: () => failureCause,
160
+ getState: () => state
161
+ });
66
162
  }
67
163
 
68
- export { createCircuitBreaker };
164
+ export { createCircuitBreaker, delayMs, useExponentialBackoff, useFibonacciBackoff, withTimeout };
69
165
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":["../lib/index.ts"],"sourcesContent":["import { type AnyFn, assertNever } from \"./util.js\"\n\nexport type CircuitState = \"closed\" | \"disposed\" | \"halfOpen\" | \"open\"\n\nexport interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {\n\t/**\n\t * Whether an error should be considered a failure that could trigger\n\t * the circuit breaker. Use this to prevent certain errors from\n\t * incrementing the failure count.\n\t *\n\t * @default () => true // Every error is considered a failure\n\t */\n\terrorIsFailure?: (error: unknown) => boolean\n\n\t/**\n\t * The number of failures before the circuit breaker opens.\n\t *\n\t * @default 1 // The first error opens the circuit\n\t */\n\tfailureThreshold?: 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/** Called when the circuit breaker is closed */\n\tonClose?: () => void\n\n\t/** Called when the circuit breaker is opened */\n\tonOpen?: (cause: unknown) => void\n\n\t/**\n\t * The amount of time to wait before allowing a half-open state.\n\t *\n\t * @default 30_000 // 30 seconds\n\t */\n\tresetAfter?: number\n}\n\nexport interface CircuitBreakerProtectedFn<\n\tRet = unknown,\n\tArgs extends unknown[] = never[]\n> {\n\t(...args: Args): Promise<Ret>\n\n\t/** Free memory and stop timers */\n\tdispose(): void\n\n\t/** Get the last error which triggered the circuit breaker */\n\tgetLatestError(): unknown | undefined\n\n\t/** Get the current state of the circuit breaker */\n\tgetState(): CircuitState\n}\n\nexport function createCircuitBreaker<\n\tRet,\n\tArgs extends unknown[],\n\tFallback extends AnyFn = (...args: Args) => Promise<Ret>\n>(\n\tmain: (...args: Args) => Promise<Ret>,\n\toptions: CircuitBreakerOptions<Fallback> = {}\n): CircuitBreakerProtectedFn<Ret, Args> {\n\tconst {\n\t\terrorIsFailure = () => true,\n\t\tfailureThreshold = 1,\n\t\tfallback = () => Promise.reject(failureCause),\n\t\tonClose,\n\t\tonOpen,\n\t\tresetAfter = 30_000,\n\t} = options\n\tlet halfOpenPending: Promise<unknown> | undefined\n\tlet state: CircuitState = \"closed\"\n\tlet failureCause: unknown | undefined = undefined\n\tlet failureCount = 0\n\tlet resetTimer: NodeJS.Timeout | undefined = undefined\n\n\t/**\n\t * Break the circuit and wait for a reset\n\t */\n\tfunction openCircuit(cause: unknown) {\n\t\tstate = \"open\"\n\t\tonOpen?.(cause)\n\t\tclearTimeout(resetTimer)\n\t\tresetTimer = setTimeout(() => (state = \"halfOpen\"), resetAfter)\n\t}\n\n\t/**\n\t * Reset the circuit and resume normal operation\n\t */\n\tfunction closeCircuit() {\n\t\tstate = \"closed\"\n\t\tfailureCause = undefined\n\t\tfailureCount = 0\n\t\tclearTimeout(resetTimer)\n\t}\n\n\t/**\n\t * Wrap calls to `main` with circuit breaker logic\n\t */\n\tasync function protectedFunction(...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\treturn main(...args).catch((cause) => {\n\t\t\t\tif (state === \"disposed\") throw cause\n\t\t\t\tfailureCause = cause\n\t\t\t\tfailureCount += errorIsFailure(cause) ? 1 : 0\n\t\t\t\tif (failureCount >= failureThreshold) openCircuit(cause)\n\t\t\t\treturn protectedFunction(...args)\n\t\t\t})\n\t\t}\n\n\t\t// Use the fallback while the circuit is open\n\t\telse if (state === \"open\" || halfOpenPending) {\n\t\t\treturn fallback(...args)\n\t\t}\n\n\t\t// While the circuit is half-open, try the main function once. If it\n\t\t// succeeds, close the circuit and resume normal operation. If it fails,\n\t\t// re-open the 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 (state !== \"disposed\") {\n\t\t\t\t\t\t\tcloseCircuit()\n\t\t\t\t\t\t\tonClose?.()\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn result\n\t\t\t\t\t},\n\t\t\t\t\t(cause) => {\n\t\t\t\t\t\tif (state === \"disposed\") throw cause\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// Shutting down...\n\t\telse if (state === \"disposed\") {\n\t\t\tthrow new Error(\"Circuit breaker has been disposed\")\n\t\t\t/* v8 ignore next */\n\t\t}\n\n\t\t// exhaustive check\n\t\t/* v8 ignore next 5 */\n\t\telse {\n\t\t\tthrow process.env.NODE_ENV !== \"production\"\n\t\t\t\t? assertNever(state)\n\t\t\t\t: undefined\n\t\t}\n\t}\n\n\tprotectedFunction.dispose = () => {\n\t\tcloseCircuit()\n\t\tstate = \"disposed\"\n\t}\n\n\tprotectedFunction.getLatestError = () => failureCause\n\n\tprotectedFunction.getState = () => state\n\n\treturn protectedFunction\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","sources":["../lib/util.ts","../lib/helpers.ts","../lib/index.ts"],"sourcesContent":["export type AnyFn = (...args: any[]) => any\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\nexport const rejectOnAbort = <T>(signal: AbortSignal, pending: T) => {\n\tlet reject: (reason?: unknown) => void\n\treturn Promise.race([\n\t\tPromise.resolve(pending).finally(() =>\n\t\t\tsignal.removeEventListener(\"abort\", reject),\n\t\t),\n\t\tnew Promise<never>((_, reject_) => {\n\t\t\treject = reject_\n\t\t\tsignal.addEventListener(\"abort\", () => reject(signal.reason))\n\t\t}),\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 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 = () => false,\n\t\terrorThreshold = 0,\n\t\terrorWindow = 10_000,\n\t\tminimumCandidates = 6,\n\t\tonClose,\n\t\tonOpen,\n\t\tresetAfter = 30_000,\n\t\tretryDelay = () => {},\n\t} = 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(() => (state = \"halfOpen\"), 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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"}
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "breaker-box",
3
- "version": "2.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "A zero-dependency circuit breaker implementation for Node.js",
5
- "repository": "github:sirlancelot/breaker-box",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/sirlancelot/breaker-box.git"
8
+ },
6
9
  "main": "./dist/index.cjs",
7
10
  "module": "./dist/index.mjs",
8
11
  "types": "./dist/index.d.cts",
@@ -19,10 +22,13 @@
19
22
  "files": [
20
23
  "dist"
21
24
  ],
25
+ "type": "module",
22
26
  "scripts": {
23
27
  "build": "pkgroll --clean-dist --src lib --target=node18 --sourcemap --env.NODE_ENV=production",
24
28
  "dev": "vitest",
29
+ "format": "NODE_OPTIONS='--experimental-strip-types' prettier . --write",
25
30
  "prepublishOnly": "npm run build",
31
+ "reinstall": "rm -rf node_modules package-lock.json && npm install",
26
32
  "test": "vitest --run"
27
33
  },
28
34
  "keywords": [
@@ -37,7 +43,7 @@
37
43
  "@types/node": "24.3.1",
38
44
  "@vitest/coverage-v8": "3.2.4",
39
45
  "pkgroll": "2.15.4",
40
- "tsx": "4.20.5",
46
+ "prettier": "3.6.2",
41
47
  "typescript": "5.9.2",
42
48
  "vitest": "3.2.4",
43
49
  "vitest-when": "0.8.0"