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