@gwash3189/fusebox 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +64 -2
- package/dist/index.cjs +229 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +62 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +62 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +222 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +71 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gwash3189
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,65 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @gwash3189/fusebox
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A fluent, zero-dependency TypeScript circuit breaker library with sliding failure windows, cooldown recovery, fallbacks, and lifecycle event hooks.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @gwash3189/fusebox
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import Breaker from '@gwash3189/fusebox'
|
|
15
|
+
|
|
16
|
+
const result = await Breaker.for('stripe-api')
|
|
17
|
+
.threshold({ failures: 5, within: 10_000 })
|
|
18
|
+
.cooldown(30_000)
|
|
19
|
+
.fallback(() => ({ cached: true }))
|
|
20
|
+
.work(async () => {
|
|
21
|
+
return await fetchStripeCustomer(id)
|
|
22
|
+
})
|
|
23
|
+
.on('open', (err, key) => {
|
|
24
|
+
console.warn(`Circuit ${key} opened due to:`, err)
|
|
25
|
+
})
|
|
26
|
+
.on('half-open', (key) => {
|
|
27
|
+
console.info(`Testing downstream recovery for ${key}`)
|
|
28
|
+
})
|
|
29
|
+
.on('close', (key) => {
|
|
30
|
+
console.info(`Circuit ${key} recovered and closed`)
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Circuit States
|
|
35
|
+
|
|
36
|
+
1. **`closed`**: Normal operation. Requests pass through to `.work()`. Failures are tracked within the configured `within` time window (or consecutively if `within` is omitted).
|
|
37
|
+
2. **`open`**: When failures reach `threshold`, the circuit trips to `open` for the duration of `cooldown` ms. Calls fail fast with `CircuitOpenError` (or invoke `.fallback()` if configured) without calling `.work()`.
|
|
38
|
+
3. **`half-open`**: Once `cooldown` has elapsed, the next call is allowed through as a single probe:
|
|
39
|
+
- If the probe succeeds, the circuit transitions back to **`closed`**.
|
|
40
|
+
- If the probe fails, the circuit immediately re-opens for another `cooldown` period.
|
|
41
|
+
- Concurrent calls while the probe is in flight fail fast.
|
|
42
|
+
|
|
43
|
+
## API
|
|
44
|
+
|
|
45
|
+
- `Breaker.for(key: string)` / `Breaker.key(key: string)`: Select or create the circuit breaker state for `key`.
|
|
46
|
+
- `.threshold(failures: number | { failures: number, within?: number })`: Number of failures required to trip the circuit (default: `5`).
|
|
47
|
+
- `.cooldown(ms: number)`: Duration in milliseconds the circuit stays open before allowing a half-open probe (default: `30_000`).
|
|
48
|
+
- `.fallback(fn: (error: unknown) => Promise<T> | T)`: Fallback function invoked when `.work()` fails or when the circuit is open.
|
|
49
|
+
- `.work(fn: () => Promise<T> | T)`: The protected operation to execute.
|
|
50
|
+
- `.on('open' | 'close' | 'half-open' | 'failure' | 'reject', handler)`: Subscribe to circuit lifecycle events.
|
|
51
|
+
- `.state`: Inspect the current state (`'closed' | 'open' | 'half-open'`) of the circuit.
|
|
52
|
+
- `.reset()` / `resetCircuits()`: Reset a specific circuit or all circuits back to `'closed'`.
|
|
53
|
+
|
|
54
|
+
## Development
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm test # Run tests with Vitest
|
|
58
|
+
npm run lint # Run linter with Oxlint
|
|
59
|
+
npm run type-check # Run TypeScript type checker
|
|
60
|
+
npm run build # Build ESM + CJS bundles with tsdown
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } });
|
|
2
|
+
// src/errors.ts
|
|
3
|
+
var FuseboxError = class extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "FuseboxError";
|
|
7
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var CircuitOpenError = class extends FuseboxError {
|
|
11
|
+
key;
|
|
12
|
+
remainingCooldownMs;
|
|
13
|
+
constructor(key, remainingCooldownMs) {
|
|
14
|
+
super(`Circuit breaker "${key}" is open. Cooldown remaining: ${Math.max(0, remainingCooldownMs)}ms`);
|
|
15
|
+
this.name = "CircuitOpenError";
|
|
16
|
+
this.key = key;
|
|
17
|
+
this.remainingCooldownMs = Math.max(0, remainingCooldownMs);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/breaker.ts
|
|
22
|
+
const circuits = /* @__PURE__ */ new Map();
|
|
23
|
+
function getOrCreateCircuit(key) {
|
|
24
|
+
let entry = circuits.get(key);
|
|
25
|
+
if (!entry) {
|
|
26
|
+
entry = {
|
|
27
|
+
state: "closed",
|
|
28
|
+
failureTimestamps: [],
|
|
29
|
+
openedAt: null,
|
|
30
|
+
halfOpenInFlight: false
|
|
31
|
+
};
|
|
32
|
+
circuits.set(key, entry);
|
|
33
|
+
}
|
|
34
|
+
return entry;
|
|
35
|
+
}
|
|
36
|
+
var BreakerBuilder = class BreakerBuilder {
|
|
37
|
+
config;
|
|
38
|
+
executionPromise;
|
|
39
|
+
constructor(config) {
|
|
40
|
+
this.config = {
|
|
41
|
+
key: config?.key ?? "default",
|
|
42
|
+
failures: config?.failures ?? 5,
|
|
43
|
+
withinMs: config?.withinMs,
|
|
44
|
+
cooldownMs: config?.cooldownMs ?? 3e4,
|
|
45
|
+
workFn: config?.workFn,
|
|
46
|
+
fallbackFn: config?.fallbackFn,
|
|
47
|
+
openHandlers: [...config?.openHandlers ?? []],
|
|
48
|
+
closeHandlers: [...config?.closeHandlers ?? []],
|
|
49
|
+
halfOpenHandlers: [...config?.halfOpenHandlers ?? []],
|
|
50
|
+
failureHandlers: [...config?.failureHandlers ?? []],
|
|
51
|
+
rejectHandlers: [...config?.rejectHandlers ?? []]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
for(key) {
|
|
55
|
+
if (!key || key.trim().length === 0) throw new RangeError("circuit key must be a non-empty string");
|
|
56
|
+
return new BreakerBuilder({
|
|
57
|
+
...this.config,
|
|
58
|
+
key
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
key(key) {
|
|
62
|
+
return this.for(key);
|
|
63
|
+
}
|
|
64
|
+
threshold(options) {
|
|
65
|
+
if (typeof options === "number") {
|
|
66
|
+
if (!Number.isInteger(options) || options < 1) throw new RangeError("failures threshold must be an integer of at least 1");
|
|
67
|
+
return new BreakerBuilder({
|
|
68
|
+
...this.config,
|
|
69
|
+
failures: options,
|
|
70
|
+
withinMs: void 0
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (!Number.isInteger(options.failures) || options.failures < 1) throw new RangeError("failures threshold must be an integer of at least 1");
|
|
74
|
+
if (options.within !== void 0 && options.within <= 0) throw new RangeError("within window must be a positive number");
|
|
75
|
+
return new BreakerBuilder({
|
|
76
|
+
...this.config,
|
|
77
|
+
failures: options.failures,
|
|
78
|
+
withinMs: options.within
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
cooldown(ms) {
|
|
82
|
+
if (ms < 0) throw new RangeError("cooldown must be a non-negative number");
|
|
83
|
+
return new BreakerBuilder({
|
|
84
|
+
...this.config,
|
|
85
|
+
cooldownMs: ms
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
fallback(fn) {
|
|
89
|
+
return new BreakerBuilder({
|
|
90
|
+
...this.config,
|
|
91
|
+
fallbackFn: fn
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
work(fn) {
|
|
95
|
+
return new BreakerBuilder({
|
|
96
|
+
...this.config,
|
|
97
|
+
workFn: fn
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
on(event, handler) {
|
|
101
|
+
switch (event) {
|
|
102
|
+
case "open": return new BreakerBuilder({
|
|
103
|
+
...this.config,
|
|
104
|
+
openHandlers: [...this.config.openHandlers, handler]
|
|
105
|
+
});
|
|
106
|
+
case "close": return new BreakerBuilder({
|
|
107
|
+
...this.config,
|
|
108
|
+
closeHandlers: [...this.config.closeHandlers, handler]
|
|
109
|
+
});
|
|
110
|
+
case "half-open": return new BreakerBuilder({
|
|
111
|
+
...this.config,
|
|
112
|
+
halfOpenHandlers: [...this.config.halfOpenHandlers, handler]
|
|
113
|
+
});
|
|
114
|
+
case "failure": return new BreakerBuilder({
|
|
115
|
+
...this.config,
|
|
116
|
+
failureHandlers: [...this.config.failureHandlers, handler]
|
|
117
|
+
});
|
|
118
|
+
case "reject": return new BreakerBuilder({
|
|
119
|
+
...this.config,
|
|
120
|
+
rejectHandlers: [...this.config.rejectHandlers, handler]
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
get state() {
|
|
125
|
+
const entry = getOrCreateCircuit(this.config.key);
|
|
126
|
+
if (entry.state === "open" && entry.openedAt !== null) {
|
|
127
|
+
if (Date.now() - entry.openedAt >= this.config.cooldownMs) return "half-open";
|
|
128
|
+
}
|
|
129
|
+
return entry.state;
|
|
130
|
+
}
|
|
131
|
+
reset(key) {
|
|
132
|
+
circuits.delete(key ?? this.config.key);
|
|
133
|
+
}
|
|
134
|
+
resetAll() {
|
|
135
|
+
circuits.clear();
|
|
136
|
+
}
|
|
137
|
+
then(onfulfilled, onrejected) {
|
|
138
|
+
if (!this.executionPromise) this.executionPromise = this.execute();
|
|
139
|
+
return this.executionPromise.then(onfulfilled, onrejected);
|
|
140
|
+
}
|
|
141
|
+
catch(onrejected) {
|
|
142
|
+
return this.then(void 0, onrejected);
|
|
143
|
+
}
|
|
144
|
+
finally(onfinally) {
|
|
145
|
+
return this.then((value) => {
|
|
146
|
+
onfinally?.();
|
|
147
|
+
return value;
|
|
148
|
+
}, (reason) => {
|
|
149
|
+
onfinally?.();
|
|
150
|
+
throw reason;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async execute() {
|
|
154
|
+
const { key, failures, withinMs, cooldownMs, workFn, fallbackFn, openHandlers, closeHandlers, halfOpenHandlers, failureHandlers, rejectHandlers } = this.config;
|
|
155
|
+
if (!workFn) throw new Error("No work function provided. Call .work(fn) before awaiting.");
|
|
156
|
+
const entry = getOrCreateCircuit(key);
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
if (entry.state === "open") {
|
|
159
|
+
const elapsed = entry.openedAt !== null ? now - entry.openedAt : cooldownMs;
|
|
160
|
+
if (elapsed >= cooldownMs) {
|
|
161
|
+
entry.state = "half-open";
|
|
162
|
+
entry.halfOpenInFlight = false;
|
|
163
|
+
for (const handler of halfOpenHandlers) await handler(key);
|
|
164
|
+
} else {
|
|
165
|
+
const openError = new CircuitOpenError(key, cooldownMs - elapsed);
|
|
166
|
+
for (const handler of rejectHandlers) await handler(openError, key);
|
|
167
|
+
if (fallbackFn) return await fallbackFn(openError);
|
|
168
|
+
throw openError;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (entry.state === "half-open") {
|
|
172
|
+
if (entry.halfOpenInFlight) {
|
|
173
|
+
const openError = new CircuitOpenError(key, cooldownMs);
|
|
174
|
+
for (const handler of rejectHandlers) await handler(openError, key);
|
|
175
|
+
if (fallbackFn) return await fallbackFn(openError);
|
|
176
|
+
throw openError;
|
|
177
|
+
}
|
|
178
|
+
entry.halfOpenInFlight = true;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const result = await workFn();
|
|
182
|
+
if (entry.state === "half-open") {
|
|
183
|
+
entry.state = "closed";
|
|
184
|
+
entry.failureTimestamps = [];
|
|
185
|
+
entry.openedAt = null;
|
|
186
|
+
entry.halfOpenInFlight = false;
|
|
187
|
+
for (const handler of closeHandlers) await handler(key);
|
|
188
|
+
} else if (withinMs === void 0) entry.failureTimestamps = [];
|
|
189
|
+
return result;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
for (const handler of failureHandlers) await handler(error, key);
|
|
192
|
+
const failTime = Date.now();
|
|
193
|
+
if (entry.state === "half-open") {
|
|
194
|
+
entry.state = "open";
|
|
195
|
+
entry.openedAt = failTime;
|
|
196
|
+
entry.halfOpenInFlight = false;
|
|
197
|
+
entry.failureTimestamps = [failTime];
|
|
198
|
+
for (const handler of openHandlers) await handler(error, key);
|
|
199
|
+
} else {
|
|
200
|
+
if (withinMs !== void 0) {
|
|
201
|
+
const cutoff = failTime - withinMs;
|
|
202
|
+
entry.failureTimestamps = entry.failureTimestamps.filter((t) => t >= cutoff);
|
|
203
|
+
}
|
|
204
|
+
entry.failureTimestamps.push(failTime);
|
|
205
|
+
if (entry.failureTimestamps.length >= failures) {
|
|
206
|
+
entry.state = "open";
|
|
207
|
+
entry.openedAt = failTime;
|
|
208
|
+
for (const handler of openHandlers) await handler(error, key);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (fallbackFn) return await fallbackFn(error);
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const Breaker = new BreakerBuilder();
|
|
217
|
+
const Fusebox = Breaker;
|
|
218
|
+
function resetCircuits() {
|
|
219
|
+
circuits.clear();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
exports.Breaker = Breaker;
|
|
223
|
+
exports.BreakerBuilder = BreakerBuilder;
|
|
224
|
+
exports.CircuitOpenError = CircuitOpenError;
|
|
225
|
+
exports.Fusebox = Fusebox;
|
|
226
|
+
exports.FuseboxError = FuseboxError;
|
|
227
|
+
exports.default = Breaker;
|
|
228
|
+
exports.resetCircuits = resetCircuits;
|
|
229
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/errors.ts","../src/breaker.ts"],"sourcesContent":["export class FuseboxError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'FuseboxError'\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n\nexport class CircuitOpenError extends FuseboxError {\n readonly key: string\n readonly remainingCooldownMs: number\n\n constructor(key: string, remainingCooldownMs: number) {\n super(\n `Circuit breaker \"${key}\" is open. Cooldown remaining: ${Math.max(0, remainingCooldownMs)}ms`\n )\n this.name = 'CircuitOpenError'\n this.key = key\n this.remainingCooldownMs = Math.max(0, remainingCooldownMs)\n }\n}\n","import { CircuitOpenError } from './errors'\n\nexport type CircuitState = 'closed' | 'open' | 'half-open'\n\nexport interface ThresholdConfig {\n failures: number\n within?: number\n}\n\nexport type BreakerEvent = 'open' | 'close' | 'half-open' | 'failure' | 'reject'\n\nexport type OpenHandler = (error: unknown, key: string) => void | Promise<void>\nexport type CloseHandler = (key: string) => void | Promise<void>\nexport type HalfOpenHandler = (key: string) => void | Promise<void>\nexport type FailureHandler = (error: unknown, key: string) => void | Promise<void>\nexport type RejectHandler = (error: CircuitOpenError, key: string) => void | Promise<void>\n\nexport type FallbackFn<T = unknown> = (error: unknown) => Promise<T> | T\nexport type WorkFn<T = unknown> = () => Promise<T> | T\n\ninterface CircuitEntry {\n state: CircuitState\n failureTimestamps: number[]\n openedAt: number | null\n halfOpenInFlight: boolean\n}\n\nconst circuits = new Map<string, CircuitEntry>()\n\nfunction getOrCreateCircuit(key: string): CircuitEntry {\n let entry = circuits.get(key)\n if (!entry) {\n entry = {\n state: 'closed',\n failureTimestamps: [],\n openedAt: null,\n halfOpenInFlight: false,\n }\n circuits.set(key, entry)\n }\n return entry\n}\n\ninterface BreakerConfig<T = unknown> {\n key: string\n failures: number\n withinMs?: number\n cooldownMs: number\n workFn?: WorkFn<T>\n fallbackFn?: FallbackFn<T>\n openHandlers: OpenHandler[]\n closeHandlers: CloseHandler[]\n halfOpenHandlers: HalfOpenHandler[]\n failureHandlers: FailureHandler[]\n rejectHandlers: RejectHandler[]\n}\n\nexport class BreakerBuilder<T = unknown> implements PromiseLike<T> {\n private readonly config: BreakerConfig<T>\n private executionPromise?: Promise<T>\n\n constructor(config?: Partial<BreakerConfig<T>>) {\n this.config = {\n key: config?.key ?? 'default',\n failures: config?.failures ?? 5,\n withinMs: config?.withinMs,\n cooldownMs: config?.cooldownMs ?? 30_000,\n workFn: config?.workFn,\n fallbackFn: config?.fallbackFn,\n openHandlers: [...(config?.openHandlers ?? [])],\n closeHandlers: [...(config?.closeHandlers ?? [])],\n halfOpenHandlers: [...(config?.halfOpenHandlers ?? [])],\n failureHandlers: [...(config?.failureHandlers ?? [])],\n rejectHandlers: [...(config?.rejectHandlers ?? [])],\n }\n }\n\n for(key: string): BreakerBuilder<T> {\n if (!key || key.trim().length === 0) {\n throw new RangeError('circuit key must be a non-empty string')\n }\n return new BreakerBuilder<T>({ ...this.config, key })\n }\n\n key(key: string): BreakerBuilder<T> {\n return this.for(key)\n }\n\n threshold(options: ThresholdConfig | number): BreakerBuilder<T> {\n if (typeof options === 'number') {\n if (!Number.isInteger(options) || options < 1) {\n throw new RangeError('failures threshold must be an integer of at least 1')\n }\n return new BreakerBuilder<T>({ ...this.config, failures: options, withinMs: undefined })\n }\n\n if (!Number.isInteger(options.failures) || options.failures < 1) {\n throw new RangeError('failures threshold must be an integer of at least 1')\n }\n if (options.within !== undefined && options.within <= 0) {\n throw new RangeError('within window must be a positive number')\n }\n\n return new BreakerBuilder<T>({\n ...this.config,\n failures: options.failures,\n withinMs: options.within,\n })\n }\n\n cooldown(ms: number): BreakerBuilder<T> {\n if (ms < 0) {\n throw new RangeError('cooldown must be a non-negative number')\n }\n return new BreakerBuilder<T>({ ...this.config, cooldownMs: ms })\n }\n\n fallback<R = T>(fn: FallbackFn<R>): BreakerBuilder<T | R> {\n return new BreakerBuilder<T | R>({\n ...(this.config as unknown as BreakerConfig<T | R>),\n fallbackFn: fn,\n })\n }\n\n work<R>(fn: WorkFn<R>): BreakerBuilder<R> {\n return new BreakerBuilder<R>({\n ...(this.config as unknown as BreakerConfig<R>),\n workFn: fn,\n })\n }\n\n on(event: 'open', handler: OpenHandler): BreakerBuilder<T>\n on(event: 'close', handler: CloseHandler): BreakerBuilder<T>\n on(event: 'half-open', handler: HalfOpenHandler): BreakerBuilder<T>\n on(event: 'failure', handler: FailureHandler): BreakerBuilder<T>\n on(event: 'reject', handler: RejectHandler): BreakerBuilder<T>\n on(\n event: BreakerEvent,\n handler: OpenHandler | CloseHandler | HalfOpenHandler | FailureHandler | RejectHandler\n ): BreakerBuilder<T> {\n switch (event) {\n case 'open':\n return new BreakerBuilder<T>({\n ...this.config,\n openHandlers: [...this.config.openHandlers, handler as OpenHandler],\n })\n case 'close':\n return new BreakerBuilder<T>({\n ...this.config,\n closeHandlers: [...this.config.closeHandlers, handler as CloseHandler],\n })\n case 'half-open':\n return new BreakerBuilder<T>({\n ...this.config,\n halfOpenHandlers: [...this.config.halfOpenHandlers, handler as HalfOpenHandler],\n })\n case 'failure':\n return new BreakerBuilder<T>({\n ...this.config,\n failureHandlers: [...this.config.failureHandlers, handler as FailureHandler],\n })\n case 'reject':\n return new BreakerBuilder<T>({\n ...this.config,\n rejectHandlers: [...this.config.rejectHandlers, handler as RejectHandler],\n })\n }\n }\n\n get state(): CircuitState {\n const entry = getOrCreateCircuit(this.config.key)\n if (entry.state === 'open' && entry.openedAt !== null) {\n const elapsed = Date.now() - entry.openedAt\n if (elapsed >= this.config.cooldownMs) {\n return 'half-open'\n }\n }\n return entry.state\n }\n\n reset(key?: string): void {\n circuits.delete(key ?? this.config.key)\n }\n\n resetAll(): void {\n circuits.clear()\n }\n\n then<TResult1 = T, TResult2 = never>(\n onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,\n onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null | undefined\n ): Promise<TResult1 | TResult2> {\n if (!this.executionPromise) {\n this.executionPromise = this.execute()\n }\n return this.executionPromise.then(onfulfilled, onrejected)\n }\n\n catch<TResult = never>(\n onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null | undefined\n ): Promise<T | TResult> {\n return this.then(undefined, onrejected)\n }\n\n finally(onfinally?: (() => void) | null | undefined): Promise<T> {\n return this.then(\n (value) => {\n onfinally?.()\n return value\n },\n (reason) => {\n onfinally?.()\n throw reason\n }\n )\n }\n\n private async execute(): Promise<T> {\n const {\n key,\n failures,\n withinMs,\n cooldownMs,\n workFn,\n fallbackFn,\n openHandlers,\n closeHandlers,\n halfOpenHandlers,\n failureHandlers,\n rejectHandlers,\n } = this.config\n\n if (!workFn) {\n throw new Error('No work function provided. Call .work(fn) before awaiting.')\n }\n\n const entry = getOrCreateCircuit(key)\n const now = Date.now()\n\n if (entry.state === 'open') {\n const elapsed = entry.openedAt !== null ? now - entry.openedAt : cooldownMs\n if (elapsed >= cooldownMs) {\n entry.state = 'half-open'\n entry.halfOpenInFlight = false\n for (const handler of halfOpenHandlers) {\n await handler(key)\n }\n } else {\n const openError = new CircuitOpenError(key, cooldownMs - elapsed)\n for (const handler of rejectHandlers) {\n await handler(openError, key)\n }\n if (fallbackFn) {\n return await fallbackFn(openError)\n }\n throw openError\n }\n }\n\n if (entry.state === 'half-open') {\n if (entry.halfOpenInFlight) {\n const openError = new CircuitOpenError(key, cooldownMs)\n for (const handler of rejectHandlers) {\n await handler(openError, key)\n }\n if (fallbackFn) {\n return await fallbackFn(openError)\n }\n throw openError\n }\n entry.halfOpenInFlight = true\n }\n\n try {\n const result = await workFn()\n\n if (entry.state === 'half-open') {\n entry.state = 'closed'\n entry.failureTimestamps = []\n entry.openedAt = null\n entry.halfOpenInFlight = false\n for (const handler of closeHandlers) {\n await handler(key)\n }\n } else if (withinMs === undefined) {\n entry.failureTimestamps = []\n }\n\n return result\n } catch (error) {\n for (const handler of failureHandlers) {\n await handler(error, key)\n }\n\n const failTime = Date.now()\n\n if (entry.state === 'half-open') {\n entry.state = 'open'\n entry.openedAt = failTime\n entry.halfOpenInFlight = false\n entry.failureTimestamps = [failTime]\n for (const handler of openHandlers) {\n await handler(error, key)\n }\n } else {\n if (withinMs !== undefined) {\n const cutoff = failTime - withinMs\n entry.failureTimestamps = entry.failureTimestamps.filter((t) => t >= cutoff)\n }\n entry.failureTimestamps.push(failTime)\n\n if (entry.failureTimestamps.length >= failures) {\n entry.state = 'open'\n entry.openedAt = failTime\n for (const handler of openHandlers) {\n await handler(error, key)\n }\n }\n }\n\n if (fallbackFn) {\n return await fallbackFn(error)\n }\n\n throw error\n }\n }\n}\n\nexport const Breaker: BreakerBuilder = new BreakerBuilder()\nexport const Fusebox: BreakerBuilder = Breaker\n\nexport function resetCircuits(): void {\n circuits.clear()\n}\n\nexport default Breaker\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export declare class FuseboxError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class CircuitOpenError extends FuseboxError {
|
|
5
|
+
readonly key: string;
|
|
6
|
+
readonly remainingCooldownMs: number;
|
|
7
|
+
constructor(key: string, remainingCooldownMs: number);
|
|
8
|
+
}
|
|
9
|
+
type CircuitState = 'closed' | 'open' | 'half-open';
|
|
10
|
+
interface ThresholdConfig {
|
|
11
|
+
failures: number;
|
|
12
|
+
within?: number;
|
|
13
|
+
}
|
|
14
|
+
type BreakerEvent = 'open' | 'close' | 'half-open' | 'failure' | 'reject';
|
|
15
|
+
type OpenHandler = (error: unknown, key: string) => void | Promise<void>;
|
|
16
|
+
type CloseHandler = (key: string) => void | Promise<void>;
|
|
17
|
+
type HalfOpenHandler = (key: string) => void | Promise<void>;
|
|
18
|
+
type FailureHandler = (error: unknown, key: string) => void | Promise<void>;
|
|
19
|
+
type RejectHandler = (error: CircuitOpenError, key: string) => void | Promise<void>;
|
|
20
|
+
type FallbackFn<T = unknown> = (error: unknown) => Promise<T> | T;
|
|
21
|
+
type WorkFn<T = unknown> = () => Promise<T> | T;
|
|
22
|
+
interface BreakerConfig<T = unknown> {
|
|
23
|
+
key: string;
|
|
24
|
+
failures: number;
|
|
25
|
+
withinMs?: number;
|
|
26
|
+
cooldownMs: number;
|
|
27
|
+
workFn?: WorkFn<T>;
|
|
28
|
+
fallbackFn?: FallbackFn<T>;
|
|
29
|
+
openHandlers: OpenHandler[];
|
|
30
|
+
closeHandlers: CloseHandler[];
|
|
31
|
+
halfOpenHandlers: HalfOpenHandler[];
|
|
32
|
+
failureHandlers: FailureHandler[];
|
|
33
|
+
rejectHandlers: RejectHandler[];
|
|
34
|
+
}
|
|
35
|
+
export declare class BreakerBuilder<T = unknown> implements PromiseLike<T> {
|
|
36
|
+
private readonly config;
|
|
37
|
+
private executionPromise?;
|
|
38
|
+
constructor(config?: Partial<BreakerConfig<T>>);
|
|
39
|
+
for(key: string): BreakerBuilder<T>;
|
|
40
|
+
key(key: string): BreakerBuilder<T>;
|
|
41
|
+
threshold(options: ThresholdConfig | number): BreakerBuilder<T>;
|
|
42
|
+
cooldown(ms: number): BreakerBuilder<T>;
|
|
43
|
+
fallback<R = T>(fn: FallbackFn<R>): BreakerBuilder<T | R>;
|
|
44
|
+
work<R>(fn: WorkFn<R>): BreakerBuilder<R>;
|
|
45
|
+
on(event: 'open', handler: OpenHandler): BreakerBuilder<T>;
|
|
46
|
+
on(event: 'close', handler: CloseHandler): BreakerBuilder<T>;
|
|
47
|
+
on(event: 'half-open', handler: HalfOpenHandler): BreakerBuilder<T>;
|
|
48
|
+
on(event: 'failure', handler: FailureHandler): BreakerBuilder<T>;
|
|
49
|
+
on(event: 'reject', handler: RejectHandler): BreakerBuilder<T>;
|
|
50
|
+
get state(): CircuitState;
|
|
51
|
+
reset(key?: string): void;
|
|
52
|
+
resetAll(): void;
|
|
53
|
+
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null | undefined): Promise<TResult1 | TResult2>;
|
|
54
|
+
catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null | undefined): Promise<T | TResult>;
|
|
55
|
+
finally(onfinally?: (() => void) | null | undefined): Promise<T>;
|
|
56
|
+
private execute;
|
|
57
|
+
}
|
|
58
|
+
export declare const Breaker: BreakerBuilder;
|
|
59
|
+
export declare const Fusebox: BreakerBuilder;
|
|
60
|
+
export declare function resetCircuits(): void;
|
|
61
|
+
export { Breaker as default, type BreakerEvent, type CircuitState, type CloseHandler, type FailureHandler, type FallbackFn, type HalfOpenHandler, type OpenHandler, type RejectHandler, type ThresholdConfig, type WorkFn };
|
|
62
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/errors.ts","../src/breaker.ts"],"mappings":"A,qBAAa,qBAAqB;EAChC,YAAY;;qBAOD,yBAAyB;WAC3B;WACA;EAET,YAAY,aAAa;;KCVf;UAEK;EACf;EACA;;KAGU;KAEA,eAAe,gBAAgB,uBAAuB;KACtD,gBAAgB,uBAAuB;KACvC,mBAAmB,uBAAuB;KAC1C,kBAAkB,gBAAgB,uBAAuB;KACzD,iBAAiB,OAAO,kBAAkB,uBAAuB;KAEjE,WAAW,gBAAgB,mBAAmB,QAAQ,KAAK;KAC3D,OAAO,qBAAqB,QAAQ,KAAK;UAyB3C,cAAc;EACtB;EACA;EACA;EACA;EACA,SAAS,OAAO;EAChB,aAAa,WAAW;EACxB,cAAc;EACd,eAAe;EACf,kBAAkB;EAClB,iBAAiB;EACjB,gBAAgB;;qBAGL,eAAe,wBAAwB,YAAY;mBAC7C;UACT;EAER,YAAY,SAAS,QAAQ,cAAc;EAgB3C,IAAI,cAAc,eAAe;EAOjC,IAAI,cAAc,eAAe;EAIjC,UAAU,SAAS,2BAA2B,eAAe;EAsB7D,SAAS,aAAa,eAAe;EAOrC,SAAS,IAAI,GAAG,IAAI,WAAW,KAAK,eAAe,IAAI;EAOvD,KAAK,GAAG,IAAI,OAAO,KAAK,eAAe;EAOvC,GAAG,eAAe,SAAS,cAAc,eAAe;EACxD,GAAG,gBAAgB,SAAS,eAAe,eAAe;EAC1D,GAAG,oBAAoB,SAAS,kBAAkB,eAAe;EACjE,GAAG,kBAAkB,SAAS,iBAAiB,eAAe;EAC9D,GAAG,iBAAiB,SAAS,gBAAgB,eAAe;MAkCxD,SAAS;EAWb,MAAM;EAIN;EAIA,KAAK,WAAW,GAAG,kBACjB,gBAAgB,OAAO,MAAM,WAAW,YAAY,+BACpD,eAAe,oBAAoB,WAAW,YAAY,gCACzD,QAAQ,WAAW;EAOtB,MAAM,iBACJ,eAAe,oBAAoB,UAAU,YAAY,+BACxD,QAAQ,IAAI;EAIf,QAAQ,8CAA8C,QAAQ;UAahD;;qBAgHH,SAAS;qBACT,SAAS;wBAEN"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export declare class FuseboxError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class CircuitOpenError extends FuseboxError {
|
|
5
|
+
readonly key: string;
|
|
6
|
+
readonly remainingCooldownMs: number;
|
|
7
|
+
constructor(key: string, remainingCooldownMs: number);
|
|
8
|
+
}
|
|
9
|
+
type CircuitState = 'closed' | 'open' | 'half-open';
|
|
10
|
+
interface ThresholdConfig {
|
|
11
|
+
failures: number;
|
|
12
|
+
within?: number;
|
|
13
|
+
}
|
|
14
|
+
type BreakerEvent = 'open' | 'close' | 'half-open' | 'failure' | 'reject';
|
|
15
|
+
type OpenHandler = (error: unknown, key: string) => void | Promise<void>;
|
|
16
|
+
type CloseHandler = (key: string) => void | Promise<void>;
|
|
17
|
+
type HalfOpenHandler = (key: string) => void | Promise<void>;
|
|
18
|
+
type FailureHandler = (error: unknown, key: string) => void | Promise<void>;
|
|
19
|
+
type RejectHandler = (error: CircuitOpenError, key: string) => void | Promise<void>;
|
|
20
|
+
type FallbackFn<T = unknown> = (error: unknown) => Promise<T> | T;
|
|
21
|
+
type WorkFn<T = unknown> = () => Promise<T> | T;
|
|
22
|
+
interface BreakerConfig<T = unknown> {
|
|
23
|
+
key: string;
|
|
24
|
+
failures: number;
|
|
25
|
+
withinMs?: number;
|
|
26
|
+
cooldownMs: number;
|
|
27
|
+
workFn?: WorkFn<T>;
|
|
28
|
+
fallbackFn?: FallbackFn<T>;
|
|
29
|
+
openHandlers: OpenHandler[];
|
|
30
|
+
closeHandlers: CloseHandler[];
|
|
31
|
+
halfOpenHandlers: HalfOpenHandler[];
|
|
32
|
+
failureHandlers: FailureHandler[];
|
|
33
|
+
rejectHandlers: RejectHandler[];
|
|
34
|
+
}
|
|
35
|
+
export declare class BreakerBuilder<T = unknown> implements PromiseLike<T> {
|
|
36
|
+
private readonly config;
|
|
37
|
+
private executionPromise?;
|
|
38
|
+
constructor(config?: Partial<BreakerConfig<T>>);
|
|
39
|
+
for(key: string): BreakerBuilder<T>;
|
|
40
|
+
key(key: string): BreakerBuilder<T>;
|
|
41
|
+
threshold(options: ThresholdConfig | number): BreakerBuilder<T>;
|
|
42
|
+
cooldown(ms: number): BreakerBuilder<T>;
|
|
43
|
+
fallback<R = T>(fn: FallbackFn<R>): BreakerBuilder<T | R>;
|
|
44
|
+
work<R>(fn: WorkFn<R>): BreakerBuilder<R>;
|
|
45
|
+
on(event: 'open', handler: OpenHandler): BreakerBuilder<T>;
|
|
46
|
+
on(event: 'close', handler: CloseHandler): BreakerBuilder<T>;
|
|
47
|
+
on(event: 'half-open', handler: HalfOpenHandler): BreakerBuilder<T>;
|
|
48
|
+
on(event: 'failure', handler: FailureHandler): BreakerBuilder<T>;
|
|
49
|
+
on(event: 'reject', handler: RejectHandler): BreakerBuilder<T>;
|
|
50
|
+
get state(): CircuitState;
|
|
51
|
+
reset(key?: string): void;
|
|
52
|
+
resetAll(): void;
|
|
53
|
+
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null | undefined): Promise<TResult1 | TResult2>;
|
|
54
|
+
catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null | undefined): Promise<T | TResult>;
|
|
55
|
+
finally(onfinally?: (() => void) | null | undefined): Promise<T>;
|
|
56
|
+
private execute;
|
|
57
|
+
}
|
|
58
|
+
export declare const Breaker: BreakerBuilder;
|
|
59
|
+
export declare const Fusebox: BreakerBuilder;
|
|
60
|
+
export declare function resetCircuits(): void;
|
|
61
|
+
export { Breaker as default, type BreakerEvent, type CircuitState, type CloseHandler, type FailureHandler, type FallbackFn, type HalfOpenHandler, type OpenHandler, type RejectHandler, type ThresholdConfig, type WorkFn };
|
|
62
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/errors.ts","../src/breaker.ts"],"mappings":"A,qBAAa,qBAAqB;EAChC,YAAY;;qBAOD,yBAAyB;WAC3B;WACA;EAET,YAAY,aAAa;;KCVf;UAEK;EACf;EACA;;KAGU;KAEA,eAAe,gBAAgB,uBAAuB;KACtD,gBAAgB,uBAAuB;KACvC,mBAAmB,uBAAuB;KAC1C,kBAAkB,gBAAgB,uBAAuB;KACzD,iBAAiB,OAAO,kBAAkB,uBAAuB;KAEjE,WAAW,gBAAgB,mBAAmB,QAAQ,KAAK;KAC3D,OAAO,qBAAqB,QAAQ,KAAK;UAyB3C,cAAc;EACtB;EACA;EACA;EACA;EACA,SAAS,OAAO;EAChB,aAAa,WAAW;EACxB,cAAc;EACd,eAAe;EACf,kBAAkB;EAClB,iBAAiB;EACjB,gBAAgB;;qBAGL,eAAe,wBAAwB,YAAY;mBAC7C;UACT;EAER,YAAY,SAAS,QAAQ,cAAc;EAgB3C,IAAI,cAAc,eAAe;EAOjC,IAAI,cAAc,eAAe;EAIjC,UAAU,SAAS,2BAA2B,eAAe;EAsB7D,SAAS,aAAa,eAAe;EAOrC,SAAS,IAAI,GAAG,IAAI,WAAW,KAAK,eAAe,IAAI;EAOvD,KAAK,GAAG,IAAI,OAAO,KAAK,eAAe;EAOvC,GAAG,eAAe,SAAS,cAAc,eAAe;EACxD,GAAG,gBAAgB,SAAS,eAAe,eAAe;EAC1D,GAAG,oBAAoB,SAAS,kBAAkB,eAAe;EACjE,GAAG,kBAAkB,SAAS,iBAAiB,eAAe;EAC9D,GAAG,iBAAiB,SAAS,gBAAgB,eAAe;MAkCxD,SAAS;EAWb,MAAM;EAIN;EAIA,KAAK,WAAW,GAAG,kBACjB,gBAAgB,OAAO,MAAM,WAAW,YAAY,+BACpD,eAAe,oBAAoB,WAAW,YAAY,gCACzD,QAAQ,WAAW;EAOtB,MAAM,iBACJ,eAAe,oBAAoB,UAAU,YAAY,+BACxD,QAAQ,IAAI;EAIf,QAAQ,8CAA8C,QAAQ;UAahD;;qBAgHH,SAAS;qBACT,SAAS;wBAEN"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var FuseboxError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "FuseboxError";
|
|
6
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var CircuitOpenError = class extends FuseboxError {
|
|
10
|
+
key;
|
|
11
|
+
remainingCooldownMs;
|
|
12
|
+
constructor(key, remainingCooldownMs) {
|
|
13
|
+
super(`Circuit breaker "${key}" is open. Cooldown remaining: ${Math.max(0, remainingCooldownMs)}ms`);
|
|
14
|
+
this.name = "CircuitOpenError";
|
|
15
|
+
this.key = key;
|
|
16
|
+
this.remainingCooldownMs = Math.max(0, remainingCooldownMs);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/breaker.ts
|
|
21
|
+
const circuits = /* @__PURE__ */ new Map();
|
|
22
|
+
function getOrCreateCircuit(key) {
|
|
23
|
+
let entry = circuits.get(key);
|
|
24
|
+
if (!entry) {
|
|
25
|
+
entry = {
|
|
26
|
+
state: "closed",
|
|
27
|
+
failureTimestamps: [],
|
|
28
|
+
openedAt: null,
|
|
29
|
+
halfOpenInFlight: false
|
|
30
|
+
};
|
|
31
|
+
circuits.set(key, entry);
|
|
32
|
+
}
|
|
33
|
+
return entry;
|
|
34
|
+
}
|
|
35
|
+
var BreakerBuilder = class BreakerBuilder {
|
|
36
|
+
config;
|
|
37
|
+
executionPromise;
|
|
38
|
+
constructor(config) {
|
|
39
|
+
this.config = {
|
|
40
|
+
key: config?.key ?? "default",
|
|
41
|
+
failures: config?.failures ?? 5,
|
|
42
|
+
withinMs: config?.withinMs,
|
|
43
|
+
cooldownMs: config?.cooldownMs ?? 3e4,
|
|
44
|
+
workFn: config?.workFn,
|
|
45
|
+
fallbackFn: config?.fallbackFn,
|
|
46
|
+
openHandlers: [...config?.openHandlers ?? []],
|
|
47
|
+
closeHandlers: [...config?.closeHandlers ?? []],
|
|
48
|
+
halfOpenHandlers: [...config?.halfOpenHandlers ?? []],
|
|
49
|
+
failureHandlers: [...config?.failureHandlers ?? []],
|
|
50
|
+
rejectHandlers: [...config?.rejectHandlers ?? []]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
for(key) {
|
|
54
|
+
if (!key || key.trim().length === 0) throw new RangeError("circuit key must be a non-empty string");
|
|
55
|
+
return new BreakerBuilder({
|
|
56
|
+
...this.config,
|
|
57
|
+
key
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
key(key) {
|
|
61
|
+
return this.for(key);
|
|
62
|
+
}
|
|
63
|
+
threshold(options) {
|
|
64
|
+
if (typeof options === "number") {
|
|
65
|
+
if (!Number.isInteger(options) || options < 1) throw new RangeError("failures threshold must be an integer of at least 1");
|
|
66
|
+
return new BreakerBuilder({
|
|
67
|
+
...this.config,
|
|
68
|
+
failures: options,
|
|
69
|
+
withinMs: void 0
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (!Number.isInteger(options.failures) || options.failures < 1) throw new RangeError("failures threshold must be an integer of at least 1");
|
|
73
|
+
if (options.within !== void 0 && options.within <= 0) throw new RangeError("within window must be a positive number");
|
|
74
|
+
return new BreakerBuilder({
|
|
75
|
+
...this.config,
|
|
76
|
+
failures: options.failures,
|
|
77
|
+
withinMs: options.within
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
cooldown(ms) {
|
|
81
|
+
if (ms < 0) throw new RangeError("cooldown must be a non-negative number");
|
|
82
|
+
return new BreakerBuilder({
|
|
83
|
+
...this.config,
|
|
84
|
+
cooldownMs: ms
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
fallback(fn) {
|
|
88
|
+
return new BreakerBuilder({
|
|
89
|
+
...this.config,
|
|
90
|
+
fallbackFn: fn
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
work(fn) {
|
|
94
|
+
return new BreakerBuilder({
|
|
95
|
+
...this.config,
|
|
96
|
+
workFn: fn
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
on(event, handler) {
|
|
100
|
+
switch (event) {
|
|
101
|
+
case "open": return new BreakerBuilder({
|
|
102
|
+
...this.config,
|
|
103
|
+
openHandlers: [...this.config.openHandlers, handler]
|
|
104
|
+
});
|
|
105
|
+
case "close": return new BreakerBuilder({
|
|
106
|
+
...this.config,
|
|
107
|
+
closeHandlers: [...this.config.closeHandlers, handler]
|
|
108
|
+
});
|
|
109
|
+
case "half-open": return new BreakerBuilder({
|
|
110
|
+
...this.config,
|
|
111
|
+
halfOpenHandlers: [...this.config.halfOpenHandlers, handler]
|
|
112
|
+
});
|
|
113
|
+
case "failure": return new BreakerBuilder({
|
|
114
|
+
...this.config,
|
|
115
|
+
failureHandlers: [...this.config.failureHandlers, handler]
|
|
116
|
+
});
|
|
117
|
+
case "reject": return new BreakerBuilder({
|
|
118
|
+
...this.config,
|
|
119
|
+
rejectHandlers: [...this.config.rejectHandlers, handler]
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
get state() {
|
|
124
|
+
const entry = getOrCreateCircuit(this.config.key);
|
|
125
|
+
if (entry.state === "open" && entry.openedAt !== null) {
|
|
126
|
+
if (Date.now() - entry.openedAt >= this.config.cooldownMs) return "half-open";
|
|
127
|
+
}
|
|
128
|
+
return entry.state;
|
|
129
|
+
}
|
|
130
|
+
reset(key) {
|
|
131
|
+
circuits.delete(key ?? this.config.key);
|
|
132
|
+
}
|
|
133
|
+
resetAll() {
|
|
134
|
+
circuits.clear();
|
|
135
|
+
}
|
|
136
|
+
then(onfulfilled, onrejected) {
|
|
137
|
+
if (!this.executionPromise) this.executionPromise = this.execute();
|
|
138
|
+
return this.executionPromise.then(onfulfilled, onrejected);
|
|
139
|
+
}
|
|
140
|
+
catch(onrejected) {
|
|
141
|
+
return this.then(void 0, onrejected);
|
|
142
|
+
}
|
|
143
|
+
finally(onfinally) {
|
|
144
|
+
return this.then((value) => {
|
|
145
|
+
onfinally?.();
|
|
146
|
+
return value;
|
|
147
|
+
}, (reason) => {
|
|
148
|
+
onfinally?.();
|
|
149
|
+
throw reason;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async execute() {
|
|
153
|
+
const { key, failures, withinMs, cooldownMs, workFn, fallbackFn, openHandlers, closeHandlers, halfOpenHandlers, failureHandlers, rejectHandlers } = this.config;
|
|
154
|
+
if (!workFn) throw new Error("No work function provided. Call .work(fn) before awaiting.");
|
|
155
|
+
const entry = getOrCreateCircuit(key);
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
if (entry.state === "open") {
|
|
158
|
+
const elapsed = entry.openedAt !== null ? now - entry.openedAt : cooldownMs;
|
|
159
|
+
if (elapsed >= cooldownMs) {
|
|
160
|
+
entry.state = "half-open";
|
|
161
|
+
entry.halfOpenInFlight = false;
|
|
162
|
+
for (const handler of halfOpenHandlers) await handler(key);
|
|
163
|
+
} else {
|
|
164
|
+
const openError = new CircuitOpenError(key, cooldownMs - elapsed);
|
|
165
|
+
for (const handler of rejectHandlers) await handler(openError, key);
|
|
166
|
+
if (fallbackFn) return await fallbackFn(openError);
|
|
167
|
+
throw openError;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (entry.state === "half-open") {
|
|
171
|
+
if (entry.halfOpenInFlight) {
|
|
172
|
+
const openError = new CircuitOpenError(key, cooldownMs);
|
|
173
|
+
for (const handler of rejectHandlers) await handler(openError, key);
|
|
174
|
+
if (fallbackFn) return await fallbackFn(openError);
|
|
175
|
+
throw openError;
|
|
176
|
+
}
|
|
177
|
+
entry.halfOpenInFlight = true;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const result = await workFn();
|
|
181
|
+
if (entry.state === "half-open") {
|
|
182
|
+
entry.state = "closed";
|
|
183
|
+
entry.failureTimestamps = [];
|
|
184
|
+
entry.openedAt = null;
|
|
185
|
+
entry.halfOpenInFlight = false;
|
|
186
|
+
for (const handler of closeHandlers) await handler(key);
|
|
187
|
+
} else if (withinMs === void 0) entry.failureTimestamps = [];
|
|
188
|
+
return result;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
for (const handler of failureHandlers) await handler(error, key);
|
|
191
|
+
const failTime = Date.now();
|
|
192
|
+
if (entry.state === "half-open") {
|
|
193
|
+
entry.state = "open";
|
|
194
|
+
entry.openedAt = failTime;
|
|
195
|
+
entry.halfOpenInFlight = false;
|
|
196
|
+
entry.failureTimestamps = [failTime];
|
|
197
|
+
for (const handler of openHandlers) await handler(error, key);
|
|
198
|
+
} else {
|
|
199
|
+
if (withinMs !== void 0) {
|
|
200
|
+
const cutoff = failTime - withinMs;
|
|
201
|
+
entry.failureTimestamps = entry.failureTimestamps.filter((t) => t >= cutoff);
|
|
202
|
+
}
|
|
203
|
+
entry.failureTimestamps.push(failTime);
|
|
204
|
+
if (entry.failureTimestamps.length >= failures) {
|
|
205
|
+
entry.state = "open";
|
|
206
|
+
entry.openedAt = failTime;
|
|
207
|
+
for (const handler of openHandlers) await handler(error, key);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (fallbackFn) return await fallbackFn(error);
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const Breaker = new BreakerBuilder();
|
|
216
|
+
const Fusebox = Breaker;
|
|
217
|
+
function resetCircuits() {
|
|
218
|
+
circuits.clear();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export { Breaker, Breaker as default, BreakerBuilder, CircuitOpenError, Fusebox, FuseboxError, resetCircuits };
|
|
222
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/errors.ts","../src/breaker.ts"],"sourcesContent":["export class FuseboxError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'FuseboxError'\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n\nexport class CircuitOpenError extends FuseboxError {\n readonly key: string\n readonly remainingCooldownMs: number\n\n constructor(key: string, remainingCooldownMs: number) {\n super(\n `Circuit breaker \"${key}\" is open. Cooldown remaining: ${Math.max(0, remainingCooldownMs)}ms`\n )\n this.name = 'CircuitOpenError'\n this.key = key\n this.remainingCooldownMs = Math.max(0, remainingCooldownMs)\n }\n}\n","import { CircuitOpenError } from './errors'\n\nexport type CircuitState = 'closed' | 'open' | 'half-open'\n\nexport interface ThresholdConfig {\n failures: number\n within?: number\n}\n\nexport type BreakerEvent = 'open' | 'close' | 'half-open' | 'failure' | 'reject'\n\nexport type OpenHandler = (error: unknown, key: string) => void | Promise<void>\nexport type CloseHandler = (key: string) => void | Promise<void>\nexport type HalfOpenHandler = (key: string) => void | Promise<void>\nexport type FailureHandler = (error: unknown, key: string) => void | Promise<void>\nexport type RejectHandler = (error: CircuitOpenError, key: string) => void | Promise<void>\n\nexport type FallbackFn<T = unknown> = (error: unknown) => Promise<T> | T\nexport type WorkFn<T = unknown> = () => Promise<T> | T\n\ninterface CircuitEntry {\n state: CircuitState\n failureTimestamps: number[]\n openedAt: number | null\n halfOpenInFlight: boolean\n}\n\nconst circuits = new Map<string, CircuitEntry>()\n\nfunction getOrCreateCircuit(key: string): CircuitEntry {\n let entry = circuits.get(key)\n if (!entry) {\n entry = {\n state: 'closed',\n failureTimestamps: [],\n openedAt: null,\n halfOpenInFlight: false,\n }\n circuits.set(key, entry)\n }\n return entry\n}\n\ninterface BreakerConfig<T = unknown> {\n key: string\n failures: number\n withinMs?: number\n cooldownMs: number\n workFn?: WorkFn<T>\n fallbackFn?: FallbackFn<T>\n openHandlers: OpenHandler[]\n closeHandlers: CloseHandler[]\n halfOpenHandlers: HalfOpenHandler[]\n failureHandlers: FailureHandler[]\n rejectHandlers: RejectHandler[]\n}\n\nexport class BreakerBuilder<T = unknown> implements PromiseLike<T> {\n private readonly config: BreakerConfig<T>\n private executionPromise?: Promise<T>\n\n constructor(config?: Partial<BreakerConfig<T>>) {\n this.config = {\n key: config?.key ?? 'default',\n failures: config?.failures ?? 5,\n withinMs: config?.withinMs,\n cooldownMs: config?.cooldownMs ?? 30_000,\n workFn: config?.workFn,\n fallbackFn: config?.fallbackFn,\n openHandlers: [...(config?.openHandlers ?? [])],\n closeHandlers: [...(config?.closeHandlers ?? [])],\n halfOpenHandlers: [...(config?.halfOpenHandlers ?? [])],\n failureHandlers: [...(config?.failureHandlers ?? [])],\n rejectHandlers: [...(config?.rejectHandlers ?? [])],\n }\n }\n\n for(key: string): BreakerBuilder<T> {\n if (!key || key.trim().length === 0) {\n throw new RangeError('circuit key must be a non-empty string')\n }\n return new BreakerBuilder<T>({ ...this.config, key })\n }\n\n key(key: string): BreakerBuilder<T> {\n return this.for(key)\n }\n\n threshold(options: ThresholdConfig | number): BreakerBuilder<T> {\n if (typeof options === 'number') {\n if (!Number.isInteger(options) || options < 1) {\n throw new RangeError('failures threshold must be an integer of at least 1')\n }\n return new BreakerBuilder<T>({ ...this.config, failures: options, withinMs: undefined })\n }\n\n if (!Number.isInteger(options.failures) || options.failures < 1) {\n throw new RangeError('failures threshold must be an integer of at least 1')\n }\n if (options.within !== undefined && options.within <= 0) {\n throw new RangeError('within window must be a positive number')\n }\n\n return new BreakerBuilder<T>({\n ...this.config,\n failures: options.failures,\n withinMs: options.within,\n })\n }\n\n cooldown(ms: number): BreakerBuilder<T> {\n if (ms < 0) {\n throw new RangeError('cooldown must be a non-negative number')\n }\n return new BreakerBuilder<T>({ ...this.config, cooldownMs: ms })\n }\n\n fallback<R = T>(fn: FallbackFn<R>): BreakerBuilder<T | R> {\n return new BreakerBuilder<T | R>({\n ...(this.config as unknown as BreakerConfig<T | R>),\n fallbackFn: fn,\n })\n }\n\n work<R>(fn: WorkFn<R>): BreakerBuilder<R> {\n return new BreakerBuilder<R>({\n ...(this.config as unknown as BreakerConfig<R>),\n workFn: fn,\n })\n }\n\n on(event: 'open', handler: OpenHandler): BreakerBuilder<T>\n on(event: 'close', handler: CloseHandler): BreakerBuilder<T>\n on(event: 'half-open', handler: HalfOpenHandler): BreakerBuilder<T>\n on(event: 'failure', handler: FailureHandler): BreakerBuilder<T>\n on(event: 'reject', handler: RejectHandler): BreakerBuilder<T>\n on(\n event: BreakerEvent,\n handler: OpenHandler | CloseHandler | HalfOpenHandler | FailureHandler | RejectHandler\n ): BreakerBuilder<T> {\n switch (event) {\n case 'open':\n return new BreakerBuilder<T>({\n ...this.config,\n openHandlers: [...this.config.openHandlers, handler as OpenHandler],\n })\n case 'close':\n return new BreakerBuilder<T>({\n ...this.config,\n closeHandlers: [...this.config.closeHandlers, handler as CloseHandler],\n })\n case 'half-open':\n return new BreakerBuilder<T>({\n ...this.config,\n halfOpenHandlers: [...this.config.halfOpenHandlers, handler as HalfOpenHandler],\n })\n case 'failure':\n return new BreakerBuilder<T>({\n ...this.config,\n failureHandlers: [...this.config.failureHandlers, handler as FailureHandler],\n })\n case 'reject':\n return new BreakerBuilder<T>({\n ...this.config,\n rejectHandlers: [...this.config.rejectHandlers, handler as RejectHandler],\n })\n }\n }\n\n get state(): CircuitState {\n const entry = getOrCreateCircuit(this.config.key)\n if (entry.state === 'open' && entry.openedAt !== null) {\n const elapsed = Date.now() - entry.openedAt\n if (elapsed >= this.config.cooldownMs) {\n return 'half-open'\n }\n }\n return entry.state\n }\n\n reset(key?: string): void {\n circuits.delete(key ?? this.config.key)\n }\n\n resetAll(): void {\n circuits.clear()\n }\n\n then<TResult1 = T, TResult2 = never>(\n onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,\n onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null | undefined\n ): Promise<TResult1 | TResult2> {\n if (!this.executionPromise) {\n this.executionPromise = this.execute()\n }\n return this.executionPromise.then(onfulfilled, onrejected)\n }\n\n catch<TResult = never>(\n onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null | undefined\n ): Promise<T | TResult> {\n return this.then(undefined, onrejected)\n }\n\n finally(onfinally?: (() => void) | null | undefined): Promise<T> {\n return this.then(\n (value) => {\n onfinally?.()\n return value\n },\n (reason) => {\n onfinally?.()\n throw reason\n }\n )\n }\n\n private async execute(): Promise<T> {\n const {\n key,\n failures,\n withinMs,\n cooldownMs,\n workFn,\n fallbackFn,\n openHandlers,\n closeHandlers,\n halfOpenHandlers,\n failureHandlers,\n rejectHandlers,\n } = this.config\n\n if (!workFn) {\n throw new Error('No work function provided. Call .work(fn) before awaiting.')\n }\n\n const entry = getOrCreateCircuit(key)\n const now = Date.now()\n\n if (entry.state === 'open') {\n const elapsed = entry.openedAt !== null ? now - entry.openedAt : cooldownMs\n if (elapsed >= cooldownMs) {\n entry.state = 'half-open'\n entry.halfOpenInFlight = false\n for (const handler of halfOpenHandlers) {\n await handler(key)\n }\n } else {\n const openError = new CircuitOpenError(key, cooldownMs - elapsed)\n for (const handler of rejectHandlers) {\n await handler(openError, key)\n }\n if (fallbackFn) {\n return await fallbackFn(openError)\n }\n throw openError\n }\n }\n\n if (entry.state === 'half-open') {\n if (entry.halfOpenInFlight) {\n const openError = new CircuitOpenError(key, cooldownMs)\n for (const handler of rejectHandlers) {\n await handler(openError, key)\n }\n if (fallbackFn) {\n return await fallbackFn(openError)\n }\n throw openError\n }\n entry.halfOpenInFlight = true\n }\n\n try {\n const result = await workFn()\n\n if (entry.state === 'half-open') {\n entry.state = 'closed'\n entry.failureTimestamps = []\n entry.openedAt = null\n entry.halfOpenInFlight = false\n for (const handler of closeHandlers) {\n await handler(key)\n }\n } else if (withinMs === undefined) {\n entry.failureTimestamps = []\n }\n\n return result\n } catch (error) {\n for (const handler of failureHandlers) {\n await handler(error, key)\n }\n\n const failTime = Date.now()\n\n if (entry.state === 'half-open') {\n entry.state = 'open'\n entry.openedAt = failTime\n entry.halfOpenInFlight = false\n entry.failureTimestamps = [failTime]\n for (const handler of openHandlers) {\n await handler(error, key)\n }\n } else {\n if (withinMs !== undefined) {\n const cutoff = failTime - withinMs\n entry.failureTimestamps = entry.failureTimestamps.filter((t) => t >= cutoff)\n }\n entry.failureTimestamps.push(failTime)\n\n if (entry.failureTimestamps.length >= failures) {\n entry.state = 'open'\n entry.openedAt = failTime\n for (const handler of openHandlers) {\n await handler(error, key)\n }\n }\n }\n\n if (fallbackFn) {\n return await fallbackFn(error)\n }\n\n throw error\n }\n }\n}\n\nexport const Breaker: BreakerBuilder = new BreakerBuilder()\nexport const Fusebox: BreakerBuilder = Breaker\n\nexport function resetCircuits(): void {\n circuits.clear()\n}\n\nexport default Breaker\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gwash3189/fusebox",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A fluent, lightweight TypeScript circuit breaker library with sliding failure windows, cooldown recovery, fallbacks, and lifecycle hooks.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "gwash3189",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Gwash3189/fusebox.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Gwash3189/fusebox#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Gwash3189/fusebox/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/index.cjs",
|
|
20
|
+
"module": "./dist/index.mjs",
|
|
21
|
+
"types": "./dist/index.d.mts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"import": {
|
|
25
|
+
"types": "./dist/index.d.mts",
|
|
26
|
+
"default": "./dist/index.mjs"
|
|
27
|
+
},
|
|
28
|
+
"require": {
|
|
29
|
+
"types": "./dist/index.d.cts",
|
|
30
|
+
"default": "./dist/index.cjs"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsdown",
|
|
41
|
+
"dev": "tsdown --watch",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"test:coverage": "vitest run --coverage",
|
|
45
|
+
"type-check": "tsc --noEmit",
|
|
46
|
+
"lint": "oxlint",
|
|
47
|
+
"lint:fix": "oxlint --fix",
|
|
48
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
49
|
+
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
50
|
+
"prepublishOnly": "npm run type-check && npm run lint && npm run test && npm run build",
|
|
51
|
+
"clean": "rm -rf dist"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^26.6.6",
|
|
55
|
+
"@vitest/coverage-v8": "^5.1.0",
|
|
56
|
+
"oxlint": "^1.90.0",
|
|
57
|
+
"prettier": "^3.9.10",
|
|
58
|
+
"tsdown": "^0.23.4",
|
|
59
|
+
"typescript": "^7.0.2",
|
|
60
|
+
"vitest": "^5.1.0"
|
|
61
|
+
},
|
|
62
|
+
"keywords": [
|
|
63
|
+
"circuit-breaker",
|
|
64
|
+
"fusebox",
|
|
65
|
+
"breaker",
|
|
66
|
+
"resilience",
|
|
67
|
+
"fault-tolerance",
|
|
68
|
+
"typescript"
|
|
69
|
+
],
|
|
70
|
+
"engines": {
|
|
71
|
+
"node": ">=22.0.0"
|
|
72
|
+
}
|
|
73
|
+
}
|