@revealui/resilience 0.2.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.
- package/LICENSE +22 -0
- package/LICENSE.commercial +112 -0
- package/dist/index.d.ts +477 -0
- package/dist/index.js +915 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
// src/logger.ts
|
|
2
|
+
var resilienceLogger = console;
|
|
3
|
+
function configureResilienceLogger(logger) {
|
|
4
|
+
resilienceLogger = logger;
|
|
5
|
+
}
|
|
6
|
+
function getResilienceLogger() {
|
|
7
|
+
return resilienceLogger;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/circuit-breaker.ts
|
|
11
|
+
var DEFAULT_CONFIG = {
|
|
12
|
+
failureThreshold: 5,
|
|
13
|
+
successThreshold: 2,
|
|
14
|
+
timeout: 6e4,
|
|
15
|
+
resetTimeout: 3e4,
|
|
16
|
+
volumeThreshold: 10,
|
|
17
|
+
errorFilter: () => true,
|
|
18
|
+
onStateChange: () => {
|
|
19
|
+
},
|
|
20
|
+
onTrip: () => {
|
|
21
|
+
},
|
|
22
|
+
onReset: () => {
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var CircuitBreaker = class {
|
|
26
|
+
state = "closed";
|
|
27
|
+
failures = 0;
|
|
28
|
+
successes = 0;
|
|
29
|
+
consecutiveFailures = 0;
|
|
30
|
+
consecutiveSuccesses = 0;
|
|
31
|
+
totalCalls = 0;
|
|
32
|
+
totalFailures = 0;
|
|
33
|
+
totalSuccesses = 0;
|
|
34
|
+
lastFailureTime;
|
|
35
|
+
lastSuccessTime;
|
|
36
|
+
stateChangedAt = Date.now();
|
|
37
|
+
resetTimer;
|
|
38
|
+
config;
|
|
39
|
+
constructor(config = {}) {
|
|
40
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Execute function with circuit breaker
|
|
44
|
+
*/
|
|
45
|
+
async execute(fn) {
|
|
46
|
+
if (this.state === "open") {
|
|
47
|
+
if (Date.now() - this.stateChangedAt >= this.config.resetTimeout) {
|
|
48
|
+
this.transitionTo("half-open");
|
|
49
|
+
} else {
|
|
50
|
+
throw new CircuitBreakerOpenError("Circuit breaker is open");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
this.totalCalls++;
|
|
54
|
+
try {
|
|
55
|
+
const result = await fn();
|
|
56
|
+
this.onSuccess();
|
|
57
|
+
return result;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
60
|
+
this.onFailure(err);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Handle successful execution
|
|
66
|
+
*/
|
|
67
|
+
onSuccess() {
|
|
68
|
+
this.successes++;
|
|
69
|
+
this.consecutiveSuccesses++;
|
|
70
|
+
this.totalSuccesses++;
|
|
71
|
+
this.consecutiveFailures = 0;
|
|
72
|
+
this.lastSuccessTime = Date.now();
|
|
73
|
+
if (this.state === "half-open") {
|
|
74
|
+
if (this.consecutiveSuccesses >= this.config.successThreshold) {
|
|
75
|
+
this.transitionTo("closed");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (this.state === "closed") {
|
|
79
|
+
this.failures = 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Handle failed execution
|
|
84
|
+
*/
|
|
85
|
+
onFailure(error) {
|
|
86
|
+
if (!this.config.errorFilter(error)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.failures++;
|
|
90
|
+
this.consecutiveFailures++;
|
|
91
|
+
this.totalFailures++;
|
|
92
|
+
this.consecutiveSuccesses = 0;
|
|
93
|
+
this.lastFailureTime = Date.now();
|
|
94
|
+
if (this.state === "half-open") {
|
|
95
|
+
this.transitionTo("open");
|
|
96
|
+
} else if (this.state === "closed") {
|
|
97
|
+
if (this.consecutiveFailures >= this.config.failureThreshold && this.totalCalls >= this.config.volumeThreshold) {
|
|
98
|
+
this.transitionTo("open");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Transition to new state
|
|
104
|
+
*/
|
|
105
|
+
transitionTo(newState) {
|
|
106
|
+
if (this.state === newState) return;
|
|
107
|
+
const oldState = this.state;
|
|
108
|
+
this.state = newState;
|
|
109
|
+
this.stateChangedAt = Date.now();
|
|
110
|
+
if (newState === "half-open" || newState === "closed") {
|
|
111
|
+
this.consecutiveFailures = 0;
|
|
112
|
+
this.consecutiveSuccesses = 0;
|
|
113
|
+
}
|
|
114
|
+
if (this.resetTimer) {
|
|
115
|
+
clearTimeout(this.resetTimer);
|
|
116
|
+
this.resetTimer = void 0;
|
|
117
|
+
}
|
|
118
|
+
if (newState === "open") {
|
|
119
|
+
this.resetTimer = setTimeout(() => {
|
|
120
|
+
this.transitionTo("half-open");
|
|
121
|
+
}, this.config.resetTimeout);
|
|
122
|
+
this.config.onTrip();
|
|
123
|
+
}
|
|
124
|
+
if (newState === "closed" && oldState === "half-open") {
|
|
125
|
+
this.failures = 0;
|
|
126
|
+
this.config.onReset();
|
|
127
|
+
}
|
|
128
|
+
this.config.onStateChange(newState);
|
|
129
|
+
getResilienceLogger().info(
|
|
130
|
+
`Circuit breaker state changed: ${oldState} -> ${newState}`,
|
|
131
|
+
this.getStats()
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get current state
|
|
136
|
+
*/
|
|
137
|
+
getState() {
|
|
138
|
+
return this.state;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get statistics
|
|
142
|
+
*/
|
|
143
|
+
getStats() {
|
|
144
|
+
return {
|
|
145
|
+
state: this.state,
|
|
146
|
+
failures: this.failures,
|
|
147
|
+
successes: this.successes,
|
|
148
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
149
|
+
consecutiveSuccesses: this.consecutiveSuccesses,
|
|
150
|
+
totalCalls: this.totalCalls,
|
|
151
|
+
totalFailures: this.totalFailures,
|
|
152
|
+
totalSuccesses: this.totalSuccesses,
|
|
153
|
+
lastFailureTime: this.lastFailureTime,
|
|
154
|
+
lastSuccessTime: this.lastSuccessTime,
|
|
155
|
+
stateChangedAt: this.stateChangedAt
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Manually open circuit
|
|
160
|
+
*/
|
|
161
|
+
trip() {
|
|
162
|
+
this.transitionTo("open");
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Manually close circuit
|
|
166
|
+
*/
|
|
167
|
+
reset() {
|
|
168
|
+
this.failures = 0;
|
|
169
|
+
this.successes = 0;
|
|
170
|
+
this.consecutiveFailures = 0;
|
|
171
|
+
this.consecutiveSuccesses = 0;
|
|
172
|
+
this.transitionTo("closed");
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Force state to half-open
|
|
176
|
+
*/
|
|
177
|
+
halfOpen() {
|
|
178
|
+
this.transitionTo("half-open");
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Check if circuit is open
|
|
182
|
+
*/
|
|
183
|
+
isOpen() {
|
|
184
|
+
return this.state === "open";
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Check if circuit is closed
|
|
188
|
+
*/
|
|
189
|
+
isClosed() {
|
|
190
|
+
return this.state === "closed";
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Check if circuit is half-open
|
|
194
|
+
*/
|
|
195
|
+
isHalfOpen() {
|
|
196
|
+
return this.state === "half-open";
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Get failure rate
|
|
200
|
+
*/
|
|
201
|
+
getFailureRate() {
|
|
202
|
+
if (this.totalCalls === 0) return 0;
|
|
203
|
+
return this.totalFailures / this.totalCalls;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Get success rate
|
|
207
|
+
*/
|
|
208
|
+
getSuccessRate() {
|
|
209
|
+
if (this.totalCalls === 0) return 0;
|
|
210
|
+
return this.totalSuccesses / this.totalCalls;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Cleanup
|
|
214
|
+
*/
|
|
215
|
+
destroy() {
|
|
216
|
+
if (this.resetTimer) {
|
|
217
|
+
clearTimeout(this.resetTimer);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
var CircuitBreakerOpenError = class extends Error {
|
|
222
|
+
constructor(message = "Circuit breaker is open") {
|
|
223
|
+
super(message);
|
|
224
|
+
this.name = "CircuitBreakerOpenError";
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
var CircuitBreakerRegistry = class {
|
|
228
|
+
breakers = /* @__PURE__ */ new Map();
|
|
229
|
+
/**
|
|
230
|
+
* Get or create circuit breaker
|
|
231
|
+
*/
|
|
232
|
+
get(name, config) {
|
|
233
|
+
let breaker = this.breakers.get(name);
|
|
234
|
+
if (!breaker) {
|
|
235
|
+
breaker = new CircuitBreaker(config);
|
|
236
|
+
this.breakers.set(name, breaker);
|
|
237
|
+
}
|
|
238
|
+
return breaker;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Check if breaker exists
|
|
242
|
+
*/
|
|
243
|
+
has(name) {
|
|
244
|
+
return this.breakers.has(name);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Remove circuit breaker
|
|
248
|
+
*/
|
|
249
|
+
remove(name) {
|
|
250
|
+
const breaker = this.breakers.get(name);
|
|
251
|
+
if (breaker) {
|
|
252
|
+
breaker.destroy();
|
|
253
|
+
return this.breakers.delete(name);
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Get all breakers
|
|
259
|
+
*/
|
|
260
|
+
getAll() {
|
|
261
|
+
return new Map(this.breakers);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get all statistics
|
|
265
|
+
*/
|
|
266
|
+
getAllStats() {
|
|
267
|
+
const stats = {};
|
|
268
|
+
for (const [name, breaker] of this.breakers) {
|
|
269
|
+
stats[name] = breaker.getStats();
|
|
270
|
+
}
|
|
271
|
+
return stats;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Reset all breakers
|
|
275
|
+
*/
|
|
276
|
+
resetAll() {
|
|
277
|
+
for (const breaker of this.breakers.values()) {
|
|
278
|
+
breaker.reset();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Clear all breakers
|
|
283
|
+
*/
|
|
284
|
+
clear() {
|
|
285
|
+
for (const breaker of this.breakers.values()) {
|
|
286
|
+
breaker.destroy();
|
|
287
|
+
}
|
|
288
|
+
this.breakers.clear();
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
var circuitBreakerRegistry = new CircuitBreakerRegistry();
|
|
292
|
+
function CircuitBreak(nameOrConfig = {}) {
|
|
293
|
+
return (target, propertyKey, descriptor) => {
|
|
294
|
+
const originalMethod = descriptor.value;
|
|
295
|
+
const name = typeof nameOrConfig === "string" ? nameOrConfig : `${target.constructor.name}.${propertyKey}`;
|
|
296
|
+
const config = typeof nameOrConfig === "object" ? nameOrConfig : void 0;
|
|
297
|
+
descriptor.value = async function(...args) {
|
|
298
|
+
const breaker = circuitBreakerRegistry.get(name, config);
|
|
299
|
+
return breaker.execute(() => originalMethod.apply(this, args));
|
|
300
|
+
};
|
|
301
|
+
return descriptor;
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
async function withCircuitBreaker(name, fn, config) {
|
|
305
|
+
const breaker = circuitBreakerRegistry.get(name, config);
|
|
306
|
+
return breaker.execute(fn);
|
|
307
|
+
}
|
|
308
|
+
function createCircuitBreakerMiddleware(name, config) {
|
|
309
|
+
const breaker = circuitBreakerRegistry.get(name, config);
|
|
310
|
+
return async (_request, next) => {
|
|
311
|
+
return breaker.execute(next);
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
async function fetchWithCircuitBreaker(name, url, init, config) {
|
|
315
|
+
const breaker = circuitBreakerRegistry.get(name, config);
|
|
316
|
+
return breaker.execute(async () => {
|
|
317
|
+
const response = await fetch(url, init);
|
|
318
|
+
if (response.status >= 500) {
|
|
319
|
+
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
320
|
+
error.statusCode = response.status;
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
return response;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
var AdaptiveCircuitBreaker = class extends CircuitBreaker {
|
|
327
|
+
errorRateWindow = [];
|
|
328
|
+
windowSize = 100;
|
|
329
|
+
adaptiveThreshold;
|
|
330
|
+
constructor(config = {}) {
|
|
331
|
+
super(config);
|
|
332
|
+
this.adaptiveThreshold = config.failureThreshold || 5;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Execute with adaptive thresholds
|
|
336
|
+
*/
|
|
337
|
+
async execute(fn) {
|
|
338
|
+
try {
|
|
339
|
+
const result = await super.execute(fn);
|
|
340
|
+
this.recordSuccess();
|
|
341
|
+
return result;
|
|
342
|
+
} catch (error) {
|
|
343
|
+
this.recordFailure();
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Record success in window
|
|
349
|
+
*/
|
|
350
|
+
recordSuccess() {
|
|
351
|
+
this.errorRateWindow.push(0);
|
|
352
|
+
this.trimWindow();
|
|
353
|
+
this.adjustThreshold();
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Record failure in window
|
|
357
|
+
*/
|
|
358
|
+
recordFailure() {
|
|
359
|
+
this.errorRateWindow.push(1);
|
|
360
|
+
this.trimWindow();
|
|
361
|
+
this.adjustThreshold();
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Trim window to size
|
|
365
|
+
*/
|
|
366
|
+
trimWindow() {
|
|
367
|
+
if (this.errorRateWindow.length > this.windowSize) {
|
|
368
|
+
this.errorRateWindow.shift();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Adjust threshold based on error rate
|
|
373
|
+
*/
|
|
374
|
+
adjustThreshold() {
|
|
375
|
+
const errorRate = this.getWindowErrorRate();
|
|
376
|
+
if (errorRate < 0.1) {
|
|
377
|
+
this.adaptiveThreshold = Math.min(this.adaptiveThreshold + 1, 20);
|
|
378
|
+
} else if (errorRate > 0.5) {
|
|
379
|
+
this.adaptiveThreshold = Math.max(this.adaptiveThreshold - 1, 2);
|
|
380
|
+
}
|
|
381
|
+
this.config.failureThreshold = this.adaptiveThreshold;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Get error rate in window
|
|
385
|
+
*/
|
|
386
|
+
getWindowErrorRate() {
|
|
387
|
+
if (this.errorRateWindow.length === 0) return 0;
|
|
388
|
+
const errors = this.errorRateWindow.reduce((sum, val) => sum + val, 0);
|
|
389
|
+
return errors / this.errorRateWindow.length;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Get adaptive threshold
|
|
393
|
+
*/
|
|
394
|
+
getAdaptiveThreshold() {
|
|
395
|
+
return this.adaptiveThreshold;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
var Bulkhead = class {
|
|
399
|
+
activeRequests = 0;
|
|
400
|
+
queue = [];
|
|
401
|
+
maxConcurrent;
|
|
402
|
+
maxQueue;
|
|
403
|
+
constructor(maxConcurrent = 10, maxQueue = 100) {
|
|
404
|
+
this.maxConcurrent = maxConcurrent;
|
|
405
|
+
this.maxQueue = maxQueue;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Execute with bulkhead
|
|
409
|
+
*/
|
|
410
|
+
async execute(fn) {
|
|
411
|
+
if (this.activeRequests >= this.maxConcurrent) {
|
|
412
|
+
if (this.queue.length >= this.maxQueue) {
|
|
413
|
+
throw new Error("Bulkhead queue is full");
|
|
414
|
+
}
|
|
415
|
+
await new Promise((resolve) => {
|
|
416
|
+
this.queue.push(resolve);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
this.activeRequests++;
|
|
420
|
+
try {
|
|
421
|
+
return await fn();
|
|
422
|
+
} finally {
|
|
423
|
+
this.activeRequests--;
|
|
424
|
+
const next = this.queue.shift();
|
|
425
|
+
if (next) {
|
|
426
|
+
next();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Get active requests
|
|
432
|
+
*/
|
|
433
|
+
getActiveRequests() {
|
|
434
|
+
return this.activeRequests;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Get queue size
|
|
438
|
+
*/
|
|
439
|
+
getQueueSize() {
|
|
440
|
+
return this.queue.length;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Get statistics
|
|
444
|
+
*/
|
|
445
|
+
getStats() {
|
|
446
|
+
return {
|
|
447
|
+
activeRequests: this.activeRequests,
|
|
448
|
+
queueSize: this.queue.length,
|
|
449
|
+
maxConcurrent: this.maxConcurrent,
|
|
450
|
+
maxQueue: this.maxQueue
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
var ResilientOperation = class {
|
|
455
|
+
constructor(fn, circuitBreaker, bulkhead) {
|
|
456
|
+
this.fn = fn;
|
|
457
|
+
this.circuitBreaker = circuitBreaker;
|
|
458
|
+
this.bulkhead = bulkhead;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Execute with all resilience patterns
|
|
462
|
+
*/
|
|
463
|
+
async execute() {
|
|
464
|
+
const executeFn = async () => {
|
|
465
|
+
if (this.bulkhead) {
|
|
466
|
+
return this.bulkhead.execute(this.fn);
|
|
467
|
+
}
|
|
468
|
+
return this.fn();
|
|
469
|
+
};
|
|
470
|
+
if (this.circuitBreaker) {
|
|
471
|
+
return this.circuitBreaker.execute(executeFn);
|
|
472
|
+
}
|
|
473
|
+
return executeFn();
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
function createResilientFunction(fn, options = {}) {
|
|
477
|
+
const breaker = options.circuitBreaker ? new CircuitBreaker(options.circuitBreaker) : void 0;
|
|
478
|
+
const bulkhead = options.bulkhead ? new Bulkhead(options.bulkhead.maxConcurrent, options.bulkhead.maxQueue) : void 0;
|
|
479
|
+
const operation = new ResilientOperation(fn, breaker, bulkhead);
|
|
480
|
+
return () => operation.execute();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/retry.ts
|
|
484
|
+
import { randomInt } from "crypto";
|
|
485
|
+
var DEFAULT_CONFIG2 = {
|
|
486
|
+
maxRetries: 3,
|
|
487
|
+
baseDelay: 1e3,
|
|
488
|
+
maxDelay: 3e4,
|
|
489
|
+
exponentialBackoff: true,
|
|
490
|
+
jitter: true,
|
|
491
|
+
retryableErrors: (error) => {
|
|
492
|
+
if ("statusCode" in error) {
|
|
493
|
+
const statusCode = error.statusCode;
|
|
494
|
+
if (statusCode !== void 0 && statusCode >= 400 && statusCode < 500) {
|
|
495
|
+
return statusCode === 408 || statusCode === 429;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return true;
|
|
499
|
+
},
|
|
500
|
+
onRetry: () => {
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
async function retry(fn, config = {}, options = {}) {
|
|
504
|
+
const mergedConfig = { ...DEFAULT_CONFIG2, ...config };
|
|
505
|
+
let lastError = new Error("Retry failed");
|
|
506
|
+
for (let attempt = 0; attempt <= mergedConfig.maxRetries; attempt++) {
|
|
507
|
+
try {
|
|
508
|
+
if (options.signal?.aborted) {
|
|
509
|
+
throw new Error("Request aborted");
|
|
510
|
+
}
|
|
511
|
+
return await fn();
|
|
512
|
+
} catch (error) {
|
|
513
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
514
|
+
if (attempt === mergedConfig.maxRetries) {
|
|
515
|
+
throw lastError;
|
|
516
|
+
}
|
|
517
|
+
if (!mergedConfig.retryableErrors(lastError)) {
|
|
518
|
+
throw lastError;
|
|
519
|
+
}
|
|
520
|
+
mergedConfig.onRetry(lastError, attempt + 1);
|
|
521
|
+
const delay = calculateDelay(
|
|
522
|
+
attempt,
|
|
523
|
+
mergedConfig.baseDelay,
|
|
524
|
+
mergedConfig.maxDelay,
|
|
525
|
+
mergedConfig.exponentialBackoff,
|
|
526
|
+
mergedConfig.jitter
|
|
527
|
+
);
|
|
528
|
+
await sleep(delay, options.signal);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
throw lastError;
|
|
532
|
+
}
|
|
533
|
+
function calculateDelay(attempt, baseDelay, maxDelay, exponentialBackoff, jitter) {
|
|
534
|
+
let delay = baseDelay;
|
|
535
|
+
if (exponentialBackoff) {
|
|
536
|
+
delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
|
|
537
|
+
}
|
|
538
|
+
if (jitter) {
|
|
539
|
+
const jitterAmount = Math.ceil(delay * 0.25);
|
|
540
|
+
if (jitterAmount > 0) {
|
|
541
|
+
delay = delay + randomInt(jitterAmount * 2 + 1) - jitterAmount;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return Math.floor(Math.min(delay, maxDelay));
|
|
545
|
+
}
|
|
546
|
+
function sleep(ms, signal) {
|
|
547
|
+
return new Promise((resolve, reject) => {
|
|
548
|
+
if (signal?.aborted) {
|
|
549
|
+
reject(new Error("Request aborted"));
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const timeout = setTimeout(resolve, ms);
|
|
553
|
+
if (signal) {
|
|
554
|
+
signal.addEventListener("abort", () => {
|
|
555
|
+
clearTimeout(timeout);
|
|
556
|
+
reject(new Error("Request aborted"));
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
async function fetchWithRetry(url, init, config) {
|
|
562
|
+
const abortController = new AbortController();
|
|
563
|
+
const signal = init?.signal || abortController.signal;
|
|
564
|
+
return retry(
|
|
565
|
+
async () => {
|
|
566
|
+
const response = await fetch(url, {
|
|
567
|
+
...init,
|
|
568
|
+
signal
|
|
569
|
+
});
|
|
570
|
+
if (!response.ok) {
|
|
571
|
+
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
572
|
+
error.statusCode = response.status;
|
|
573
|
+
error.response = response;
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
return response;
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
...config,
|
|
580
|
+
retryableErrors: (error) => {
|
|
581
|
+
if (config?.retryableErrors && !config.retryableErrors(error)) {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
if ("statusCode" in error) {
|
|
585
|
+
const statusCode = error.statusCode;
|
|
586
|
+
if (statusCode !== void 0 && statusCode >= 400 && statusCode < 500) {
|
|
587
|
+
return statusCode === 408 || statusCode === 429;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
{ signal }
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
var RetryableOperation = class {
|
|
597
|
+
constructor(fn, config = {}) {
|
|
598
|
+
this.fn = fn;
|
|
599
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
600
|
+
this.abortController = new AbortController();
|
|
601
|
+
}
|
|
602
|
+
config;
|
|
603
|
+
abortController;
|
|
604
|
+
attempts = 0;
|
|
605
|
+
lastError;
|
|
606
|
+
/**
|
|
607
|
+
* Execute with retry
|
|
608
|
+
*/
|
|
609
|
+
async execute() {
|
|
610
|
+
return retry(this.fn, this.config, { signal: this.abortController.signal });
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Abort operation
|
|
614
|
+
*/
|
|
615
|
+
abort() {
|
|
616
|
+
this.abortController.abort();
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Get retry statistics
|
|
620
|
+
*/
|
|
621
|
+
getStats() {
|
|
622
|
+
return {
|
|
623
|
+
attempts: this.attempts,
|
|
624
|
+
lastError: this.lastError
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
function Retryable(config) {
|
|
629
|
+
return (_target, _propertyKey, descriptor) => {
|
|
630
|
+
const originalMethod = descriptor.value;
|
|
631
|
+
descriptor.value = async function(...args) {
|
|
632
|
+
return retry(() => originalMethod.apply(this, args), config);
|
|
633
|
+
};
|
|
634
|
+
return descriptor;
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function createRetryMiddleware(config = {}) {
|
|
638
|
+
return async (_request, next) => {
|
|
639
|
+
return retry(next, config);
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
async function retryBatch(operations, config = {}) {
|
|
643
|
+
return Promise.all(
|
|
644
|
+
operations.map(async (op) => {
|
|
645
|
+
try {
|
|
646
|
+
return await retry(op, config);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
649
|
+
}
|
|
650
|
+
})
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
async function retryWithFallback(primary, fallback, config = {}) {
|
|
654
|
+
try {
|
|
655
|
+
return await retry(primary, config);
|
|
656
|
+
} catch (error) {
|
|
657
|
+
getResilienceLogger().warn("Primary operation failed, trying fallback", {
|
|
658
|
+
error: error instanceof Error ? error.message : String(error)
|
|
659
|
+
});
|
|
660
|
+
return fallback();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async function retryIf(fn, condition, config = {}) {
|
|
664
|
+
return retry(fn, {
|
|
665
|
+
...config,
|
|
666
|
+
retryableErrors: (error) => {
|
|
667
|
+
const originalCheck = config.retryableErrors?.(error) ?? DEFAULT_CONFIG2.retryableErrors(error);
|
|
668
|
+
if (!originalCheck) return false;
|
|
669
|
+
return condition(error, 0);
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
async function retryUntil(fn, predicate, config = {}, maxAttempts = 10) {
|
|
674
|
+
let attempts = 0;
|
|
675
|
+
while (attempts < maxAttempts) {
|
|
676
|
+
try {
|
|
677
|
+
const result = await fn();
|
|
678
|
+
if (predicate(result)) {
|
|
679
|
+
return result;
|
|
680
|
+
}
|
|
681
|
+
attempts++;
|
|
682
|
+
if (attempts >= maxAttempts) {
|
|
683
|
+
throw new Error("Max attempts reached without matching predicate");
|
|
684
|
+
}
|
|
685
|
+
const delay = calculateDelay(
|
|
686
|
+
attempts - 1,
|
|
687
|
+
config.baseDelay ?? DEFAULT_CONFIG2.baseDelay,
|
|
688
|
+
config.maxDelay ?? DEFAULT_CONFIG2.maxDelay,
|
|
689
|
+
config.exponentialBackoff ?? DEFAULT_CONFIG2.exponentialBackoff,
|
|
690
|
+
config.jitter ?? DEFAULT_CONFIG2.jitter
|
|
691
|
+
);
|
|
692
|
+
await sleep(delay);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
attempts++;
|
|
695
|
+
if (attempts >= maxAttempts) {
|
|
696
|
+
throw error;
|
|
697
|
+
}
|
|
698
|
+
const delay = calculateDelay(
|
|
699
|
+
attempts - 1,
|
|
700
|
+
config.baseDelay ?? DEFAULT_CONFIG2.baseDelay,
|
|
701
|
+
config.maxDelay ?? DEFAULT_CONFIG2.maxDelay,
|
|
702
|
+
config.exponentialBackoff ?? DEFAULT_CONFIG2.exponentialBackoff,
|
|
703
|
+
config.jitter ?? DEFAULT_CONFIG2.jitter
|
|
704
|
+
);
|
|
705
|
+
await sleep(delay);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
throw new Error("Max attempts reached");
|
|
709
|
+
}
|
|
710
|
+
var ExponentialBackoff = class {
|
|
711
|
+
constructor(baseDelay = 1e3, maxDelay = 3e4, maxAttempts = 10, jitter = true) {
|
|
712
|
+
this.baseDelay = baseDelay;
|
|
713
|
+
this.maxDelay = maxDelay;
|
|
714
|
+
this.maxAttempts = maxAttempts;
|
|
715
|
+
this.jitter = jitter;
|
|
716
|
+
}
|
|
717
|
+
async *[Symbol.asyncIterator]() {
|
|
718
|
+
for (let attempt = 0; attempt < this.maxAttempts; attempt++) {
|
|
719
|
+
const delay = calculateDelay(attempt, this.baseDelay, this.maxDelay, true, this.jitter);
|
|
720
|
+
yield delay;
|
|
721
|
+
await sleep(delay);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
var RetryPolicyBuilder = class {
|
|
726
|
+
config = {};
|
|
727
|
+
/**
|
|
728
|
+
* Set max retries
|
|
729
|
+
*/
|
|
730
|
+
maxRetries(count) {
|
|
731
|
+
this.config.maxRetries = count;
|
|
732
|
+
return this;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Set base delay
|
|
736
|
+
*/
|
|
737
|
+
baseDelay(ms) {
|
|
738
|
+
this.config.baseDelay = ms;
|
|
739
|
+
return this;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Set max delay
|
|
743
|
+
*/
|
|
744
|
+
maxDelay(ms) {
|
|
745
|
+
this.config.maxDelay = ms;
|
|
746
|
+
return this;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Enable/disable exponential backoff
|
|
750
|
+
*/
|
|
751
|
+
exponentialBackoff(enabled = true) {
|
|
752
|
+
this.config.exponentialBackoff = enabled;
|
|
753
|
+
return this;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Enable/disable jitter
|
|
757
|
+
*/
|
|
758
|
+
jitter(enabled = true) {
|
|
759
|
+
this.config.jitter = enabled;
|
|
760
|
+
return this;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Set custom retryable errors function
|
|
764
|
+
*/
|
|
765
|
+
retryOn(fn) {
|
|
766
|
+
this.config.retryableErrors = fn;
|
|
767
|
+
return this;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Set retry callback
|
|
771
|
+
*/
|
|
772
|
+
onRetry(fn) {
|
|
773
|
+
this.config.onRetry = fn;
|
|
774
|
+
return this;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Build retry config
|
|
778
|
+
*/
|
|
779
|
+
build() {
|
|
780
|
+
return this.config;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Execute function with built policy
|
|
784
|
+
*/
|
|
785
|
+
async execute(fn) {
|
|
786
|
+
return retry(fn, this.build());
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
var RetryPolicies = {
|
|
790
|
+
/**
|
|
791
|
+
* Default policy - 3 retries with exponential backoff
|
|
792
|
+
*/
|
|
793
|
+
default: () => ({
|
|
794
|
+
maxRetries: 3,
|
|
795
|
+
baseDelay: 1e3,
|
|
796
|
+
maxDelay: 3e4,
|
|
797
|
+
exponentialBackoff: true,
|
|
798
|
+
jitter: true
|
|
799
|
+
}),
|
|
800
|
+
/**
|
|
801
|
+
* Aggressive policy - more retries, faster backoff
|
|
802
|
+
*/
|
|
803
|
+
aggressive: () => ({
|
|
804
|
+
maxRetries: 5,
|
|
805
|
+
baseDelay: 500,
|
|
806
|
+
maxDelay: 1e4,
|
|
807
|
+
exponentialBackoff: true,
|
|
808
|
+
jitter: true
|
|
809
|
+
}),
|
|
810
|
+
/**
|
|
811
|
+
* Conservative policy - fewer retries, longer backoff
|
|
812
|
+
*/
|
|
813
|
+
conservative: () => ({
|
|
814
|
+
maxRetries: 2,
|
|
815
|
+
baseDelay: 2e3,
|
|
816
|
+
maxDelay: 6e4,
|
|
817
|
+
exponentialBackoff: true,
|
|
818
|
+
jitter: true
|
|
819
|
+
}),
|
|
820
|
+
/**
|
|
821
|
+
* Linear backoff policy
|
|
822
|
+
*/
|
|
823
|
+
linear: () => ({
|
|
824
|
+
maxRetries: 3,
|
|
825
|
+
baseDelay: 1e3,
|
|
826
|
+
maxDelay: 1e4,
|
|
827
|
+
exponentialBackoff: false,
|
|
828
|
+
jitter: false
|
|
829
|
+
}),
|
|
830
|
+
/**
|
|
831
|
+
* Immediate retry policy - no delay
|
|
832
|
+
*/
|
|
833
|
+
immediate: () => ({
|
|
834
|
+
maxRetries: 3,
|
|
835
|
+
baseDelay: 0,
|
|
836
|
+
maxDelay: 0,
|
|
837
|
+
exponentialBackoff: false,
|
|
838
|
+
jitter: false
|
|
839
|
+
}),
|
|
840
|
+
/**
|
|
841
|
+
* Network error only policy
|
|
842
|
+
*/
|
|
843
|
+
networkOnly: () => ({
|
|
844
|
+
maxRetries: 3,
|
|
845
|
+
baseDelay: 1e3,
|
|
846
|
+
maxDelay: 3e4,
|
|
847
|
+
exponentialBackoff: true,
|
|
848
|
+
jitter: true,
|
|
849
|
+
retryableErrors: (error) => error.name === "NetworkError"
|
|
850
|
+
}),
|
|
851
|
+
/**
|
|
852
|
+
* Idempotent operations policy (safe to retry)
|
|
853
|
+
*/
|
|
854
|
+
idempotent: () => ({
|
|
855
|
+
maxRetries: 5,
|
|
856
|
+
baseDelay: 1e3,
|
|
857
|
+
maxDelay: 3e4,
|
|
858
|
+
exponentialBackoff: true,
|
|
859
|
+
jitter: true
|
|
860
|
+
})
|
|
861
|
+
};
|
|
862
|
+
var GlobalRetryConfig = class {
|
|
863
|
+
config = RetryPolicies.default();
|
|
864
|
+
/**
|
|
865
|
+
* Set global retry config
|
|
866
|
+
*/
|
|
867
|
+
setConfig(config) {
|
|
868
|
+
this.config = { ...this.config, ...config };
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Get global retry config
|
|
872
|
+
*/
|
|
873
|
+
getConfig() {
|
|
874
|
+
return this.config;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Reset to default config
|
|
878
|
+
*/
|
|
879
|
+
reset() {
|
|
880
|
+
this.config = RetryPolicies.default();
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
var globalRetryConfig = new GlobalRetryConfig();
|
|
884
|
+
export {
|
|
885
|
+
AdaptiveCircuitBreaker,
|
|
886
|
+
Bulkhead,
|
|
887
|
+
CircuitBreak,
|
|
888
|
+
CircuitBreaker,
|
|
889
|
+
CircuitBreakerOpenError,
|
|
890
|
+
CircuitBreakerRegistry,
|
|
891
|
+
ExponentialBackoff,
|
|
892
|
+
ResilientOperation,
|
|
893
|
+
RetryPolicies,
|
|
894
|
+
RetryPolicyBuilder,
|
|
895
|
+
Retryable,
|
|
896
|
+
RetryableOperation,
|
|
897
|
+
calculateDelay,
|
|
898
|
+
circuitBreakerRegistry,
|
|
899
|
+
configureResilienceLogger,
|
|
900
|
+
createCircuitBreakerMiddleware,
|
|
901
|
+
createResilientFunction,
|
|
902
|
+
createRetryMiddleware,
|
|
903
|
+
fetchWithCircuitBreaker,
|
|
904
|
+
fetchWithRetry,
|
|
905
|
+
getResilienceLogger,
|
|
906
|
+
globalRetryConfig,
|
|
907
|
+
retry,
|
|
908
|
+
retryBatch,
|
|
909
|
+
retryIf,
|
|
910
|
+
retryUntil,
|
|
911
|
+
retryWithFallback,
|
|
912
|
+
sleep,
|
|
913
|
+
withCircuitBreaker
|
|
914
|
+
};
|
|
915
|
+
//# sourceMappingURL=index.js.map
|