breaker-box 4.1.0 → 5.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,7 +27,6 @@ async function unreliableApiCall(data: string) {
27
27
  }
28
28
 
29
29
  const protectedApiCall = createCircuitBreaker(unreliableApiCall, {
30
- errorIsFailure: (error) => error.message.includes("404"), // Don't retry 404s
31
30
  errorThreshold: 0.5, // Open circuit when 50% of calls fail
32
31
  errorWindow: 10_000, // Track errors over 10 second window
33
32
  minimumCandidates: 6, // Need at least 6 calls before calculating error rate
@@ -44,37 +43,54 @@ try {
44
43
 
45
44
  ### Retry Strategies
46
45
 
46
+ The circuit breaker doesn't retry - it only tracks failures and prevents calls when too many fail. Use `withRetry` to add retry logic:
47
+
47
48
  ```typescript
48
49
  import {
49
50
  createCircuitBreaker,
51
+ withRetry,
50
52
  useExponentialBackoff,
51
53
  useFibonacciBackoff,
52
54
  } from "breaker-box"
53
55
 
54
56
  // Exponential backoff: 1s, 2s, 4s, 8s, up to 30s max
55
- const protectedWithExponential = createCircuitBreaker(unreliableApiCall, {
56
- retryDelay: useExponentialBackoff(30),
57
- })
57
+ const protectedWithExponential = createCircuitBreaker(
58
+ withRetry(unreliableApiCall, { retryDelay: useExponentialBackoff(30) }),
59
+ )
58
60
 
59
61
  // Fibonacci backoff: 1s, 2s, 3s, 5s, 8s, up to 60s max
60
- const protectedWithFibonacci = createCircuitBreaker(unreliableApiCall, {
61
- retryDelay: useFibonacciBackoff(60),
62
- })
62
+ const protectedWithFibonacci = createCircuitBreaker(
63
+ withRetry(unreliableApiCall, { retryDelay: useFibonacciBackoff(60) }),
64
+ )
63
65
  ```
64
66
 
65
67
  ### Timeout Protection
66
68
 
69
+ Compose `withTimeout`, `withRetry`, and circuit breaker for complete fault tolerance:
70
+
67
71
  ```typescript
68
- import { createCircuitBreaker, withTimeout } from "breaker-box"
72
+ import {
73
+ createCircuitBreaker,
74
+ withRetry,
75
+ withTimeout,
76
+ useExponentialBackoff,
77
+ } from "breaker-box"
69
78
 
70
- // Wrap function with 5-second timeout
71
- const timeoutProtectedCall = withTimeout(
72
- unreliableApiCall,
73
- 5_000,
74
- "Request timed out",
75
- )
79
+ // 1. Add timeout (innermost - fails fast)
80
+ const timedCall = withTimeout(unreliableApiCall, 5_000, "Request timed out")
81
+
82
+ // 2. Add retry logic (middle - retries on transient errors)
83
+ const retryCall = withRetry(timedCall, {
84
+ maxAttempts: 3,
85
+ retryDelay: useExponentialBackoff(10),
86
+ shouldRetry: (error) => !error.message.includes("404"), // Don't retry 404s
87
+ })
76
88
 
77
- const protectedApiCall = createCircuitBreaker(timeoutProtectedCall, {})
89
+ // 3. Add circuit breaker (outermost - prevents cascading failures)
90
+ const protectedApiCall = createCircuitBreaker(retryCall, {
91
+ errorThreshold: 0.5,
92
+ minimumCandidates: 5,
93
+ })
78
94
  ```
79
95
 
80
96
  ### Event Monitoring
@@ -82,10 +98,13 @@ const protectedApiCall = createCircuitBreaker(timeoutProtectedCall, {})
82
98
  ```typescript
83
99
  const protectedFunction = createCircuitBreaker(unreliableApiCall, {
84
100
  onClose: () => {
85
- console.log("Circuit closed - normal operation resumed")
101
+ console.log("🟢 Circuit closed - normal operation resumed")
102
+ },
103
+ onHalfOpen: () => {
104
+ console.log("🟡 Circuit half-opened - waiting for success")
86
105
  },
87
106
  onOpen: (cause) => {
88
- console.log("Circuit opened due to:", cause.message)
107
+ console.log("🔴 Circuit opened due to:", cause.message)
89
108
  },
90
109
  })
91
110
 
@@ -111,15 +130,15 @@ Creates a circuit breaker around the provided async function.
111
130
 
112
131
  - `fn`: The async function to protect
113
132
  - `options`: Configuration object (optional)
114
- - `errorIsFailure`: Function to determine if an error is non-retryable (default: `() => false`)
133
+ - `errorIsFailure`: Function to determine if an error should not count toward circuit breaker metrics (default: `() => false`)
115
134
  - `errorThreshold`: Percentage (0-1) of errors that triggers circuit opening (default: `0`)
116
135
  - `errorWindow`: Time window in ms for tracking errors (default: `10_000`)
117
136
  - `fallback`: Function to call when circuit is open (default: undefined)
118
137
  - `minimumCandidates`: Minimum calls before calculating error rate (default: `6`)
119
138
  - `onClose`: Function called when circuit closes (default: undefined)
139
+ - `onHalfOpen`: Function called when circuit enters half-open state (default: undefined)
120
140
  - `onOpen`: Function called when circuit opens (default: undefined)
121
141
  - `resetAfter`: Milliseconds to wait before trying half-open (default: `30_000`)
122
- - `retryDelay`: Function returning promise for retry delays (default: immediate retry)
123
142
 
124
143
  #### Returns
125
144
 
@@ -131,6 +150,26 @@ A function with the same signature as `fn` and additional methods:
131
150
 
132
151
  ### Helper Functions
133
152
 
153
+ #### `withRetry(fn, options?)`
154
+
155
+ Wraps a function with retry logic. Failures will be retried according to the provided options.
156
+
157
+ **Options:**
158
+
159
+ - `maxAttempts`: Maximum number of attempts (default: `3`)
160
+ - `retryDelay`: Function returning promise for when to retry (default: immediate)
161
+ - `shouldRetry`: Function to determine if error should be retried (default: `() => true`)
162
+
163
+ **Example:**
164
+
165
+ ```typescript
166
+ const retryCall = withRetry(apiCall, {
167
+ maxAttempts: 5,
168
+ retryDelay: useExponentialBackoff(30),
169
+ shouldRetry: (error) => error.statusCode !== 404,
170
+ })
171
+ ```
172
+
134
173
  #### `useExponentialBackoff(maxSeconds)`
135
174
 
136
175
  Returns a retry delay function that implements exponential backoff (2^n seconds, capped at maxSeconds).
package/dist/index.cjs CHANGED
@@ -3,6 +3,23 @@
3
3
  function assert(value, message) {
4
4
  if (!value) throw new TypeError(message);
5
5
  }
6
+ const assertNever = (val, msg = "Unexpected value") => {
7
+ throw new TypeError(`${msg}: ${val}`);
8
+ };
9
+ const delayMs = (ms) => new Promise((next) => setTimeout(next, ms));
10
+ const rejectOnAbort = (signal, pending) => {
11
+ let teardown;
12
+ return Promise.race([
13
+ Promise.resolve(pending).finally(() => {
14
+ signal.removeEventListener("abort", teardown);
15
+ }),
16
+ new Promise((_, reject) => {
17
+ teardown = () => reject(signal.reason);
18
+ signal.addEventListener("abort", teardown, { once: true });
19
+ })
20
+ ]);
21
+ };
22
+
6
23
  function parseOptions(options) {
7
24
  const {
8
25
  errorIsFailure = () => false,
@@ -10,9 +27,9 @@ function parseOptions(options) {
10
27
  errorWindow = 1e4,
11
28
  minimumCandidates = 6,
12
29
  onClose,
30
+ onHalfOpen,
13
31
  onOpen,
14
- resetAfter = 3e4,
15
- retryDelay = () => void 0
32
+ resetAfter = 3e4
16
33
  } = options;
17
34
  assert(
18
35
  typeof errorIsFailure === "function",
@@ -20,37 +37,35 @@ function parseOptions(options) {
20
37
  );
21
38
  assert(
22
39
  errorThreshold >= 0 && errorThreshold <= 1,
23
- `"errorThreshold" must be a number between 0 and 1 (received ${errorThreshold})`
40
+ `"errorThreshold" must be between 0 and 1 (received ${errorThreshold})`
24
41
  );
25
42
  assert(
26
- errorWindow > 0,
27
- `"errorWindow" must be milliseconds greater than 0 (received ${errorWindow})`
43
+ errorWindow >= 1e3,
44
+ `"errorWindow" must be milliseconds of at least 1 second (received ${errorWindow})`
28
45
  );
29
46
  assert(
30
- minimumCandidates > 1,
31
- `"minimumCandidates" must be a number greater than 1 (received ${minimumCandidates})`
47
+ minimumCandidates >= 1,
48
+ `"minimumCandidates" must be greater than 0 (received ${minimumCandidates})`
32
49
  );
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
50
  assert(
44
- resetAfter > 0,
45
- `"resetAfter" must be milliseconds greater than 0 (received ${resetAfter})`
51
+ !onClose || typeof onClose === "function",
52
+ `"onClose" must be a function (received ${typeof onClose})`
46
53
  );
47
54
  assert(
48
- resetAfter >= errorWindow,
49
- `"resetAfter" must be milliseconds greater than or equal to "errorWindow" (received ${resetAfter})`
55
+ !onHalfOpen || typeof onHalfOpen === "function",
56
+ `"onHalfOpen" must be a function (received ${typeof onHalfOpen})`
57
+ );
58
+ assert(
59
+ !onOpen || typeof onOpen === "function",
60
+ `"onOpen" must be a function (received ${typeof onOpen})`
61
+ );
62
+ assert(
63
+ resetAfter >= 1e3,
64
+ `"resetAfter" must be milliseconds of at least 1 second (received ${resetAfter})`
50
65
  );
51
66
  assert(
52
- typeof retryDelay === "function",
53
- `"retryDelay" must be a function (received ${typeof retryDelay})`
67
+ resetAfter >= errorWindow,
68
+ `"resetAfter" must be greater than or equal to "errorWindow" (received ${resetAfter}, expected >= ${errorWindow})`
54
69
  );
55
70
  return {
56
71
  errorIsFailure,
@@ -58,28 +73,13 @@ function parseOptions(options) {
58
73
  errorWindow,
59
74
  minimumCandidates,
60
75
  onClose,
76
+ onHalfOpen,
61
77
  onOpen,
62
- resetAfter,
63
- retryDelay
78
+ resetAfter
64
79
  };
65
80
  }
66
81
 
67
- const assertNever = (val, msg = "Unexpected value") => {
68
- throw new TypeError(`${msg}: ${val}`);
69
- };
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
- };
82
+ const disposeKey = Symbol("disposeKey");
83
83
 
84
84
  function useExponentialBackoff(maxSeconds) {
85
85
  return function exponentialBackoff(attempt) {
@@ -96,17 +96,70 @@ function useFibonacciBackoff(maxSeconds) {
96
96
  return delayMs(delay * 1e3);
97
97
  };
98
98
  }
99
+ function withRetry(main, options = {}) {
100
+ const {
101
+ shouldRetry = () => true,
102
+ maxAttempts = 3,
103
+ retryDelay = () => Promise.resolve()
104
+ } = options;
105
+ assert(maxAttempts >= 1, "maxAttempts must be a number greater than 0");
106
+ const controller = new AbortController();
107
+ const { signal } = controller;
108
+ async function execute(args, attempt = 1) {
109
+ try {
110
+ return await main(...args);
111
+ } catch (cause) {
112
+ if (!shouldRetry(cause, attempt)) throw cause;
113
+ if (attempt >= maxAttempts)
114
+ throw new Error(`ERR_CIRCUIT_BREAKER_MAX_ATTEMPTS (${maxAttempts})`, {
115
+ cause
116
+ });
117
+ await rejectOnAbort(signal, retryDelay(attempt + 1, signal));
118
+ return execute(args, attempt + 1);
119
+ }
120
+ }
121
+ return Object.assign(
122
+ function withRetryFunction(...args) {
123
+ return execute(args);
124
+ },
125
+ {
126
+ [disposeKey]: (disposeMessage = "ERR_CIRCUIT_BREAKER_DISPOSED") => {
127
+ const reason = new ReferenceError(disposeMessage);
128
+ main[disposeKey]?.(disposeMessage);
129
+ controller.abort(reason);
130
+ }
131
+ }
132
+ );
133
+ }
99
134
  function withTimeout(main, timeoutMs, timeoutMessage = "ERR_CIRCUIT_BREAKER_TIMEOUT") {
100
135
  const error = new Error(timeoutMessage);
101
- return function withTimeoutFunction(...args) {
136
+ const controller = new AbortController();
137
+ const { signal } = controller;
138
+ function withTimeoutFunction(...args) {
139
+ let teardown;
102
140
  let timer;
103
141
  return Promise.race([
104
- main(...args).finally(() => clearTimeout(timer)),
142
+ main(...args).finally(() => {
143
+ clearTimeout(timer);
144
+ signal.removeEventListener("abort", teardown);
145
+ }),
105
146
  new Promise((_, reject) => {
147
+ teardown = () => {
148
+ clearTimeout(timer);
149
+ reject(signal.reason);
150
+ };
106
151
  timer = setTimeout(reject, timeoutMs, error);
152
+ signal.addEventListener("abort", teardown, { once: true });
107
153
  })
108
154
  ]);
109
- };
155
+ }
156
+ return Object.assign(withTimeoutFunction, {
157
+ [disposeKey]: (disposeMessage = "ERR_CIRCUIT_BREAKER_DISPOSED") => {
158
+ const reason = new ReferenceError(disposeMessage);
159
+ main[disposeKey]?.(disposeMessage);
160
+ controller.abort(reason);
161
+ }
162
+ });
110
163
  }
111
164
 
112
165
  function createCircuitBreaker(main, options = {}) {
@@ -116,9 +169,9 @@ function createCircuitBreaker(main, options = {}) {
116
169
  errorWindow,
117
170
  minimumCandidates,
118
171
  onClose,
172
+ onHalfOpen,
119
173
  onOpen,
120
- resetAfter,
121
- retryDelay
174
+ resetAfter
122
175
  } = parseOptions(options);
123
176
  const controller = new AbortController();
124
177
  const history = /* @__PURE__ */ new Map();
@@ -151,7 +204,10 @@ function createCircuitBreaker(main, options = {}) {
151
204
  failureCause = cause;
152
205
  state = "open";
153
206
  clearTimeout(resetTimer);
154
- resetTimer = setTimeout(() => state = "halfOpen", resetAfter);
207
+ resetTimer = setTimeout(() => {
208
+ state = "halfOpen";
209
+ onHalfOpen?.();
210
+ }, resetAfter);
155
211
  onOpen?.(cause);
156
212
  }
157
213
  function createHistoryItem(pending) {
@@ -161,7 +217,7 @@ function createCircuitBreaker(main, options = {}) {
161
217
  history.delete(pending);
162
218
  signal.removeEventListener("abort", teardown);
163
219
  };
164
- signal.addEventListener("abort", teardown);
220
+ signal.addEventListener("abort", teardown, { once: true });
165
221
  const settle = (value) => {
166
222
  if (signal.aborted) return;
167
223
  entry.status = value;
@@ -170,7 +226,7 @@ function createCircuitBreaker(main, options = {}) {
170
226
  history.set(pending, entry);
171
227
  return { pending, settle, teardown };
172
228
  }
173
- function execute(attempt, args) {
229
+ function execute(args) {
174
230
  if (state === "closed") {
175
231
  const { pending, settle, teardown } = createHistoryItem(main(...args));
176
232
  return pending.then(
@@ -178,16 +234,14 @@ function createCircuitBreaker(main, options = {}) {
178
234
  settle("resolved");
179
235
  return result;
180
236
  },
181
- async (cause) => {
237
+ (cause) => {
182
238
  if (signal.aborted || errorIsFailure(cause)) {
183
239
  teardown();
184
240
  throw cause;
185
241
  }
186
242
  settle("rejected");
187
243
  if (failureRate() > errorThreshold) openCircuit(cause);
188
- const next = attempt + 1;
189
- await rejectOnAbort(signal, retryDelay(next, signal));
190
- return execute(next, args);
244
+ return fallback(...args);
191
245
  }
192
246
  );
193
247
  } else if (state === "open" || halfOpenPending) {
@@ -199,20 +253,19 @@ function createCircuitBreaker(main, options = {}) {
199
253
  closeCircuit();
200
254
  return result;
201
255
  },
202
- async (cause) => {
256
+ (cause) => {
203
257
  if (signal.aborted || errorIsFailure(cause)) throw cause;
204
258
  openCircuit(cause);
205
- const next = attempt + 1;
206
- await rejectOnAbort(signal, retryDelay(next, signal));
207
- return execute(next, args);
259
+ return fallback(...args);
208
260
  }
209
261
  );
210
262
  }
211
263
  return assertNever(state);
212
264
  }
213
- return Object.assign((...args) => execute(1, args), {
265
+ return Object.assign((...args) => execute(args), {
214
266
  dispose: (disposeMessage = "ERR_CIRCUIT_BREAKER_DISPOSED") => {
215
267
  const reason = new ReferenceError(disposeMessage);
268
+ main[disposeKey]?.(disposeMessage);
216
269
  clearFailure();
217
270
  clearTimeout(resetTimer);
218
271
  history.forEach((entry) => clearTimeout(entry.timer));
@@ -230,5 +283,6 @@ exports.createCircuitBreaker = createCircuitBreaker;
230
283
  exports.delayMs = delayMs;
231
284
  exports.useExponentialBackoff = useExponentialBackoff;
232
285
  exports.useFibonacciBackoff = useFibonacciBackoff;
286
+ exports.withRetry = withRetry;
233
287
  exports.withTimeout = withTimeout;
234
288
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
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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"}
1
+ {"version":3,"file":"index.cjs","sources":["../lib/util.ts","../lib/options.ts","../lib/types.ts","../lib/helpers.ts","../lib/index.ts"],"sourcesContent":["export type AnyFn = (...args: any[]) => any\n\n/**\n * Asserts that the given value is truthy. If not, throws a `TypeError`.\n */\nexport function assert(value: unknown, message?: string): asserts value {\n\tif (!value) throw new TypeError(message)\n}\n\n/**\n * `[TypeScript]` For exhaustive checks in switch statements or if/else. Add\n * this check to `default` case or final `else` to ensure all possible values\n * have been handled. If a new value is added to the type, TypeScript will\n * throw an error and the editor will underline the `value`.\n */\n/* v8 ignore next 3 */\nexport const assertNever = (val: never, msg = \"Unexpected value\") => {\n\tthrow new TypeError(`${msg}: ${val}`)\n}\n\n/**\n * Returns a promise that resolves after the specified number of milliseconds.\n */\nexport const delayMs = (ms: number): Promise<void> =>\n\tnew Promise((next) => setTimeout(next, ms))\n\n/**\n * Rejects the given promise when the abort signal is triggered.\n */\nexport const rejectOnAbort = <T extends Promise<unknown> | undefined>(\n\tsignal: AbortSignal,\n\tpending: T,\n): Promise<Awaited<T>> => {\n\tlet teardown: () => void\n\treturn Promise.race([\n\t\tPromise.resolve(pending).finally(() => {\n\t\t\tsignal.removeEventListener(\"abort\", teardown)\n\t\t}),\n\t\tnew Promise<never>((_, reject) => {\n\t\t\tteardown = () => reject(signal.reason)\n\t\t\tsignal.addEventListener(\"abort\", teardown, { once: true })\n\t\t}),\n\t])\n}\n","import type { CircuitBreakerOptions } from \"./types.js\"\nimport { assert, type AnyFn } from \"./util.js\"\n\nexport function parseOptions<Fallback extends AnyFn>(\n\toptions: CircuitBreakerOptions<Fallback>,\n) {\n\tconst {\n\t\terrorIsFailure = () => false,\n\t\terrorThreshold = 0,\n\t\terrorWindow = 10_000,\n\t\tminimumCandidates = 6,\n\t\tonClose,\n\t\tonHalfOpen,\n\t\tonOpen,\n\t\tresetAfter = 30_000,\n\t} = options\n\n\t// errorIsFailure\n\tassert(\n\t\ttypeof errorIsFailure === \"function\",\n\t\t`\"errorIsFailure\" must be a function (received ${typeof errorIsFailure})`,\n\t)\n\n\t// errorThreshold\n\tassert(\n\t\terrorThreshold >= 0 && errorThreshold <= 1,\n\t\t`\"errorThreshold\" must be between 0 and 1 (received ${errorThreshold})`,\n\t)\n\n\t// errorWindow\n\tassert(\n\t\terrorWindow >= 1_000,\n\t\t`\"errorWindow\" must be milliseconds of at least 1 second (received ${errorWindow})`,\n\t)\n\n\t// minimumCandidates\n\tassert(\n\t\tminimumCandidates >= 1,\n\t\t`\"minimumCandidates\" must be greater than 0 (received ${minimumCandidates})`,\n\t)\n\n\t// (optional) onClose\n\tassert(\n\t\t!onClose || typeof onClose === \"function\",\n\t\t`\"onClose\" must be a function (received ${typeof onClose})`,\n\t)\n\n\t// (optional) onHalfOpen\n\tassert(\n\t\t!onHalfOpen || typeof onHalfOpen === \"function\",\n\t\t`\"onHalfOpen\" must be a function (received ${typeof onHalfOpen})`,\n\t)\n\n\t// (optional) onOpen\n\tassert(\n\t\t!onOpen || typeof onOpen === \"function\",\n\t\t`\"onOpen\" must be a function (received ${typeof onOpen})`,\n\t)\n\n\t// resetAfter\n\tassert(\n\t\tresetAfter >= 1_000,\n\t\t`\"resetAfter\" must be milliseconds of at least 1 second (received ${resetAfter})`,\n\t)\n\tassert(\n\t\tresetAfter >= errorWindow,\n\t\t`\"resetAfter\" must be greater than or equal to \"errorWindow\" (received ${resetAfter}, expected >= ${errorWindow})`,\n\t)\n\n\treturn {\n\t\terrorIsFailure,\n\t\terrorThreshold,\n\t\terrorWindow,\n\t\tminimumCandidates,\n\t\tonClose,\n\t\tonHalfOpen,\n\t\tonOpen,\n\t\tresetAfter,\n\t}\n}\n","import type { AnyFn } from \"./util\"\n\nexport const disposeKey = Symbol(\"disposeKey\")\n\nexport type CircuitState = \"closed\" | \"halfOpen\" | \"open\"\n\nexport interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {\n\t/**\n\t * Whether an error should be treated as non-retryable failure. When used and\n\t * when an error is considered a failure, the error will be thrown to the\n\t * caller.\n\t *\n\t * @default () => false // Errors are retryable by default\n\t */\n\terrorIsFailure?: (error: unknown) => boolean\n\n\t/**\n\t * The percentage of errors (as a number between 0 and 1) which must occur\n\t * within the error window before the circuit breaker opens.\n\t *\n\t * @default 0 // Any error opens the circuit\n\t */\n\terrorThreshold?: number\n\n\t/**\n\t * The sliding window of time in milliseconds over which errors are counted.\n\t *\n\t * @default 10_000 // 10 seconds\n\t */\n\terrorWindow?: number\n\n\t/**\n\t * If provided, then all rejected calls to `main` will be forwarded to\n\t * this function instead.\n\t *\n\t * @default undefined // No fallback, errors are propagated\n\t */\n\tfallback?: Fallback\n\n\t/**\n\t * The minimum number of calls that must be made before calculating the\n\t * error rate and determining whether the circuit breaker should open based on\n\t * the `errorThreshold`.\n\t *\n\t * @default 6\n\t */\n\tminimumCandidates?: number\n\n\t/**\n\t * Provide a function to be called when the circuit breaker is closed.\n\t */\n\tonClose?: () => void\n\n\t/**\n\t * Provide a function to be called when the circuit breaker transitions to\n\t * half-open state.\n\t */\n\tonHalfOpen?: () => void\n\n\t/**\n\t * Provide a function to be called when the circuit breaker is opened. It\n\t * receives the error as its only argument.\n\t */\n\tonOpen?: (cause: unknown) => void\n\n\t/**\n\t * The amount of time in milliseconds to wait before transitioning to a\n\t * half-open state.\n\t *\n\t * @default 30_000 // 30 seconds\n\t */\n\tresetAfter?: number\n}\n\nexport interface CircuitBreakerProtectedFn<\n\tRet = unknown,\n\tArgs extends unknown[] = never[],\n> {\n\t(...args: Args): Promise<Ret>\n\n\t/**\n\t * Free memory and stop timers. All future calls will be rejected with the\n\t * provided message.\n\t *\n\t * @default \"ERR_CIRCUIT_BREAKER_DISPOSED\"\n\t */\n\tdispose(disposeMessage?: string): 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 interface HistoryEntry {\n\ttimer: NodeJS.Timeout | undefined\n\tstatus: \"pending\" | \"resolved\" | \"rejected\"\n}\n\nexport type HistoryMap = Map<Promise<unknown>, HistoryEntry>\n\nexport interface MainFn<Ret = unknown, Args extends unknown[] = never[]> {\n\t(...args: Args): Promise<Ret>\n\n\t[disposeKey]?: (disposeMessage?: string) => void\n}\n","import { disposeKey, type MainFn } from \"./types.js\"\nimport { assert, delayMs, rejectOnAbort } 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\nexport interface RetryOptions {\n\t/**\n\t * Whether an error should be treated as non-retryable. When this returns\n\t * true, the error will be thrown immediately without retrying.\n\t *\n\t * @default () => false // All errors are retried\n\t */\n\tshouldRetry?: (error: unknown, attempt: number) => boolean\n\n\t/**\n\t * Maximum number of retries\n\t *\n\t * @default 3\n\t */\n\tmaxAttempts?: number\n\n\t/**\n\t * Function that returns a promise resolving when the next retry should occur.\n\t * Receives the attempt number (starting at 2) and an abort signal.\n\t *\n\t * @default () => Promise.resolve() // Immediate retry\n\t */\n\tretryDelay?: (attempt: number, signal: AbortSignal) => Promise<void>\n}\n\n/**\n * Wrap a function with retry logic. Errors will be retried according to the\n * provided options.\n *\n * @example\n * ```ts\n * // Compose with circuit breaker. Retry up to 3 times with no delay\n * const protectedA = createCircuitBreaker(\n * withRetry(unreliableApiCall, { maxAttempts: 3 })\n * )\n *\n * // Retry up to 5 times with exponential backoff\n * const protectedB = createCircuitBreaker(\n * withRetry(unreliableApiCall, {\n * maxAttempts: 5,\n * retryDelay: useExponentialBackoff(30),\n * })\n * )\n * ```\n */\nexport function withRetry<Ret, Args extends unknown[]>(\n\tmain: MainFn<Ret, Args>,\n\toptions: RetryOptions = {},\n): MainFn<Ret, Args> {\n\tconst {\n\t\tshouldRetry = () => true,\n\t\tmaxAttempts = 3,\n\t\tretryDelay = () => Promise.resolve(),\n\t} = options\n\n\tassert(maxAttempts >= 1, \"maxAttempts must be a number greater than 0\")\n\n\tconst controller = new AbortController()\n\tconst { signal } = controller\n\n\tasync function execute(args: Args, attempt = 1): Promise<Ret> {\n\t\ttry {\n\t\t\treturn await main(...args)\n\t\t} catch (cause) {\n\t\t\t// Check if we should retry this error\n\t\t\tif (!shouldRetry(cause, attempt)) throw cause\n\n\t\t\t// Check if we've exhausted attempts\n\t\t\tif (attempt >= maxAttempts)\n\t\t\t\tthrow new Error(`ERR_CIRCUIT_BREAKER_MAX_ATTEMPTS (${maxAttempts})`, {\n\t\t\t\t\tcause,\n\t\t\t\t})\n\n\t\t\t// Wait before retrying\n\t\t\tawait rejectOnAbort(signal, retryDelay(attempt + 1, signal))\n\n\t\t\t// Retry\n\t\t\treturn execute(args, attempt + 1)\n\t\t}\n\t}\n\n\treturn Object.assign(\n\t\tfunction withRetryFunction(...args: Args) {\n\t\t\treturn execute(args)\n\t\t},\n\t\t{\n\t\t\t[disposeKey]: (disposeMessage = \"ERR_CIRCUIT_BREAKER_DISPOSED\") => {\n\t\t\t\tconst reason = new ReferenceError(disposeMessage)\n\t\t\t\tmain[disposeKey]?.(disposeMessage)\n\t\t\t\tcontroller.abort(reason)\n\t\t\t},\n\t\t},\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\tconst controller = new AbortController()\n\tconst { signal } = controller\n\n\tfunction withTimeoutFunction(...args: Args) {\n\t\tlet teardown: () => void\n\t\tlet timer: NodeJS.Timeout\n\t\treturn Promise.race([\n\t\t\tmain(...args).finally(() => {\n\t\t\t\tclearTimeout(timer)\n\t\t\t\tsignal.removeEventListener(\"abort\", teardown)\n\t\t\t}),\n\t\t\tnew Promise<never>((_, reject) => {\n\t\t\t\tteardown = () => {\n\t\t\t\t\tclearTimeout(timer)\n\t\t\t\t\treject(signal.reason)\n\t\t\t\t}\n\t\t\t\ttimer = setTimeout(reject, timeoutMs, error)\n\t\t\t\tsignal.addEventListener(\"abort\", teardown, { once: true })\n\t\t\t}),\n\t\t])\n\t}\n\n\treturn Object.assign(withTimeoutFunction, {\n\t\t[disposeKey]: (disposeMessage = \"ERR_CIRCUIT_BREAKER_DISPOSED\") => {\n\t\t\tconst reason = new ReferenceError(disposeMessage)\n\t\t\tmain[disposeKey]?.(disposeMessage)\n\t\t\tcontroller.abort(reason)\n\t\t},\n\t})\n}\n","import { parseOptions } from \"./options.js\"\nimport {\n\tdisposeKey,\n\ttype CircuitBreakerOptions,\n\ttype CircuitBreakerProtectedFn,\n\ttype CircuitState,\n\ttype HistoryEntry,\n\ttype HistoryMap,\n\ttype MainFn,\n} from \"./types.js\"\nimport { assertNever, type AnyFn } from \"./util.js\"\n\nexport * from \"./helpers.js\"\nexport { delayMs } from \"./util.js\"\n\nexport function createCircuitBreaker<\n\tRet,\n\tArgs extends unknown[],\n\tFallback extends AnyFn = MainFn<Ret, Args>,\n>(\n\tmain: MainFn<Ret, Args>,\n\toptions: CircuitBreakerOptions<Fallback> = {},\n): CircuitBreakerProtectedFn<Ret, Args> {\n\tconst {\n\t\terrorIsFailure,\n\t\terrorThreshold,\n\t\terrorWindow,\n\t\tminimumCandidates,\n\t\tonClose,\n\t\tonHalfOpen,\n\t\tonOpen,\n\t\tresetAfter,\n\t} = parseOptions(options)\n\tconst controller = new AbortController()\n\tconst history: HistoryMap = new Map()\n\tconst signal = controller.signal\n\tlet failureCause: unknown\n\tlet fallback = options.fallback || (() => Promise.reject(failureCause))\n\tlet halfOpenPending: Promise<unknown> | undefined\n\tlet resetTimer: NodeJS.Timeout\n\tlet state: CircuitState = \"closed\"\n\n\tfunction clearFailure() {\n\t\tfailureCause = undefined\n\t}\n\n\tfunction closeCircuit() {\n\t\tstate = \"closed\"\n\t\tclearFailure()\n\t\tclearTimeout(resetTimer)\n\t\tonClose?.()\n\t}\n\n\tfunction failureRate() {\n\t\tlet failures = 0\n\t\tlet total = 0\n\t\tfor (const { status } of history.values()) {\n\t\t\tif (status === \"rejected\") failures++\n\t\t\tif (status !== \"pending\") total++\n\t\t}\n\t\t// Don't calculate anything until we have enough data\n\t\tif (!total || total < minimumCandidates) return 0\n\t\treturn failures / total\n\t}\n\n\t/**\n\t * Break the circuit and wait for a reset\n\t */\n\tfunction openCircuit(cause: unknown) {\n\t\tfailureCause = cause\n\t\tstate = \"open\"\n\t\tclearTimeout(resetTimer)\n\t\tresetTimer = setTimeout(() => {\n\t\t\tstate = \"halfOpen\"\n\t\t\tonHalfOpen?.()\n\t\t}, resetAfter)\n\t\tonOpen?.(cause)\n\t}\n\n\tfunction createHistoryItem<T>(pending: Promise<T>) {\n\t\tconst entry: HistoryEntry = { status: \"pending\", timer: undefined }\n\t\tconst teardown = () => {\n\t\t\tclearTimeout(entry.timer)\n\t\t\thistory.delete(pending)\n\t\t\tsignal.removeEventListener(\"abort\", teardown)\n\t\t}\n\t\tsignal.addEventListener(\"abort\", teardown, { once: true })\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(args: Args): Promise<Ret> {\n\t\t// Normal operation when circuit is closed. If an error occurs, keep track\n\t\t// of the failure count and open the circuit if it exceeds the threshold.\n\t\tif (state === \"closed\") {\n\t\t\tconst { pending, settle, teardown } = createHistoryItem(main(...args))\n\t\t\treturn pending.then(\n\t\t\t\t(result) => {\n\t\t\t\t\tsettle(\"resolved\")\n\t\t\t\t\treturn result\n\t\t\t\t},\n\t\t\t\t(cause: unknown) => {\n\t\t\t\t\t// Was the circuit disposed, or is this error considered a failure?\n\t\t\t\t\tif (signal.aborted || errorIsFailure(cause)) {\n\t\t\t\t\t\tteardown()\n\t\t\t\t\t\tthrow cause\n\t\t\t\t\t}\n\n\t\t\t\t\t// Should this error open the circuit?\n\t\t\t\t\tsettle(\"rejected\")\n\t\t\t\t\tif (failureRate() > errorThreshold) openCircuit(cause)\n\n\t\t\t\t\treturn fallback(...args)\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\n\t\t// Use the fallback while the circuit is open, or if a half-open trial\n\t\t// attempt was already made.\n\t\telse if (state === \"open\" || halfOpenPending) {\n\t\t\treturn fallback(...args)\n\t\t}\n\n\t\t// If the circuit is half-open, make one attempt. If it succeeds, close\n\t\t// the circuit and resume normal operation. If it fails, re-open the\n\t\t// circuit and run the fallback instead.\n\t\telse if (state === \"halfOpen\") {\n\t\t\treturn (halfOpenPending = main(...args))\n\t\t\t\t.finally(() => (halfOpenPending = undefined))\n\t\t\t\t.then(\n\t\t\t\t\t(result) => {\n\t\t\t\t\t\tif (signal.aborted) return result // disposed\n\t\t\t\t\t\tcloseCircuit()\n\t\t\t\t\t\treturn result\n\t\t\t\t\t},\n\t\t\t\t\t(cause: unknown) => {\n\t\t\t\t\t\t// Was the circuit disposed, or was this a non-retryable error?\n\t\t\t\t\t\tif (signal.aborted || errorIsFailure(cause)) throw cause\n\n\t\t\t\t\t\t// Open the circuit and use fallback\n\t\t\t\t\t\topenCircuit(cause)\n\t\t\t\t\t\treturn fallback(...args)\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\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(args), {\n\t\tdispose: (disposeMessage = \"ERR_CIRCUIT_BREAKER_DISPOSED\") => {\n\t\t\tconst reason = new ReferenceError(disposeMessage)\n\t\t\tmain[disposeKey]?.(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
@@ -4,6 +4,7 @@ type AnyFn = (...args: any[]) => any;
4
4
  */
5
5
  declare const delayMs: (ms: number) => Promise<void>;
6
6
 
7
+ declare const disposeKey: unique symbol;
7
8
  type CircuitState = "closed" | "halfOpen" | "open";
8
9
  interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
9
10
  /**
@@ -46,6 +47,11 @@ interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
46
47
  * Provide a function to be called when the circuit breaker is closed.
47
48
  */
48
49
  onClose?: () => void;
50
+ /**
51
+ * Provide a function to be called when the circuit breaker transitions to
52
+ * half-open state.
53
+ */
54
+ onHalfOpen?: () => void;
49
55
  /**
50
56
  * Provide a function to be called when the circuit breaker is opened. It
51
57
  * receives the error as its only argument.
@@ -58,34 +64,6 @@ interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
58
64
  * @default 30_000 // 30 seconds
59
65
  */
60
66
  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>;
89
67
  }
90
68
  interface CircuitBreakerProtectedFn<Ret = unknown, Args extends unknown[] = never[]> {
91
69
  (...args: Args): Promise<Ret>;
@@ -101,7 +79,10 @@ interface CircuitBreakerProtectedFn<Ret = unknown, Args extends unknown[] = neve
101
79
  /** Get the current state of the circuit breaker */
102
80
  getState(): CircuitState;
103
81
  }
104
- type MainFn<Ret = unknown, Args extends unknown[] = never[]> = (...args: Args) => Promise<Ret>;
82
+ interface MainFn<Ret = unknown, Args extends unknown[] = never[]> {
83
+ (...args: Args): Promise<Ret>;
84
+ [disposeKey]?: (disposeMessage?: string) => void;
85
+ }
105
86
 
106
87
  /**
107
88
  * Returns a function which implements exponential backoff.
@@ -119,6 +100,49 @@ declare function useExponentialBackoff(maxSeconds: number): (attempt: number) =>
119
100
  * that resolves after the calculated delay.
120
101
  */
121
102
  declare function useFibonacciBackoff(maxSeconds: number): (attempt: number) => Promise<void>;
103
+ interface RetryOptions {
104
+ /**
105
+ * Whether an error should be treated as non-retryable. When this returns
106
+ * true, the error will be thrown immediately without retrying.
107
+ *
108
+ * @default () => false // All errors are retried
109
+ */
110
+ shouldRetry?: (error: unknown, attempt: number) => boolean;
111
+ /**
112
+ * Maximum number of retries
113
+ *
114
+ * @default 3
115
+ */
116
+ maxAttempts?: number;
117
+ /**
118
+ * Function that returns a promise resolving when the next retry should occur.
119
+ * Receives the attempt number (starting at 2) and an abort signal.
120
+ *
121
+ * @default () => Promise.resolve() // Immediate retry
122
+ */
123
+ retryDelay?: (attempt: number, signal: AbortSignal) => Promise<void>;
124
+ }
125
+ /**
126
+ * Wrap a function with retry logic. Errors will be retried according to the
127
+ * provided options.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * // Compose with circuit breaker. Retry up to 3 times with no delay
132
+ * const protectedA = createCircuitBreaker(
133
+ * withRetry(unreliableApiCall, { maxAttempts: 3 })
134
+ * )
135
+ *
136
+ * // Retry up to 5 times with exponential backoff
137
+ * const protectedB = createCircuitBreaker(
138
+ * withRetry(unreliableApiCall, {
139
+ * maxAttempts: 5,
140
+ * retryDelay: useExponentialBackoff(30),
141
+ * })
142
+ * )
143
+ * ```
144
+ */
145
+ declare function withRetry<Ret, Args extends unknown[]>(main: MainFn<Ret, Args>, options?: RetryOptions): MainFn<Ret, Args>;
122
146
  /**
123
147
  * Wrap a function with a timeout. If execution of `main` exceeds `timeoutMs`,
124
148
  * then the call is rejected with `new Error(timeoutMessage)`.
@@ -127,4 +151,5 @@ declare function withTimeout<Ret, Args extends unknown[]>(main: MainFn<Ret, Args
127
151
 
128
152
  declare function createCircuitBreaker<Ret, Args extends unknown[], Fallback extends AnyFn = MainFn<Ret, Args>>(main: MainFn<Ret, Args>, options?: CircuitBreakerOptions<Fallback>): CircuitBreakerProtectedFn<Ret, Args>;
129
153
 
130
- export { createCircuitBreaker, delayMs, useExponentialBackoff, useFibonacciBackoff, withTimeout };
154
+ export { createCircuitBreaker, delayMs, useExponentialBackoff, useFibonacciBackoff, withRetry, withTimeout };
155
+ export type { RetryOptions };
package/dist/index.d.mts CHANGED
@@ -4,6 +4,7 @@ type AnyFn = (...args: any[]) => any;
4
4
  */
5
5
  declare const delayMs: (ms: number) => Promise<void>;
6
6
 
7
+ declare const disposeKey: unique symbol;
7
8
  type CircuitState = "closed" | "halfOpen" | "open";
8
9
  interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
9
10
  /**
@@ -46,6 +47,11 @@ interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
46
47
  * Provide a function to be called when the circuit breaker is closed.
47
48
  */
48
49
  onClose?: () => void;
50
+ /**
51
+ * Provide a function to be called when the circuit breaker transitions to
52
+ * half-open state.
53
+ */
54
+ onHalfOpen?: () => void;
49
55
  /**
50
56
  * Provide a function to be called when the circuit breaker is opened. It
51
57
  * receives the error as its only argument.
@@ -58,34 +64,6 @@ interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
58
64
  * @default 30_000 // 30 seconds
59
65
  */
60
66
  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>;
89
67
  }
90
68
  interface CircuitBreakerProtectedFn<Ret = unknown, Args extends unknown[] = never[]> {
91
69
  (...args: Args): Promise<Ret>;
@@ -101,7 +79,10 @@ interface CircuitBreakerProtectedFn<Ret = unknown, Args extends unknown[] = neve
101
79
  /** Get the current state of the circuit breaker */
102
80
  getState(): CircuitState;
103
81
  }
104
- type MainFn<Ret = unknown, Args extends unknown[] = never[]> = (...args: Args) => Promise<Ret>;
82
+ interface MainFn<Ret = unknown, Args extends unknown[] = never[]> {
83
+ (...args: Args): Promise<Ret>;
84
+ [disposeKey]?: (disposeMessage?: string) => void;
85
+ }
105
86
 
106
87
  /**
107
88
  * Returns a function which implements exponential backoff.
@@ -119,6 +100,49 @@ declare function useExponentialBackoff(maxSeconds: number): (attempt: number) =>
119
100
  * that resolves after the calculated delay.
120
101
  */
121
102
  declare function useFibonacciBackoff(maxSeconds: number): (attempt: number) => Promise<void>;
103
+ interface RetryOptions {
104
+ /**
105
+ * Whether an error should be treated as non-retryable. When this returns
106
+ * true, the error will be thrown immediately without retrying.
107
+ *
108
+ * @default () => false // All errors are retried
109
+ */
110
+ shouldRetry?: (error: unknown, attempt: number) => boolean;
111
+ /**
112
+ * Maximum number of retries
113
+ *
114
+ * @default 3
115
+ */
116
+ maxAttempts?: number;
117
+ /**
118
+ * Function that returns a promise resolving when the next retry should occur.
119
+ * Receives the attempt number (starting at 2) and an abort signal.
120
+ *
121
+ * @default () => Promise.resolve() // Immediate retry
122
+ */
123
+ retryDelay?: (attempt: number, signal: AbortSignal) => Promise<void>;
124
+ }
125
+ /**
126
+ * Wrap a function with retry logic. Errors will be retried according to the
127
+ * provided options.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * // Compose with circuit breaker. Retry up to 3 times with no delay
132
+ * const protectedA = createCircuitBreaker(
133
+ * withRetry(unreliableApiCall, { maxAttempts: 3 })
134
+ * )
135
+ *
136
+ * // Retry up to 5 times with exponential backoff
137
+ * const protectedB = createCircuitBreaker(
138
+ * withRetry(unreliableApiCall, {
139
+ * maxAttempts: 5,
140
+ * retryDelay: useExponentialBackoff(30),
141
+ * })
142
+ * )
143
+ * ```
144
+ */
145
+ declare function withRetry<Ret, Args extends unknown[]>(main: MainFn<Ret, Args>, options?: RetryOptions): MainFn<Ret, Args>;
122
146
  /**
123
147
  * Wrap a function with a timeout. If execution of `main` exceeds `timeoutMs`,
124
148
  * then the call is rejected with `new Error(timeoutMessage)`.
@@ -127,4 +151,5 @@ declare function withTimeout<Ret, Args extends unknown[]>(main: MainFn<Ret, Args
127
151
 
128
152
  declare function createCircuitBreaker<Ret, Args extends unknown[], Fallback extends AnyFn = MainFn<Ret, Args>>(main: MainFn<Ret, Args>, options?: CircuitBreakerOptions<Fallback>): CircuitBreakerProtectedFn<Ret, Args>;
129
153
 
130
- export { createCircuitBreaker, delayMs, useExponentialBackoff, useFibonacciBackoff, withTimeout };
154
+ export { createCircuitBreaker, delayMs, useExponentialBackoff, useFibonacciBackoff, withRetry, withTimeout };
155
+ export type { RetryOptions };
package/dist/index.mjs CHANGED
@@ -1,6 +1,23 @@
1
1
  function assert(value, message) {
2
2
  if (!value) throw new TypeError(message);
3
3
  }
4
+ const assertNever = (val, msg = "Unexpected value") => {
5
+ throw new TypeError(`${msg}: ${val}`);
6
+ };
7
+ const delayMs = (ms) => new Promise((next) => setTimeout(next, ms));
8
+ const rejectOnAbort = (signal, pending) => {
9
+ let teardown;
10
+ return Promise.race([
11
+ Promise.resolve(pending).finally(() => {
12
+ signal.removeEventListener("abort", teardown);
13
+ }),
14
+ new Promise((_, reject) => {
15
+ teardown = () => reject(signal.reason);
16
+ signal.addEventListener("abort", teardown, { once: true });
17
+ })
18
+ ]);
19
+ };
20
+
4
21
  function parseOptions(options) {
5
22
  const {
6
23
  errorIsFailure = () => false,
@@ -8,9 +25,9 @@ function parseOptions(options) {
8
25
  errorWindow = 1e4,
9
26
  minimumCandidates = 6,
10
27
  onClose,
28
+ onHalfOpen,
11
29
  onOpen,
12
- resetAfter = 3e4,
13
- retryDelay = () => void 0
30
+ resetAfter = 3e4
14
31
  } = options;
15
32
  assert(
16
33
  typeof errorIsFailure === "function",
@@ -18,37 +35,35 @@ function parseOptions(options) {
18
35
  );
19
36
  assert(
20
37
  errorThreshold >= 0 && errorThreshold <= 1,
21
- `"errorThreshold" must be a number between 0 and 1 (received ${errorThreshold})`
38
+ `"errorThreshold" must be between 0 and 1 (received ${errorThreshold})`
22
39
  );
23
40
  assert(
24
- errorWindow > 0,
25
- `"errorWindow" must be milliseconds greater than 0 (received ${errorWindow})`
41
+ errorWindow >= 1e3,
42
+ `"errorWindow" must be milliseconds of at least 1 second (received ${errorWindow})`
26
43
  );
27
44
  assert(
28
- minimumCandidates > 1,
29
- `"minimumCandidates" must be a number greater than 1 (received ${minimumCandidates})`
45
+ minimumCandidates >= 1,
46
+ `"minimumCandidates" must be greater than 0 (received ${minimumCandidates})`
30
47
  );
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
48
  assert(
42
- resetAfter > 0,
43
- `"resetAfter" must be milliseconds greater than 0 (received ${resetAfter})`
49
+ !onClose || typeof onClose === "function",
50
+ `"onClose" must be a function (received ${typeof onClose})`
44
51
  );
45
52
  assert(
46
- resetAfter >= errorWindow,
47
- `"resetAfter" must be milliseconds greater than or equal to "errorWindow" (received ${resetAfter})`
53
+ !onHalfOpen || typeof onHalfOpen === "function",
54
+ `"onHalfOpen" must be a function (received ${typeof onHalfOpen})`
55
+ );
56
+ assert(
57
+ !onOpen || typeof onOpen === "function",
58
+ `"onOpen" must be a function (received ${typeof onOpen})`
59
+ );
60
+ assert(
61
+ resetAfter >= 1e3,
62
+ `"resetAfter" must be milliseconds of at least 1 second (received ${resetAfter})`
48
63
  );
49
64
  assert(
50
- typeof retryDelay === "function",
51
- `"retryDelay" must be a function (received ${typeof retryDelay})`
65
+ resetAfter >= errorWindow,
66
+ `"resetAfter" must be greater than or equal to "errorWindow" (received ${resetAfter}, expected >= ${errorWindow})`
52
67
  );
53
68
  return {
54
69
  errorIsFailure,
@@ -56,28 +71,13 @@ function parseOptions(options) {
56
71
  errorWindow,
57
72
  minimumCandidates,
58
73
  onClose,
74
+ onHalfOpen,
59
75
  onOpen,
60
- resetAfter,
61
- retryDelay
76
+ resetAfter
62
77
  };
63
78
  }
64
79
 
65
- const assertNever = (val, msg = "Unexpected value") => {
66
- throw new TypeError(`${msg}: ${val}`);
67
- };
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
- };
80
+ const disposeKey = Symbol("disposeKey");
81
81
 
82
82
  function useExponentialBackoff(maxSeconds) {
83
83
  return function exponentialBackoff(attempt) {
@@ -94,17 +94,70 @@ function useFibonacciBackoff(maxSeconds) {
94
94
  return delayMs(delay * 1e3);
95
95
  };
96
96
  }
97
+ function withRetry(main, options = {}) {
98
+ const {
99
+ shouldRetry = () => true,
100
+ maxAttempts = 3,
101
+ retryDelay = () => Promise.resolve()
102
+ } = options;
103
+ assert(maxAttempts >= 1, "maxAttempts must be a number greater than 0");
104
+ const controller = new AbortController();
105
+ const { signal } = controller;
106
+ async function execute(args, attempt = 1) {
107
+ try {
108
+ return await main(...args);
109
+ } catch (cause) {
110
+ if (!shouldRetry(cause, attempt)) throw cause;
111
+ if (attempt >= maxAttempts)
112
+ throw new Error(`ERR_CIRCUIT_BREAKER_MAX_ATTEMPTS (${maxAttempts})`, {
113
+ cause
114
+ });
115
+ await rejectOnAbort(signal, retryDelay(attempt + 1, signal));
116
+ return execute(args, attempt + 1);
117
+ }
118
+ }
119
+ return Object.assign(
120
+ function withRetryFunction(...args) {
121
+ return execute(args);
122
+ },
123
+ {
124
+ [disposeKey]: (disposeMessage = "ERR_CIRCUIT_BREAKER_DISPOSED") => {
125
+ const reason = new ReferenceError(disposeMessage);
126
+ main[disposeKey]?.(disposeMessage);
127
+ controller.abort(reason);
128
+ }
129
+ }
130
+ );
131
+ }
97
132
  function withTimeout(main, timeoutMs, timeoutMessage = "ERR_CIRCUIT_BREAKER_TIMEOUT") {
98
133
  const error = new Error(timeoutMessage);
99
- return function withTimeoutFunction(...args) {
134
+ const controller = new AbortController();
135
+ const { signal } = controller;
136
+ function withTimeoutFunction(...args) {
137
+ let teardown;
100
138
  let timer;
101
139
  return Promise.race([
102
- main(...args).finally(() => clearTimeout(timer)),
140
+ main(...args).finally(() => {
141
+ clearTimeout(timer);
142
+ signal.removeEventListener("abort", teardown);
143
+ }),
103
144
  new Promise((_, reject) => {
145
+ teardown = () => {
146
+ clearTimeout(timer);
147
+ reject(signal.reason);
148
+ };
104
149
  timer = setTimeout(reject, timeoutMs, error);
150
+ signal.addEventListener("abort", teardown, { once: true });
105
151
  })
106
152
  ]);
107
- };
153
+ }
154
+ return Object.assign(withTimeoutFunction, {
155
+ [disposeKey]: (disposeMessage = "ERR_CIRCUIT_BREAKER_DISPOSED") => {
156
+ const reason = new ReferenceError(disposeMessage);
157
+ main[disposeKey]?.(disposeMessage);
158
+ controller.abort(reason);
159
+ }
160
+ });
108
161
  }
109
162
 
110
163
  function createCircuitBreaker(main, options = {}) {
@@ -114,9 +167,9 @@ function createCircuitBreaker(main, options = {}) {
114
167
  errorWindow,
115
168
  minimumCandidates,
116
169
  onClose,
170
+ onHalfOpen,
117
171
  onOpen,
118
- resetAfter,
119
- retryDelay
172
+ resetAfter
120
173
  } = parseOptions(options);
121
174
  const controller = new AbortController();
122
175
  const history = /* @__PURE__ */ new Map();
@@ -149,7 +202,10 @@ function createCircuitBreaker(main, options = {}) {
149
202
  failureCause = cause;
150
203
  state = "open";
151
204
  clearTimeout(resetTimer);
152
- resetTimer = setTimeout(() => state = "halfOpen", resetAfter);
205
+ resetTimer = setTimeout(() => {
206
+ state = "halfOpen";
207
+ onHalfOpen?.();
208
+ }, resetAfter);
153
209
  onOpen?.(cause);
154
210
  }
155
211
  function createHistoryItem(pending) {
@@ -159,7 +215,7 @@ function createCircuitBreaker(main, options = {}) {
159
215
  history.delete(pending);
160
216
  signal.removeEventListener("abort", teardown);
161
217
  };
162
- signal.addEventListener("abort", teardown);
218
+ signal.addEventListener("abort", teardown, { once: true });
163
219
  const settle = (value) => {
164
220
  if (signal.aborted) return;
165
221
  entry.status = value;
@@ -168,7 +224,7 @@ function createCircuitBreaker(main, options = {}) {
168
224
  history.set(pending, entry);
169
225
  return { pending, settle, teardown };
170
226
  }
171
- function execute(attempt, args) {
227
+ function execute(args) {
172
228
  if (state === "closed") {
173
229
  const { pending, settle, teardown } = createHistoryItem(main(...args));
174
230
  return pending.then(
@@ -176,16 +232,14 @@ function createCircuitBreaker(main, options = {}) {
176
232
  settle("resolved");
177
233
  return result;
178
234
  },
179
- async (cause) => {
235
+ (cause) => {
180
236
  if (signal.aborted || errorIsFailure(cause)) {
181
237
  teardown();
182
238
  throw cause;
183
239
  }
184
240
  settle("rejected");
185
241
  if (failureRate() > errorThreshold) openCircuit(cause);
186
- const next = attempt + 1;
187
- await rejectOnAbort(signal, retryDelay(next, signal));
188
- return execute(next, args);
242
+ return fallback(...args);
189
243
  }
190
244
  );
191
245
  } else if (state === "open" || halfOpenPending) {
@@ -197,20 +251,19 @@ function createCircuitBreaker(main, options = {}) {
197
251
  closeCircuit();
198
252
  return result;
199
253
  },
200
- async (cause) => {
254
+ (cause) => {
201
255
  if (signal.aborted || errorIsFailure(cause)) throw cause;
202
256
  openCircuit(cause);
203
- const next = attempt + 1;
204
- await rejectOnAbort(signal, retryDelay(next, signal));
205
- return execute(next, args);
257
+ return fallback(...args);
206
258
  }
207
259
  );
208
260
  }
209
261
  return assertNever(state);
210
262
  }
211
- return Object.assign((...args) => execute(1, args), {
263
+ return Object.assign((...args) => execute(args), {
212
264
  dispose: (disposeMessage = "ERR_CIRCUIT_BREAKER_DISPOSED") => {
213
265
  const reason = new ReferenceError(disposeMessage);
266
+ main[disposeKey]?.(disposeMessage);
214
267
  clearFailure();
215
268
  clearTimeout(resetTimer);
216
269
  history.forEach((entry) => clearTimeout(entry.timer));
@@ -224,5 +277,5 @@ function createCircuitBreaker(main, options = {}) {
224
277
  });
225
278
  }
226
279
 
227
- export { createCircuitBreaker, delayMs, useExponentialBackoff, useFibonacciBackoff, withTimeout };
280
+ export { createCircuitBreaker, delayMs, useExponentialBackoff, useFibonacciBackoff, withRetry, withTimeout };
228
281
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
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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"}
1
+ {"version":3,"file":"index.mjs","sources":["../lib/util.ts","../lib/options.ts","../lib/types.ts","../lib/helpers.ts","../lib/index.ts"],"sourcesContent":["export type AnyFn = (...args: any[]) => any\n\n/**\n * Asserts that the given value is truthy. If not, throws a `TypeError`.\n */\nexport function assert(value: unknown, message?: string): asserts value {\n\tif (!value) throw new TypeError(message)\n}\n\n/**\n * `[TypeScript]` For exhaustive checks in switch statements or if/else. Add\n * this check to `default` case or final `else` to ensure all possible values\n * have been handled. If a new value is added to the type, TypeScript will\n * throw an error and the editor will underline the `value`.\n */\n/* v8 ignore next 3 */\nexport const assertNever = (val: never, msg = \"Unexpected value\") => {\n\tthrow new TypeError(`${msg}: ${val}`)\n}\n\n/**\n * Returns a promise that resolves after the specified number of milliseconds.\n */\nexport const delayMs = (ms: number): Promise<void> =>\n\tnew Promise((next) => setTimeout(next, ms))\n\n/**\n * Rejects the given promise when the abort signal is triggered.\n */\nexport const rejectOnAbort = <T extends Promise<unknown> | undefined>(\n\tsignal: AbortSignal,\n\tpending: T,\n): Promise<Awaited<T>> => {\n\tlet teardown: () => void\n\treturn Promise.race([\n\t\tPromise.resolve(pending).finally(() => {\n\t\t\tsignal.removeEventListener(\"abort\", teardown)\n\t\t}),\n\t\tnew Promise<never>((_, reject) => {\n\t\t\tteardown = () => reject(signal.reason)\n\t\t\tsignal.addEventListener(\"abort\", teardown, { once: true })\n\t\t}),\n\t])\n}\n","import type { CircuitBreakerOptions } from \"./types.js\"\nimport { assert, type AnyFn } from \"./util.js\"\n\nexport function parseOptions<Fallback extends AnyFn>(\n\toptions: CircuitBreakerOptions<Fallback>,\n) {\n\tconst {\n\t\terrorIsFailure = () => false,\n\t\terrorThreshold = 0,\n\t\terrorWindow = 10_000,\n\t\tminimumCandidates = 6,\n\t\tonClose,\n\t\tonHalfOpen,\n\t\tonOpen,\n\t\tresetAfter = 30_000,\n\t} = options\n\n\t// errorIsFailure\n\tassert(\n\t\ttypeof errorIsFailure === \"function\",\n\t\t`\"errorIsFailure\" must be a function (received ${typeof errorIsFailure})`,\n\t)\n\n\t// errorThreshold\n\tassert(\n\t\terrorThreshold >= 0 && errorThreshold <= 1,\n\t\t`\"errorThreshold\" must be between 0 and 1 (received ${errorThreshold})`,\n\t)\n\n\t// errorWindow\n\tassert(\n\t\terrorWindow >= 1_000,\n\t\t`\"errorWindow\" must be milliseconds of at least 1 second (received ${errorWindow})`,\n\t)\n\n\t// minimumCandidates\n\tassert(\n\t\tminimumCandidates >= 1,\n\t\t`\"minimumCandidates\" must be greater than 0 (received ${minimumCandidates})`,\n\t)\n\n\t// (optional) onClose\n\tassert(\n\t\t!onClose || typeof onClose === \"function\",\n\t\t`\"onClose\" must be a function (received ${typeof onClose})`,\n\t)\n\n\t// (optional) onHalfOpen\n\tassert(\n\t\t!onHalfOpen || typeof onHalfOpen === \"function\",\n\t\t`\"onHalfOpen\" must be a function (received ${typeof onHalfOpen})`,\n\t)\n\n\t// (optional) onOpen\n\tassert(\n\t\t!onOpen || typeof onOpen === \"function\",\n\t\t`\"onOpen\" must be a function (received ${typeof onOpen})`,\n\t)\n\n\t// resetAfter\n\tassert(\n\t\tresetAfter >= 1_000,\n\t\t`\"resetAfter\" must be milliseconds of at least 1 second (received ${resetAfter})`,\n\t)\n\tassert(\n\t\tresetAfter >= errorWindow,\n\t\t`\"resetAfter\" must be greater than or equal to \"errorWindow\" (received ${resetAfter}, expected >= ${errorWindow})`,\n\t)\n\n\treturn {\n\t\terrorIsFailure,\n\t\terrorThreshold,\n\t\terrorWindow,\n\t\tminimumCandidates,\n\t\tonClose,\n\t\tonHalfOpen,\n\t\tonOpen,\n\t\tresetAfter,\n\t}\n}\n","import type { AnyFn } from \"./util\"\n\nexport const disposeKey = Symbol(\"disposeKey\")\n\nexport type CircuitState = \"closed\" | \"halfOpen\" | \"open\"\n\nexport interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {\n\t/**\n\t * Whether an error should be treated as non-retryable failure. When used and\n\t * when an error is considered a failure, the error will be thrown to the\n\t * caller.\n\t *\n\t * @default () => false // Errors are retryable by default\n\t */\n\terrorIsFailure?: (error: unknown) => boolean\n\n\t/**\n\t * The percentage of errors (as a number between 0 and 1) which must occur\n\t * within the error window before the circuit breaker opens.\n\t *\n\t * @default 0 // Any error opens the circuit\n\t */\n\terrorThreshold?: number\n\n\t/**\n\t * The sliding window of time in milliseconds over which errors are counted.\n\t *\n\t * @default 10_000 // 10 seconds\n\t */\n\terrorWindow?: number\n\n\t/**\n\t * If provided, then all rejected calls to `main` will be forwarded to\n\t * this function instead.\n\t *\n\t * @default undefined // No fallback, errors are propagated\n\t */\n\tfallback?: Fallback\n\n\t/**\n\t * The minimum number of calls that must be made before calculating the\n\t * error rate and determining whether the circuit breaker should open based on\n\t * the `errorThreshold`.\n\t *\n\t * @default 6\n\t */\n\tminimumCandidates?: number\n\n\t/**\n\t * Provide a function to be called when the circuit breaker is closed.\n\t */\n\tonClose?: () => void\n\n\t/**\n\t * Provide a function to be called when the circuit breaker transitions to\n\t * half-open state.\n\t */\n\tonHalfOpen?: () => void\n\n\t/**\n\t * Provide a function to be called when the circuit breaker is opened. It\n\t * receives the error as its only argument.\n\t */\n\tonOpen?: (cause: unknown) => void\n\n\t/**\n\t * The amount of time in milliseconds to wait before transitioning to a\n\t * half-open state.\n\t *\n\t * @default 30_000 // 30 seconds\n\t */\n\tresetAfter?: number\n}\n\nexport interface CircuitBreakerProtectedFn<\n\tRet = unknown,\n\tArgs extends unknown[] = never[],\n> {\n\t(...args: Args): Promise<Ret>\n\n\t/**\n\t * Free memory and stop timers. All future calls will be rejected with the\n\t * provided message.\n\t *\n\t * @default \"ERR_CIRCUIT_BREAKER_DISPOSED\"\n\t */\n\tdispose(disposeMessage?: string): 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 interface HistoryEntry {\n\ttimer: NodeJS.Timeout | undefined\n\tstatus: \"pending\" | \"resolved\" | \"rejected\"\n}\n\nexport type HistoryMap = Map<Promise<unknown>, HistoryEntry>\n\nexport interface MainFn<Ret = unknown, Args extends unknown[] = never[]> {\n\t(...args: Args): Promise<Ret>\n\n\t[disposeKey]?: (disposeMessage?: string) => void\n}\n","import { disposeKey, type MainFn } from \"./types.js\"\nimport { assert, delayMs, rejectOnAbort } 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\nexport interface RetryOptions {\n\t/**\n\t * Whether an error should be treated as non-retryable. When this returns\n\t * true, the error will be thrown immediately without retrying.\n\t *\n\t * @default () => false // All errors are retried\n\t */\n\tshouldRetry?: (error: unknown, attempt: number) => boolean\n\n\t/**\n\t * Maximum number of retries\n\t *\n\t * @default 3\n\t */\n\tmaxAttempts?: number\n\n\t/**\n\t * Function that returns a promise resolving when the next retry should occur.\n\t * Receives the attempt number (starting at 2) and an abort signal.\n\t *\n\t * @default () => Promise.resolve() // Immediate retry\n\t */\n\tretryDelay?: (attempt: number, signal: AbortSignal) => Promise<void>\n}\n\n/**\n * Wrap a function with retry logic. Errors will be retried according to the\n * provided options.\n *\n * @example\n * ```ts\n * // Compose with circuit breaker. Retry up to 3 times with no delay\n * const protectedA = createCircuitBreaker(\n * withRetry(unreliableApiCall, { maxAttempts: 3 })\n * )\n *\n * // Retry up to 5 times with exponential backoff\n * const protectedB = createCircuitBreaker(\n * withRetry(unreliableApiCall, {\n * maxAttempts: 5,\n * retryDelay: useExponentialBackoff(30),\n * })\n * )\n * ```\n */\nexport function withRetry<Ret, Args extends unknown[]>(\n\tmain: MainFn<Ret, Args>,\n\toptions: RetryOptions = {},\n): MainFn<Ret, Args> {\n\tconst {\n\t\tshouldRetry = () => true,\n\t\tmaxAttempts = 3,\n\t\tretryDelay = () => Promise.resolve(),\n\t} = options\n\n\tassert(maxAttempts >= 1, \"maxAttempts must be a number greater than 0\")\n\n\tconst controller = new AbortController()\n\tconst { signal } = controller\n\n\tasync function execute(args: Args, attempt = 1): Promise<Ret> {\n\t\ttry {\n\t\t\treturn await main(...args)\n\t\t} catch (cause) {\n\t\t\t// Check if we should retry this error\n\t\t\tif (!shouldRetry(cause, attempt)) throw cause\n\n\t\t\t// Check if we've exhausted attempts\n\t\t\tif (attempt >= maxAttempts)\n\t\t\t\tthrow new Error(`ERR_CIRCUIT_BREAKER_MAX_ATTEMPTS (${maxAttempts})`, {\n\t\t\t\t\tcause,\n\t\t\t\t})\n\n\t\t\t// Wait before retrying\n\t\t\tawait rejectOnAbort(signal, retryDelay(attempt + 1, signal))\n\n\t\t\t// Retry\n\t\t\treturn execute(args, attempt + 1)\n\t\t}\n\t}\n\n\treturn Object.assign(\n\t\tfunction withRetryFunction(...args: Args) {\n\t\t\treturn execute(args)\n\t\t},\n\t\t{\n\t\t\t[disposeKey]: (disposeMessage = \"ERR_CIRCUIT_BREAKER_DISPOSED\") => {\n\t\t\t\tconst reason = new ReferenceError(disposeMessage)\n\t\t\t\tmain[disposeKey]?.(disposeMessage)\n\t\t\t\tcontroller.abort(reason)\n\t\t\t},\n\t\t},\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\tconst controller = new AbortController()\n\tconst { signal } = controller\n\n\tfunction withTimeoutFunction(...args: Args) {\n\t\tlet teardown: () => void\n\t\tlet timer: NodeJS.Timeout\n\t\treturn Promise.race([\n\t\t\tmain(...args).finally(() => {\n\t\t\t\tclearTimeout(timer)\n\t\t\t\tsignal.removeEventListener(\"abort\", teardown)\n\t\t\t}),\n\t\t\tnew Promise<never>((_, reject) => {\n\t\t\t\tteardown = () => {\n\t\t\t\t\tclearTimeout(timer)\n\t\t\t\t\treject(signal.reason)\n\t\t\t\t}\n\t\t\t\ttimer = setTimeout(reject, timeoutMs, error)\n\t\t\t\tsignal.addEventListener(\"abort\", teardown, { once: true })\n\t\t\t}),\n\t\t])\n\t}\n\n\treturn Object.assign(withTimeoutFunction, {\n\t\t[disposeKey]: (disposeMessage = \"ERR_CIRCUIT_BREAKER_DISPOSED\") => {\n\t\t\tconst reason = new ReferenceError(disposeMessage)\n\t\t\tmain[disposeKey]?.(disposeMessage)\n\t\t\tcontroller.abort(reason)\n\t\t},\n\t})\n}\n","import { parseOptions } from \"./options.js\"\nimport {\n\tdisposeKey,\n\ttype CircuitBreakerOptions,\n\ttype CircuitBreakerProtectedFn,\n\ttype CircuitState,\n\ttype HistoryEntry,\n\ttype HistoryMap,\n\ttype MainFn,\n} from \"./types.js\"\nimport { assertNever, type AnyFn } from \"./util.js\"\n\nexport * from \"./helpers.js\"\nexport { delayMs } from \"./util.js\"\n\nexport function createCircuitBreaker<\n\tRet,\n\tArgs extends unknown[],\n\tFallback extends AnyFn = MainFn<Ret, Args>,\n>(\n\tmain: MainFn<Ret, Args>,\n\toptions: CircuitBreakerOptions<Fallback> = {},\n): CircuitBreakerProtectedFn<Ret, Args> {\n\tconst {\n\t\terrorIsFailure,\n\t\terrorThreshold,\n\t\terrorWindow,\n\t\tminimumCandidates,\n\t\tonClose,\n\t\tonHalfOpen,\n\t\tonOpen,\n\t\tresetAfter,\n\t} = parseOptions(options)\n\tconst controller = new AbortController()\n\tconst history: HistoryMap = new Map()\n\tconst signal = controller.signal\n\tlet failureCause: unknown\n\tlet fallback = options.fallback || (() => Promise.reject(failureCause))\n\tlet halfOpenPending: Promise<unknown> | undefined\n\tlet resetTimer: NodeJS.Timeout\n\tlet state: CircuitState = \"closed\"\n\n\tfunction clearFailure() {\n\t\tfailureCause = undefined\n\t}\n\n\tfunction closeCircuit() {\n\t\tstate = \"closed\"\n\t\tclearFailure()\n\t\tclearTimeout(resetTimer)\n\t\tonClose?.()\n\t}\n\n\tfunction failureRate() {\n\t\tlet failures = 0\n\t\tlet total = 0\n\t\tfor (const { status } of history.values()) {\n\t\t\tif (status === \"rejected\") failures++\n\t\t\tif (status !== \"pending\") total++\n\t\t}\n\t\t// Don't calculate anything until we have enough data\n\t\tif (!total || total < minimumCandidates) return 0\n\t\treturn failures / total\n\t}\n\n\t/**\n\t * Break the circuit and wait for a reset\n\t */\n\tfunction openCircuit(cause: unknown) {\n\t\tfailureCause = cause\n\t\tstate = \"open\"\n\t\tclearTimeout(resetTimer)\n\t\tresetTimer = setTimeout(() => {\n\t\t\tstate = \"halfOpen\"\n\t\t\tonHalfOpen?.()\n\t\t}, resetAfter)\n\t\tonOpen?.(cause)\n\t}\n\n\tfunction createHistoryItem<T>(pending: Promise<T>) {\n\t\tconst entry: HistoryEntry = { status: \"pending\", timer: undefined }\n\t\tconst teardown = () => {\n\t\t\tclearTimeout(entry.timer)\n\t\t\thistory.delete(pending)\n\t\t\tsignal.removeEventListener(\"abort\", teardown)\n\t\t}\n\t\tsignal.addEventListener(\"abort\", teardown, { once: true })\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(args: Args): Promise<Ret> {\n\t\t// Normal operation when circuit is closed. If an error occurs, keep track\n\t\t// of the failure count and open the circuit if it exceeds the threshold.\n\t\tif (state === \"closed\") {\n\t\t\tconst { pending, settle, teardown } = createHistoryItem(main(...args))\n\t\t\treturn pending.then(\n\t\t\t\t(result) => {\n\t\t\t\t\tsettle(\"resolved\")\n\t\t\t\t\treturn result\n\t\t\t\t},\n\t\t\t\t(cause: unknown) => {\n\t\t\t\t\t// Was the circuit disposed, or is this error considered a failure?\n\t\t\t\t\tif (signal.aborted || errorIsFailure(cause)) {\n\t\t\t\t\t\tteardown()\n\t\t\t\t\t\tthrow cause\n\t\t\t\t\t}\n\n\t\t\t\t\t// Should this error open the circuit?\n\t\t\t\t\tsettle(\"rejected\")\n\t\t\t\t\tif (failureRate() > errorThreshold) openCircuit(cause)\n\n\t\t\t\t\treturn fallback(...args)\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\n\t\t// Use the fallback while the circuit is open, or if a half-open trial\n\t\t// attempt was already made.\n\t\telse if (state === \"open\" || halfOpenPending) {\n\t\t\treturn fallback(...args)\n\t\t}\n\n\t\t// If the circuit is half-open, make one attempt. If it succeeds, close\n\t\t// the circuit and resume normal operation. If it fails, re-open the\n\t\t// circuit and run the fallback instead.\n\t\telse if (state === \"halfOpen\") {\n\t\t\treturn (halfOpenPending = main(...args))\n\t\t\t\t.finally(() => (halfOpenPending = undefined))\n\t\t\t\t.then(\n\t\t\t\t\t(result) => {\n\t\t\t\t\t\tif (signal.aborted) return result // disposed\n\t\t\t\t\t\tcloseCircuit()\n\t\t\t\t\t\treturn result\n\t\t\t\t\t},\n\t\t\t\t\t(cause: unknown) => {\n\t\t\t\t\t\t// Was the circuit disposed, or was this a non-retryable error?\n\t\t\t\t\t\tif (signal.aborted || errorIsFailure(cause)) throw cause\n\n\t\t\t\t\t\t// Open the circuit and use fallback\n\t\t\t\t\t\topenCircuit(cause)\n\t\t\t\t\t\treturn fallback(...args)\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\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(args), {\n\t\tdispose: (disposeMessage = \"ERR_CIRCUIT_BREAKER_DISPOSED\") => {\n\t\t\tconst reason = new ReferenceError(disposeMessage)\n\t\t\tmain[disposeKey]?.(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,6 +1,6 @@
1
1
  {
2
2
  "name": "breaker-box",
3
- "version": "4.1.0",
3
+ "version": "5.0.0",
4
4
  "description": "A zero-dependency circuit breaker implementation for Node.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,26 +26,29 @@
26
26
  "scripts": {
27
27
  "build": "pkgroll --clean-dist --src lib --target=node18 --sourcemap --env.NODE_ENV=production",
28
28
  "dev": "vitest",
29
- "format": "NODE_OPTIONS='--experimental-strip-types' prettier . --write",
30
- "prepublishOnly": "npm run test && npm run build",
29
+ "format": "prettier --cache --write .",
30
+ "prepublishOnly": "npm run test:ts && npm run test && npm run build",
31
31
  "reinstall": "rm -rf node_modules package-lock.json && npm install",
32
- "test": "vitest --run"
32
+ "test": "vitest --run",
33
+ "test:coverage": "vitest --run --coverage",
34
+ "test:ts": "tsc --noEmit"
33
35
  },
34
36
  "keywords": [
35
- "circuit-breaker",
37
+ "async",
36
38
  "fallback",
39
+ "fault-tolerance",
37
40
  "reliability",
38
41
  "try-again"
39
42
  ],
40
43
  "author": "Matthew Pietz <sirlancelot@gmail.com>",
41
44
  "license": "ISC",
42
45
  "devDependencies": {
43
- "@types/node": "24.5.0",
44
- "@vitest/coverage-v8": "3.2.4",
45
- "pkgroll": "2.15.4",
46
- "prettier": "3.6.2",
47
- "typescript": "5.9.2",
48
- "vitest": "3.2.4",
49
- "vitest-when": "0.8.0"
46
+ "@types/node": "24.10.2",
47
+ "@vitest/coverage-v8": "4.0.15",
48
+ "pkgroll": "2.21.4",
49
+ "prettier": "3.7.4",
50
+ "typescript": "5.9.3",
51
+ "vitest": "4.0.15",
52
+ "vitest-when": "0.10.0"
50
53
  }
51
54
  }