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.
Files changed (231) hide show
  1. package/LICENSE +16 -4
  2. package/README.md +2434 -701
  3. package/dist/browser/kinetex.esm.js +86 -0
  4. package/dist/browser/kinetex.js +19056 -1999
  5. package/dist/browser/kinetex.min.js +85 -8
  6. package/dist/cjs/aws-sigv4.js +903 -0
  7. package/dist/cjs/cache.js +1099 -0
  8. package/dist/cjs/circuit-breaker.js +362 -0
  9. package/dist/cjs/client.js +2890 -0
  10. package/dist/cjs/cookie-parser.js +2182 -0
  11. package/dist/cjs/cookie-store.js +783 -0
  12. package/dist/cjs/cookiejar.js +175 -0
  13. package/dist/cjs/core.js +1267 -0
  14. package/dist/cjs/dedup.js +267 -0
  15. package/dist/cjs/digest.js +552 -0
  16. package/dist/cjs/graphql.js +1086 -0
  17. package/dist/cjs/headers.js +2264 -0
  18. package/dist/cjs/interceptors.js +1115 -0
  19. package/dist/cjs/lifecycle.js +886 -0
  20. package/dist/cjs/logging.js +887 -0
  21. package/dist/cjs/mod.js +98 -0
  22. package/dist/cjs/package.json +1 -3
  23. package/dist/cjs/pagination.js +535 -0
  24. package/dist/cjs/progress.js +694 -0
  25. package/dist/cjs/response.js +1212 -0
  26. package/dist/cjs/socks5.js +681 -0
  27. package/dist/cjs/sse.js +827 -0
  28. package/dist/cjs/types.js +217 -0
  29. package/dist/cjs/url.js +1269 -0
  30. package/dist/cjs/utils.js +901 -0
  31. package/dist/cjs/worker.js +56 -0
  32. package/dist/cjs/ws.js +1099 -0
  33. package/dist/esm/aws-sigv4.js +904 -0
  34. package/dist/esm/aws-sigv4.js.map +1 -0
  35. package/dist/esm/cache.js +1100 -0
  36. package/dist/esm/cache.js.map +1 -0
  37. package/dist/esm/circuit-breaker.js +363 -0
  38. package/dist/esm/circuit-breaker.js.map +1 -0
  39. package/dist/esm/client.js +2891 -0
  40. package/dist/esm/client.js.map +1 -0
  41. package/dist/esm/cookie-parser.js +2183 -0
  42. package/dist/esm/cookie-parser.js.map +1 -0
  43. package/dist/esm/cookie-store.js +784 -0
  44. package/dist/esm/cookie-store.js.map +1 -0
  45. package/dist/esm/cookiejar.js +176 -0
  46. package/dist/esm/cookiejar.js.map +1 -0
  47. package/dist/esm/core.js +1268 -0
  48. package/dist/esm/core.js.map +1 -0
  49. package/dist/esm/dedup.js +268 -0
  50. package/dist/esm/dedup.js.map +1 -0
  51. package/dist/esm/digest.js +553 -0
  52. package/dist/esm/digest.js.map +1 -0
  53. package/dist/esm/graphql.js +1087 -0
  54. package/dist/esm/graphql.js.map +1 -0
  55. package/dist/esm/headers.js +2265 -0
  56. package/dist/esm/headers.js.map +1 -0
  57. package/dist/esm/interceptors.js +1116 -0
  58. package/dist/esm/interceptors.js.map +1 -0
  59. package/dist/esm/lifecycle.js +887 -0
  60. package/dist/esm/lifecycle.js.map +1 -0
  61. package/dist/esm/logging.js +888 -0
  62. package/dist/esm/logging.js.map +1 -0
  63. package/dist/esm/mod.js +99 -0
  64. package/dist/esm/mod.js.map +1 -0
  65. package/dist/esm/package.json +1 -0
  66. package/dist/esm/pagination.js +536 -0
  67. package/dist/esm/pagination.js.map +1 -0
  68. package/dist/esm/progress.js +695 -0
  69. package/dist/esm/progress.js.map +1 -0
  70. package/dist/esm/response.js +1213 -0
  71. package/dist/esm/response.js.map +1 -0
  72. package/dist/esm/socks5.js +682 -0
  73. package/dist/esm/socks5.js.map +1 -0
  74. package/dist/esm/sse.js +828 -0
  75. package/dist/esm/sse.js.map +1 -0
  76. package/dist/esm/types.js +218 -0
  77. package/dist/esm/types.js.map +1 -0
  78. package/dist/esm/url.js +1270 -0
  79. package/dist/esm/url.js.map +1 -0
  80. package/dist/esm/utils.js +902 -0
  81. package/dist/esm/utils.js.map +1 -0
  82. package/dist/esm/worker.js +57 -0
  83. package/dist/esm/worker.js.map +1 -0
  84. package/dist/esm/ws.js +1100 -0
  85. package/dist/esm/ws.js.map +1 -0
  86. package/dist/types/aws-sigv4.d.ts +382 -0
  87. package/dist/types/aws-sigv4.d.ts.map +1 -0
  88. package/dist/types/cache.d.ts +397 -0
  89. package/dist/types/cache.d.ts.map +1 -0
  90. package/dist/types/circuit-breaker.d.ts +241 -0
  91. package/dist/types/circuit-breaker.d.ts.map +1 -0
  92. package/dist/types/client.d.ts +616 -0
  93. package/dist/types/client.d.ts.map +1 -0
  94. package/dist/types/cookie-parser.d.ts +203 -0
  95. package/dist/types/cookie-parser.d.ts.map +1 -0
  96. package/dist/types/cookie-store.d.ts +316 -0
  97. package/dist/types/cookie-store.d.ts.map +1 -0
  98. package/dist/types/cookiejar.d.ts +165 -0
  99. package/dist/types/cookiejar.d.ts.map +1 -0
  100. package/dist/types/core.d.ts +297 -0
  101. package/dist/types/core.d.ts.map +1 -0
  102. package/dist/types/dedup.d.ts +164 -0
  103. package/dist/types/dedup.d.ts.map +1 -0
  104. package/dist/types/digest.d.ts +136 -0
  105. package/dist/types/digest.d.ts.map +1 -0
  106. package/dist/types/graphql.d.ts +530 -0
  107. package/dist/types/graphql.d.ts.map +1 -0
  108. package/dist/types/headers.d.ts +1119 -0
  109. package/dist/types/headers.d.ts.map +1 -0
  110. package/dist/types/interceptors.d.ts +681 -0
  111. package/dist/types/interceptors.d.ts.map +1 -0
  112. package/dist/types/lifecycle.d.ts +553 -0
  113. package/dist/types/lifecycle.d.ts.map +1 -0
  114. package/dist/types/logging.d.ts +518 -0
  115. package/dist/types/logging.d.ts.map +1 -0
  116. package/dist/types/mod.d.ts +92 -0
  117. package/dist/types/mod.d.ts.map +1 -0
  118. package/dist/types/pagination.d.ts +401 -0
  119. package/dist/types/pagination.d.ts.map +1 -0
  120. package/dist/types/progress.d.ts +344 -0
  121. package/dist/types/progress.d.ts.map +1 -0
  122. package/dist/types/response.d.ts +484 -0
  123. package/dist/types/response.d.ts.map +1 -0
  124. package/dist/types/socks5.d.ts +218 -0
  125. package/dist/types/socks5.d.ts.map +1 -0
  126. package/dist/types/sse.d.ts +399 -0
  127. package/dist/types/sse.d.ts.map +1 -0
  128. package/dist/types/types.d.ts +967 -0
  129. package/dist/types/types.d.ts.map +1 -0
  130. package/dist/types/url.d.ts +530 -0
  131. package/dist/types/url.d.ts.map +1 -0
  132. package/dist/types/utils.d.ts +300 -0
  133. package/dist/types/utils.d.ts.map +1 -0
  134. package/dist/types/worker.d.ts +55 -0
  135. package/dist/types/worker.d.ts.map +1 -0
  136. package/dist/types/ws.d.ts +480 -0
  137. package/dist/types/ws.d.ts.map +1 -0
  138. package/package.json +194 -127
  139. package/CHANGELOG.md +0 -50
  140. package/SECURITY.md +0 -31
  141. package/dist/browser/kinetex.cjs.js +0 -9
  142. package/dist/browser/kinetex.esm.min.js +0 -9
  143. package/dist/browser/kinetex.js.map +0 -7
  144. package/dist/cjs/auth/index.cjs +0 -333
  145. package/dist/cjs/auth/index.d.cts +0 -41
  146. package/dist/cjs/auth/index.d.ts.map +0 -1
  147. package/dist/cjs/auth/index.js.map +0 -1
  148. package/dist/cjs/core/chain.cjs +0 -127
  149. package/dist/cjs/core/chain.d.cts +0 -44
  150. package/dist/cjs/core/chain.d.ts.map +0 -1
  151. package/dist/cjs/core/chain.js.map +0 -1
  152. package/dist/cjs/core/transport.cjs +0 -544
  153. package/dist/cjs/core/transport.d.cts +0 -11
  154. package/dist/cjs/core/transport.d.ts.map +0 -1
  155. package/dist/cjs/core/transport.js.map +0 -1
  156. package/dist/cjs/core/types.cjs +0 -54
  157. package/dist/cjs/core/types.d.cts +0 -322
  158. package/dist/cjs/core/types.d.ts.map +0 -1
  159. package/dist/cjs/core/types.js.map +0 -1
  160. package/dist/cjs/index.cjs +0 -272
  161. package/dist/cjs/index.d.cts +0 -12
  162. package/dist/cjs/index.d.ts.map +0 -1
  163. package/dist/cjs/index.js.map +0 -1
  164. package/dist/cjs/middleware/pipeline.cjs +0 -451
  165. package/dist/cjs/middleware/pipeline.d.cts +0 -32
  166. package/dist/cjs/middleware/pipeline.d.ts.map +0 -1
  167. package/dist/cjs/middleware/pipeline.js.map +0 -1
  168. package/dist/cjs/plugins/cookies.cjs +0 -192
  169. package/dist/cjs/plugins/cookies.d.cts +0 -57
  170. package/dist/cjs/plugins/cookies.d.ts.map +0 -1
  171. package/dist/cjs/plugins/cookies.js.map +0 -1
  172. package/dist/cjs/plugins/extras.cjs +0 -316
  173. package/dist/cjs/plugins/extras.d.cts +0 -87
  174. package/dist/cjs/plugins/extras.d.ts.map +0 -1
  175. package/dist/cjs/plugins/extras.js.map +0 -1
  176. package/dist/cjs/plugins/index.cjs +0 -18
  177. package/dist/cjs/plugins/index.d.cts +0 -7
  178. package/dist/cjs/plugins/index.d.ts.map +0 -1
  179. package/dist/cjs/plugins/index.js.map +0 -1
  180. package/dist/cjs/plugins/proxy.cjs +0 -161
  181. package/dist/cjs/plugins/proxy.d.cts +0 -25
  182. package/dist/cjs/plugins/proxy.d.ts.map +0 -1
  183. package/dist/cjs/plugins/proxy.js.map +0 -1
  184. package/dist/cjs/utils/index.cjs +0 -12
  185. package/dist/cjs/utils/index.d.cts +0 -6
  186. package/dist/cjs/utils/index.d.ts.map +0 -1
  187. package/dist/cjs/utils/index.js.map +0 -1
  188. package/dist/esm/auth/index.js +0 -324
  189. package/dist/esm/auth/index.js.map +0 -1
  190. package/dist/esm/core/chain.js +0 -123
  191. package/dist/esm/core/chain.js.map +0 -1
  192. package/dist/esm/core/transport.js +0 -536
  193. package/dist/esm/core/transport.js.map +0 -1
  194. package/dist/esm/core/types.js +0 -46
  195. package/dist/esm/core/types.js.map +0 -1
  196. package/dist/esm/index.js +0 -241
  197. package/dist/esm/index.js.map +0 -1
  198. package/dist/esm/middleware/pipeline.js +0 -438
  199. package/dist/esm/middleware/pipeline.js.map +0 -1
  200. package/dist/esm/plugins/cookies.js +0 -187
  201. package/dist/esm/plugins/cookies.js.map +0 -1
  202. package/dist/esm/plugins/extras.js +0 -306
  203. package/dist/esm/plugins/extras.js.map +0 -1
  204. package/dist/esm/plugins/index.js +0 -4
  205. package/dist/esm/plugins/index.js.map +0 -1
  206. package/dist/esm/plugins/proxy.js +0 -124
  207. package/dist/esm/plugins/proxy.js.map +0 -1
  208. package/dist/esm/utils/index.js +0 -6
  209. package/dist/esm/utils/index.js.map +0 -1
  210. package/dist/types/auth/index.d.ts +0 -41
  211. package/dist/types/auth/index.d.ts.map +0 -1
  212. package/dist/types/core/chain.d.ts +0 -44
  213. package/dist/types/core/chain.d.ts.map +0 -1
  214. package/dist/types/core/transport.d.ts +0 -11
  215. package/dist/types/core/transport.d.ts.map +0 -1
  216. package/dist/types/core/types.d.ts +0 -322
  217. package/dist/types/core/types.d.ts.map +0 -1
  218. package/dist/types/index.d.ts +0 -12
  219. package/dist/types/index.d.ts.map +0 -1
  220. package/dist/types/middleware/pipeline.d.ts +0 -32
  221. package/dist/types/middleware/pipeline.d.ts.map +0 -1
  222. package/dist/types/plugins/cookies.d.ts +0 -57
  223. package/dist/types/plugins/cookies.d.ts.map +0 -1
  224. package/dist/types/plugins/extras.d.ts +0 -87
  225. package/dist/types/plugins/extras.d.ts.map +0 -1
  226. package/dist/types/plugins/index.d.ts +0 -7
  227. package/dist/types/plugins/index.d.ts.map +0 -1
  228. package/dist/types/plugins/proxy.d.ts +0 -25
  229. package/dist/types/plugins/proxy.d.ts.map +0 -1
  230. package/dist/types/utils/index.d.ts +0 -6
  231. 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
+ }