@hazeljs/resilience 0.2.0-beta.41

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 (60) hide show
  1. package/LICENSE +192 -0
  2. package/README.md +95 -0
  3. package/dist/__tests__/bulkhead-timeout.test.d.ts +2 -0
  4. package/dist/__tests__/bulkhead-timeout.test.d.ts.map +1 -0
  5. package/dist/__tests__/bulkhead-timeout.test.js +74 -0
  6. package/dist/__tests__/circuit-breaker.test.d.ts +2 -0
  7. package/dist/__tests__/circuit-breaker.test.d.ts.map +1 -0
  8. package/dist/__tests__/circuit-breaker.test.js +160 -0
  9. package/dist/__tests__/decorators.test.d.ts +2 -0
  10. package/dist/__tests__/decorators.test.d.ts.map +1 -0
  11. package/dist/__tests__/decorators.test.js +288 -0
  12. package/dist/__tests__/index.test.d.ts +2 -0
  13. package/dist/__tests__/index.test.d.ts.map +1 -0
  14. package/dist/__tests__/index.test.js +50 -0
  15. package/dist/__tests__/metrics.test.d.ts +2 -0
  16. package/dist/__tests__/metrics.test.d.ts.map +1 -0
  17. package/dist/__tests__/metrics.test.js +83 -0
  18. package/dist/__tests__/rate-limiter.test.d.ts +2 -0
  19. package/dist/__tests__/rate-limiter.test.d.ts.map +1 -0
  20. package/dist/__tests__/rate-limiter.test.js +143 -0
  21. package/dist/__tests__/retry-policy.test.d.ts +2 -0
  22. package/dist/__tests__/retry-policy.test.d.ts.map +1 -0
  23. package/dist/__tests__/retry-policy.test.js +84 -0
  24. package/dist/__tests__/sliding-window.test.d.ts +2 -0
  25. package/dist/__tests__/sliding-window.test.d.ts.map +1 -0
  26. package/dist/__tests__/sliding-window.test.js +93 -0
  27. package/dist/bulkhead/bulkhead.d.ts +34 -0
  28. package/dist/bulkhead/bulkhead.d.ts.map +1 -0
  29. package/dist/bulkhead/bulkhead.js +97 -0
  30. package/dist/circuit-breaker/circuit-breaker-registry.d.ts +38 -0
  31. package/dist/circuit-breaker/circuit-breaker-registry.d.ts.map +1 -0
  32. package/dist/circuit-breaker/circuit-breaker-registry.js +61 -0
  33. package/dist/circuit-breaker/circuit-breaker.d.ts +51 -0
  34. package/dist/circuit-breaker/circuit-breaker.d.ts.map +1 -0
  35. package/dist/circuit-breaker/circuit-breaker.js +182 -0
  36. package/dist/circuit-breaker/sliding-window.d.ts +49 -0
  37. package/dist/circuit-breaker/sliding-window.d.ts.map +1 -0
  38. package/dist/circuit-breaker/sliding-window.js +89 -0
  39. package/dist/decorators/index.d.ts +51 -0
  40. package/dist/decorators/index.d.ts.map +1 -0
  41. package/dist/decorators/index.js +133 -0
  42. package/dist/index.d.ts +18 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +55 -0
  45. package/dist/metrics/metrics-collector.d.ts +69 -0
  46. package/dist/metrics/metrics-collector.d.ts.map +1 -0
  47. package/dist/metrics/metrics-collector.js +180 -0
  48. package/dist/rate-limiter/rate-limiter.d.ts +72 -0
  49. package/dist/rate-limiter/rate-limiter.d.ts.map +1 -0
  50. package/dist/rate-limiter/rate-limiter.js +147 -0
  51. package/dist/retry/retry-policy.d.ts +19 -0
  52. package/dist/retry/retry-policy.d.ts.map +1 -0
  53. package/dist/retry/retry-policy.js +87 -0
  54. package/dist/timeout/timeout.d.ts +23 -0
  55. package/dist/timeout/timeout.d.ts.map +1 -0
  56. package/dist/timeout/timeout.js +55 -0
  57. package/dist/types/index.d.ts +135 -0
  58. package/dist/types/index.d.ts.map +1 -0
  59. package/dist/types/index.js +61 -0
  60. package/package.json +63 -0
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sliding-window.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sliding-window.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/sliding-window.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const sliding_window_1 = require("../circuit-breaker/sliding-window");
4
+ describe('CountBasedSlidingWindow', () => {
5
+ it('should record entries and compute result', () => {
6
+ const window = new sliding_window_1.CountBasedSlidingWindow(5);
7
+ window.record(true);
8
+ window.record(false);
9
+ window.record(true);
10
+ const result = window.getResult();
11
+ expect(result.totalCalls).toBe(3);
12
+ expect(result.failureCount).toBe(1);
13
+ expect(result.failureRate).toBeCloseTo(33.33, 1);
14
+ });
15
+ it('should evict oldest when exceeding size', () => {
16
+ const window = new sliding_window_1.CountBasedSlidingWindow(3);
17
+ window.record(true);
18
+ window.record(false);
19
+ window.record(false);
20
+ window.record(true); // evicts first true
21
+ const result = window.getResult();
22
+ expect(result.totalCalls).toBe(3);
23
+ expect(result.failureCount).toBe(2);
24
+ });
25
+ it('should return zeros when empty', () => {
26
+ const window = new sliding_window_1.CountBasedSlidingWindow(5);
27
+ const result = window.getResult();
28
+ expect(result.totalCalls).toBe(0);
29
+ expect(result.failureCount).toBe(0);
30
+ expect(result.failureRate).toBe(0);
31
+ });
32
+ it('reset should clear entries', () => {
33
+ const window = new sliding_window_1.CountBasedSlidingWindow(5);
34
+ window.record(true);
35
+ window.record(false);
36
+ window.reset();
37
+ const result = window.getResult();
38
+ expect(result.totalCalls).toBe(0);
39
+ expect(result.failureCount).toBe(0);
40
+ });
41
+ });
42
+ describe('TimeBasedSlidingWindow', () => {
43
+ it('should record entries and compute result', () => {
44
+ const window = new sliding_window_1.TimeBasedSlidingWindow(60000);
45
+ window.record(true);
46
+ window.record(false);
47
+ window.record(true);
48
+ const result = window.getResult();
49
+ expect(result.totalCalls).toBe(3);
50
+ expect(result.failureCount).toBe(1);
51
+ expect(result.failureRate).toBeCloseTo(33.33, 1);
52
+ });
53
+ it('should evict entries outside window', async () => {
54
+ const window = new sliding_window_1.TimeBasedSlidingWindow(50);
55
+ window.record(true);
56
+ window.record(false);
57
+ await new Promise((r) => setTimeout(r, 60));
58
+ window.record(true);
59
+ const result = window.getResult();
60
+ expect(result.totalCalls).toBe(1);
61
+ expect(result.failureCount).toBe(0);
62
+ });
63
+ it('should return zeros when empty', () => {
64
+ const window = new sliding_window_1.TimeBasedSlidingWindow(60000);
65
+ const result = window.getResult();
66
+ expect(result.totalCalls).toBe(0);
67
+ expect(result.failureCount).toBe(0);
68
+ expect(result.failureRate).toBe(0);
69
+ });
70
+ it('reset should clear entries', () => {
71
+ const window = new sliding_window_1.TimeBasedSlidingWindow(60000);
72
+ window.record(true);
73
+ window.record(false);
74
+ window.reset();
75
+ const result = window.getResult();
76
+ expect(result.totalCalls).toBe(0);
77
+ expect(result.failureCount).toBe(0);
78
+ });
79
+ });
80
+ describe('createSlidingWindow', () => {
81
+ it('should create CountBasedSlidingWindow for count type', () => {
82
+ const window = (0, sliding_window_1.createSlidingWindow)('count', 10);
83
+ expect(window).toBeInstanceOf(sliding_window_1.CountBasedSlidingWindow);
84
+ window.record(true);
85
+ expect(window.getResult().totalCalls).toBe(1);
86
+ });
87
+ it('should create TimeBasedSlidingWindow for time type', () => {
88
+ const window = (0, sliding_window_1.createSlidingWindow)('time', 5000);
89
+ expect(window).toBeInstanceOf(sliding_window_1.TimeBasedSlidingWindow);
90
+ window.record(true);
91
+ expect(window.getResult().totalCalls).toBe(1);
92
+ });
93
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Bulkhead
3
+ * Concurrency limiter that isolates failures by limiting
4
+ * the number of concurrent calls and queue depth.
5
+ */
6
+ import { BulkheadConfig, BulkheadMetrics } from '../types';
7
+ export declare class Bulkhead {
8
+ private activeCalls;
9
+ private queue;
10
+ private rejectedCount;
11
+ private maxConcurrent;
12
+ private maxQueue;
13
+ private queueTimeout;
14
+ constructor(config: BulkheadConfig);
15
+ /**
16
+ * Execute a function within the bulkhead constraints
17
+ */
18
+ execute<T>(fn: () => Promise<T>): Promise<T>;
19
+ /**
20
+ * Get current metrics
21
+ */
22
+ getMetrics(): BulkheadMetrics;
23
+ /**
24
+ * Get the number of currently active calls
25
+ */
26
+ getActiveCalls(): number;
27
+ /**
28
+ * Get the number of calls waiting in the queue
29
+ */
30
+ getQueueLength(): number;
31
+ private acquirePermit;
32
+ private releasePermit;
33
+ }
34
+ //# sourceMappingURL=bulkhead.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bulkhead.d.ts","sourceRoot":"","sources":["../../src/bulkhead/bulkhead.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,cAAc,EAAiB,eAAe,EAAE,MAAM,UAAU,CAAC;AAQ1E,qBAAa,QAAQ;IACnB,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,YAAY,CAAS;gBAEjB,MAAM,EAAE,cAAc;IAMlC;;OAEG;IACG,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAUlD;;OAEG;IACH,UAAU,IAAI,eAAe;IAU7B;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;OAEG;IACH,cAAc,IAAI,MAAM;YAMV,aAAa;IAmC3B,OAAO,CAAC,aAAa;CAatB"}
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /**
3
+ * Bulkhead
4
+ * Concurrency limiter that isolates failures by limiting
5
+ * the number of concurrent calls and queue depth.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.Bulkhead = void 0;
9
+ const types_1 = require("../types");
10
+ class Bulkhead {
11
+ constructor(config) {
12
+ this.activeCalls = 0;
13
+ this.queue = [];
14
+ this.rejectedCount = 0;
15
+ this.maxConcurrent = config.maxConcurrent;
16
+ this.maxQueue = config.maxQueue;
17
+ this.queueTimeout = config.queueTimeout ?? 0; // 0 = no timeout
18
+ }
19
+ /**
20
+ * Execute a function within the bulkhead constraints
21
+ */
22
+ async execute(fn) {
23
+ await this.acquirePermit();
24
+ try {
25
+ return await fn();
26
+ }
27
+ finally {
28
+ this.releasePermit();
29
+ }
30
+ }
31
+ /**
32
+ * Get current metrics
33
+ */
34
+ getMetrics() {
35
+ return {
36
+ activeCalls: this.activeCalls,
37
+ queueLength: this.queue.length,
38
+ maxConcurrent: this.maxConcurrent,
39
+ maxQueue: this.maxQueue,
40
+ rejectedCount: this.rejectedCount,
41
+ };
42
+ }
43
+ /**
44
+ * Get the number of currently active calls
45
+ */
46
+ getActiveCalls() {
47
+ return this.activeCalls;
48
+ }
49
+ /**
50
+ * Get the number of calls waiting in the queue
51
+ */
52
+ getQueueLength() {
53
+ return this.queue.length;
54
+ }
55
+ // ─── Internal ───
56
+ async acquirePermit() {
57
+ // If there's capacity, immediately grant
58
+ if (this.activeCalls < this.maxConcurrent) {
59
+ this.activeCalls++;
60
+ return;
61
+ }
62
+ // If the queue is full, reject immediately
63
+ if (this.queue.length >= this.maxQueue) {
64
+ this.rejectedCount++;
65
+ throw new types_1.BulkheadError(`Bulkhead capacity exceeded: ${this.activeCalls}/${this.maxConcurrent} active, ${this.queue.length}/${this.maxQueue} queued`);
66
+ }
67
+ // Enqueue and wait
68
+ return new Promise((resolve, reject) => {
69
+ const queuedCall = { resolve, reject };
70
+ // Optional queue timeout
71
+ if (this.queueTimeout > 0) {
72
+ queuedCall.timeoutId = setTimeout(() => {
73
+ const idx = this.queue.indexOf(queuedCall);
74
+ if (idx !== -1) {
75
+ this.queue.splice(idx, 1);
76
+ this.rejectedCount++;
77
+ reject(new types_1.BulkheadError(`Bulkhead queue timeout after ${this.queueTimeout}ms`));
78
+ }
79
+ }, this.queueTimeout);
80
+ }
81
+ this.queue.push(queuedCall);
82
+ });
83
+ }
84
+ releasePermit() {
85
+ this.activeCalls--;
86
+ // If there are queued calls, dequeue one
87
+ if (this.queue.length > 0) {
88
+ const next = this.queue.shift();
89
+ if (next.timeoutId) {
90
+ clearTimeout(next.timeoutId);
91
+ }
92
+ this.activeCalls++;
93
+ next.resolve();
94
+ }
95
+ }
96
+ }
97
+ exports.Bulkhead = Bulkhead;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Circuit Breaker Registry
3
+ * Global registry for managing named circuit breaker instances
4
+ */
5
+ import { CircuitBreaker } from './circuit-breaker';
6
+ import { CircuitBreakerConfig } from '../types';
7
+ export declare class CircuitBreakerRegistry {
8
+ private static breakers;
9
+ /**
10
+ * Get or create a circuit breaker by name
11
+ */
12
+ static getOrCreate(name: string, config?: Partial<CircuitBreakerConfig>): CircuitBreaker;
13
+ /**
14
+ * Get an existing circuit breaker
15
+ */
16
+ static get(name: string): CircuitBreaker | undefined;
17
+ /**
18
+ * Register a circuit breaker instance
19
+ */
20
+ static register(name: string, breaker: CircuitBreaker): void;
21
+ /**
22
+ * Remove a circuit breaker
23
+ */
24
+ static remove(name: string): boolean;
25
+ /**
26
+ * Get all registered breakers
27
+ */
28
+ static getAll(): Map<string, CircuitBreaker>;
29
+ /**
30
+ * Reset all circuit breakers
31
+ */
32
+ static resetAll(): void;
33
+ /**
34
+ * Clear the registry
35
+ */
36
+ static clear(): void;
37
+ }
38
+ //# sourceMappingURL=circuit-breaker-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-breaker-registry.d.ts","sourceRoot":"","sources":["../../src/circuit-breaker/circuit-breaker-registry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAEhD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAqC;IAE5D;;OAEG;IACH,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,GAAG,cAAc;IASxF;;OAEG;IACH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAIpD;;OAEG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI;IAI5D;;OAEG;IACH,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIpC;;OAEG;IACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC;IAI5C;;OAEG;IACH,MAAM,CAAC,QAAQ,IAAI,IAAI;IAMvB;;OAEG;IACH,MAAM,CAAC,KAAK,IAAI,IAAI;CAGrB"}
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ /**
3
+ * Circuit Breaker Registry
4
+ * Global registry for managing named circuit breaker instances
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.CircuitBreakerRegistry = void 0;
8
+ const circuit_breaker_1 = require("./circuit-breaker");
9
+ class CircuitBreakerRegistry {
10
+ /**
11
+ * Get or create a circuit breaker by name
12
+ */
13
+ static getOrCreate(name, config) {
14
+ let breaker = this.breakers.get(name);
15
+ if (!breaker) {
16
+ breaker = new circuit_breaker_1.CircuitBreaker(config);
17
+ this.breakers.set(name, breaker);
18
+ }
19
+ return breaker;
20
+ }
21
+ /**
22
+ * Get an existing circuit breaker
23
+ */
24
+ static get(name) {
25
+ return this.breakers.get(name);
26
+ }
27
+ /**
28
+ * Register a circuit breaker instance
29
+ */
30
+ static register(name, breaker) {
31
+ this.breakers.set(name, breaker);
32
+ }
33
+ /**
34
+ * Remove a circuit breaker
35
+ */
36
+ static remove(name) {
37
+ return this.breakers.delete(name);
38
+ }
39
+ /**
40
+ * Get all registered breakers
41
+ */
42
+ static getAll() {
43
+ return new Map(this.breakers);
44
+ }
45
+ /**
46
+ * Reset all circuit breakers
47
+ */
48
+ static resetAll() {
49
+ for (const breaker of this.breakers.values()) {
50
+ breaker.reset();
51
+ }
52
+ }
53
+ /**
54
+ * Clear the registry
55
+ */
56
+ static clear() {
57
+ this.breakers.clear();
58
+ }
59
+ }
60
+ exports.CircuitBreakerRegistry = CircuitBreakerRegistry;
61
+ CircuitBreakerRegistry.breakers = new Map();
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Circuit Breaker
3
+ * Prevents cascading failures by stopping calls to failing services.
4
+ *
5
+ * States:
6
+ * CLOSED -> normal operation, calls pass through
7
+ * OPEN -> calls are rejected immediately
8
+ * HALF_OPEN -> a limited number of trial calls are allowed through
9
+ *
10
+ * Enhanced with sliding window metrics, failure predicates,
11
+ * event emitter, and fallback support.
12
+ */
13
+ import { EventEmitter } from 'events';
14
+ import { CircuitState, CircuitBreakerConfig, CircuitBreakerMetrics } from '../types';
15
+ import { MetricsCollector } from '../metrics/metrics-collector';
16
+ export declare class CircuitBreaker extends EventEmitter {
17
+ private state;
18
+ private nextAttempt;
19
+ private halfOpenSuccessCount;
20
+ private slidingWindow;
21
+ private metrics;
22
+ private config;
23
+ constructor(config?: Partial<CircuitBreakerConfig>);
24
+ /**
25
+ * Execute a function with circuit breaker protection
26
+ */
27
+ execute<T>(fn: () => Promise<T>): Promise<T>;
28
+ getState(): CircuitState;
29
+ isOpen(): boolean;
30
+ isClosed(): boolean;
31
+ isHalfOpen(): boolean;
32
+ getMetrics(): CircuitBreakerMetrics;
33
+ /**
34
+ * Get the failure count within the current sliding window
35
+ */
36
+ getFailureCount(): number;
37
+ /**
38
+ * Get the success count within the current sliding window
39
+ */
40
+ getSuccessCount(): number;
41
+ getMetricsCollector(): MetricsCollector;
42
+ getTimeUntilNextAttempt(): number;
43
+ /**
44
+ * Manually reset the circuit to CLOSED
45
+ */
46
+ reset(): void;
47
+ private onSuccess;
48
+ private onFailure;
49
+ private transitionTo;
50
+ }
51
+ //# sourceMappingURL=circuit-breaker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../../src/circuit-breaker/circuit-breaker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EACL,YAAY,EACZ,oBAAoB,EAEpB,qBAAqB,EAEtB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAmBhE,qBAAa,cAAe,SAAQ,YAAY;IAC9C,OAAO,CAAC,KAAK,CAAqC;IAClD,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,oBAAoB,CAAK;IACjC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,MAAM,CAAwB;gBAE1B,MAAM,GAAE,OAAO,CAAC,oBAAoB,CAAM;IAStD;;OAEG;IACG,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IA8ClD,QAAQ,IAAI,YAAY;IAIxB,MAAM,IAAI,OAAO;IAIjB,QAAQ,IAAI,OAAO;IAInB,UAAU,IAAI,OAAO;IAIrB,UAAU,IAAI,qBAAqB;IAenC;;OAEG;IACH,eAAe,IAAI,MAAM;IAIzB;;OAEG;IACH,eAAe,IAAI,MAAM;IAKzB,mBAAmB,IAAI,gBAAgB;IAIvC,uBAAuB,IAAI,MAAM;IAKjC;;OAEG;IACH,KAAK,IAAI,IAAI;IAQb,OAAO,CAAC,SAAS;IAajB,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,YAAY;CAkBrB"}
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ /**
3
+ * Circuit Breaker
4
+ * Prevents cascading failures by stopping calls to failing services.
5
+ *
6
+ * States:
7
+ * CLOSED -> normal operation, calls pass through
8
+ * OPEN -> calls are rejected immediately
9
+ * HALF_OPEN -> a limited number of trial calls are allowed through
10
+ *
11
+ * Enhanced with sliding window metrics, failure predicates,
12
+ * event emitter, and fallback support.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.CircuitBreaker = void 0;
16
+ const events_1 = require("events");
17
+ const types_1 = require("../types");
18
+ const sliding_window_1 = require("./sliding-window");
19
+ const metrics_collector_1 = require("../metrics/metrics-collector");
20
+ const DEFAULT_CONFIG = {
21
+ failureThreshold: 5,
22
+ successThreshold: 2,
23
+ timeout: 60000,
24
+ resetTimeout: 30000,
25
+ slidingWindow: { type: 'count', size: 20 },
26
+ fallback: undefined,
27
+ onStateChange: undefined,
28
+ failurePredicate: undefined,
29
+ };
30
+ class CircuitBreaker extends events_1.EventEmitter {
31
+ constructor(config = {}) {
32
+ super();
33
+ this.state = types_1.CircuitState.CLOSED;
34
+ this.nextAttempt = 0;
35
+ this.halfOpenSuccessCount = 0;
36
+ this.config = { ...DEFAULT_CONFIG, ...config };
37
+ const swCfg = this.config.slidingWindow;
38
+ this.slidingWindow = (0, sliding_window_1.createSlidingWindow)(swCfg.type, swCfg.size);
39
+ this.metrics = new metrics_collector_1.MetricsCollector(swCfg.type === 'time' ? swCfg.size : 60000);
40
+ }
41
+ /**
42
+ * Execute a function with circuit breaker protection
43
+ */
44
+ async execute(fn) {
45
+ // If OPEN, check if it's time to try again
46
+ if (this.state === types_1.CircuitState.OPEN) {
47
+ if (Date.now() < this.nextAttempt) {
48
+ throw new types_1.CircuitBreakerError('Circuit breaker is OPEN - service unavailable', this.state);
49
+ }
50
+ this.transitionTo(types_1.CircuitState.HALF_OPEN);
51
+ }
52
+ const startTime = Date.now();
53
+ let timeoutId;
54
+ try {
55
+ const timeoutPromise = new Promise((_, reject) => {
56
+ timeoutId = setTimeout(() => reject(new types_1.TimeoutError('Circuit breaker timeout')), this.config.timeout);
57
+ });
58
+ const result = await Promise.race([fn(), timeoutPromise]);
59
+ const duration = Date.now() - startTime;
60
+ this.onSuccess(duration);
61
+ return result;
62
+ }
63
+ catch (error) {
64
+ const duration = Date.now() - startTime;
65
+ // Check if this error should count as a failure
66
+ if (this.config.failurePredicate && !this.config.failurePredicate(error)) {
67
+ // Error does not count as failure — still record as success for metrics
68
+ this.metrics.recordSuccess(duration);
69
+ throw error;
70
+ }
71
+ this.onFailure(duration, error);
72
+ throw error;
73
+ }
74
+ finally {
75
+ if (timeoutId) {
76
+ clearTimeout(timeoutId);
77
+ }
78
+ }
79
+ }
80
+ // ─── State Queries ───
81
+ getState() {
82
+ return this.state;
83
+ }
84
+ isOpen() {
85
+ return this.state === types_1.CircuitState.OPEN;
86
+ }
87
+ isClosed() {
88
+ return this.state === types_1.CircuitState.CLOSED;
89
+ }
90
+ isHalfOpen() {
91
+ return this.state === types_1.CircuitState.HALF_OPEN;
92
+ }
93
+ getMetrics() {
94
+ const snapshot = this.metrics.getSnapshot();
95
+ return {
96
+ totalRequests: snapshot.totalCalls,
97
+ successCount: snapshot.successCalls,
98
+ failureCount: snapshot.failureCalls,
99
+ failureRate: snapshot.failureRate,
100
+ state: this.state,
101
+ lastFailureTime: snapshot.failureCalls > 0 ? snapshot.lastCallTime : undefined,
102
+ lastSuccessTime: snapshot.successCalls > 0 ? snapshot.lastCallTime : undefined,
103
+ averageResponseTime: snapshot.averageResponseTime,
104
+ p99ResponseTime: snapshot.p99ResponseTime,
105
+ };
106
+ }
107
+ /**
108
+ * Get the failure count within the current sliding window
109
+ */
110
+ getFailureCount() {
111
+ return this.slidingWindow.getResult().failureCount;
112
+ }
113
+ /**
114
+ * Get the success count within the current sliding window
115
+ */
116
+ getSuccessCount() {
117
+ const result = this.slidingWindow.getResult();
118
+ return result.totalCalls - result.failureCount;
119
+ }
120
+ getMetricsCollector() {
121
+ return this.metrics;
122
+ }
123
+ getTimeUntilNextAttempt() {
124
+ if (this.state !== types_1.CircuitState.OPEN)
125
+ return 0;
126
+ return Math.max(0, this.nextAttempt - Date.now());
127
+ }
128
+ /**
129
+ * Manually reset the circuit to CLOSED
130
+ */
131
+ reset() {
132
+ this.transitionTo(types_1.CircuitState.CLOSED);
133
+ this.slidingWindow.reset();
134
+ this.metrics.reset();
135
+ }
136
+ // ─── Internal ───
137
+ onSuccess(duration) {
138
+ this.slidingWindow.record(true);
139
+ this.metrics.recordSuccess(duration);
140
+ this.emit('success', { duration });
141
+ if (this.state === types_1.CircuitState.HALF_OPEN) {
142
+ this.halfOpenSuccessCount++;
143
+ if (this.halfOpenSuccessCount >= this.config.successThreshold) {
144
+ this.transitionTo(types_1.CircuitState.CLOSED);
145
+ }
146
+ }
147
+ }
148
+ onFailure(duration, error) {
149
+ this.slidingWindow.record(false);
150
+ this.metrics.recordFailure(duration, String(error));
151
+ this.emit('failure', { duration, error });
152
+ if (this.state === types_1.CircuitState.HALF_OPEN) {
153
+ // Any failure in HALF_OPEN immediately opens the circuit
154
+ this.transitionTo(types_1.CircuitState.OPEN);
155
+ return;
156
+ }
157
+ // Check sliding window failure rate / count
158
+ const result = this.slidingWindow.getResult();
159
+ if (result.failureCount >= this.config.failureThreshold) {
160
+ this.transitionTo(types_1.CircuitState.OPEN);
161
+ }
162
+ }
163
+ transitionTo(newState) {
164
+ if (this.state === newState)
165
+ return;
166
+ const previousState = this.state;
167
+ this.state = newState;
168
+ if (newState === types_1.CircuitState.OPEN) {
169
+ this.nextAttempt = Date.now() + this.config.resetTimeout;
170
+ }
171
+ else if (newState === types_1.CircuitState.CLOSED) {
172
+ this.halfOpenSuccessCount = 0;
173
+ this.slidingWindow.reset();
174
+ }
175
+ else if (newState === types_1.CircuitState.HALF_OPEN) {
176
+ this.halfOpenSuccessCount = 0;
177
+ }
178
+ this.emit('stateChange', previousState, newState);
179
+ this.config.onStateChange?.(previousState, newState);
180
+ }
181
+ }
182
+ exports.CircuitBreaker = CircuitBreaker;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Sliding Window implementations for Circuit Breaker
3
+ *
4
+ * Count-based: tracks the last N calls
5
+ * Time-based: tracks calls within a rolling time window
6
+ */
7
+ export interface SlidingWindowResult {
8
+ totalCalls: number;
9
+ failureCount: number;
10
+ failureRate: number;
11
+ }
12
+ /**
13
+ * Base sliding window interface
14
+ */
15
+ export interface SlidingWindow {
16
+ record(success: boolean): void;
17
+ getResult(): SlidingWindowResult;
18
+ reset(): void;
19
+ }
20
+ /**
21
+ * Count-based sliding window
22
+ * Tracks the last `size` calls regardless of time
23
+ */
24
+ export declare class CountBasedSlidingWindow implements SlidingWindow {
25
+ private readonly size;
26
+ private entries;
27
+ constructor(size: number);
28
+ record(success: boolean): void;
29
+ getResult(): SlidingWindowResult;
30
+ reset(): void;
31
+ }
32
+ /**
33
+ * Time-based sliding window
34
+ * Tracks calls within a rolling time window of `sizeMs` milliseconds
35
+ */
36
+ export declare class TimeBasedSlidingWindow implements SlidingWindow {
37
+ private readonly sizeMs;
38
+ private entries;
39
+ constructor(sizeMs: number);
40
+ record(success: boolean): void;
41
+ getResult(): SlidingWindowResult;
42
+ reset(): void;
43
+ private evict;
44
+ }
45
+ /**
46
+ * Factory to create the appropriate sliding window
47
+ */
48
+ export declare function createSlidingWindow(type: 'count' | 'time', size: number): SlidingWindow;
49
+ //# sourceMappingURL=sliding-window.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sliding-window.d.ts","sourceRoot":"","sources":["../../src/circuit-breaker/sliding-window.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,SAAS,IAAI,mBAAmB,CAAC;IACjC,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;GAGG;AACH,qBAAa,uBAAwB,YAAW,aAAa;IAG/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,OAAO,CAAiB;gBAEH,IAAI,EAAE,MAAM;IAEzC,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAO9B,SAAS,IAAI,mBAAmB;IAahC,KAAK,IAAI,IAAI;CAGd;AAED;;;GAGG;AACH,qBAAa,sBAAuB,YAAW,aAAa;IAG9C,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,OAAO,CAAC,OAAO,CAAqB;gBAEP,MAAM,EAAE,MAAM;IAE3C,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAK9B,SAAS,IAAI,mBAAmB;IAchC,KAAK,IAAI,IAAI;IAIb,OAAO,CAAC,KAAK;CAUd;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAEvF"}