breaker-box 3.0.0 → 4.1.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,28 +1,151 @@
1
1
  'use strict';
2
2
 
3
+ function assert(value, message) {
4
+ if (!value) throw new TypeError(message);
5
+ }
6
+ function parseOptions(options) {
7
+ const {
8
+ errorIsFailure = () => false,
9
+ errorThreshold = 0,
10
+ errorWindow = 1e4,
11
+ minimumCandidates = 6,
12
+ onClose,
13
+ onOpen,
14
+ resetAfter = 3e4,
15
+ retryDelay = () => void 0
16
+ } = options;
17
+ assert(
18
+ typeof errorIsFailure === "function",
19
+ `"errorIsFailure" must be a function (received ${typeof errorIsFailure})`
20
+ );
21
+ assert(
22
+ errorThreshold >= 0 && errorThreshold <= 1,
23
+ `"errorThreshold" must be a number between 0 and 1 (received ${errorThreshold})`
24
+ );
25
+ assert(
26
+ errorWindow > 0,
27
+ `"errorWindow" must be milliseconds greater than 0 (received ${errorWindow})`
28
+ );
29
+ assert(
30
+ minimumCandidates > 1,
31
+ `"minimumCandidates" must be a number greater than 1 (received ${minimumCandidates})`
32
+ );
33
+ if (onClose)
34
+ assert(
35
+ typeof onClose === "function",
36
+ `"onClose" must be a function (received ${typeof onClose})`
37
+ );
38
+ if (onOpen)
39
+ assert(
40
+ typeof onOpen === "function",
41
+ `"onOpen" must be a function (received ${typeof onOpen})`
42
+ );
43
+ assert(
44
+ resetAfter > 0,
45
+ `"resetAfter" must be milliseconds greater than 0 (received ${resetAfter})`
46
+ );
47
+ assert(
48
+ resetAfter >= errorWindow,
49
+ `"resetAfter" must be milliseconds greater than or equal to "errorWindow" (received ${resetAfter})`
50
+ );
51
+ assert(
52
+ typeof retryDelay === "function",
53
+ `"retryDelay" must be a function (received ${typeof retryDelay})`
54
+ );
55
+ return {
56
+ errorIsFailure,
57
+ errorThreshold,
58
+ errorWindow,
59
+ minimumCandidates,
60
+ onClose,
61
+ onOpen,
62
+ resetAfter,
63
+ retryDelay
64
+ };
65
+ }
66
+
3
67
  const assertNever = (val, msg = "Unexpected value") => {
4
68
  throw new TypeError(`${msg}: ${val}`);
5
69
  };
6
- const resolvedPromise = Promise.resolve();
7
- const nextTick = (fn) => resolvedPromise.then(fn);
70
+ const delayMs = (ms) => new Promise((next) => setTimeout(next, ms));
71
+ const rejectOnAbort = (signal, pending) => {
72
+ let teardown;
73
+ return Promise.race([
74
+ Promise.resolve(pending).finally(
75
+ () => signal.removeEventListener("abort", teardown)
76
+ ),
77
+ new Promise((_, reject) => {
78
+ teardown = () => reject(signal.reason);
79
+ signal.addEventListener("abort", teardown);
80
+ })
81
+ ]);
82
+ };
83
+
84
+ function useExponentialBackoff(maxSeconds) {
85
+ return function exponentialBackoff(attempt) {
86
+ const num = Math.max(attempt - 2, 0);
87
+ const delay = Math.min(2 ** num, maxSeconds);
88
+ return delayMs(delay * 1e3);
89
+ };
90
+ }
91
+ const sqrt5 = /* @__PURE__ */ Math.sqrt(5);
92
+ const binet = (n) => Math.round(((1 + sqrt5) ** n - (1 - sqrt5) ** n) / (2 ** n * sqrt5));
93
+ function useFibonacciBackoff(maxSeconds) {
94
+ return function fibonacciBackoff(attempt) {
95
+ const delay = Math.min(binet(attempt), maxSeconds);
96
+ return delayMs(delay * 1e3);
97
+ };
98
+ }
99
+ function withTimeout(main, timeoutMs, timeoutMessage = "ERR_CIRCUIT_BREAKER_TIMEOUT") {
100
+ const error = new Error(timeoutMessage);
101
+ return function withTimeoutFunction(...args) {
102
+ let timer;
103
+ return Promise.race([
104
+ main(...args).finally(() => clearTimeout(timer)),
105
+ new Promise((_, reject) => {
106
+ timer = setTimeout(reject, timeoutMs, error);
107
+ })
108
+ ]);
109
+ };
110
+ }
8
111
 
