@xyph3r/circuit-breaker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +286 -0
  3. package/dist/core/circuit-breaker-builder.d.ts +30 -0
  4. package/dist/core/circuit-breaker-builder.d.ts.map +1 -0
  5. package/dist/core/circuit-breaker-builder.js +79 -0
  6. package/dist/core/circuit-breaker-builder.js.map +1 -0
  7. package/dist/core/circuit-breaker.d.ts +38 -0
  8. package/dist/core/circuit-breaker.d.ts.map +1 -0
  9. package/dist/core/circuit-breaker.js +80 -0
  10. package/dist/core/circuit-breaker.js.map +1 -0
  11. package/dist/core/create-circuit-breaker.d.ts +4 -0
  12. package/dist/core/create-circuit-breaker.d.ts.map +1 -0
  13. package/dist/core/create-circuit-breaker.js +30 -0
  14. package/dist/core/create-circuit-breaker.js.map +1 -0
  15. package/dist/core/event-emitter.d.ts +8 -0
  16. package/dist/core/event-emitter.d.ts.map +1 -0
  17. package/dist/core/event-emitter.js +24 -0
  18. package/dist/core/event-emitter.js.map +1 -0
  19. package/dist/errors.d.ts +8 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +15 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/index.d.ts +9 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +7 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/states/circuit-state.d.ts +26 -0
  28. package/dist/states/circuit-state.d.ts.map +1 -0
  29. package/dist/states/circuit-state.js +2 -0
  30. package/dist/states/circuit-state.js.map +1 -0
  31. package/dist/states/closed-state.d.ts +9 -0
  32. package/dist/states/closed-state.d.ts.map +1 -0
  33. package/dist/states/closed-state.js +34 -0
  34. package/dist/states/closed-state.js.map +1 -0
  35. package/dist/states/half-open-state.d.ts +10 -0
  36. package/dist/states/half-open-state.d.ts.map +1 -0
  37. package/dist/states/half-open-state.js +45 -0
  38. package/dist/states/half-open-state.js.map +1 -0
  39. package/dist/states/open-state.d.ts +10 -0
  40. package/dist/states/open-state.d.ts.map +1 -0
  41. package/dist/states/open-state.js +30 -0
  42. package/dist/states/open-state.js.map +1 -0
  43. package/dist/strategies/consecutive-failure-strategy.d.ts +11 -0
  44. package/dist/strategies/consecutive-failure-strategy.d.ts.map +1 -0
  45. package/dist/strategies/consecutive-failure-strategy.js +20 -0
  46. package/dist/strategies/consecutive-failure-strategy.js.map +1 -0
  47. package/dist/strategies/failure-detection-strategy.d.ts +13 -0
  48. package/dist/strategies/failure-detection-strategy.d.ts.map +1 -0
  49. package/dist/strategies/failure-detection-strategy.js +2 -0
  50. package/dist/strategies/failure-detection-strategy.js.map +1 -0
  51. package/dist/strategies/failure-rate-strategy.d.ts +15 -0
  52. package/dist/strategies/failure-rate-strategy.d.ts.map +1 -0
  53. package/dist/strategies/failure-rate-strategy.js +37 -0
  54. package/dist/strategies/failure-rate-strategy.js.map +1 -0
  55. package/dist/types.d.ts +28 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +2 -0
  58. package/dist/types.js.map +1 -0
  59. package/package.json +57 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xyph3r
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 ADDED
@@ -0,0 +1,286 @@
1
+ # `@xyph3r/circuit-breaker`
2
+
3
+ Wrap any async function with circuit breaker semantics.
4
+
5
+ When a dependency starts failing, the circuit breaker stops calling it. Your service degrades gracefully instead of piling up timeouts. When the dependency recovers, traffic resumes automatically.
6
+
7
+ Three states:
8
+
9
+ - **Closed** — normal operation. Calls go through. Failures are tracked.
10
+ - **Open** — the dependency is considered down. Calls are rejected immediately or routed to a fallback. No traffic reaches the dependency.
11
+ - **Half-open** — a probe call is allowed through to test recovery. Success closes the circuit. Failure reopens it.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @xyph3r/circuit-breaker
17
+ ```
18
+
19
+ or
20
+
21
+ ```bash
22
+ bun add @xyph3r/circuit-breaker
23
+ ```
24
+
25
+ ## When to use it
26
+
27
+ Use a circuit breaker when:
28
+
29
+ - you call an external API, database, or service that can go down
30
+ - slow responses from a dependency can exhaust your connection pool or threads
31
+ - you want automatic recovery detection without manual intervention
32
+ - you need a fallback path when a dependency is unavailable
33
+
34
+ Do not use it when:
35
+
36
+ - the call is purely in-memory with no external dependency
37
+ - failures are expected and handled by business logic (e.g. validation errors)
38
+ - you need per-request rate limiting (use `@xyph3r/rate-limiter` instead)
39
+
40
+ ## Quick start
41
+
42
+ ```ts
43
+ import { CircuitBreakerBuilder } from "@xyph3r/circuit-breaker";
44
+
45
+ const breaker = new CircuitBreakerBuilder()
46
+ .forConsecutiveFailures({ threshold: 5 })
47
+ .withResetTimeout(30_000)
48
+ .build();
49
+
50
+ const result = await breaker.fire(() => fetch("https://api.example.com/data"));
51
+ ```
52
+
53
+ ## Failure detection strategies
54
+
55
+ ### Consecutive failures
56
+
57
+ Open the circuit after N consecutive failures. A single success resets the count.
58
+
59
+ ```ts
60
+ const breaker = new CircuitBreakerBuilder()
61
+ .forConsecutiveFailures({ threshold: 5 })
62
+ .withResetTimeout(30_000)
63
+ .build();
64
+ ```
65
+
66
+ Use this when:
67
+
68
+ - a dependency is either up or down
69
+ - intermittent errors should not trip the breaker
70
+ - you want the simplest possible configuration
71
+
72
+ ### Failure rate
73
+
74
+ Open the circuit when the failure rate exceeds a threshold within a sliding time window. Requires a minimum number of samples before the rate is evaluated.
75
+
76
+ ```ts
77
+ const breaker = new CircuitBreakerBuilder()
78
+ .forFailureRate({
79
+ threshold: 0.5,
80
+ windowMs: 60_000,
81
+ minSamples: 10,
82
+ })
83
+ .withResetTimeout(30_000)
84
+ .build();
85
+ ```
86
+
87
+ Use this when:
88
+
89
+ - a low background error rate is acceptable
90
+ - you want to react to sustained degradation, not a single burst
91
+ - the dependency handles mixed traffic with different reliability profiles
92
+
93
+ ## Fallback
94
+
95
+ Provide a fallback function when you want graceful degradation instead of errors.
96
+
97
+ ```ts
98
+ const result = await breaker.fire(
99
+ () => fetch("https://api.example.com/data"),
100
+ () => ({ cached: true, data: getCachedData() }),
101
+ );
102
+ ```
103
+
104
+ The fallback runs when the circuit is open or when all half-open probe slots are occupied.
105
+
106
+ ## Wrapping a function
107
+
108
+ Use `wrap()` to create a permanently protected version of a function.
109
+
110
+ ```ts
111
+ async function fetchUser(id: string): Promise<User> {
112
+ const response = await fetch(`https://api.example.com/users/${id}`);
113
+ return response.json();
114
+ }
115
+
116
+ const safeFetchUser = breaker.wrap(fetchUser);
117
+
118
+ const user = await safeFetchUser("42");
119
+ ```
120
+
121
+ With a fallback:
122
+
123
+ ```ts
124
+ const safeFetchUser = breaker.wrap(
125
+ fetchUser,
126
+ async (id) => ({ id, name: "Unknown", cached: true }),
127
+ );
128
+ ```
129
+
130
+ ## Filtering failures
131
+
132
+ By default, every thrown error counts as a failure. Use `isFailure()` to control which errors trip the breaker.
133
+
134
+ ```ts
135
+ const breaker = new CircuitBreakerBuilder()
136
+ .forConsecutiveFailures({ threshold: 5 })
137
+ .withResetTimeout(30_000)
138
+ .isFailure((error) => {
139
+ if (error instanceof HttpError && error.status === 404) return false;
140
+ if (error instanceof HttpError && error.status === 400) return false;
141
+ return true;
142
+ })
143
+ .build();
144
+ ```
145
+
146
+ Errors that `isFailure` returns `false` for are still thrown to the caller — they just don't affect the circuit state.
147
+
148
+ ## Events
149
+
150
+ Subscribe to lifecycle events for logging, metrics, or alerting.
151
+
152
+ ```ts
153
+ breaker.on("stateChange", (info) => {
154
+ console.log(`Circuit ${info.previousState} → ${info.state}`);
155
+ });
156
+
157
+ breaker.on("success", (info) => {
158
+ metrics.increment("circuit.success");
159
+ });
160
+
161
+ breaker.on("failure", (info) => {
162
+ metrics.increment("circuit.failure");
163
+ logger.warn("dependency failure", { error: info.error });
164
+ });
165
+
166
+ breaker.on("reject", (info) => {
167
+ metrics.increment("circuit.reject");
168
+ });
169
+ ```
170
+
171
+ Remove a listener with `off()`:
172
+
173
+ ```ts
174
+ breaker.off("stateChange", handler);
175
+ ```
176
+
177
+ ## Half-open probes
178
+
179
+ When the circuit is open and the reset timeout elapses, the next call transitions to half-open. One probe call (configurable with `withHalfOpenMax()`) is allowed through:
180
+
181
+ - If the probe succeeds, the circuit closes and normal traffic resumes.
182
+ - If the probe fails, the circuit reopens and the reset timeout restarts.
183
+ - Additional calls while the probe slot is occupied are rejected.
184
+
185
+ ```ts
186
+ const breaker = new CircuitBreakerBuilder()
187
+ .forConsecutiveFailures({ threshold: 5 })
188
+ .withResetTimeout(30_000)
189
+ .withHalfOpenMax(1)
190
+ .build();
191
+ ```
192
+
193
+ ## Factory vs Builder
194
+
195
+ Use the Builder when you want readability and progressive configuration.
196
+
197
+ ```ts
198
+ const breaker = new CircuitBreakerBuilder()
199
+ .forConsecutiveFailures({ threshold: 5 })
200
+ .withResetTimeout(30_000)
201
+ .withHalfOpenMax(1)
202
+ .isFailure((error) => error.message !== "not-a-real-error")
203
+ .build();
204
+ ```
205
+
206
+ Use `createCircuitBreaker()` when a plain config object is enough.
207
+
208
+ ```ts
209
+ import { createCircuitBreaker } from "@xyph3r/circuit-breaker";
210
+
211
+ const breaker = createCircuitBreaker({
212
+ strategy: "consecutive",
213
+ threshold: 5,
214
+ resetTimeoutMs: 30_000,
215
+ });
216
+ ```
217
+
218
+ ## Custom strategies
219
+
220
+ Implement `FailureDetectionStrategy` to plug in your own failure detection logic.
221
+
222
+ ```ts
223
+ import type { FailureDetectionStrategy } from "@xyph3r/circuit-breaker";
224
+
225
+ class JitteredFailureStrategy implements FailureDetectionStrategy {
226
+ recordSuccess(): void { /* ... */ }
227
+ recordFailure(): void { /* ... */ }
228
+ shouldOpen(): boolean { /* ... */ }
229
+ reset(): void { /* ... */ }
230
+ }
231
+
232
+ const breaker = new CircuitBreakerBuilder()
233
+ .useStrategy(new JitteredFailureStrategy())
234
+ .withResetTimeout(30_000)
235
+ .build();
236
+ ```
237
+
238
+ ## Public API
239
+
240
+ ### `CircuitBreakerBuilder`
241
+
242
+ - `.forConsecutiveFailures({ threshold })`
243
+ - `.forFailureRate({ threshold, windowMs, minSamples })`
244
+ - `.useStrategy(strategy)`
245
+ - `.withResetTimeout(ms)`
246
+ - `.withHalfOpenMax(max)`
247
+ - `.isFailure(predicate)`
248
+ - `.withClock(now)`
249
+ - `.build()`
250
+
251
+ ### `CircuitBreaker`
252
+
253
+ - `fire(fn, fallback?)`
254
+ - `wrap(fn, fallback?)`
255
+ - `state` — current state: `"closed"`, `"open"`, or `"half-open"`
256
+ - `on(event, handler)`
257
+ - `off(event, handler)`
258
+ - `reset()`
259
+
260
+ ### Events
261
+
262
+ - `stateChange` — `{ state, previousState, timestamp }`
263
+ - `success` — `{ state, timestamp }`
264
+ - `failure` — `{ state, error, timestamp }`
265
+ - `reject` — `{ state, error, timestamp }`
266
+
267
+ ### Errors
268
+
269
+ - `CircuitBreakerOpenError` — thrown when the circuit is open and no fallback is provided
270
+ - `CircuitBreakerConfigurationError` — thrown by the builder on invalid configuration
271
+
272
+ ## Publishing
273
+
274
+ The package is set up so `npm publish` builds `dist/` during `prepack` and runs tests during `prepublishOnly`.
275
+
276
+ Release flow:
277
+
278
+ 1. Install dependencies with `bun install` or `npm install`.
279
+ 2. Verify with `bun run test` and `bun run build`.
280
+ 3. Inspect the tarball with `npm pack --dry-run`.
281
+ 4. Log in with `npm login` if needed.
282
+ 5. Publish with `npm publish --access public --provenance`.
283
+
284
+ ## License
285
+
286
+ MIT
@@ -0,0 +1,30 @@
1
+ import type { FailureDetectionStrategy } from "../strategies/failure-detection-strategy.js";
2
+ import { CircuitBreaker } from "./circuit-breaker.js";
3
+ /**
4
+ * Pattern: Builder
5
+ * Problem: Construction spans strategy selection, timeouts, failure predicates, and probe limits.
6
+ * Solution: The builder makes each choice explicit and validates the combination at build time.
7
+ * Trade-off: More ceremony than a constructor; justified because the config surface is non-trivial.
8
+ */
9
+ export declare class CircuitBreakerBuilder {
10
+ private strategy;
11
+ private resetTimeoutMs;
12
+ private halfOpenMax;
13
+ private failurePredicate;
14
+ private clock;
15
+ forConsecutiveFailures(options: {
16
+ threshold: number;
17
+ }): this;
18
+ forFailureRate(options: {
19
+ threshold: number;
20
+ windowMs: number;
21
+ minSamples: number;
22
+ }): this;
23
+ useStrategy(strategy: FailureDetectionStrategy): this;
24
+ withResetTimeout(ms: number): this;
25
+ withHalfOpenMax(max: number): this;
26
+ isFailure(predicate: (error: Error) => boolean): this;
27
+ withClock(now: () => number): this;
28
+ build(): CircuitBreaker;
29
+ }
30
+ //# sourceMappingURL=circuit-breaker-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-breaker-builder.d.ts","sourceRoot":"","sources":["../../src/core/circuit-breaker-builder.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,6CAA6C,CAAC;AAG5F,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD;;;;;GAKG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAuC;IACvD,OAAO,CAAC,cAAc,CAAU;IAChC,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,gBAAgB,CAA0C;IAClE,OAAO,CAAC,KAAK,CAA6B;IAE1C,sBAAsB,CAAC,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAM5D,cAAc,CAAC,OAAO,EAAE;QACtB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,IAAI;IAuBR,WAAW,CAAC,QAAQ,EAAE,wBAAwB,GAAG,IAAI;IAKrD,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAMlC,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAMlC,SAAS,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,GAAG,IAAI;IAKrD,SAAS,CAAC,GAAG,EAAE,MAAM,MAAM,GAAG,IAAI;IAKlC,KAAK,IAAI,cAAc;CAgBxB"}
@@ -0,0 +1,79 @@
1
+ import { CircuitBreakerConfigurationError } from "../errors.js";
2
+ import { ConsecutiveFailureStrategy } from "../strategies/consecutive-failure-strategy.js";
3
+ import { FailureRateStrategy } from "../strategies/failure-rate-strategy.js";
4
+ import { CircuitBreaker } from "./circuit-breaker.js";
5
+ /**
6
+ * Pattern: Builder
7
+ * Problem: Construction spans strategy selection, timeouts, failure predicates, and probe limits.
8
+ * Solution: The builder makes each choice explicit and validates the combination at build time.
9
+ * Trade-off: More ceremony than a constructor; justified because the config surface is non-trivial.
10
+ */
11
+ export class CircuitBreakerBuilder {
12
+ strategy;
13
+ resetTimeoutMs = 30_000;
14
+ halfOpenMax = 1;
15
+ failurePredicate;
16
+ clock;
17
+ forConsecutiveFailures(options) {
18
+ validatePositiveInteger(options.threshold, "forConsecutiveFailures()");
19
+ this.strategy = new ConsecutiveFailureStrategy(options.threshold);
20
+ return this;
21
+ }
22
+ forFailureRate(options) {
23
+ if (!Number.isFinite(options.threshold) ||
24
+ options.threshold <= 0 ||
25
+ options.threshold > 1) {
26
+ throw new CircuitBreakerConfigurationError("forFailureRate() threshold must be between 0 (exclusive) and 1 (inclusive).");
27
+ }
28
+ validatePositiveNumber(options.windowMs, "forFailureRate() windowMs");
29
+ validatePositiveInteger(options.minSamples, "forFailureRate() minSamples");
30
+ this.strategy = new FailureRateStrategy(options.threshold, options.windowMs, options.minSamples, this.clock ?? (() => Date.now()));
31
+ return this;
32
+ }
33
+ useStrategy(strategy) {
34
+ this.strategy = strategy;
35
+ return this;
36
+ }
37
+ withResetTimeout(ms) {
38
+ validatePositiveNumber(ms, "withResetTimeout()");
39
+ this.resetTimeoutMs = ms;
40
+ return this;
41
+ }
42
+ withHalfOpenMax(max) {
43
+ validatePositiveInteger(max, "withHalfOpenMax()");
44
+ this.halfOpenMax = max;
45
+ return this;
46
+ }
47
+ isFailure(predicate) {
48
+ this.failurePredicate = predicate;
49
+ return this;
50
+ }
51
+ withClock(now) {
52
+ this.clock = now;
53
+ return this;
54
+ }
55
+ build() {
56
+ if (!this.strategy) {
57
+ throw new CircuitBreakerConfigurationError("A failure detection strategy is required. " +
58
+ "Call forConsecutiveFailures(), forFailureRate(), or useStrategy().");
59
+ }
60
+ return new CircuitBreaker({
61
+ strategy: this.strategy,
62
+ resetTimeoutMs: this.resetTimeoutMs,
63
+ halfOpenMax: this.halfOpenMax,
64
+ isFailure: this.failurePredicate ?? (() => true),
65
+ now: this.clock ?? (() => Date.now()),
66
+ });
67
+ }
68
+ }
69
+ function validatePositiveNumber(value, label) {
70
+ if (!Number.isFinite(value) || value <= 0) {
71
+ throw new CircuitBreakerConfigurationError(`${label} requires a positive number.`);
72
+ }
73
+ }
74
+ function validatePositiveInteger(value, label) {
75
+ if (!Number.isInteger(value) || value <= 0) {
76
+ throw new CircuitBreakerConfigurationError(`${label} requires a positive integer.`);
77
+ }
78
+ }
79
+ //# sourceMappingURL=circuit-breaker-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-breaker-builder.js","sourceRoot":"","sources":["../../src/core/circuit-breaker-builder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { FailureDetectionStrategy } from "../strategies/failure-detection-strategy.js";
2
+ import type { CircuitBreakerEventHandler, CircuitBreakerEventType, CircuitBreakerState } from "../types.js";
3
+ import type { CircuitBreakerContext, CircuitState } from "../states/circuit-state.js";
4
+ export interface CircuitBreakerOptions {
5
+ strategy: FailureDetectionStrategy;
6
+ resetTimeoutMs: number;
7
+ halfOpenMax: number;
8
+ isFailure: (error: Error) => boolean;
9
+ now: () => number;
10
+ }
11
+ /**
12
+ * Pattern: Proxy
13
+ * Problem: Callers should not manage failure tracking, timeouts, or state transitions themselves.
14
+ * Solution: CircuitBreaker.fire(fn) wraps fn transparently — sometimes calling through, sometimes short-circuiting.
15
+ * Trade-off: One wrapper per dependency call; justified because the alternative is scattered failure handling.
16
+ */
17
+ export declare class CircuitBreaker implements CircuitBreakerContext {
18
+ readonly resetTimeoutMs: number;
19
+ readonly halfOpenMax: number;
20
+ readonly now: () => number;
21
+ readonly isFailure: (error: Error) => boolean;
22
+ readonly strategy: FailureDetectionStrategy;
23
+ private currentState;
24
+ private readonly emitter;
25
+ constructor(options: CircuitBreakerOptions);
26
+ get state(): CircuitBreakerState;
27
+ fire<T>(fn: () => Promise<T>, fallback?: () => T | Promise<T>): Promise<T>;
28
+ wrap<TArgs extends unknown[], TResult>(fn: (...args: TArgs) => Promise<TResult>, fallback?: (...args: TArgs) => TResult | Promise<TResult>): (...args: TArgs) => Promise<TResult>;
29
+ on(event: CircuitBreakerEventType, handler: CircuitBreakerEventHandler): void;
30
+ off(event: CircuitBreakerEventType, handler: CircuitBreakerEventHandler): void;
31
+ reset(): void;
32
+ transition(state: CircuitState): void;
33
+ emitStateChange(from: CircuitBreakerState, to: CircuitBreakerState): void;
34
+ emitSuccess(): void;
35
+ emitFailure(error: Error): void;
36
+ emitReject(error: Error): void;
37
+ }
38
+ //# sourceMappingURL=circuit-breaker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../../src/core/circuit-breaker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,6CAA6C,CAAC;AAC5F,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,mBAAmB,EACpB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,qBAAqB,EACrB,YAAY,EACb,MAAM,4BAA4B,CAAC;AAIpC,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,wBAAwB,CAAC;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACrC,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,qBAAa,cAAe,YAAW,qBAAqB;IAC1D,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAC;IAE5C,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;gBAEhD,OAAO,EAAE,qBAAqB;IAU1C,IAAI,KAAK,IAAI,mBAAmB,CAE/B;IAEK,IAAI,CAAC,CAAC,EACV,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAC9B,OAAO,CAAC,CAAC,CAAC;IAIb,IAAI,CAAC,KAAK,SAAS,OAAO,EAAE,EAAE,OAAO,EACnC,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,CAAC,EACxC,QAAQ,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GACxD,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,CAAC;IAQvC,EAAE,CACA,KAAK,EAAE,uBAAuB,EAC9B,OAAO,EAAE,0BAA0B,GAClC,IAAI;IAIP,GAAG,CACD,KAAK,EAAE,uBAAuB,EAC9B,OAAO,EAAE,0BAA0B,GAClC,IAAI;IAIP,KAAK,IAAI,IAAI;IASb,UAAU,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI;IAKrC,eAAe,CACb,IAAI,EAAE,mBAAmB,EACzB,EAAE,EAAE,mBAAmB,GACtB,IAAI;IAQP,WAAW,IAAI,IAAI;IAOnB,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAQ/B,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;CAO/B"}
@@ -0,0 +1,80 @@
1
+ import { ClosedState } from "../states/closed-state.js";
2
+ import { CircuitBreakerEventEmitter } from "./event-emitter.js";
3
+ /**
4
+ * Pattern: Proxy
5
+ * Problem: Callers should not manage failure tracking, timeouts, or state transitions themselves.
6
+ * Solution: CircuitBreaker.fire(fn) wraps fn transparently — sometimes calling through, sometimes short-circuiting.
7
+ * Trade-off: One wrapper per dependency call; justified because the alternative is scattered failure handling.
8
+ */
9
+ export class CircuitBreaker {
10
+ resetTimeoutMs;
11
+ halfOpenMax;
12
+ now;
13
+ isFailure;
14
+ strategy;
15
+ currentState;
16
+ emitter = new CircuitBreakerEventEmitter();
17
+ constructor(options) {
18
+ this.strategy = options.strategy;
19
+ this.resetTimeoutMs = options.resetTimeoutMs;
20
+ this.halfOpenMax = options.halfOpenMax;
21
+ this.isFailure = options.isFailure;
22
+ this.now = options.now;
23
+ this.currentState = new ClosedState(this);
24
+ }
25
+ get state() {
26
+ return this.currentState.name;
27
+ }
28
+ async fire(fn, fallback) {
29
+ return this.currentState.fire(fn, fallback);
30
+ }
31
+ wrap(fn, fallback) {
32
+ return (...args) => this.fire(() => fn(...args), fallback ? () => fallback(...args) : undefined);
33
+ }
34
+ on(event, handler) {
35
+ this.emitter.on(event, handler);
36
+ }
37
+ off(event, handler) {
38
+ this.emitter.off(event, handler);
39
+ }
40
+ reset() {
41
+ this.strategy.reset();
42
+ const closed = new ClosedState(this);
43
+ if (this.currentState.name !== "closed") {
44
+ this.emitStateChange(this.currentState.name, "closed");
45
+ }
46
+ this.currentState = closed;
47
+ }
48
+ transition(state) {
49
+ this.currentState = state;
50
+ state.onEntry();
51
+ }
52
+ emitStateChange(from, to) {
53
+ this.emitter.emit("stateChange", {
54
+ state: to,
55
+ previousState: from,
56
+ timestamp: this.now(),
57
+ });
58
+ }
59
+ emitSuccess() {
60
+ this.emitter.emit("success", {
61
+ state: this.currentState.name,
62
+ timestamp: this.now(),
63
+ });
64
+ }
65
+ emitFailure(error) {
66
+ this.emitter.emit("failure", {
67
+ state: this.currentState.name,
68
+ error,
69
+ timestamp: this.now(),
70
+ });
71
+ }
72
+ emitReject(error) {
73
+ this.emitter.emit("reject", {
74
+ state: this.currentState.name,
75
+ error,
76
+ timestamp: this.now(),
77
+ });
78
+ }
79
+ }
80
+ //# sourceMappingURL=circuit-breaker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-breaker.js","sourceRoot":"","sources":["../../src/core/circuit-breaker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { CreateCircuitBreakerOptions } from "../types.js";
2
+ import type { CircuitBreaker } from "./circuit-breaker.js";
3
+ export declare function createCircuitBreaker(options?: CreateCircuitBreakerOptions): CircuitBreaker;
4
+ //# sourceMappingURL=create-circuit-breaker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-circuit-breaker.d.ts","sourceRoot":"","sources":["../../src/core/create-circuit-breaker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,wBAAgB,oBAAoB,CAClC,OAAO,GAAE,2BAAgC,GACxC,cAAc,CAgChB"}
@@ -0,0 +1,30 @@
1
+ import { CircuitBreakerBuilder } from "./circuit-breaker-builder.js";
2
+ export function createCircuitBreaker(options = {}) {
3
+ const builder = new CircuitBreakerBuilder();
4
+ if (options.now) {
5
+ builder.withClock(options.now);
6
+ }
7
+ if (options.strategy === "failure-rate") {
8
+ builder.forFailureRate({
9
+ threshold: options.threshold ?? 0.5,
10
+ windowMs: options.windowMs ?? 60_000,
11
+ minSamples: options.minSamples ?? 10,
12
+ });
13
+ }
14
+ else {
15
+ builder.forConsecutiveFailures({
16
+ threshold: options.threshold ?? 5,
17
+ });
18
+ }
19
+ if (options.resetTimeoutMs !== undefined) {
20
+ builder.withResetTimeout(options.resetTimeoutMs);
21
+ }
22
+ if (options.halfOpenMax !== undefined) {
23
+ builder.withHalfOpenMax(options.halfOpenMax);
24
+ }
25
+ if (options.isFailure) {
26
+ builder.isFailure(options.isFailure);
27
+ }
28
+ return builder.build();
29
+ }
30
+ //# sourceMappingURL=create-circuit-breaker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-circuit-breaker.js","sourceRoot":"","sources":["../../src/core/create-circuit-breaker.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAErE,MAAM,UAAU,oBAAoB,CAClC,UAAuC,EAAE;IAEzC,MAAM,OAAO,GAAG,IAAI,qBAAqB,EAAE,CAAC;IAE5C,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;QAChB,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,KAAK,cAAc,EAAE,CAAC;QACxC,OAAO,CAAC,cAAc,CAAC;YACrB,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,GAAG;YACnC,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,MAAM;YACpC,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,EAAE;SACrC,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,sBAAsB,CAAC;YAC7B,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,CAAC;SAClC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,OAAO,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;QACzC,OAAO,CAAC,gBAAgB,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IACnD,CAAC;IAED,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACtC,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/C,CAAC;IAED,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QACtB,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACvC,CAAC;IAED,OAAO,OAAO,CAAC,KAAK,EAAE,CAAC;AACzB,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { CircuitBreakerEventHandler, CircuitBreakerEventType } from "../types.js";
2
+ export declare class CircuitBreakerEventEmitter {
3
+ private readonly listeners;
4
+ on(event: CircuitBreakerEventType, handler: CircuitBreakerEventHandler): void;
5
+ off(event: CircuitBreakerEventType, handler: CircuitBreakerEventHandler): void;
6
+ emit(event: CircuitBreakerEventType, ...args: Parameters<CircuitBreakerEventHandler>): void;
7
+ }
8
+ //# sourceMappingURL=event-emitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-emitter.d.ts","sourceRoot":"","sources":["../../src/core/event-emitter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACxB,MAAM,aAAa,CAAC;AAErB,qBAAa,0BAA0B;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAGtB;IAEJ,EAAE,CACA,KAAK,EAAE,uBAAuB,EAC9B,OAAO,EAAE,0BAA0B,GAClC,IAAI;IASP,GAAG,CACD,KAAK,EAAE,uBAAuB,EAC9B,OAAO,EAAE,0BAA0B,GAClC,IAAI;IAIP,IAAI,CACF,KAAK,EAAE,uBAAuB,EAC9B,GAAG,IAAI,EAAE,UAAU,CAAC,0BAA0B,CAAC,GAC9C,IAAI;CAUR"}
@@ -0,0 +1,24 @@
1
+ export class CircuitBreakerEventEmitter {
2
+ listeners = new Map();
3
+ on(event, handler) {
4
+ let set = this.listeners.get(event);
5
+ if (!set) {
6
+ set = new Set();
7
+ this.listeners.set(event, set);
8
+ }
9
+ set.add(handler);
10
+ }
11
+ off(event, handler) {
12
+ this.listeners.get(event)?.delete(handler);
13
+ }
14
+ emit(event, ...args) {
15
+ const set = this.listeners.get(event);
16
+ if (!set) {
17
+ return;
18
+ }
19
+ for (const handler of set) {
20
+ handler(...args);
21
+ }
22
+ }
23
+ }
24
+ //# sourceMappingURL=event-emitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-emitter.js","sourceRoot":"","sources":["../../src/core/event-emitter.ts"],"names":[],"mappings":"AAKA,MAAM,OAAO,0BAA0B;IACpB,SAAS,GAAG,IAAI,GAAG,EAGjC,CAAC;IAEJ,EAAE,CACA,KAA8B,EAC9B,OAAmC;QAEnC,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACjC,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACnB,CAAC;IAED,GAAG,CACD,KAA8B,EAC9B,OAAmC;QAEnC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7C,CAAC;IAED,IAAI,CACF,KAA8B,EAC9B,GAAG,IAA4C;QAE/C,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO;QACT,CAAC;QAED,KAAK,MAAM,OAAO,IAAI,GAAG,EAAE,CAAC;YAC1B,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,8 @@
1
+ export declare class CircuitBreakerConfigurationError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class CircuitBreakerOpenError extends Error {
5
+ readonly remainingMs: number;
6
+ constructor(message: string, remainingMs: number);
7
+ }
8
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,gCAAiC,SAAQ,KAAK;gBAC7C,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,uBAAwB,SAAQ,KAAK;aAG9B,WAAW,EAAE,MAAM;gBADnC,OAAO,EAAE,MAAM,EACC,WAAW,EAAE,MAAM;CAKtC"}
package/dist/errors.js ADDED
@@ -0,0 +1,15 @@
1
+ export class CircuitBreakerConfigurationError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "CircuitBreakerConfigurationError";
5
+ }
6
+ }
7
+ export class CircuitBreakerOpenError extends Error {
8
+ remainingMs;
9
+ constructor(message, remainingMs) {
10
+ super(message);
11
+ this.remainingMs = remainingMs;
12
+ this.name = "CircuitBreakerOpenError";
13
+ }
14
+ }
15
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,gCAAiC,SAAQ,KAAK;IACzD,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,kCAAkC,CAAC;IACjD,CAAC;CACF;AAED,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAG9B;IAFlB,YACE,OAAe,EACC,WAAmB;QAEnC,KAAK,CAAC,OAAO,CAAC,CAAC;QAFC,gBAAW,GAAX,WAAW,CAAQ;QAGnC,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF"}
@@ -0,0 +1,9 @@
1
+ export { CircuitBreaker } from "./core/circuit-breaker.js";
2
+ export { CircuitBreakerBuilder } from "./core/circuit-breaker-builder.js";
3
+ export { createCircuitBreaker, } from "./core/create-circuit-breaker.js";
4
+ export { ConsecutiveFailureStrategy } from "./strategies/consecutive-failure-strategy.js";
5
+ export { FailureRateStrategy } from "./strategies/failure-rate-strategy.js";
6
+ export type { FailureDetectionStrategy } from "./strategies/failure-detection-strategy.js";
7
+ export { CircuitBreakerConfigurationError, CircuitBreakerOpenError, } from "./errors.js";
8
+ export type { CircuitBreakerEventHandler, CircuitBreakerEventInfo, CircuitBreakerEventType, CircuitBreakerLike, CircuitBreakerState, CreateCircuitBreakerOptions, } from "./types.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EACL,oBAAoB,GACrB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,0BAA0B,EAAE,MAAM,8CAA8C,CAAC;AAC1F,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAC5E,YAAY,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AAC3F,OAAO,EACL,gCAAgC,EAChC,uBAAuB,GACxB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,uBAAuB,EACvB,kBAAkB,EAClB,mBAAmB,EACnB,2BAA2B,GAC5B,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { CircuitBreaker } from "./core/circuit-breaker.js";
2
+ export { CircuitBreakerBuilder } from "./core/circuit-breaker-builder.js";
3
+ export { createCircuitBreaker, } from "./core/create-circuit-breaker.js";
4
+ export { ConsecutiveFailureStrategy } from "./strategies/consecutive-failure-strategy.js";
5
+ export { FailureRateStrategy } from "./strategies/failure-rate-strategy.js";
6
+ export { CircuitBreakerConfigurationError, CircuitBreakerOpenError, } from "./errors.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EACL,oBAAoB,GACrB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,0BAA0B,EAAE,MAAM,8CAA8C,CAAC;AAC1F,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAE5E,OAAO,EACL,gCAAgC,EAChC,uBAAuB,GACxB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { CircuitBreakerState } from "../types.js";
2
+ import type { FailureDetectionStrategy } from "../strategies/failure-detection-strategy.js";
3
+ export interface CircuitBreakerContext {
4
+ readonly resetTimeoutMs: number;
5
+ readonly halfOpenMax: number;
6
+ readonly now: () => number;
7
+ readonly isFailure: (error: Error) => boolean;
8
+ readonly strategy: FailureDetectionStrategy;
9
+ transition(state: CircuitState): void;
10
+ emitStateChange(from: CircuitBreakerState, to: CircuitBreakerState): void;
11
+ emitSuccess(): void;
12
+ emitFailure(error: Error): void;
13
+ emitReject(error: Error): void;
14
+ }
15
+ /**
16
+ * Pattern: State
17
+ * Problem: fire() behaves completely differently depending on circuit state.
18
+ * Solution: Each state is a class that implements CircuitState; the breaker delegates to the current state.
19
+ * Trade-off: Three classes instead of if/else; justified because state transitions are the core logic.
20
+ */
21
+ export interface CircuitState {
22
+ readonly name: CircuitBreakerState;
23
+ onEntry(): void;
24
+ fire<T>(fn: () => Promise<T>, fallback: (() => T | Promise<T>) | undefined): Promise<T>;
25
+ }
26
+ //# sourceMappingURL=circuit-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-state.d.ts","sourceRoot":"","sources":["../../src/states/circuit-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,6CAA6C,CAAC;AAE5F,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAC;IAC5C,UAAU,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;IACtC,eAAe,CACb,IAAI,EAAE,mBAAmB,EACzB,EAAE,EAAE,mBAAmB,GACtB,IAAI,CAAC;IACR,WAAW,IAAI,IAAI,CAAC;IACpB,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAChC,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CAChC;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,OAAO,IAAI,IAAI,CAAC;IAChB,IAAI,CAAC,CAAC,EACJ,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,QAAQ,EAAE,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,GAC3C,OAAO,CAAC,CAAC,CAAC,CAAC;CACf"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=circuit-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-state.js","sourceRoot":"","sources":["../../src/states/circuit-state.ts"],"names":[],"mappings":""}
@@ -0,0 +1,9 @@
1
+ import type { CircuitBreakerContext, CircuitState } from "./circuit-state.js";
2
+ export declare class ClosedState implements CircuitState {
3
+ private readonly context;
4
+ readonly name: "closed";
5
+ constructor(context: CircuitBreakerContext);
6
+ onEntry(): void;
7
+ fire<T>(fn: () => Promise<T>, _fallback: (() => T | Promise<T>) | undefined): Promise<T>;
8
+ }
9
+ //# sourceMappingURL=closed-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"closed-state.d.ts","sourceRoot":"","sources":["../../src/states/closed-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAG9E,qBAAa,WAAY,YAAW,YAAY;IAGlC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;gBAEL,OAAO,EAAE,qBAAqB;IAE3D,OAAO,IAAI,IAAI;IAIT,IAAI,CAAC,CAAC,EACV,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,SAAS,EAAE,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,GAC5C,OAAO,CAAC,CAAC,CAAC;CA0Bd"}
@@ -0,0 +1,34 @@
1
+ import { OpenState } from "./open-state.js";
2
+ export class ClosedState {
3
+ context;
4
+ name = "closed";
5
+ constructor(context) {
6
+ this.context = context;
7
+ }
8
+ onEntry() {
9
+ this.context.strategy.reset();
10
+ }
11
+ async fire(fn, _fallback) {
12
+ try {
13
+ const result = await fn();
14
+ this.context.strategy.recordSuccess();
15
+ this.context.emitSuccess();
16
+ return result;
17
+ }
18
+ catch (error) {
19
+ const asError = error instanceof Error ? error : new Error(String(error));
20
+ if (!this.context.isFailure(asError)) {
21
+ throw error;
22
+ }
23
+ this.context.strategy.recordFailure();
24
+ this.context.emitFailure(asError);
25
+ if (this.context.strategy.shouldOpen()) {
26
+ const next = new OpenState(this.context);
27
+ this.context.emitStateChange("closed", "open");
28
+ this.context.transition(next);
29
+ }
30
+ throw error;
31
+ }
32
+ }
33
+ }
34
+ //# sourceMappingURL=closed-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"closed-state.js","sourceRoot":"","sources":["../../src/states/closed-state.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,OAAO,WAAW;IAGO;IAFpB,IAAI,GAAG,QAAiB,CAAC;IAElC,YAA6B,OAA8B;QAA9B,YAAO,GAAP,OAAO,CAAuB;IAAG,CAAC;IAE/D,OAAO;QACL,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IAChC,CAAC;IAED,KAAK,CAAC,IAAI,CACR,EAAoB,EACpB,SAA6C;QAE7C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC;YAC1B,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;YACtC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;YAC3B,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GACX,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAE5D,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;gBACrC,MAAM,KAAK,CAAC;YACd,CAAC;YAED,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;YACtC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;YAElC,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAE,CAAC;gBACvC,MAAM,IAAI,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACzC,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;gBAC/C,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YAChC,CAAC;YAED,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,10 @@
1
+ import type { CircuitBreakerContext, CircuitState } from "./circuit-state.js";
2
+ export declare class HalfOpenState implements CircuitState {
3
+ private readonly context;
4
+ readonly name: "half-open";
5
+ private activeProbes;
6
+ constructor(context: CircuitBreakerContext);
7
+ onEntry(): void;
8
+ fire<T>(fn: () => Promise<T>, fallback: (() => T | Promise<T>) | undefined): Promise<T>;
9
+ }
10
+ //# sourceMappingURL=half-open-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"half-open-state.d.ts","sourceRoot":"","sources":["../../src/states/half-open-state.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAI9E,qBAAa,aAAc,YAAW,YAAY;IAIpC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,QAAQ,CAAC,IAAI,EAAG,WAAW,CAAU;IACrC,OAAO,CAAC,YAAY,CAAK;gBAEI,OAAO,EAAE,qBAAqB;IAE3D,OAAO,IAAI,IAAI;IAIT,IAAI,CAAC,CAAC,EACV,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,QAAQ,EAAE,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,GAC3C,OAAO,CAAC,CAAC,CAAC;CA2Cd"}
@@ -0,0 +1,45 @@
1
+ import { CircuitBreakerOpenError } from "../errors.js";
2
+ import { ClosedState } from "./closed-state.js";
3
+ import { OpenState } from "./open-state.js";
4
+ export class HalfOpenState {
5
+ context;
6
+ name = "half-open";
7
+ activeProbes = 0;
8
+ constructor(context) {
9
+ this.context = context;
10
+ }
11
+ onEntry() {
12
+ this.activeProbes = 0;
13
+ }
14
+ async fire(fn, fallback) {
15
+ if (this.activeProbes >= this.context.halfOpenMax) {
16
+ const error = new CircuitBreakerOpenError("Circuit breaker is half-open and all probe slots are occupied.", 0);
17
+ this.context.emitReject(error);
18
+ if (fallback) {
19
+ return fallback();
20
+ }
21
+ throw error;
22
+ }
23
+ this.activeProbes += 1;
24
+ try {
25
+ const result = await fn();
26
+ this.context.emitSuccess();
27
+ const next = new ClosedState(this.context);
28
+ this.context.emitStateChange("half-open", "closed");
29
+ this.context.transition(next);
30
+ return result;
31
+ }
32
+ catch (error) {
33
+ const asError = error instanceof Error ? error : new Error(String(error));
34
+ if (!this.context.isFailure(asError)) {
35
+ throw error;
36
+ }
37
+ this.context.emitFailure(asError);
38
+ const next = new OpenState(this.context);
39
+ this.context.emitStateChange("half-open", "open");
40
+ this.context.transition(next);
41
+ throw error;
42
+ }
43
+ }
44
+ }
45
+ //# sourceMappingURL=half-open-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"half-open-state.js","sourceRoot":"","sources":["../../src/states/half-open-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,OAAO,aAAa;IAIK;IAHpB,IAAI,GAAG,WAAoB,CAAC;IAC7B,YAAY,GAAG,CAAC,CAAC;IAEzB,YAA6B,OAA8B;QAA9B,YAAO,GAAP,OAAO,CAAuB;IAAG,CAAC;IAE/D,OAAO;QACL,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,IAAI,CACR,EAAoB,EACpB,QAA4C;QAE5C,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;YAClD,MAAM,KAAK,GAAG,IAAI,uBAAuB,CACvC,gEAAgE,EAChE,CAAC,CACF,CAAC;YACF,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;YAE/B,IAAI,QAAQ,EAAE,CAAC;gBACb,OAAO,QAAQ,EAAE,CAAC;YACpB,CAAC;YAED,MAAM,KAAK,CAAC;QACd,CAAC;QAED,IAAI,CAAC,YAAY,IAAI,CAAC,CAAC;QAEvB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC;YAC1B,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;YAE3B,MAAM,IAAI,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC3C,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;YACpD,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YAE9B,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GACX,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAE5D,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;gBACrC,MAAM,KAAK,CAAC;YACd,CAAC;YAED,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;YAElC,MAAM,IAAI,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACzC,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;YAClD,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YAE9B,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,10 @@
1
+ import type { CircuitBreakerContext, CircuitState } from "./circuit-state.js";
2
+ export declare class OpenState implements CircuitState {
3
+ private readonly context;
4
+ readonly name: "open";
5
+ private openedAt;
6
+ constructor(context: CircuitBreakerContext);
7
+ onEntry(): void;
8
+ fire<T>(fn: () => Promise<T>, fallback: (() => T | Promise<T>) | undefined): Promise<T>;
9
+ }
10
+ //# sourceMappingURL=open-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-state.d.ts","sourceRoot":"","sources":["../../src/states/open-state.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAG9E,qBAAa,SAAU,YAAW,YAAY;IAIhC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,QAAQ,CAAC,IAAI,EAAG,MAAM,CAAU;IAChC,OAAO,CAAC,QAAQ,CAAK;gBAEQ,OAAO,EAAE,qBAAqB;IAE3D,OAAO,IAAI,IAAI;IAIT,IAAI,CAAC,CAAC,EACV,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,QAAQ,EAAE,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,GAC3C,OAAO,CAAC,CAAC,CAAC;CAwBd"}
@@ -0,0 +1,30 @@
1
+ import { CircuitBreakerOpenError } from "../errors.js";
2
+ import { HalfOpenState } from "./half-open-state.js";
3
+ export class OpenState {
4
+ context;
5
+ name = "open";
6
+ openedAt = 0;
7
+ constructor(context) {
8
+ this.context = context;
9
+ }
10
+ onEntry() {
11
+ this.openedAt = this.context.now();
12
+ }
13
+ async fire(fn, fallback) {
14
+ const elapsed = this.context.now() - this.openedAt;
15
+ if (elapsed >= this.context.resetTimeoutMs) {
16
+ const next = new HalfOpenState(this.context);
17
+ this.context.emitStateChange("open", "half-open");
18
+ this.context.transition(next);
19
+ return next.fire(fn, fallback);
20
+ }
21
+ const remainingMs = this.context.resetTimeoutMs - elapsed;
22
+ const error = new CircuitBreakerOpenError("Circuit breaker is open.", remainingMs);
23
+ this.context.emitReject(error);
24
+ if (fallback) {
25
+ return fallback();
26
+ }
27
+ throw error;
28
+ }
29
+ }
30
+ //# sourceMappingURL=open-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-state.js","sourceRoot":"","sources":["../../src/states/open-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,MAAM,OAAO,SAAS;IAIS;IAHpB,IAAI,GAAG,MAAe,CAAC;IACxB,QAAQ,GAAG,CAAC,CAAC;IAErB,YAA6B,OAA8B;QAA9B,YAAO,GAAP,OAAO,CAAuB;IAAG,CAAC;IAE/D,OAAO;QACL,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;IAED,KAAK,CAAC,IAAI,CACR,EAAoB,EACpB,QAA4C;QAE5C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC;QAEnD,IAAI,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;YAC3C,MAAM,IAAI,GAAG,IAAI,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC7C,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;YAClD,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YAC9B,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;QACjC,CAAC;QAED,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,GAAG,OAAO,CAAC;QAC1D,MAAM,KAAK,GAAG,IAAI,uBAAuB,CACvC,0BAA0B,EAC1B,WAAW,CACZ,CAAC;QAEF,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAE/B,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,QAAQ,EAAE,CAAC;QACpB,CAAC;QAED,MAAM,KAAK,CAAC;IACd,CAAC;CACF"}
@@ -0,0 +1,11 @@
1
+ import type { FailureDetectionStrategy } from "./failure-detection-strategy.js";
2
+ export declare class ConsecutiveFailureStrategy implements FailureDetectionStrategy {
3
+ private readonly threshold;
4
+ private consecutiveFailures;
5
+ constructor(threshold: number);
6
+ recordSuccess(): void;
7
+ recordFailure(): void;
8
+ shouldOpen(): boolean;
9
+ reset(): void;
10
+ }
11
+ //# sourceMappingURL=consecutive-failure-strategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consecutive-failure-strategy.d.ts","sourceRoot":"","sources":["../../src/strategies/consecutive-failure-strategy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAEhF,qBAAa,0BAA2B,YAAW,wBAAwB;IAG7D,OAAO,CAAC,QAAQ,CAAC,SAAS;IAFtC,OAAO,CAAC,mBAAmB,CAAK;gBAEH,SAAS,EAAE,MAAM;IAE9C,aAAa,IAAI,IAAI;IAIrB,aAAa,IAAI,IAAI;IAIrB,UAAU,IAAI,OAAO;IAIrB,KAAK,IAAI,IAAI;CAGd"}
@@ -0,0 +1,20 @@
1
+ export class ConsecutiveFailureStrategy {
2
+ threshold;
3
+ consecutiveFailures = 0;
4
+ constructor(threshold) {
5
+ this.threshold = threshold;
6
+ }
7
+ recordSuccess() {
8
+ this.consecutiveFailures = 0;
9
+ }
10
+ recordFailure() {
11
+ this.consecutiveFailures += 1;
12
+ }
13
+ shouldOpen() {
14
+ return this.consecutiveFailures >= this.threshold;
15
+ }
16
+ reset() {
17
+ this.consecutiveFailures = 0;
18
+ }
19
+ }
20
+ //# sourceMappingURL=consecutive-failure-strategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consecutive-failure-strategy.js","sourceRoot":"","sources":["../../src/strategies/consecutive-failure-strategy.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,0BAA0B;IAGR;IAFrB,mBAAmB,GAAG,CAAC,CAAC;IAEhC,YAA6B,SAAiB;QAAjB,cAAS,GAAT,SAAS,CAAQ;IAAG,CAAC;IAElD,aAAa;QACX,IAAI,CAAC,mBAAmB,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED,aAAa;QACX,IAAI,CAAC,mBAAmB,IAAI,CAAC,CAAC;IAChC,CAAC;IAED,UAAU;QACR,OAAO,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC,SAAS,CAAC;IACpD,CAAC;IAED,KAAK;QACH,IAAI,CAAC,mBAAmB,GAAG,CAAC,CAAC;IAC/B,CAAC;CACF"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Pattern: Strategy
3
+ * Problem: Failure detection logic differs between consecutive-count and failure-rate approaches.
4
+ * Solution: Both implement FailureDetectionStrategy; the circuit breaker delegates without knowing which.
5
+ * Trade-off: One extra indirection; justified because strategies diverge significantly.
6
+ */
7
+ export interface FailureDetectionStrategy {
8
+ recordSuccess(): void;
9
+ recordFailure(): void;
10
+ shouldOpen(): boolean;
11
+ reset(): void;
12
+ }
13
+ //# sourceMappingURL=failure-detection-strategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failure-detection-strategy.d.ts","sourceRoot":"","sources":["../../src/strategies/failure-detection-strategy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,wBAAwB;IACvC,aAAa,IAAI,IAAI,CAAC;IACtB,aAAa,IAAI,IAAI,CAAC;IACtB,UAAU,IAAI,OAAO,CAAC;IACtB,KAAK,IAAI,IAAI,CAAC;CACf"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=failure-detection-strategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failure-detection-strategy.js","sourceRoot":"","sources":["../../src/strategies/failure-detection-strategy.ts"],"names":[],"mappings":""}
@@ -0,0 +1,15 @@
1
+ import type { FailureDetectionStrategy } from "./failure-detection-strategy.js";
2
+ export declare class FailureRateStrategy implements FailureDetectionStrategy {
3
+ private readonly threshold;
4
+ private readonly windowMs;
5
+ private readonly minSamples;
6
+ private readonly now;
7
+ private samples;
8
+ constructor(threshold: number, windowMs: number, minSamples: number, now: () => number);
9
+ recordSuccess(): void;
10
+ recordFailure(): void;
11
+ shouldOpen(): boolean;
12
+ reset(): void;
13
+ private prune;
14
+ }
15
+ //# sourceMappingURL=failure-rate-strategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failure-rate-strategy.d.ts","sourceRoot":"","sources":["../../src/strategies/failure-rate-strategy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAOhF,qBAAa,mBAAoB,YAAW,wBAAwB;IAIhE,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,GAAG;IANtB,OAAO,CAAC,OAAO,CAAgB;gBAGZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,MAAM;IAGpC,aAAa,IAAI,IAAI;IAKrB,aAAa,IAAI,IAAI;IAKrB,UAAU,IAAI,OAAO;IAUrB,KAAK,IAAI,IAAI;IAIb,OAAO,CAAC,KAAK;CAId"}
@@ -0,0 +1,37 @@
1
+ export class FailureRateStrategy {
2
+ threshold;
3
+ windowMs;
4
+ minSamples;
5
+ now;
6
+ samples = [];
7
+ constructor(threshold, windowMs, minSamples, now) {
8
+ this.threshold = threshold;
9
+ this.windowMs = windowMs;
10
+ this.minSamples = minSamples;
11
+ this.now = now;
12
+ }
13
+ recordSuccess() {
14
+ this.prune();
15
+ this.samples.push({ timestamp: this.now(), success: true });
16
+ }
17
+ recordFailure() {
18
+ this.prune();
19
+ this.samples.push({ timestamp: this.now(), success: false });
20
+ }
21
+ shouldOpen() {
22
+ this.prune();
23
+ if (this.samples.length < this.minSamples) {
24
+ return false;
25
+ }
26
+ const failures = this.samples.filter((s) => !s.success).length;
27
+ return failures / this.samples.length >= this.threshold;
28
+ }
29
+ reset() {
30
+ this.samples = [];
31
+ }
32
+ prune() {
33
+ const cutoff = this.now() - this.windowMs;
34
+ this.samples = this.samples.filter((s) => s.timestamp > cutoff);
35
+ }
36
+ }
37
+ //# sourceMappingURL=failure-rate-strategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failure-rate-strategy.js","sourceRoot":"","sources":["../../src/strategies/failure-rate-strategy.ts"],"names":[],"mappings":"AAOA,MAAM,OAAO,mBAAmB;IAIX;IACA;IACA;IACA;IANX,OAAO,GAAa,EAAE,CAAC;IAE/B,YACmB,SAAiB,EACjB,QAAgB,EAChB,UAAkB,EAClB,GAAiB;QAHjB,cAAS,GAAT,SAAS,CAAQ;QACjB,aAAQ,GAAR,QAAQ,CAAQ;QAChB,eAAU,GAAV,UAAU,CAAQ;QAClB,QAAG,GAAH,GAAG,CAAc;IACjC,CAAC;IAEJ,aAAa;QACX,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED,aAAa;QACX,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/D,CAAC;IAED,UAAU;QACR,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YAC1C,OAAO,KAAK,CAAC;QACf,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;QAC/D,OAAO,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,SAAS,CAAC;IAC1D,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;IACpB,CAAC;IAEO,KAAK;QACX,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC1C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,MAAM,CAAC,CAAC;IAClE,CAAC;CACF"}
@@ -0,0 +1,28 @@
1
+ export type CircuitBreakerState = "closed" | "open" | "half-open";
2
+ export type CircuitBreakerEventType = "stateChange" | "success" | "failure" | "reject";
3
+ export interface CircuitBreakerEventInfo {
4
+ state: CircuitBreakerState;
5
+ previousState?: CircuitBreakerState;
6
+ error?: Error;
7
+ timestamp: number;
8
+ }
9
+ export type CircuitBreakerEventHandler = (info: CircuitBreakerEventInfo) => void;
10
+ export interface CircuitBreakerLike {
11
+ readonly state: CircuitBreakerState;
12
+ fire<T>(fn: () => Promise<T>, fallback?: () => T | Promise<T>): Promise<T>;
13
+ wrap<TArgs extends unknown[], TResult>(fn: (...args: TArgs) => Promise<TResult>, fallback?: (...args: TArgs) => TResult | Promise<TResult>): (...args: TArgs) => Promise<TResult>;
14
+ on(event: CircuitBreakerEventType, handler: CircuitBreakerEventHandler): void;
15
+ off(event: CircuitBreakerEventType, handler: CircuitBreakerEventHandler): void;
16
+ reset(): void;
17
+ }
18
+ export interface CreateCircuitBreakerOptions {
19
+ strategy?: "consecutive" | "failure-rate";
20
+ threshold?: number;
21
+ windowMs?: number;
22
+ minSamples?: number;
23
+ resetTimeoutMs?: number;
24
+ halfOpenMax?: number;
25
+ isFailure?: (error: Error) => boolean;
26
+ now?: () => number;
27
+ }
28
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAElE,MAAM,MAAM,uBAAuB,GAC/B,aAAa,GACb,SAAS,GACT,SAAS,GACT,QAAQ,CAAC;AAEb,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,mBAAmB,CAAC;IAC3B,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,0BAA0B,GAAG,CACvC,IAAI,EAAE,uBAAuB,KAC1B,IAAI,CAAC;AAEV,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAC;IAEpC,IAAI,CAAC,CAAC,EACJ,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAC9B,OAAO,CAAC,CAAC,CAAC,CAAC;IAEd,IAAI,CAAC,KAAK,SAAS,OAAO,EAAE,EAAE,OAAO,EACnC,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,CAAC,EACxC,QAAQ,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GACxD,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAExC,EAAE,CACA,KAAK,EAAE,uBAAuB,EAC9B,OAAO,EAAE,0BAA0B,GAClC,IAAI,CAAC;IAER,GAAG,CACD,KAAK,EAAE,uBAAuB,EAC9B,OAAO,EAAE,0BAA0B,GAClC,IAAI,CAAC;IAER,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,EAAE,aAAa,GAAG,cAAc,CAAC;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@xyph3r/circuit-breaker",
3
+ "version": "0.1.0",
4
+ "description": "Wrap any async function with circuit breaker semantics — closed, open, half-open.",
5
+ "author": "xyph3r",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/faisalahmedsifat/circuit-breaker.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/faisalahmedsifat/circuit-breaker/issues"
29
+ },
30
+ "homepage": "https://github.com/faisalahmedsifat/circuit-breaker#readme",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "engines": {
35
+ "node": ">=20"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.build.json",
39
+ "typecheck": "tsc -p tsconfig.build.json --noEmit",
40
+ "test": "bun test test/*.test.ts",
41
+ "test:node": "rm -rf .test-dist && tsc -p tsconfig.test.json && node --test .test-dist/test/*.test.js",
42
+ "prepack": "npm run build",
43
+ "prepublishOnly": "npm run test:node"
44
+ },
45
+ "devDependencies": {
46
+ "typescript": "^5.6.3"
47
+ },
48
+ "keywords": [
49
+ "circuit-breaker",
50
+ "resilience",
51
+ "fault-tolerance",
52
+ "retry",
53
+ "fallback",
54
+ "state-machine",
55
+ "proxy"
56
+ ]
57
+ }