kinetex 0.0.2 → 1.0.0-rc.2
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 +16 -4
- package/README.md +2434 -701
- package/dist/browser/kinetex.esm.js +86 -0
- package/dist/browser/kinetex.js +19056 -1999
- package/dist/browser/kinetex.min.js +85 -8
- package/dist/cjs/aws-sigv4.js +903 -0
- package/dist/cjs/cache.js +1099 -0
- package/dist/cjs/circuit-breaker.js +362 -0
- package/dist/cjs/client.js +2890 -0
- package/dist/cjs/cookie-parser.js +2182 -0
- package/dist/cjs/cookie-store.js +783 -0
- package/dist/cjs/cookiejar.js +175 -0
- package/dist/cjs/core.js +1267 -0
- package/dist/cjs/dedup.js +267 -0
- package/dist/cjs/digest.js +552 -0
- package/dist/cjs/graphql.js +1086 -0
- package/dist/cjs/headers.js +2264 -0
- package/dist/cjs/interceptors.js +1115 -0
- package/dist/cjs/lifecycle.js +886 -0
- package/dist/cjs/logging.js +887 -0
- package/dist/cjs/mod.js +98 -0
- package/dist/cjs/package.json +1 -3
- package/dist/cjs/pagination.js +535 -0
- package/dist/cjs/progress.js +694 -0
- package/dist/cjs/response.js +1212 -0
- package/dist/cjs/socks5.js +681 -0
- package/dist/cjs/sse.js +827 -0
- package/dist/cjs/types.js +217 -0
- package/dist/cjs/url.js +1269 -0
- package/dist/cjs/utils.js +901 -0
- package/dist/cjs/worker.js +56 -0
- package/dist/cjs/ws.js +1099 -0
- package/dist/esm/aws-sigv4.js +904 -0
- package/dist/esm/aws-sigv4.js.map +1 -0
- package/dist/esm/cache.js +1100 -0
- package/dist/esm/cache.js.map +1 -0
- package/dist/esm/circuit-breaker.js +363 -0
- package/dist/esm/circuit-breaker.js.map +1 -0
- package/dist/esm/client.js +2891 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/cookie-parser.js +2183 -0
- package/dist/esm/cookie-parser.js.map +1 -0
- package/dist/esm/cookie-store.js +784 -0
- package/dist/esm/cookie-store.js.map +1 -0
- package/dist/esm/cookiejar.js +176 -0
- package/dist/esm/cookiejar.js.map +1 -0
- package/dist/esm/core.js +1268 -0
- package/dist/esm/core.js.map +1 -0
- package/dist/esm/dedup.js +268 -0
- package/dist/esm/dedup.js.map +1 -0
- package/dist/esm/digest.js +553 -0
- package/dist/esm/digest.js.map +1 -0
- package/dist/esm/graphql.js +1087 -0
- package/dist/esm/graphql.js.map +1 -0
- package/dist/esm/headers.js +2265 -0
- package/dist/esm/headers.js.map +1 -0
- package/dist/esm/interceptors.js +1116 -0
- package/dist/esm/interceptors.js.map +1 -0
- package/dist/esm/lifecycle.js +887 -0
- package/dist/esm/lifecycle.js.map +1 -0
- package/dist/esm/logging.js +888 -0
- package/dist/esm/logging.js.map +1 -0
- package/dist/esm/mod.js +99 -0
- package/dist/esm/mod.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/pagination.js +536 -0
- package/dist/esm/pagination.js.map +1 -0
- package/dist/esm/progress.js +695 -0
- package/dist/esm/progress.js.map +1 -0
- package/dist/esm/response.js +1213 -0
- package/dist/esm/response.js.map +1 -0
- package/dist/esm/socks5.js +682 -0
- package/dist/esm/socks5.js.map +1 -0
- package/dist/esm/sse.js +828 -0
- package/dist/esm/sse.js.map +1 -0
- package/dist/esm/types.js +218 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/url.js +1270 -0
- package/dist/esm/url.js.map +1 -0
- package/dist/esm/utils.js +902 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/esm/worker.js +57 -0
- package/dist/esm/worker.js.map +1 -0
- package/dist/esm/ws.js +1100 -0
- package/dist/esm/ws.js.map +1 -0
- package/dist/types/aws-sigv4.d.ts +382 -0
- package/dist/types/aws-sigv4.d.ts.map +1 -0
- package/dist/types/cache.d.ts +397 -0
- package/dist/types/cache.d.ts.map +1 -0
- package/dist/types/circuit-breaker.d.ts +241 -0
- package/dist/types/circuit-breaker.d.ts.map +1 -0
- package/dist/types/client.d.ts +616 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/cookie-parser.d.ts +203 -0
- package/dist/types/cookie-parser.d.ts.map +1 -0
- package/dist/types/cookie-store.d.ts +316 -0
- package/dist/types/cookie-store.d.ts.map +1 -0
- package/dist/types/cookiejar.d.ts +165 -0
- package/dist/types/cookiejar.d.ts.map +1 -0
- package/dist/types/core.d.ts +297 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/types/dedup.d.ts +164 -0
- package/dist/types/dedup.d.ts.map +1 -0
- package/dist/types/digest.d.ts +136 -0
- package/dist/types/digest.d.ts.map +1 -0
- package/dist/types/graphql.d.ts +530 -0
- package/dist/types/graphql.d.ts.map +1 -0
- package/dist/types/headers.d.ts +1119 -0
- package/dist/types/headers.d.ts.map +1 -0
- package/dist/types/interceptors.d.ts +681 -0
- package/dist/types/interceptors.d.ts.map +1 -0
- package/dist/types/lifecycle.d.ts +553 -0
- package/dist/types/lifecycle.d.ts.map +1 -0
- package/dist/types/logging.d.ts +518 -0
- package/dist/types/logging.d.ts.map +1 -0
- package/dist/types/mod.d.ts +92 -0
- package/dist/types/mod.d.ts.map +1 -0
- package/dist/types/pagination.d.ts +401 -0
- package/dist/types/pagination.d.ts.map +1 -0
- package/dist/types/progress.d.ts +344 -0
- package/dist/types/progress.d.ts.map +1 -0
- package/dist/types/response.d.ts +484 -0
- package/dist/types/response.d.ts.map +1 -0
- package/dist/types/socks5.d.ts +218 -0
- package/dist/types/socks5.d.ts.map +1 -0
- package/dist/types/sse.d.ts +399 -0
- package/dist/types/sse.d.ts.map +1 -0
- package/dist/types/types.d.ts +967 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/url.d.ts +530 -0
- package/dist/types/url.d.ts.map +1 -0
- package/dist/types/utils.d.ts +300 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/dist/types/worker.d.ts +55 -0
- package/dist/types/worker.d.ts.map +1 -0
- package/dist/types/ws.d.ts +480 -0
- package/dist/types/ws.d.ts.map +1 -0
- package/package.json +194 -127
- package/CHANGELOG.md +0 -50
- package/SECURITY.md +0 -31
- package/dist/browser/kinetex.cjs.js +0 -9
- package/dist/browser/kinetex.esm.min.js +0 -9
- package/dist/browser/kinetex.js.map +0 -7
- package/dist/cjs/auth/index.cjs +0 -333
- package/dist/cjs/auth/index.d.cts +0 -41
- package/dist/cjs/auth/index.d.ts.map +0 -1
- package/dist/cjs/auth/index.js.map +0 -1
- package/dist/cjs/core/chain.cjs +0 -127
- package/dist/cjs/core/chain.d.cts +0 -44
- package/dist/cjs/core/chain.d.ts.map +0 -1
- package/dist/cjs/core/chain.js.map +0 -1
- package/dist/cjs/core/transport.cjs +0 -544
- package/dist/cjs/core/transport.d.cts +0 -11
- package/dist/cjs/core/transport.d.ts.map +0 -1
- package/dist/cjs/core/transport.js.map +0 -1
- package/dist/cjs/core/types.cjs +0 -54
- package/dist/cjs/core/types.d.cts +0 -322
- package/dist/cjs/core/types.d.ts.map +0 -1
- package/dist/cjs/core/types.js.map +0 -1
- package/dist/cjs/index.cjs +0 -272
- package/dist/cjs/index.d.cts +0 -12
- package/dist/cjs/index.d.ts.map +0 -1
- package/dist/cjs/index.js.map +0 -1
- package/dist/cjs/middleware/pipeline.cjs +0 -451
- package/dist/cjs/middleware/pipeline.d.cts +0 -32
- package/dist/cjs/middleware/pipeline.d.ts.map +0 -1
- package/dist/cjs/middleware/pipeline.js.map +0 -1
- package/dist/cjs/plugins/cookies.cjs +0 -192
- package/dist/cjs/plugins/cookies.d.cts +0 -57
- package/dist/cjs/plugins/cookies.d.ts.map +0 -1
- package/dist/cjs/plugins/cookies.js.map +0 -1
- package/dist/cjs/plugins/extras.cjs +0 -316
- package/dist/cjs/plugins/extras.d.cts +0 -87
- package/dist/cjs/plugins/extras.d.ts.map +0 -1
- package/dist/cjs/plugins/extras.js.map +0 -1
- package/dist/cjs/plugins/index.cjs +0 -18
- package/dist/cjs/plugins/index.d.cts +0 -7
- package/dist/cjs/plugins/index.d.ts.map +0 -1
- package/dist/cjs/plugins/index.js.map +0 -1
- package/dist/cjs/plugins/proxy.cjs +0 -161
- package/dist/cjs/plugins/proxy.d.cts +0 -25
- package/dist/cjs/plugins/proxy.d.ts.map +0 -1
- package/dist/cjs/plugins/proxy.js.map +0 -1
- package/dist/cjs/utils/index.cjs +0 -12
- package/dist/cjs/utils/index.d.cts +0 -6
- package/dist/cjs/utils/index.d.ts.map +0 -1
- package/dist/cjs/utils/index.js.map +0 -1
- package/dist/esm/auth/index.js +0 -324
- package/dist/esm/auth/index.js.map +0 -1
- package/dist/esm/core/chain.js +0 -123
- package/dist/esm/core/chain.js.map +0 -1
- package/dist/esm/core/transport.js +0 -536
- package/dist/esm/core/transport.js.map +0 -1
- package/dist/esm/core/types.js +0 -46
- package/dist/esm/core/types.js.map +0 -1
- package/dist/esm/index.js +0 -241
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/middleware/pipeline.js +0 -438
- package/dist/esm/middleware/pipeline.js.map +0 -1
- package/dist/esm/plugins/cookies.js +0 -187
- package/dist/esm/plugins/cookies.js.map +0 -1
- package/dist/esm/plugins/extras.js +0 -306
- package/dist/esm/plugins/extras.js.map +0 -1
- package/dist/esm/plugins/index.js +0 -4
- package/dist/esm/plugins/index.js.map +0 -1
- package/dist/esm/plugins/proxy.js +0 -124
- package/dist/esm/plugins/proxy.js.map +0 -1
- package/dist/esm/utils/index.js +0 -6
- package/dist/esm/utils/index.js.map +0 -1
- package/dist/types/auth/index.d.ts +0 -41
- package/dist/types/auth/index.d.ts.map +0 -1
- package/dist/types/core/chain.d.ts +0 -44
- package/dist/types/core/chain.d.ts.map +0 -1
- package/dist/types/core/transport.d.ts +0 -11
- package/dist/types/core/transport.d.ts.map +0 -1
- package/dist/types/core/types.d.ts +0 -322
- package/dist/types/core/types.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -12
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/middleware/pipeline.d.ts +0 -32
- package/dist/types/middleware/pipeline.d.ts.map +0 -1
- package/dist/types/plugins/cookies.d.ts +0 -57
- package/dist/types/plugins/cookies.d.ts.map +0 -1
- package/dist/types/plugins/extras.d.ts +0 -87
- package/dist/types/plugins/extras.d.ts.map +0 -1
- package/dist/types/plugins/index.d.ts +0 -7
- package/dist/types/plugins/index.d.ts.map +0 -1
- package/dist/types/plugins/proxy.d.ts +0 -25
- package/dist/types/plugins/proxy.d.ts.map +0 -1
- package/dist/types/utils/index.d.ts +0 -6
- package/dist/types/utils/index.d.ts.map +0 -1
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP circuit breaker for kinetex.
|
|
3
|
+
* Implements the classic three-state machine:
|
|
4
|
+
*
|
|
5
|
+
* CLOSED ──(threshold failures)──► OPEN ──(resetTimeoutMs)──► HALF_OPEN
|
|
6
|
+
* ▲ │
|
|
7
|
+
* └──────────────────(probe succeeded)────────────────────────────┘
|
|
8
|
+
* Probe failures re-open: HALF_OPEN ──► OPEN
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Per-origin (or per-key) isolation — one breaker per service
|
|
12
|
+
* - Configurable failure threshold, success threshold, and reset window
|
|
13
|
+
* - Separate tracking for network errors vs HTTP errors vs timeouts
|
|
14
|
+
* - Sliding-window or consecutive-count failure detection
|
|
15
|
+
* - Half-open probe with configurable concurrency limit
|
|
16
|
+
* - Callbacks: onOpen, onClose, onHalfOpen, onRejected
|
|
17
|
+
* - Manual trip / reset for admin operations
|
|
18
|
+
* - State serialization for persistence / dashboards
|
|
19
|
+
* - Zero dependencies, cross-runtime (Node / Deno / Bun / Browser / Edge)
|
|
20
|
+
*/
|
|
21
|
+
/** Error thrown when a request is rejected by an open circuit. */
|
|
22
|
+
export class CircuitOpenError extends Error {
|
|
23
|
+
/** Machine-readable error code: "ECIRCUITOPEN" */
|
|
24
|
+
code = "ECIRCUITOPEN";
|
|
25
|
+
/** Snapshot of breaker state at the time of rejection */
|
|
26
|
+
state;
|
|
27
|
+
/**
|
|
28
|
+
* @param key - The circuit breaker key (typically the origin/host)
|
|
29
|
+
* @param state - Snapshot of breaker state at rejection time
|
|
30
|
+
*/
|
|
31
|
+
constructor(key, state) {
|
|
32
|
+
super(`Circuit breaker OPEN for "${key}" — request rejected`);
|
|
33
|
+
this.name = "CircuitOpenError";
|
|
34
|
+
this.state = state;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// #2 CIRCUIT BREAKER
|
|
39
|
+
// ============================================================================
|
|
40
|
+
/**
|
|
41
|
+
* Per-key circuit breaker implementing the CLOSED → OPEN → HALF_OPEN state
|
|
42
|
+
* machine with sliding-window or consecutive-count failure detection.
|
|
43
|
+
*
|
|
44
|
+
* Thread-safe for async use. Rejects with {@link CircuitOpenError} when OPEN.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const cb = new CircuitBreaker("api.example.com", { failureThreshold: 3 });
|
|
49
|
+
* const result = await cb.execute(() => fetch("https://api.example.com/data"));
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export class CircuitBreaker {
|
|
53
|
+
/** The breaker key (typically origin/host) */
|
|
54
|
+
key;
|
|
55
|
+
/** Failure count needed to open the circuit */
|
|
56
|
+
failureThreshold;
|
|
57
|
+
/** Sliding-window size (0 = consecutive-count mode) */
|
|
58
|
+
windowSize;
|
|
59
|
+
/** Duration in ms before transitioning OPEN → HALF_OPEN */
|
|
60
|
+
resetTimeoutMs;
|
|
61
|
+
/** Consecutive successes in HALF_OPEN needed to close */
|
|
62
|
+
successThreshold;
|
|
63
|
+
/** Maximum in-flight probe requests allowed in HALF_OPEN */
|
|
64
|
+
halfOpenConcurrency;
|
|
65
|
+
/** Which error categories count as failures */
|
|
66
|
+
failureFilter;
|
|
67
|
+
/** Raw config reference (for callbacks) */
|
|
68
|
+
cfg;
|
|
69
|
+
/** Current circuit state */
|
|
70
|
+
_state = "CLOSED";
|
|
71
|
+
/** Sliding window of success/failure booleans */
|
|
72
|
+
_window = [];
|
|
73
|
+
/** Consecutive failure counter (used in consecutive mode) */
|
|
74
|
+
_consecutiveFails = 0;
|
|
75
|
+
/** Consecutive success counter (used in HALF_OPEN) */
|
|
76
|
+
_consecutiveSucc = 0;
|
|
77
|
+
/** Timestamp the circuit was opened */
|
|
78
|
+
_openedAt = null;
|
|
79
|
+
/** Timestamp the circuit entered HALF_OPEN */
|
|
80
|
+
_halfOpenAt = null;
|
|
81
|
+
/** Timestamp of the last failure */
|
|
82
|
+
_lastFailureAt = null;
|
|
83
|
+
/** Timestamp of the last success */
|
|
84
|
+
_lastSuccessAt = null;
|
|
85
|
+
/** Current number of in-flight probe requests */
|
|
86
|
+
_inFlightProbes = 0;
|
|
87
|
+
// Lifetime counters
|
|
88
|
+
_totalRequests = 0;
|
|
89
|
+
_totalFailures = 0;
|
|
90
|
+
_totalSuccesses = 0;
|
|
91
|
+
_totalRejected = 0;
|
|
92
|
+
/**
|
|
93
|
+
* @param key - The breaker key (typically origin/host like "https://api.example.com")
|
|
94
|
+
* @param config - Circuit breaker configuration
|
|
95
|
+
*/
|
|
96
|
+
constructor(key, config = {}) {
|
|
97
|
+
this.key = key;
|
|
98
|
+
this.failureThreshold = config.failureThreshold ?? 5;
|
|
99
|
+
this.windowSize = config.windowSize ?? 10;
|
|
100
|
+
this.resetTimeoutMs = config.resetTimeoutMs ?? 30_000;
|
|
101
|
+
this.successThreshold = config.successThreshold ?? 2;
|
|
102
|
+
this.halfOpenConcurrency = config.halfOpenConcurrency ?? 1;
|
|
103
|
+
this.failureFilter = {
|
|
104
|
+
networkErrors: config.failures?.networkErrors ?? true,
|
|
105
|
+
timeouts: config.failures?.timeouts ?? true,
|
|
106
|
+
serverErrors: config.failures?.serverErrors ?? false,
|
|
107
|
+
statusCodes: config.failures?.statusCodes ?? [],
|
|
108
|
+
};
|
|
109
|
+
this.cfg = config;
|
|
110
|
+
}
|
|
111
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
112
|
+
/** The current circuit state. */
|
|
113
|
+
get state() {
|
|
114
|
+
return this._state;
|
|
115
|
+
}
|
|
116
|
+
/** Snapshot of all counters — suitable for logging or dashboards. */
|
|
117
|
+
get snapshot() {
|
|
118
|
+
return {
|
|
119
|
+
state: this._state,
|
|
120
|
+
failureCount: this.windowSize > 0 ? this._window.filter((v) => !v).length : this._consecutiveFails,
|
|
121
|
+
successCount: this._consecutiveSucc,
|
|
122
|
+
lastFailureAt: this._lastFailureAt,
|
|
123
|
+
lastSuccessAt: this._lastSuccessAt,
|
|
124
|
+
openedAt: this._openedAt,
|
|
125
|
+
halfOpenAt: this._halfOpenAt,
|
|
126
|
+
totalRequests: this._totalRequests,
|
|
127
|
+
totalFailures: this._totalFailures,
|
|
128
|
+
totalSuccesses: this._totalSuccesses,
|
|
129
|
+
totalRejected: this._totalRejected,
|
|
130
|
+
inFlightProbes: this._inFlightProbes,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Execute a function through the circuit breaker.
|
|
135
|
+
* Throws `CircuitOpenError` if the circuit is open.
|
|
136
|
+
* Records success/failure and transitions state accordingly.
|
|
137
|
+
*/
|
|
138
|
+
execute(fn) {
|
|
139
|
+
this._checkTransition();
|
|
140
|
+
if (this._state === "OPEN") {
|
|
141
|
+
this._totalRejected++;
|
|
142
|
+
const snap = this.snapshot;
|
|
143
|
+
this.cfg.onRejected?.(snap);
|
|
144
|
+
return Promise.reject(new CircuitOpenError(this.key, snap));
|
|
145
|
+
}
|
|
146
|
+
if (this._state === "HALF_OPEN") {
|
|
147
|
+
if (this._inFlightProbes >= this.halfOpenConcurrency) {
|
|
148
|
+
this._totalRejected++;
|
|
149
|
+
const snap = this.snapshot;
|
|
150
|
+
this.cfg.onRejected?.(snap);
|
|
151
|
+
return Promise.reject(new CircuitOpenError(this.key, snap));
|
|
152
|
+
}
|
|
153
|
+
this._inFlightProbes++;
|
|
154
|
+
}
|
|
155
|
+
this._totalRequests++;
|
|
156
|
+
return fn().then((result) => {
|
|
157
|
+
if (this._state === "HALF_OPEN")
|
|
158
|
+
this._inFlightProbes = Math.max(0, this._inFlightProbes - 1);
|
|
159
|
+
this._recordSuccess();
|
|
160
|
+
return result;
|
|
161
|
+
}, (err) => {
|
|
162
|
+
if (this._state === "HALF_OPEN")
|
|
163
|
+
this._inFlightProbes = Math.max(0, this._inFlightProbes - 1);
|
|
164
|
+
if (this._isCountableFailure(err)) {
|
|
165
|
+
this._recordFailure();
|
|
166
|
+
}
|
|
167
|
+
// Never decrement _totalRequests — keep a faithful count of all attempts,
|
|
168
|
+
// including non-countable failures (e.g. app-level errors we don't treat as outages).
|
|
169
|
+
throw err;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Manually trip the circuit open — useful for maintenance windows.
|
|
174
|
+
*/
|
|
175
|
+
trip() {
|
|
176
|
+
if (this._state !== "OPEN") {
|
|
177
|
+
this._openedAt = Date.now();
|
|
178
|
+
this._state = "OPEN";
|
|
179
|
+
this.cfg.onOpen?.(this.snapshot);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Manually reset the circuit to CLOSED — useful after a fix deployment.
|
|
184
|
+
*/
|
|
185
|
+
reset() {
|
|
186
|
+
this._state = "CLOSED";
|
|
187
|
+
this._window = [];
|
|
188
|
+
this._consecutiveFails = 0;
|
|
189
|
+
this._consecutiveSucc = 0;
|
|
190
|
+
this._openedAt = null;
|
|
191
|
+
this._halfOpenAt = null;
|
|
192
|
+
this._inFlightProbes = 0;
|
|
193
|
+
this.cfg.onClose?.(this.snapshot);
|
|
194
|
+
}
|
|
195
|
+
// ── Private ─────────────────────────────────────────────────────────────
|
|
196
|
+
/** Check if the OPEN state should transition to HALF_OPEN based on resetTimeoutMs. */
|
|
197
|
+
_checkTransition() {
|
|
198
|
+
if (this._state === "OPEN" && this._openedAt !== null) {
|
|
199
|
+
if (Date.now() - this._openedAt >= this.resetTimeoutMs) {
|
|
200
|
+
this._state = "HALF_OPEN";
|
|
201
|
+
this._halfOpenAt = Date.now();
|
|
202
|
+
this._consecutiveSucc = 0;
|
|
203
|
+
this._inFlightProbes = 0;
|
|
204
|
+
this.cfg.onHalfOpen?.(this.snapshot);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Record a successful call and potentially transition HALF_OPEN → CLOSED. */
|
|
209
|
+
_recordSuccess() {
|
|
210
|
+
this._lastSuccessAt = Date.now();
|
|
211
|
+
this._totalSuccesses++;
|
|
212
|
+
this._consecutiveFails = 0;
|
|
213
|
+
if (this._state === "HALF_OPEN") {
|
|
214
|
+
this._consecutiveSucc++;
|
|
215
|
+
if (this._consecutiveSucc >= this.successThreshold) {
|
|
216
|
+
this.reset();
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// CLOSED state — add success to sliding window
|
|
221
|
+
if (this.windowSize > 0) {
|
|
222
|
+
this._window.push(true);
|
|
223
|
+
if (this._window.length > this.windowSize)
|
|
224
|
+
this._window.shift();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** Record a failure and potentially transition CLOSED → OPEN or HALF_OPEN → OPEN. */
|
|
228
|
+
_recordFailure() {
|
|
229
|
+
this._lastFailureAt = Date.now();
|
|
230
|
+
this._totalFailures++;
|
|
231
|
+
this._consecutiveSucc = 0;
|
|
232
|
+
this._consecutiveFails++;
|
|
233
|
+
if (this._state === "HALF_OPEN") {
|
|
234
|
+
// Single failure in HALF_OPEN re-opens immediately
|
|
235
|
+
this._openedAt = Date.now();
|
|
236
|
+
this._state = "OPEN";
|
|
237
|
+
this.cfg.onOpen?.(this.snapshot);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// CLOSED state
|
|
241
|
+
if (this.windowSize > 0) {
|
|
242
|
+
this._window.push(false);
|
|
243
|
+
if (this._window.length > this.windowSize)
|
|
244
|
+
this._window.shift();
|
|
245
|
+
const failures = this._window.filter((v) => !v).length;
|
|
246
|
+
if (failures >= this.failureThreshold) {
|
|
247
|
+
this._openedAt = Date.now();
|
|
248
|
+
this._state = "OPEN";
|
|
249
|
+
this.cfg.onOpen?.(this.snapshot);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
// Consecutive mode
|
|
254
|
+
if (this._consecutiveFails >= this.failureThreshold) {
|
|
255
|
+
this._openedAt = Date.now();
|
|
256
|
+
this._state = "OPEN";
|
|
257
|
+
this.cfg.onOpen?.(this.snapshot);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Determine whether an error should count toward the failure threshold based on the configured filter. */
|
|
262
|
+
_isCountableFailure(err) {
|
|
263
|
+
if (!(err instanceof Error))
|
|
264
|
+
return false;
|
|
265
|
+
const code = err.code;
|
|
266
|
+
if (this.failureFilter.networkErrors && code === "ENETWORK")
|
|
267
|
+
return true;
|
|
268
|
+
if (this.failureFilter.timeouts && code === "ETIMEOUT")
|
|
269
|
+
return true;
|
|
270
|
+
if (this.failureFilter.serverErrors) {
|
|
271
|
+
const status = err.status;
|
|
272
|
+
if (status && status >= 500)
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
if (this.failureFilter.statusCodes && this.failureFilter.statusCodes.length > 0) {
|
|
276
|
+
const status = err.status;
|
|
277
|
+
if (status && this.failureFilter.statusCodes.includes(status))
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// #3 CIRCUIT BREAKER REGISTRY
|
|
285
|
+
// ============================================================================
|
|
286
|
+
/**
|
|
287
|
+
* Registry that manages one `CircuitBreaker` per key (typically per origin/host).
|
|
288
|
+
* Automatically creates breakers on first use.
|
|
289
|
+
*/
|
|
290
|
+
export class CircuitBreakerRegistry {
|
|
291
|
+
/** Map of key → CircuitBreaker instances */
|
|
292
|
+
breakers = new Map();
|
|
293
|
+
/** Default config applied to all new breakers */
|
|
294
|
+
defaultConfig;
|
|
295
|
+
/**
|
|
296
|
+
* @param defaultConfig - Default configuration inherited by all breakers
|
|
297
|
+
*/
|
|
298
|
+
constructor(defaultConfig = {}) {
|
|
299
|
+
this.defaultConfig = defaultConfig;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Get or create a circuit breaker for `key`.
|
|
303
|
+
* `key` is typically the request origin, e.g. `"https://api.example.com"`.
|
|
304
|
+
*/
|
|
305
|
+
get(key, overrides) {
|
|
306
|
+
if (!this.breakers.has(key)) {
|
|
307
|
+
this.breakers.set(key, new CircuitBreaker(key, { ...this.defaultConfig, ...overrides }));
|
|
308
|
+
}
|
|
309
|
+
return this.breakers.get(key);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Wrap a function call through the breaker for `key`.
|
|
313
|
+
* Creates the breaker if it doesn't exist.
|
|
314
|
+
*/
|
|
315
|
+
execute(key, fn, overrides) {
|
|
316
|
+
return this.get(key, overrides).execute(fn);
|
|
317
|
+
}
|
|
318
|
+
/** Manually trip the breaker for `key` (creates it if it doesn't exist). */
|
|
319
|
+
trip(key) {
|
|
320
|
+
this.get(key).trip();
|
|
321
|
+
}
|
|
322
|
+
/** Manually reset the breaker for `key` (creates it if it doesn't exist). */
|
|
323
|
+
reset(key) {
|
|
324
|
+
this.get(key).reset();
|
|
325
|
+
}
|
|
326
|
+
/** Snapshot all breakers — for monitoring / health endpoints. */
|
|
327
|
+
snapshots() {
|
|
328
|
+
const out = {};
|
|
329
|
+
for (const [k, b] of this.breakers)
|
|
330
|
+
out[k] = b.snapshot;
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
/** Remove a specific breaker. */
|
|
334
|
+
delete(key) {
|
|
335
|
+
this.breakers.delete(key);
|
|
336
|
+
}
|
|
337
|
+
/** Remove all breakers. */
|
|
338
|
+
clear() {
|
|
339
|
+
this.breakers.clear();
|
|
340
|
+
}
|
|
341
|
+
/** How many breakers are registered. */
|
|
342
|
+
get size() {
|
|
343
|
+
return this.breakers.size;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// ============================================================================
|
|
347
|
+
// #4 FACTORY HELPERS
|
|
348
|
+
// ============================================================================
|
|
349
|
+
/**
|
|
350
|
+
* Create a single-key circuit breaker with a convenient default config.
|
|
351
|
+
* Suitable when you manage the key externally.
|
|
352
|
+
*/
|
|
353
|
+
export function createCircuitBreaker(key, config) {
|
|
354
|
+
return new CircuitBreaker(key, config);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Create a registry for managing multiple per-origin breakers.
|
|
358
|
+
* All breakers inherit `defaultConfig`, with per-key overrides at `.get()`.
|
|
359
|
+
*/
|
|
360
|
+
export function createCircuitBreakerRegistry(defaultConfig) {
|
|
361
|
+
return new CircuitBreakerRegistry(defaultConfig);
|
|
362
|
+
}
|