9
112
  function createCircuitBreaker(main, options = {}) {
10
113
  const {
11
- errorIsFailure = () => true,
12
- failureThreshold = 1,
114
+ errorIsFailure,
115
+ errorThreshold,
116
+ errorWindow,
117
+ minimumCandidates,
13
118
  onClose,
14
119
  onOpen,
15
- resetAfter = 3e4
16
- } = options;
120
+ resetAfter,
121
+ retryDelay
122
+ } = parseOptions(options);
123
+ const controller = new AbortController();
124
+ const history = /* @__PURE__ */ new Map();
125
+ const signal = controller.signal;
126
+ let failureCause;
17
127
  let fallback = options.fallback || (() => Promise.reject(failureCause));
18
128
  let halfOpenPending;
19
- let state = "closed";
20
- let failureCause;
21
- let failureCount = 0;
22
129
  let resetTimer;
130
+ let state = "closed";
23
131
  function clearFailure() {
24
132
  failureCause = void 0;
25
- failureCount = 0;
133
+ }
134
+ function closeCircuit() {
135
+ state = "closed";
136
+ clearFailure();
137
+ clearTimeout(resetTimer);
138
+ onClose?.();
139
+ }
140
+ function failureRate() {
141
+ let failures = 0;
142
+ let total = 0;
143
+ for (const { status } of history.values()) {
144
+ if (status === "rejected") failures++;
145
+ if (status !== "pending") total++;
146
+ }
147
+ if (!total || total < minimumCandidates) return 0;
148
+ return failures / total;
26
149
  }
