forked from Smartdevs17/SubTrackr
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcircuitBreaker.ts
More file actions
109 lines (93 loc) · 3.12 KB
/
Copy pathcircuitBreaker.ts
File metadata and controls
109 lines (93 loc) · 3.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
export type CircuitState = 'closed' | 'open' | 'half-open';
export interface CircuitBreakerOptions {
/** Number of consecutive failures before tripping to OPEN. Default: 5 */
failureThreshold?: number;
/** Time (ms) to wait in OPEN before moving to HALF-OPEN. Default: 30000 */
recoveryTimeoutMs?: number;
/** Number of successful calls in HALF-OPEN before moving to CLOSED. Default: 2 */
successThreshold?: number;
/** A name or identifier for this circuit breaker. */
name?: string;
}
export class CircuitOpenError extends Error {
constructor(public readonly name: string, public readonly openUntil: number) {
super(`Circuit breaker "${name}" is OPEN. Recovery attempt allowed at ${new Date(openUntil).toISOString()}`);
this.name = 'CircuitOpenError';
}
}
export class CircuitBreaker {
public state: CircuitState = 'closed';
private consecutiveFailures = 0;
private consecutiveSuccesses = 0;
private openUntil: number | null = null;
private readonly failureThreshold: number;
private readonly recoveryTimeoutMs: number;
private readonly successThreshold: number;
public readonly name: string;
constructor(options: CircuitBreakerOptions = {}) {
this.failureThreshold = options.failureThreshold ?? 5;
this.recoveryTimeoutMs = options.recoveryTimeoutMs ?? 30_000;
this.successThreshold = options.successThreshold ?? 2;
this.name = options.name ?? 'default';
}
/**
* Wraps an async action with the circuit breaker logic.
*/
async execute<T>(action: () => Promise<T>): Promise<T> {
this.checkState();
if (this.state === 'open') {
throw new CircuitOpenError(this.name, this.openUntil!);
}
try {
const result = await action();
this.recordSuccess();
return result;
} catch (error) {
this.recordFailure();
throw error;
}
}
private checkState(): void {
if (this.state === 'open' && this.openUntil !== null && Date.now() >= this.openUntil) {
this.transitionTo('half-open');
}
}
private recordSuccess(): void {
this.consecutiveFailures = 0;
if (this.state === 'half-open') {
this.consecutiveSuccesses += 1;
if (this.consecutiveSuccesses >= this.successThreshold) {
this.transitionTo('closed');
}
}
}
private recordFailure(): void {
this.consecutiveSuccesses = 0;
if (this.state === 'half-open') {
this.transitionTo('open');
return;
}
this.consecutiveFailures += 1;
if (this.state === 'closed' && this.consecutiveFailures >= this.failureThreshold) {
this.transitionTo('open');
}
}
private transitionTo(newState: CircuitState): void {
this.state = newState;
if (newState === 'open') {
this.openUntil = Date.now() + this.recoveryTimeoutMs;
this.consecutiveFailures = 0;
} else if (newState === 'half-open') {
this.consecutiveSuccesses = 0;
this.openUntil = null;
} else if (newState === 'closed') {
this.consecutiveFailures = 0;
this.consecutiveSuccesses = 0;
this.openUntil = null;
}
}
// Allow manual resets
public reset(): void {
this.transitionTo('closed');
}
}