breaker-box 1.0.0 → 2.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 +17 -31
- package/dist/index.cjs +31 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -16
- package/dist/index.d.mts +14 -16
- package/dist/index.mjs +31 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -27,9 +27,10 @@ async function unreliableApiCall(data: string) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const protectedApiCall = createCircuitBreaker(unreliableApiCall, {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
errorIsFailure: () => true, // Any error is considered a failure
|
|
31
|
+
failureThreshold: 1, // Open circuit after first failure
|
|
32
|
+
fallback: undefined, // No fallback, errors are propagated
|
|
33
|
+
resetAfter: 30_000, // Try again after 30 seconds
|
|
33
34
|
})
|
|
34
35
|
|
|
35
36
|
try {
|
|
@@ -43,22 +44,13 @@ try {
|
|
|
43
44
|
### Event Monitoring
|
|
44
45
|
|
|
45
46
|
```typescript
|
|
46
|
-
const protectedFunction = createCircuitBreaker(unreliableApiCall
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
console.log("Circuit closed - normal operation resumed")
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
protectedFunction.on("reject", (error) => {
|
|
57
|
-
console.log("Function call rejected:", error.message)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
protectedFunction.on("resolve", () => {
|
|
61
|
-
console.log("Function call succeeded")
|
|
47
|
+
const protectedFunction = createCircuitBreaker(unreliableApiCall, {
|
|
48
|
+
onClose: () => {
|
|
49
|
+
console.log("Circuit closed - normal operation resumed")
|
|
50
|
+
},
|
|
51
|
+
onOpen: (cause) => {
|
|
52
|
+
console.log("Circuit opened due to:", cause.message)
|
|
53
|
+
},
|
|
62
54
|
})
|
|
63
55
|
|
|
64
56
|
// Check current state
|
|
@@ -83,26 +75,20 @@ Creates a circuit breaker around the provided async function.
|
|
|
83
75
|
|
|
84
76
|
- `fn`: The async function to protect
|
|
85
77
|
- `options`: Configuration object (optional)
|
|
78
|
+
- `errorIsFailure`: Function to determine if an error counts as failure (default: all errors)
|
|
86
79
|
- `failureThreshold`: Number of failures before opening circuit (default: 1)
|
|
80
|
+
- `fallback`: Function to call when circuit is open (default: undefined)
|
|
81
|
+
- `onClose`: Function to call when circuit is closed (default: undefined)
|
|
82
|
+
- `onOpen`: Function to call when circuit is opened (default: undefined)
|
|
87
83
|
- `resetAfter`: Milliseconds to wait before trying again (default: 30000)
|
|
88
|
-
- `errorIsFailure`: Function to determine if an error counts as failure (default: all errors)
|
|
89
|
-
- `fallback`: Function to call when circuit is open (default: throws CircuitOpenError)
|
|
90
84
|
|
|
91
85
|
#### Returns
|
|
92
86
|
|
|
93
87
|
A function with the same signature as `fn` and additional methods:
|
|
94
88
|
|
|
95
|
-
- `.getState()`: Returns current circuit state
|
|
96
|
-
- `.on(event, listener)`: Add event listener
|
|
97
|
-
- `.off(event, listener)`: Remove event listener
|
|
98
89
|
- `.dispose()`: Clean up resources
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
- `open`: Circuit opened due to failures
|
|
103
|
-
- `close`: Circuit closed and resumed normal operation
|
|
104
|
-
- `reject`: Function call was rejected
|
|
105
|
-
- `resolve`: Function call succeeded
|
|
90
|
+
- `.getLatestError()`: Returns the error which triggered the circuit breaker
|
|
91
|
+
- `.getState()`: Returns current circuit state
|
|
106
92
|
|
|
107
93
|
### Development
|
|
108
94
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,88 +1,69 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var EventEmitter = require('node:events');
|
|
4
|
-
|
|
5
|
-
const assertNever = (value, message = "Unexpected value") => (
|
|
6
|
-
/* v8 ignore next */
|
|
7
|
-
new TypeError(`${message}: ${value}`)
|
|
8
|
-
);
|
|
9
|
-
|
|
10
3
|
function createCircuitBreaker(main, options = {}) {
|
|
11
|
-
const events = new EventEmitter();
|
|
12
4
|
const {
|
|
13
5
|
errorIsFailure = () => true,
|
|
14
6
|
failureThreshold = 1,
|
|
15
|
-
fallback = () =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
throw new Error(`CircuitOpenError: ${msg}`, { cause });
|
|
19
|
-
},
|
|
7
|
+
fallback = () => Promise.reject(failureCause),
|
|
8
|
+
onClose,
|
|
9
|
+
onOpen,
|
|
20
10
|
resetAfter = 3e4
|
|
21
11
|
} = options;
|
|
12
|
+
let halfOpenPending;
|
|
22
13
|
let state = "closed";
|
|
23
14
|
let failureCause = void 0;
|
|
24
15
|
let failureCount = 0;
|
|
25
16
|
let resetTimer = void 0;
|
|
26
17
|
function openCircuit(cause) {
|
|
27
|
-
if (state === "disposed") return;
|
|
28
18
|
state = "open";
|
|
29
|
-
|
|
30
|
-
failureCause = cause;
|
|
19
|
+
onOpen?.(cause);
|
|
31
20
|
clearTimeout(resetTimer);
|
|
32
21
|
resetTimer = setTimeout(() => state = "halfOpen", resetAfter);
|
|
33
22
|
}
|
|
34
23
|
function closeCircuit() {
|
|
35
|
-
if (state === "disposed") return;
|
|
36
|
-
if (state === "halfOpen") events.emit("close");
|
|
37
24
|
state = "closed";
|
|
38
25
|
failureCause = void 0;
|
|
39
26
|
failureCount = 0;
|
|
40
27
|
clearTimeout(resetTimer);
|
|
41
28
|
}
|
|
42
|
-
const mainWithEmit = async (...args) => {
|
|
43
|
-
try {
|
|
44
|
-
const result = await main(...args);
|
|
45
|
-
events.emit("resolve");
|
|
46
|
-
closeCircuit();
|
|
47
|
-
return result;
|
|
48
|
-
} catch (cause) {
|
|
49
|
-
events.emit("reject", cause);
|
|
50
|
-
throw cause;
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
29
|
async function protectedFunction(...args) {
|
|
54
|
-
if (state === "
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
return mainWithEmit(...args).catch((cause) => {
|
|
58
|
-
openCircuit(cause);
|
|
59
|
-
return fallback(...args);
|
|
60
|
-
});
|
|
61
|
-
} else if (state === "closed") {
|
|
62
|
-
return mainWithEmit(...args).catch((cause) => {
|
|
30
|
+
if (state === "closed") {
|
|
31
|
+
return main(...args).catch((cause) => {
|
|
32
|
+
if (state === "disposed") throw cause;
|
|
63
33
|
failureCause = cause;
|
|
64
34
|
failureCount += errorIsFailure(cause) ? 1 : 0;
|
|
65
35
|
if (failureCount >= failureThreshold) openCircuit(cause);
|
|
66
|
-
return
|
|
36
|
+
return protectedFunction(...args);
|
|
67
37
|
});
|
|
68
|
-
} else if (state === "
|
|
38
|
+
} else if (state === "open" || halfOpenPending) {
|
|
39
|
+
return fallback(...args);
|
|
40
|
+
} else if (state === "halfOpen") {
|
|
41
|
+
return (halfOpenPending = main(...args)).finally(() => halfOpenPending = void 0).then(
|
|
42
|
+
(result) => {
|
|
43
|
+
if (state !== "disposed") {
|
|
44
|
+
closeCircuit();
|
|
45
|
+
onClose?.();
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
},
|
|
49
|
+
(cause) => {
|
|
50
|
+
if (state === "disposed") throw cause;
|
|
51
|
+
openCircuit(cause);
|
|
52
|
+
return fallback(...args);
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
} else if (state === "disposed") {
|
|
69
56
|
throw new Error("Circuit breaker has been disposed");
|
|
70
|
-
else
|
|
57
|
+
} else {
|
|
58
|
+
throw void 0;
|
|
59
|
+
}
|
|
71
60
|
}
|
|
72
61
|
protectedFunction.dispose = () => {
|
|
73
|
-
events.removeAllListeners();
|
|
74
62
|
closeCircuit();
|
|
75
63
|
state = "disposed";
|
|
76
64
|
};
|
|
65
|
+
protectedFunction.getLatestError = () => failureCause;
|
|
77
66
|
protectedFunction.getState = () => state;
|
|
78
|
-
protectedFunction.off = (event, listener) => {
|
|
79
|
-
events.removeListener(event, listener);
|
|
80
|
-
return protectedFunction;
|
|
81
|
-
};
|
|
82
|
-
protectedFunction.on = (event, listener) => {
|
|
83
|
-
events.addListener(event, listener);
|
|
84
|
-
return protectedFunction;
|
|
85
|
-
};
|
|
86
67
|
return protectedFunction;
|
|
87
68
|
}
|
|
88
69
|
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../lib/
|
|
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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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
type AnyFn = (...args: any[]) => any;
|
|
2
|
+
|
|
1
3
|
type CircuitState = "closed" | "disposed" | "halfOpen" | "open";
|
|
2
|
-
interface CircuitBreakerOptions<
|
|
4
|
+
interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
|
|
3
5
|
/**
|
|
4
6
|
* Whether an error should be considered a failure that could trigger
|
|
5
7
|
* the circuit breaker. Use this to prevent certain errors from
|
|
@@ -18,9 +20,13 @@ interface CircuitBreakerOptions<Args extends unknown[], Ret> {
|
|
|
18
20
|
* If provided, then all rejected calls to `main` will be forwarded to
|
|
19
21
|
* this function instead.
|
|
20
22
|
*
|
|
21
|
-
* @default undefined // No fallback,
|
|
23
|
+
* @default undefined // No fallback, errors are propagated
|
|
22
24
|
*/
|
|
23
|
-
fallback?:
|
|
25
|
+
fallback?: Fallback;
|
|
26
|
+
/** Called when the circuit breaker is closed */
|
|
27
|
+
onClose?: () => void;
|
|
28
|
+
/** Called when the circuit breaker is opened */
|
|
29
|
+
onOpen?: (cause: unknown) => void;
|
|
24
30
|
/**
|
|
25
31
|
* The amount of time to wait before allowing a half-open state.
|
|
26
32
|
*
|
|
@@ -28,24 +34,16 @@ interface CircuitBreakerOptions<Args extends unknown[], Ret> {
|
|
|
28
34
|
*/
|
|
29
35
|
resetAfter?: number;
|
|
30
36
|
}
|
|
31
|
-
interface
|
|
32
|
-
close: [];
|
|
33
|
-
open: [cause: unknown];
|
|
34
|
-
reject: [cause: unknown];
|
|
35
|
-
resolve: [];
|
|
36
|
-
}
|
|
37
|
-
interface ProtectedFunction<Args extends unknown[], Ret> {
|
|
37
|
+
interface CircuitBreakerProtectedFn<Ret = unknown, Args extends unknown[] = never[]> {
|
|
38
38
|
(...args: Args): Promise<Ret>;
|
|
39
39
|
/** Free memory and stop timers */
|
|
40
40
|
dispose(): void;
|
|
41
|
+
/** Get the last error which triggered the circuit breaker */
|
|
42
|
+
getLatestError(): unknown | undefined;
|
|
41
43
|
/** Get the current state of the circuit breaker */
|
|
42
44
|
getState(): CircuitState;
|
|
43
|
-
/** Remove a listener from the circuit breaker */
|
|
44
|
-
off<T extends keyof EventMap>(event: T, listener: (...args: EventMap[T]) => void): this;
|
|
45
|
-
/** Add a listener to the circuit breaker */
|
|
46
|
-
on<T extends keyof EventMap>(event: T, listener: (...args: EventMap[T]) => void): this;
|
|
47
45
|
}
|
|
48
|
-
declare function createCircuitBreaker<Args extends unknown[], Ret
|
|
46
|
+
declare function createCircuitBreaker<Ret, Args extends unknown[], Fallback extends AnyFn = (...args: Args) => Promise<Ret>>(main: (...args: Args) => Promise<Ret>, options?: CircuitBreakerOptions<Fallback>): CircuitBreakerProtectedFn<Ret, Args>;
|
|
49
47
|
|
|
50
48
|
export { createCircuitBreaker };
|
|
51
|
-
export type { CircuitBreakerOptions,
|
|
49
|
+
export type { CircuitBreakerOptions, CircuitBreakerProtectedFn, CircuitState };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
type AnyFn = (...args: any[]) => any;
|
|
2
|
+
|
|
1
3
|
type CircuitState = "closed" | "disposed" | "halfOpen" | "open";
|
|
2
|
-
interface CircuitBreakerOptions<
|
|
4
|
+
interface CircuitBreakerOptions<Fallback extends AnyFn = AnyFn> {
|
|
3
5
|
/**
|
|
4
6
|
* Whether an error should be considered a failure that could trigger
|
|
5
7
|
* the circuit breaker. Use this to prevent certain errors from
|
|
@@ -18,9 +20,13 @@ interface CircuitBreakerOptions<Args extends unknown[], Ret> {
|
|
|
18
20
|
* If provided, then all rejected calls to `main` will be forwarded to
|
|
19
21
|
* this function instead.
|
|
20
22
|
*
|
|
21
|
-
* @default undefined // No fallback,
|
|
23
|
+
* @default undefined // No fallback, errors are propagated
|
|
22
24
|
*/
|
|
23
|
-
fallback?:
|
|
25
|
+
fallback?: Fallback;
|
|
26
|
+
/** Called when the circuit breaker is closed */
|
|
27
|
+
onClose?: () => void;
|
|
28
|
+
/** Called when the circuit breaker is opened */
|
|
29
|
+
onOpen?: (cause: unknown) => void;
|
|
24
30
|
/**
|
|
25
31
|
* The amount of time to wait before allowing a half-open state.
|
|
26
32
|
*
|
|
@@ -28,24 +34,16 @@ interface CircuitBreakerOptions<Args extends unknown[], Ret> {
|
|
|
28
34
|
*/
|
|
29
35
|
resetAfter?: number;
|
|
30
36
|
}
|
|
31
|
-
interface
|
|
32
|
-
close: [];
|
|
33
|
-
open: [cause: unknown];
|
|
34
|
-
reject: [cause: unknown];
|
|
35
|
-
resolve: [];
|
|
36
|
-
}
|
|
37
|
-
interface ProtectedFunction<Args extends unknown[], Ret> {
|
|
37
|
+
interface CircuitBreakerProtectedFn<Ret = unknown, Args extends unknown[] = never[]> {
|
|
38
38
|
(...args: Args): Promise<Ret>;
|
|
39
39
|
/** Free memory and stop timers */
|
|
40
40
|
dispose(): void;
|
|
41
|
+
/** Get the last error which triggered the circuit breaker */
|
|
42
|
+
getLatestError(): unknown | undefined;
|
|
41
43
|
/** Get the current state of the circuit breaker */
|
|
42
44
|
getState(): CircuitState;
|
|
43
|
-
/** Remove a listener from the circuit breaker */
|
|
44
|
-
off<T extends keyof EventMap>(event: T, listener: (...args: EventMap[T]) => void): this;
|
|
45
|
-
/** Add a listener to the circuit breaker */
|
|
46
|
-
on<T extends keyof EventMap>(event: T, listener: (...args: EventMap[T]) => void): this;
|
|
47
45
|
}
|
|
48
|
-
declare function createCircuitBreaker<Args extends unknown[], Ret
|
|
46
|
+
declare function createCircuitBreaker<Ret, Args extends unknown[], Fallback extends AnyFn = (...args: Args) => Promise<Ret>>(main: (...args: Args) => Promise<Ret>, options?: CircuitBreakerOptions<Fallback>): CircuitBreakerProtectedFn<Ret, Args>;
|
|
49
47
|
|
|
50
48
|
export { createCircuitBreaker };
|
|
51
|
-
export type { CircuitBreakerOptions,
|
|
49
|
+
export type { CircuitBreakerOptions, CircuitBreakerProtectedFn, CircuitState };
|
package/dist/index.mjs
CHANGED
|
@@ -1,86 +1,67 @@
|
|
|
1
|
-
import EventEmitter from 'node:events';
|
|
2
|
-
|
|
3
|
-
const assertNever = (value, message = "Unexpected value") => (
|
|
4
|
-
/* v8 ignore next */
|
|
5
|
-
new TypeError(`${message}: ${value}`)
|
|
6
|
-
);
|
|
7
|
-
|
|
8
1
|
function createCircuitBreaker(main, options = {}) {
|
|
9
|
-
const events = new EventEmitter();
|
|
10
2
|
const {
|
|
11
3
|
errorIsFailure = () => true,
|
|
12
4
|
failureThreshold = 1,
|
|
13
|
-
fallback = () =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
throw new Error(`CircuitOpenError: ${msg}`, { cause });
|
|
17
|
-
},
|
|
5
|
+
fallback = () => Promise.reject(failureCause),
|
|
6
|
+
onClose,
|
|
7
|
+
onOpen,
|
|
18
8
|
resetAfter = 3e4
|
|
19
9
|
} = options;
|
|
10
|
+
let halfOpenPending;
|
|
20
11
|
let state = "closed";
|
|
21
12
|
let failureCause = void 0;
|
|
22
13
|
let failureCount = 0;
|
|
23
14
|
let resetTimer = void 0;
|
|
24
15
|
function openCircuit(cause) {
|
|
25
|
-
if (state === "disposed") return;
|
|
26
16
|
state = "open";
|
|
27
|
-
|
|
28
|
-
failureCause = cause;
|
|
17
|
+
onOpen?.(cause);
|
|
29
18
|
clearTimeout(resetTimer);
|
|
30
19
|
resetTimer = setTimeout(() => state = "halfOpen", resetAfter);
|
|
31
20
|
}
|
|
32
21
|
function closeCircuit() {
|
|
33
|
-
if (state === "disposed") return;
|
|
34
|
-
if (state === "halfOpen") events.emit("close");
|
|
35
22
|
state = "closed";
|
|
36
23
|
failureCause = void 0;
|
|
37
24
|
failureCount = 0;
|
|
38
25
|
clearTimeout(resetTimer);
|
|
39
26
|
}
|
|
40
|
-
const mainWithEmit = async (...args) => {
|
|
41
|
-
try {
|
|
42
|
-
const result = await main(...args);
|
|
43
|
-
events.emit("resolve");
|
|
44
|
-
closeCircuit();
|
|
45
|
-
return result;
|
|
46
|
-
} catch (cause) {
|
|
47
|
-
events.emit("reject", cause);
|
|
48
|
-
throw cause;
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
27
|
async function protectedFunction(...args) {
|
|
52
|
-
if (state === "
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
return mainWithEmit(...args).catch((cause) => {
|
|
56
|
-
openCircuit(cause);
|
|
57
|
-
return fallback(...args);
|
|
58
|
-
});
|
|
59
|
-
} else if (state === "closed") {
|
|
60
|
-
return mainWithEmit(...args).catch((cause) => {
|
|
28
|
+
if (state === "closed") {
|
|
29
|
+
return main(...args).catch((cause) => {
|
|
30
|
+
if (state === "disposed") throw cause;
|
|
61
31
|
failureCause = cause;
|
|
62
32
|
failureCount += errorIsFailure(cause) ? 1 : 0;
|
|
63
33
|
if (failureCount >= failureThreshold) openCircuit(cause);
|
|
64
|
-
return
|
|
34
|
+
return protectedFunction(...args);
|
|
65
35
|
});
|
|
66
|
-
} else if (state === "
|
|
36
|
+
} else if (state === "open" || halfOpenPending) {
|
|
37
|
+
return fallback(...args);
|
|
38
|
+
} else if (state === "halfOpen") {
|
|
39
|
+
return (halfOpenPending = main(...args)).finally(() => halfOpenPending = void 0).then(
|
|
40
|
+
(result) => {
|
|
41
|
+
if (state !== "disposed") {
|
|
42
|
+
closeCircuit();
|
|
43
|
+
onClose?.();
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
},
|
|
47
|
+
(cause) => {
|
|
48
|
+
if (state === "disposed") throw cause;
|
|
49
|
+
openCircuit(cause);
|
|
50
|
+
return fallback(...args);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
} else if (state === "disposed") {
|
|
67
54
|
throw new Error("Circuit breaker has been disposed");
|
|
68
|
-
else
|
|
55
|
+
} else {
|
|
56
|
+
throw void 0;
|
|
57
|
+
}
|
|
69
58
|
}
|
|
70
59
|
protectedFunction.dispose = () => {
|
|
71
|
-
events.removeAllListeners();
|
|
72
60
|
closeCircuit();
|
|
73
61
|
state = "disposed";
|
|
74
62
|
};
|
|
63
|
+
protectedFunction.getLatestError = () => failureCause;
|
|
75
64
|
protectedFunction.getState = () => state;
|
|
76
|
-
protectedFunction.off = (event, listener) => {
|
|
77
|
-
events.removeListener(event, listener);
|
|
78
|
-
return protectedFunction;
|
|
79
|
-
};
|
|
80
|
-
protectedFunction.on = (event, listener) => {
|
|
81
|
-
events.addListener(event, listener);
|
|
82
|
-
return protectedFunction;
|
|
83
|
-
};
|
|
84
65
|
return protectedFunction;
|
|
85
66
|
}
|
|
86
67
|
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":["../lib/
|
|
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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breaker-box",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A zero-dependency circuit breaker implementation for Node.js",
|
|
5
5
|
"repository": "github:sirlancelot/breaker-box",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -20,14 +20,16 @@
|
|
|
20
20
|
"dist"
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
|
-
"build": "pkgroll --clean-dist --src lib --target=node18 --sourcemap",
|
|
23
|
+
"build": "pkgroll --clean-dist --src lib --target=node18 --sourcemap --env.NODE_ENV=production",
|
|
24
24
|
"dev": "vitest",
|
|
25
25
|
"prepublishOnly": "npm run build",
|
|
26
26
|
"test": "vitest --run"
|
|
27
27
|
},
|
|
28
28
|
"keywords": [
|
|
29
29
|
"circuit-breaker",
|
|
30
|
-
"
|
|
30
|
+
"fallback",
|
|
31
|
+
"reliability",
|
|
32
|
+
"try-again"
|
|
31
33
|
],
|
|
32
34
|
"author": "Matthew Pietz <sirlancelot@gmail.com>",
|
|
33
35
|
"license": "ISC",
|