27
150
  function openCircuit(cause) {
28
151
  failureCause = cause;
@@ -31,53 +154,81 @@ function createCircuitBreaker(main, options = {}) {
31
154
  resetTimer = setTimeout(() => state = "halfOpen", resetAfter);
32
155
  onOpen?.(cause);
33
156
  }
34
- function protectedFunction(...args) {
157
+ function createHistoryItem(pending) {
158
+ const entry = { status: "pending", timer: void 0 };
159
+ const teardown = () => {
160
+ clearTimeout(entry.timer);
161
+ history.delete(pending);
162
+ signal.removeEventListener("abort", teardown);
163
+ };
164
+ signal.addEventListener("abort", teardown);
165
+ const settle = (value) => {
166
+ if (signal.aborted) return;
167
+ entry.status = value;
168
+ entry.timer = setTimeout(teardown, errorWindow);
169
+ };
170
+ history.set(pending, entry);
171
+ return { pending, settle, teardown };
172
+ }
173
+ function execute(attempt, args) {
35
174
  if (state === "closed") {
36
- const thisFallback = fallback;
37
- return main(...args).then(
175
+ const { pending, settle, teardown } = createHistoryItem(main(...args));
176
+ return pending.then(
38
177
  (result) => {
39
- if (state === "closed") clearFailure();
178
+ settle("resolved");
40
179
  return result;
41
180
  },
42
- (cause) => {
43
- if (thisFallback !== fallback) throw cause;
44
- failureCount += errorIsFailure(cause) ? 1 : 0;
45
- if (failureCount === failureThreshold) openCircuit(cause);
46
- return nextTick(() => protectedFunction(...args));
181
+ async (cause) => {
182
+ if (signal.aborted || errorIsFailure(cause)) {
183
+ teardown();
184
+ throw cause;
185
+ }
186
+ settle("rejected");
187
+ if (failureRate() > errorThreshold) openCircuit(cause);
188
+ const next = attempt + 1;
189
+ await rejectOnAbort(signal, retryDelay(next, signal));
190
+ return execute(next, args);
47
191
  }
48
192
  );
49
193
  } else if (state === "open" || halfOpenPending) {
50
194
  return fallback(...args);
51
195
  } else if (state === "halfOpen") {
52
- const thisFallback = fallback;
53
196
  return (halfOpenPending = main(...args)).finally(() => halfOpenPending = void 0).then(
54
197
  (result) => {
55
- if (thisFallback !== fallback) return result;
56
- state = "closed";
57
- clearFailure();
58
- clearTimeout(resetTimer);
59
- onClose?.();
198
+ if (signal.aborted) return result;
199
+ closeCircuit();
60
200
  return result;
61
201
  },
62
- (cause) => {
63
- if (thisFallback !== fallback) throw cause;
202
+ async (cause) => {
203
+ if (signal.aborted || errorIsFailure(cause)) throw cause;
64
204
  openCircuit(cause);
65
- return nextTick(() => protectedFunction(...args));
205
+ const next = attempt + 1;
206
+ await rejectOnAbort(signal, retryDelay(next, signal));
207
+ return execute(next, args);
66
208
  }
67
209
  );
68
210
  }
69
211
  return assertNever(state);
70
212
  }
71
- protectedFunction.dispose = () => {
72
- clearFailure();
73
- clearTimeout(resetTimer);
74
- fallback = () => Promise.reject(new ReferenceError("ERR_CIRCUIT_BREAKER_DISPOSED"));
75
- state = "open";
76
- };
77
- protectedFunction.getLatestError = () => failureCause;
78
- protectedFunction.getState = () => state;
79
- return protectedFunction;
213
+ return Object.assign((...args) => execute(1, args), {
214
+ dispose: (disposeMessage = "ERR_CIRCUIT_BREAKER_DISPOSED") => {
215
+ const reason = new ReferenceError(disposeMessage);
216
+ clearFailure();
217
+ clearTimeout(resetTimer);
218
+ history.forEach((entry) => clearTimeout(entry.timer));
219
+ history.clear();
220
+ fallback = () => Promise.reject(reason);
221
+ state = "open";
222
+ controller.abort(reason);
223
+ },
224
+ getLatestError: () => failureCause,
225
+ getState: () => state
226
+ });
80
227
  }
81
228
 
82
229
  exports.createCircuitBreaker = createCircuitBreaker;
230
+ exports.delayMs = delayMs;
231
+ exports.useExponentialBackoff = useExponentialBackoff;
232
+ exports.useFibonacciBackoff = useFibonacciBackoff;
233
+ exports.withTimeout = withTimeout;
83
234
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":["../lib/util.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\nconst resolvedPromise = Promise.resolve()\n\nexport const nextTick = <T>(fn: () => T | PromiseLike<T>): Promise<T> =>\n\tresolvedPromise.then(fn)\n","import { type AnyFn, assertNever, nextTick } from \"./util.js\"\n\nexport type CircuitState = \"closed\" | \"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\tonClose,\n\t\tonOpen,\n\t\tresetAfter = 30_000,\n\t} = options\n\tlet fallback = options.fallback || (() => Promise.reject(failureCause))\n\tlet halfOpenPending: Promise<unknown> | undefined\n\tlet state: CircuitState = \"closed\"\n\tlet failureCause: unknown | undefined\n\tlet failureCount = 0\n\tlet resetTimer: NodeJS.Timeout | undefined\n\n\tfunction clearFailure() {\n\t\tfailureCause = undefined\n\t\tfailureCount = 0\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\t/**\n\t * Wrap calls to `main` with circuit breaker logic\n\t */\n\tfunction 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\tconst thisFallback = fallback\n\t\t\treturn main(...args).then(\n\t\t\t\t(result) => {\n\t\t\t\t\t// Reset accumulated failures if circuit is still closed\n\t\t\t\t\tif (state === \"closed\") clearFailure()\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 breaker disposed while the call was in flight?\n\t\t\t\t\tif (thisFallback !== fallback) throw cause\n\t\t\t\t\tfailureCount += errorIsFailure(cause) ? 1 : 0\n\t\t\t\t\tif (failureCount === failureThreshold) openCircuit(cause)\n\t\t\t\t\treturn nextTick(() => protectedFunction(...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\tconst thisFallback = fallback\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\t// Was the circuit breaker disposed while the call was\n\t\t\t\t\t\t// in flight?\n\t\t\t\t\t\tif (thisFallback !== fallback) return result\n\t\t\t\t\t\t// Close the circuit and resume normal operation\n\t\t\t\t\t\tstate = \"closed\"\n\t\t\t\t\t\tclearFailure()\n\t\t\t\t\t\tclearTimeout(resetTimer)\n\t\t\t\t\t\tonClose?.()\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 breaker disposed while the call was\n\t\t\t\t\t\t// in flight?\n\t\t\t\t\t\tif (thisFallback !== fallback) throw cause\n\t\t\t\t\t\topenCircuit(cause)\n\t\t\t\t\t\treturn nextTick(() => protectedFunction(...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\tprotectedFunction.dispose = () => {\n\t\tclearFailure()\n\t\tclearTimeout(resetTimer)\n\t\tfallback = () =>\n\t\t\tPromise.reject(new ReferenceError(\"ERR_CIRCUIT_BREAKER_DISPOSED\"))\n\t\tstate = \"open\"\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/options.ts","../lib/util.ts","../lib/helpers.ts","../lib/index.ts"],"sourcesContent":["import type { CircuitBreakerOptions } from \"./types.js\"\nimport type { AnyFn } from \"./util.js\"\n\nfunction assert(value: unknown, message?: string): asserts value {\n\tif (!value) throw new TypeError(message)\n}\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\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 a number between 0 and 1 (received ${errorThreshold})`,\n\t)\n\n\t// errorWindow\n\tassert(\n\t\terrorWindow > 0,\n\t\t`\"errorWindow\" must be milliseconds greater than 0 (received ${errorWindow})`,\n\t)\n\n\t// minimumCandidates\n\tassert(\n\t\tminimumCandidates > 1,\n\t\t`\"minimumCandidates\" must be a number greater than 1 (received ${minimumCandidates})`,\n\t)\n\n\t// (optional) onClose\n\tif (onClose)\n\t\tassert(\n\t\t\ttypeof onClose === \"function\",\n\t\t\t`\"onClose\" must be a function (received ${typeof onClose})`,\n\t\t)\n\n\t// (optional) onOpen\n\tif (onOpen)\n\t\tassert(\n\t\t\ttypeof onOpen === \"function\",\n\t\t\t`\"onOpen\" must be a function (received ${typeof onOpen})`,\n\t\t)\n\n\t// resetAfter\n\tassert(\n\t\tresetAfter > 0,\n\t\t`\"resetAfter\" must be milliseconds greater than 0 (received ${resetAfter})`,\n\t)\n\tassert(\n\t\tresetAfter >= errorWindow,\n\t\t`\"resetAfter\" must be milliseconds greater than or equal to \"errorWindow\" (received ${resetAfter})`,\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\tonOpen,\n\t\tresetAfter,\n\t\tretryDelay,\n\t}\n}\n","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\n/**\n * Rejects the given promise when the abort signal is triggered.\n */\nexport const rejectOnAbort = <T extends Promise<any> | 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 { 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\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(() => (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
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
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,26 +1,149 @@
1
+ function assert(value, message) {
2
+ if (!value) throw new TypeError(message);
3
+ }
4
+ function parseOptions(options) {
5
+ const {
6
+ errorIsFailure = () => false,
7
+ errorThreshold = 0,
8
+ errorWindow = 1e4,
9
+ minimumCandidates = 6,
10
+ onClose,
11
+ onOpen,
12
+ resetAfter = 3e4,
13
+ retryDelay = () => void 0
14
+ } = options;
15
+ assert(
16
+ typeof errorIsFailure === "function",
17
+ `"errorIsFailure" must be a function (received ${typeof errorIsFailure})`
18
+ );
19
+ assert(
20
+ errorThreshold >= 0 && errorThreshold <= 1,
21
+ `"errorThreshold" must be a number between 0 and 1 (received ${errorThreshold})`
22
+ );
23
+ assert(
24
+ errorWindow > 0,
25
+ `"errorWindow" must be milliseconds greater than 0 (received ${errorWindow})`
26
+ );
27
+ assert(
28
+ minimumCandidates > 1,
29
+ `"minimumCandidates" must be a number greater than 1 (received ${minimumCandidates})`
30
+ );
31
+ if (onClose)
32
+ assert(
33
+ typeof onClose === "function",
34
+ `"onClose" must be a function (received ${typeof onClose})`
35
+ );
36
+ if (onOpen)
37
+ assert(
38
+ typeof onOpen === "function",
39
+ `"onOpen" must be a function (received ${typeof onOpen})`
40
+ );
41
+ assert(
42
+ resetAfter > 0,
43
+ `"resetAfter" must be milliseconds greater than 0 (received ${resetAfter})`
44
+ );
45
+ assert(
46
+ resetAfter >= errorWindow,
47
+ `"resetAfter" must be milliseconds greater than or equal to "errorWindow" (received ${resetAfter})`
48
+ );
49
+ assert(
50
+ typeof retryDelay === "function",
51
+ `"retryDelay" must be a function (received ${typeof retryDelay})`
52
+ );
53
+ return {
54
+ errorIsFailure,
55
+ errorThreshold,
56
+ errorWindow,
57
+ minimumCandidates,
58
+ onClose,
59
+ onOpen,
60
+ resetAfter,
61
+ retryDelay
62
+ };
63
+ }
64
+
1
65
  const assertNever = (val, msg = "Unexpected value") => {
2
66
  throw new TypeError(`${msg}: ${val}`);
3
67
  };
4
- const resolvedPromise = Promise.resolve();
5
- const nextTick = (fn) => resolvedPromise.then(fn);
68
+ const delayMs = (ms) => new Promise((next) => setTimeout(next, ms));
69
+ const rejectOnAbort = (signal, pending) => {
70
+ let teardown;
71
+ return Promise.race([
72
+ Promise.resolve(pending).finally(
73
+ () => signal.removeEventListener("abort", teardown)
74
+ ),
75
+ new Promise((_, reject) => {
76
+ teardown = () => reject(signal.reason);
77
+ signal.addEventListener("abort", teardown);
78
+ })
79
+ ]);
80
+ };
81
+
82
+ function useExponentialBackoff(maxSeconds) {
83
+ return function exponentialBackoff(attempt) {
84
+ const num = Math.max(attempt - 2, 0);
85
+ const delay = Math.min(2 ** num, maxSeconds);
86
+ return delayMs(delay * 1e3);
87
+ };
88
+ }
89
+ const sqrt5 = /* @__PURE__ */ Math.sqrt(5);
90
+ const binet = (n) => Math.round(((1 + sqrt5) ** n - (1 - sqrt5) ** n) / (2 ** n * sqrt5));
91
+ function useFibonacciBackoff(maxSeconds) {
92
+ return function fibonacciBackoff(attempt) {
93
+ const delay = Math.min(binet(attempt), maxSeconds);
94
+ return delayMs(delay * 1e3);
95
+ };
96
+ }
97
+ function withTimeout(main, timeoutMs, timeoutMessage = "ERR_CIRCUIT_BREAKER_TIMEOUT") {
98
+ const error = new Error(timeoutMessage);
99
+ return function withTimeoutFunction(...args) {
100
+ let timer;
101
+ return Promise.race([
102
+ main(...args).finally(() => clearTimeout(timer)),
103
+ new Promise((_, reject) => {
104
+ timer = setTimeout(reject, timeoutMs, error);
105
+ })
106
+ ]);
107
+ };
108
+ }
6
109
 
7
110
  function createCircuitBreaker(main, options = {}) {
8
111
  const {
9
- errorIsFailure = () => true,
10
- failureThreshold = 1,
112
+ errorIsFailure,
113
+ errorThreshold,
114
+ errorWindow,
115
+ minimumCandidates,
11
116
  onClose,
12
117
  onOpen,
13
- resetAfter = 3e4
14
- } = options;
118
+ resetAfter,
119
+ retryDelay
120
+ } = parseOptions(options);
121
+ const controller = new AbortController();
122
+ const history = /* @__PURE__ */ new Map();
123
+ const signal = controller.signal;
124
+ let failureCause;
15
125
  let fallback = options.fallback || (() => Promise.reject(failureCause));
16
126
  let halfOpenPending;
17
- let state = "closed";
18
- let failureCause;
19
- let failureCount = 0;
20
127
  let resetTimer;
128
+ let state = "closed";
21
129
  function clearFailure() {
22
130
  failureCause = void 0;
23
- failureCount = 0;
131
+ }
132
+ function closeCircuit() {
133
+ state = "closed";
134
+ clearFailure();
135
+ clearTimeout(resetTimer);
136
+ onClose?.();
137
+ }
138
+ function failureRate() {
139
+ let failures = 0;
140
+ let total = 0;
141
+ for (const { status } of history.values()) {
142
+ if (status === "rejected") failures++;
143
+ if (status !== "pending") total++;
144
+ }
145
+ if (!total || total < minimumCandidates) return 0;
146
+ return failures / total;
24
147
  }
25
148
  function openCircuit(cause) {
26
149
  failureCause = cause;
@@ -29,53 +152,77 @@ function createCircuitBreaker(main, options = {}) {
29
152
  resetTimer = setTimeout(() => state = "halfOpen", resetAfter);
30
153
  onOpen?.(cause);
31
154
  }
32
- function protectedFunction(...args) {
155
+ function createHistoryItem(pending) {
156
+ const entry = { status: "pending", timer: void 0 };
157
+ const teardown = () => {
158
+ clearTimeout(entry.timer);
159
+ history.delete(pending);
160
+ signal.removeEventListener("abort", teardown);
161
+ };
162
+ signal.addEventListener("abort", teardown);
163
+ const settle = (value) => {
164
+ if (signal.aborted) return;
165
+ entry.status = value;
166
+ entry.timer = setTimeout(teardown, errorWindow);
167
+ };
168
+ history.set(pending, entry);
169
+ return { pending, settle, teardown };
170
+ }
171
+ function execute(attempt, args) {
33
172
  if (state === "closed") {
34
- const thisFallback = fallback;
35
- return main(...args).then(
173
+ const { pending, settle, teardown } = createHistoryItem(main(...args));
174
+ return pending.then(
36
175
  (result) => {
37
- if (state === "closed") clearFailure();
176
+ settle("resolved");
38
177
  return result;
39
178
  },
40
- (cause) => {
41
- if (thisFallback !== fallback) throw cause;
42
- failureCount += errorIsFailure(cause) ? 1 : 0;
43
- if (failureCount === failureThreshold) openCircuit(cause);
44
- return nextTick(() => protectedFunction(...args));
179
+ async (cause) => {
180
+ if (signal.aborted || errorIsFailure(cause)) {
181
+ teardown();
182
+ throw cause;
183
+ }
184
+ settle("rejected");
185
+ if (failureRate() > errorThreshold) openCircuit(cause);
186
+ const next = attempt + 1;
187
+ await rejectOnAbort(signal, retryDelay(next, signal));
188
+ return execute(next, args);
45
189
  }
46
190
  );
47
191
  } else if (state === "open" || halfOpenPending) {
48
192
  return fallback(...args);
49
193
  } else if (state === "halfOpen") {
50
- const thisFallback = fallback;
51
194
  return (halfOpenPending = main(...args)).finally(() => halfOpenPending = void 0).then(
52
195
  (result) => {
53
- if (thisFallback !== fallback) return result;
54
- state = "closed";
55
- clearFailure();
56
- clearTimeout(resetTimer);
57
- onClose?.();
196
+ if (signal.aborted) return result;
197
+ closeCircuit();
58
198
  return result;
59
199
  },
60
- (cause) => {
61
- if (thisFallback !== fallback) throw cause;
200
+ async (cause) => {
201
+ if (signal.aborted || errorIsFailure(cause)) throw cause;
62
202
  openCircuit(cause);
63
- return nextTick(() => protectedFunction(...args));
203
+ const next = attempt + 1;
204
+ await rejectOnAbort(signal, retryDelay(next, signal));
205
+ return execute(next, args);
64
206
  }
65
207
  );
66
208
  }
67
209
  return assertNever(state);
68
210
  }
69
- protectedFunction.dispose = () => {
70
- clearFailure();
71
- clearTimeout(resetTimer);
72
- fallback = () => Promise.reject(new ReferenceError("ERR_CIRCUIT_BREAKER_DISPOSED"));
73
- state = "open";
74
- };
75
- protectedFunction.getLatestError = () => failureCause;
76
- protectedFunction.getState = () => state;
77
- return protectedFunction;
211
+ return Object.assign((...args) => execute(1, args), {
212
+ dispose: (disposeMessage = "ERR_CIRCUIT_BREAKER_DISPOSED") => {
213
+ const reason = new ReferenceError(disposeMessage);
214
+ clearFailure();
215
+ clearTimeout(resetTimer);
216
+ history.forEach((entry) => clearTimeout(entry.timer));
217
+ history.clear();
218
+ fallback = () => Promise.reject(reason);
219
+ state = "open";
220
+ controller.abort(reason);
221
+ },
222
+ getLatestError: () => failureCause,
223
+ getState: () => state
224
+ });
78
225
  }
79
226
 
80
- export { createCircuitBreaker };
227
+ export { createCircuitBreaker, delayMs, useExponentialBackoff, useFibonacciBackoff, withTimeout };
81
228
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":["../lib/util.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\nconst resolvedPromise = Promise.resolve()\n\nexport const nextTick = <T>(fn: () => T | PromiseLike<T>): Promise<T> =>\n\tresolvedPromise.then(fn)\n","import { type AnyFn, assertNever, nextTick } from \"./util.js\"\n\nexport type CircuitState = \"closed\" | \"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\tonClose,\n\t\tonOpen,\n\t\tresetAfter = 30_000,\n\t} = options\n\tlet fallback = options.fallback || (() => Promise.reject(failureCause))\n\tlet halfOpenPending: Promise<unknown> | undefined\n\tlet state: CircuitState = \"closed\"\n\tlet failureCause: unknown | undefined\n\tlet failureCount = 0\n\tlet resetTimer: NodeJS.Timeout | undefined\n\n\tfunction clearFailure() {\n\t\tfailureCause = undefined\n\t\tfailureCount = 0\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\t/**\n\t * Wrap calls to `main` with circuit breaker logic\n\t */\n\tfunction 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\tconst thisFallback = fallback\n\t\t\treturn main(...args).then(\n\t\t\t\t(result) => {\n\t\t\t\t\t// Reset accumulated failures if circuit is still closed\n\t\t\t\t\tif (state === \"closed\") clearFailure()\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 breaker disposed while the call was in flight?\n\t\t\t\t\tif (thisFallback !== fallback) throw cause\n\t\t\t\t\tfailureCount += errorIsFailure(cause) ? 1 : 0\n\t\t\t\t\tif (failureCount === failureThreshold) openCircuit(cause)\n\t\t\t\t\treturn nextTick(() => protectedFunction(...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\tconst thisFallback = fallback\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\t// Was the circuit breaker disposed while the call was\n\t\t\t\t\t\t// in flight?\n\t\t\t\t\t\tif (thisFallback !== fallback) return result\n\t\t\t\t\t\t// Close the circuit and resume normal operation\n\t\t\t\t\t\tstate = \"closed\"\n\t\t\t\t\t\tclearFailure()\n\t\t\t\t\t\tclearTimeout(resetTimer)\n\t\t\t\t\t\tonClose?.()\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 breaker disposed while the call was\n\t\t\t\t\t\t// in flight?\n\t\t\t\t\t\tif (thisFallback !== fallback) throw cause\n\t\t\t\t\t\topenCircuit(cause)\n\t\t\t\t\t\treturn nextTick(() => protectedFunction(...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\tprotectedFunction.dispose = () => {\n\t\tclearFailure()\n\t\tclearTimeout(resetTimer)\n\t\tfallback = () =>\n\t\t\tPromise.reject(new ReferenceError(\"ERR_CIRCUIT_BREAKER_DISPOSED\"))\n\t\tstate = \"open\"\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/options.ts","../lib/util.ts","../lib/helpers.ts","../lib/index.ts"],"sourcesContent":["import type { CircuitBreakerOptions } from \"./types.js\"\nimport type { AnyFn } from \"./util.js\"\n\nfunction assert(value: unknown, message?: string): asserts value {\n\tif (!value) throw new TypeError(message)\n}\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\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 a number between 0 and 1 (received ${errorThreshold})`,\n\t)\n\n\t// errorWindow\n\tassert(\n\t\terrorWindow > 0,\n\t\t`\"errorWindow\" must be milliseconds greater than 0 (received ${errorWindow})`,\n\t)\n\n\t// minimumCandidates\n\tassert(\n\t\tminimumCandidates > 1,\n\t\t`\"minimumCandidates\" must be a number greater than 1 (received ${minimumCandidates})`,\n\t)\n\n\t// (optional) onClose\n\tif (onClose)\n\t\tassert(\n\t\t\ttypeof onClose === \"function\",\n\t\t\t`\"onClose\" must be a function (received ${typeof onClose})`,\n\t\t)\n\n\t// (optional) onOpen\n\tif (onOpen)\n\t\tassert(\n\t\t\ttypeof onOpen === \"function\",\n\t\t\t`\"onOpen\" must be a function (received ${typeof onOpen})`,\n\t\t)\n\n\t// resetAfter\n\tassert(\n\t\tresetAfter > 0,\n\t\t`\"resetAfter\" must be milliseconds greater than 0 (received ${resetAfter})`,\n\t)\n\tassert(\n\t\tresetAfter >= errorWindow,\n\t\t`\"resetAfter\" must be milliseconds greater than or equal to \"errorWindow\" (received ${resetAfter})`,\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\tonOpen,\n\t\tresetAfter,\n\t\tretryDelay,\n\t}\n}\n","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\n/**\n * Rejects the given promise when the abort signal is triggered.\n */\nexport const rejectOnAbort = <T extends Promise<any> | 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 { 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\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(() => (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": "3.0.0",
3
+ "version": "4.1.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",
25
- "prepublishOnly": "npm run build",
29
+ "format": "NODE_OPTIONS='--experimental-strip-types' prettier . --write",
30
+ "prepublishOnly": "npm run test && npm run build",
31
+ "reinstall": "rm -rf node_modules package-lock.json && npm install",
26
32
  "test": "vitest --run"
27
33
  },
28
34
  "keywords": [
@@ -34,10 +40,10 @@
34
40
  "author": "Matthew Pietz <sirlancelot@gmail.com>",
35
41
  "license": "ISC",
36
42
  "devDependencies": {
37
- "@types/node": "24.3.1",
43
+ "@types/node": "24.5.0",
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"