@autonomaai/service-utils 1.0.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/dist/env.d.ts +7 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +12 -0
- package/dist/env.js.map +1 -0
- package/dist/fastify.d.ts +15 -0
- package/dist/fastify.d.ts.map +1 -0
- package/dist/fastify.js +57 -0
- package/dist/fastify.js.map +1 -0
- package/dist/health.d.ts +17 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +15 -0
- package/dist/health.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +22 -0
- package/dist/logger.js.map +1 -0
- package/dist/resilience/bulkhead.d.ts +95 -0
- package/dist/resilience/bulkhead.d.ts.map +1 -0
- package/dist/resilience/bulkhead.js +186 -0
- package/dist/resilience/bulkhead.js.map +1 -0
- package/dist/resilience/circuit-breaker.d.ts +111 -0
- package/dist/resilience/circuit-breaker.d.ts.map +1 -0
- package/dist/resilience/circuit-breaker.js +267 -0
- package/dist/resilience/circuit-breaker.js.map +1 -0
- package/dist/resilience/index.d.ts +15 -0
- package/dist/resilience/index.d.ts.map +1 -0
- package/dist/resilience/index.js +15 -0
- package/dist/resilience/index.js.map +1 -0
- package/dist/resilience/rate-limiter.d.ts +115 -0
- package/dist/resilience/rate-limiter.d.ts.map +1 -0
- package/dist/resilience/rate-limiter.js +257 -0
- package/dist/resilience/rate-limiter.js.map +1 -0
- package/dist/resilience/retry.d.ts +63 -0
- package/dist/resilience/retry.d.ts.map +1 -0
- package/dist/resilience/retry.js +190 -0
- package/dist/resilience/retry.js.map +1 -0
- package/dist/resilience/timeout.d.ts +62 -0
- package/dist/resilience/timeout.d.ts.map +1 -0
- package/dist/resilience/timeout.js +135 -0
- package/dist/resilience/timeout.js.map +1 -0
- package/dist/resilience/types.d.ts +163 -0
- package/dist/resilience/types.d.ts.map +1 -0
- package/dist/resilience/types.js +64 -0
- package/dist/resilience/types.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit Breaker Pattern
|
|
3
|
+
*
|
|
4
|
+
* Prevents cascading failures by failing fast when a service is down.
|
|
5
|
+
* After a threshold of failures, the circuit opens and rejects requests
|
|
6
|
+
* for a recovery period before allowing test requests through.
|
|
7
|
+
*
|
|
8
|
+
* States:
|
|
9
|
+
* - CLOSED: Normal operation, requests flow through
|
|
10
|
+
* - OPEN: Service is down, fail immediately
|
|
11
|
+
* - HALF_OPEN: Testing if service recovered
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const breaker = new CircuitBreaker({
|
|
16
|
+
* name: 'exchange-api',
|
|
17
|
+
* failureThreshold: 5,
|
|
18
|
+
* recoveryTimeout: 30000,
|
|
19
|
+
* halfOpenMaxCalls: 3
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* try {
|
|
23
|
+
* const result = await breaker.execute(() => fetchData());
|
|
24
|
+
* } catch (error) {
|
|
25
|
+
* if (error instanceof CircuitBreakerOpenError) {
|
|
26
|
+
* // Use fallback
|
|
27
|
+
* return getCachedData();
|
|
28
|
+
* }
|
|
29
|
+
* throw error;
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
import { CircuitState, CircuitBreakerOpenError } from './types.js';
|
|
34
|
+
const DEFAULT_CONFIG = {
|
|
35
|
+
failureThreshold: 5,
|
|
36
|
+
recoveryTimeout: 30000,
|
|
37
|
+
halfOpenMaxCalls: 3,
|
|
38
|
+
failureRateThreshold: 0.5,
|
|
39
|
+
excludedErrors: []
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Circuit Breaker implementation
|
|
43
|
+
*/
|
|
44
|
+
export class CircuitBreaker {
|
|
45
|
+
static instances = new Map();
|
|
46
|
+
config;
|
|
47
|
+
state = CircuitState.CLOSED;
|
|
48
|
+
failureCount = 0;
|
|
49
|
+
successCount = 0;
|
|
50
|
+
lastFailureTime;
|
|
51
|
+
halfOpenCalls = 0;
|
|
52
|
+
stats = {
|
|
53
|
+
totalCalls: 0,
|
|
54
|
+
successfulCalls: 0,
|
|
55
|
+
failedCalls: 0,
|
|
56
|
+
rejectedCalls: 0,
|
|
57
|
+
state: CircuitState.CLOSED
|
|
58
|
+
};
|
|
59
|
+
constructor(config) {
|
|
60
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
61
|
+
CircuitBreaker.instances.set(this.config.name, this);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get current circuit state
|
|
65
|
+
*/
|
|
66
|
+
getState() {
|
|
67
|
+
if (this.state === CircuitState.OPEN) {
|
|
68
|
+
if (this.shouldAttemptRecovery()) {
|
|
69
|
+
this.transitionTo(CircuitState.HALF_OPEN);
|
|
70
|
+
this.halfOpenCalls = 0;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return this.state;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get circuit breaker statistics
|
|
77
|
+
*/
|
|
78
|
+
getStats() {
|
|
79
|
+
return {
|
|
80
|
+
...this.stats,
|
|
81
|
+
state: this.getState()
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Execute a function through the circuit breaker
|
|
86
|
+
*/
|
|
87
|
+
async execute(fn) {
|
|
88
|
+
// Wait for any concurrent state transition
|
|
89
|
+
const release = await this.acquireLock();
|
|
90
|
+
try {
|
|
91
|
+
const currentState = this.getState();
|
|
92
|
+
this.stats.totalCalls++;
|
|
93
|
+
if (currentState === CircuitState.OPEN) {
|
|
94
|
+
this.stats.rejectedCalls++;
|
|
95
|
+
const retryAfter = this.config.recoveryTimeout -
|
|
96
|
+
(Date.now() - (this.lastFailureTime || 0));
|
|
97
|
+
throw new CircuitBreakerOpenError(this.config.name, Math.max(0, retryAfter));
|
|
98
|
+
}
|
|
99
|
+
if (currentState === CircuitState.HALF_OPEN) {
|
|
100
|
+
if (this.halfOpenCalls >= this.config.halfOpenMaxCalls) {
|
|
101
|
+
this.stats.rejectedCalls++;
|
|
102
|
+
throw new CircuitBreakerOpenError(this.config.name, this.config.recoveryTimeout);
|
|
103
|
+
}
|
|
104
|
+
this.halfOpenCalls++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
release();
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const result = await fn();
|
|
112
|
+
await this.onSuccess();
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
// Check if error should be excluded
|
|
117
|
+
if (this.isExcludedError(error)) {
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
await this.onFailure();
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Manually reset the circuit breaker
|
|
126
|
+
*/
|
|
127
|
+
reset() {
|
|
128
|
+
this.transitionTo(CircuitState.CLOSED);
|
|
129
|
+
this.failureCount = 0;
|
|
130
|
+
this.successCount = 0;
|
|
131
|
+
this.halfOpenCalls = 0;
|
|
132
|
+
this.lastFailureTime = undefined;
|
|
133
|
+
console.log(`Circuit breaker '${this.config.name}' manually reset`);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check if recovery should be attempted
|
|
137
|
+
*/
|
|
138
|
+
shouldAttemptRecovery() {
|
|
139
|
+
if (!this.lastFailureTime)
|
|
140
|
+
return true;
|
|
141
|
+
return Date.now() - this.lastFailureTime >= this.config.recoveryTimeout;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Handle successful execution
|
|
145
|
+
*/
|
|
146
|
+
async onSuccess() {
|
|
147
|
+
const release = await this.acquireLock();
|
|
148
|
+
try {
|
|
149
|
+
this.successCount++;
|
|
150
|
+
this.stats.successfulCalls++;
|
|
151
|
+
this.stats.lastSuccessTime = Date.now();
|
|
152
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
153
|
+
// Recovered - close the circuit
|
|
154
|
+
this.transitionTo(CircuitState.CLOSED);
|
|
155
|
+
this.failureCount = 0;
|
|
156
|
+
console.log(`Circuit breaker '${this.config.name}' recovered -> CLOSED`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
release();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Handle failed execution
|
|
165
|
+
*/
|
|
166
|
+
async onFailure() {
|
|
167
|
+
const release = await this.acquireLock();
|
|
168
|
+
try {
|
|
169
|
+
this.failureCount++;
|
|
170
|
+
this.stats.failedCalls++;
|
|
171
|
+
this.stats.lastFailureTime = Date.now();
|
|
172
|
+
this.lastFailureTime = Date.now();
|
|
173
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
174
|
+
// Failed during recovery - open again
|
|
175
|
+
this.transitionTo(CircuitState.OPEN);
|
|
176
|
+
console.warn(`Circuit breaker '${this.config.name}' failed during recovery -> OPEN`);
|
|
177
|
+
}
|
|
178
|
+
else if (this.state === CircuitState.CLOSED) {
|
|
179
|
+
if (this.failureCount >= this.config.failureThreshold) {
|
|
180
|
+
this.transitionTo(CircuitState.OPEN);
|
|
181
|
+
console.warn(`Circuit breaker '${this.config.name}' threshold reached -> OPEN`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
release();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Transition to a new state
|
|
191
|
+
*/
|
|
192
|
+
transitionTo(newState) {
|
|
193
|
+
const oldState = this.state;
|
|
194
|
+
this.state = newState;
|
|
195
|
+
this.stats.state = newState;
|
|
196
|
+
// Call callbacks
|
|
197
|
+
switch (newState) {
|
|
198
|
+
case CircuitState.OPEN:
|
|
199
|
+
this.config.onOpen?.();
|
|
200
|
+
break;
|
|
201
|
+
case CircuitState.CLOSED:
|
|
202
|
+
this.config.onClose?.();
|
|
203
|
+
break;
|
|
204
|
+
case CircuitState.HALF_OPEN:
|
|
205
|
+
this.config.onHalfOpen?.();
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Check if error should be excluded from failure count
|
|
211
|
+
*/
|
|
212
|
+
isExcludedError(error) {
|
|
213
|
+
if (!this.config.excludedErrors?.length)
|
|
214
|
+
return false;
|
|
215
|
+
return this.config.excludedErrors.some(ErrorClass => error instanceof ErrorClass);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Lock mechanism using a promise-based queue for atomic operations
|
|
219
|
+
*/
|
|
220
|
+
lockQueue = Promise.resolve();
|
|
221
|
+
async acquireLock() {
|
|
222
|
+
let release = () => { };
|
|
223
|
+
const previousLock = this.lockQueue;
|
|
224
|
+
this.lockQueue = new Promise(resolve => {
|
|
225
|
+
release = resolve;
|
|
226
|
+
});
|
|
227
|
+
await previousLock;
|
|
228
|
+
return release;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get stats for all circuit breakers (for monitoring)
|
|
232
|
+
*/
|
|
233
|
+
static getAllStats() {
|
|
234
|
+
const stats = new Map();
|
|
235
|
+
for (const [name, breaker] of this.instances) {
|
|
236
|
+
stats.set(name, breaker.getStats());
|
|
237
|
+
}
|
|
238
|
+
return stats;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get a circuit breaker by name
|
|
242
|
+
*/
|
|
243
|
+
static get(name) {
|
|
244
|
+
return this.instances.get(name);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Decorator to apply circuit breaker to a method
|
|
249
|
+
*/
|
|
250
|
+
export function withCircuitBreaker(config) {
|
|
251
|
+
const breaker = new CircuitBreaker(config);
|
|
252
|
+
return function (target, propertyKey, descriptor) {
|
|
253
|
+
const originalMethod = descriptor.value;
|
|
254
|
+
descriptor.value = async function (...args) {
|
|
255
|
+
return breaker.execute(() => originalMethod.apply(this, args));
|
|
256
|
+
};
|
|
257
|
+
return descriptor;
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Create a circuit breaker wrapper function
|
|
262
|
+
*/
|
|
263
|
+
export function createCircuitBreaker(config) {
|
|
264
|
+
const breaker = new CircuitBreaker(config);
|
|
265
|
+
return (fn) => breaker.execute(fn);
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=circuit-breaker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.js","sourceRoot":"","sources":["../../src/resilience/circuit-breaker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EACL,YAAY,EAGZ,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAEpB,MAAM,cAAc,GAAuC;IACzD,gBAAgB,EAAE,CAAC;IACnB,eAAe,EAAE,KAAK;IACtB,gBAAgB,EAAE,CAAC;IACnB,oBAAoB,EAAE,GAAG;IACzB,cAAc,EAAE,EAAE;CACnB,CAAC;AAEF;;GAEG;AACH,MAAM,OAAO,cAAc;IACjB,MAAM,CAAC,SAAS,GAAgC,IAAI,GAAG,EAAE,CAAC;IAEjD,MAAM,CAAuB;IACtC,KAAK,GAAiB,YAAY,CAAC,MAAM,CAAC;IAC1C,YAAY,GAAG,CAAC,CAAC;IACjB,YAAY,GAAG,CAAC,CAAC;IACjB,eAAe,CAAU;IACzB,aAAa,GAAG,CAAC,CAAC;IAElB,KAAK,GAAwB;QACnC,UAAU,EAAE,CAAC;QACb,eAAe,EAAE,CAAC;QAClB,WAAW,EAAE,CAAC;QACd,aAAa,EAAE,CAAC;QAChB,KAAK,EAAE,YAAY,CAAC,MAAM;KAC3B,CAAC;IAEF,YAAY,MAAwD;QAClE,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,cAAc,EAAE,GAAG,MAAM,EAAE,CAAC;QAC/C,cAAc,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACvD,CAAC;IAED;;OAEG;IACH,QAAQ;QACN,IAAI,IAAI,CAAC,KAAK,KAAK,YAAY,CAAC,IAAI,EAAE,CAAC;YACrC,IAAI,IAAI,CAAC,qBAAqB,EAAE,EAAE,CAAC;gBACjC,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;gBAC1C,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;YACzB,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED;;OAEG;IACH,QAAQ;QACN,OAAO;YACL,GAAG,IAAI,CAAC,KAAK;YACb,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE;SACvB,CAAC;IACJ,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,OAAO,CAAI,EAAoB;QACnC,2CAA2C;QAC3C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAEzC,IAAI,CAAC;YACH,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YACrC,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;YAExB,IAAI,YAAY,KAAK,YAAY,CAAC,IAAI,EAAE,CAAC;gBACvC,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC;gBAC3B,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,eAAe;oBAC5C,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,CAAC,CAAC,CAAC;gBAC7C,MAAM,IAAI,uBAAuB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;YAC/E,CAAC;YAED,IAAI,YAAY,KAAK,YAAY,CAAC,SAAS,EAAE,CAAC;gBAC5C,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,MAAM,CAAC,gBAAgB,EAAE,CAAC;oBACvD,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC;oBAC3B,MAAM,IAAI,uBAAuB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;gBACnF,CAAC;gBACD,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC;YAC1B,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;YACvB,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,oCAAoC;YACpC,IAAI,IAAI,CAAC,eAAe,CAAC,KAAc,CAAC,EAAE,CAAC;gBACzC,MAAM,KAAK,CAAC;YACd,CAAC;YACD,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;YACvB,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK;QACH,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;QACvB,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;QACjC,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,CAAC,MAAM,CAAC,IAAI,kBAAkB,CAAC,CAAC;IACtE,CAAC;IAED;;OAEG;IACK,qBAAqB;QAC3B,IAAI,CAAC,IAAI,CAAC,eAAe;YAAE,OAAO,IAAI,CAAC;QACvC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC;IAC1E,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,SAAS;QACrB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACzC,IAAI,CAAC;YACH,IAAI,CAAC,YAAY,EAAE,CAAC;YACpB,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,CAAC;YAC7B,IAAI,CAAC,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAExC,IAAI,IAAI,CAAC,KAAK,KAAK,YAAY,CAAC,SAAS,EAAE,CAAC;gBAC1C,gCAAgC;gBAChC,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;gBACvC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;gBACtB,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,CAAC,MAAM,CAAC,IAAI,uBAAuB,CAAC,CAAC;YAC3E,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,SAAS;QACrB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACzC,IAAI,CAAC;YACH,IAAI,CAAC,YAAY,EAAE,CAAC;YACpB,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;YACzB,IAAI,CAAC,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACxC,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAElC,IAAI,IAAI,CAAC,KAAK,KAAK,YAAY,CAAC,SAAS,EAAE,CAAC;gBAC1C,sCAAsC;gBACtC,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;gBACrC,OAAO,CAAC,IAAI,CAAC,oBAAoB,IAAI,CAAC,MAAM,CAAC,IAAI,kCAAkC,CAAC,CAAC;YACvF,CAAC;iBAAM,IAAI,IAAI,CAAC,KAAK,KAAK,YAAY,CAAC,MAAM,EAAE,CAAC;gBAC9C,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,MAAM,CAAC,gBAAgB,EAAE,CAAC;oBACtD,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;oBACrC,OAAO,CAAC,IAAI,CAAC,oBAAoB,IAAI,CAAC,MAAM,CAAC,IAAI,6BAA6B,CAAC,CAAC;gBAClF,CAAC;YACH,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAED;;OAEG;IACK,YAAY,CAAC,QAAsB;QACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC;QAC5B,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC;QACtB,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,QAAQ,CAAC;QAE5B,iBAAiB;QACjB,QAAQ,QAAQ,EAAE,CAAC;YACjB,KAAK,YAAY,CAAC,IAAI;gBACpB,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;gBACvB,MAAM;YACR,KAAK,YAAY,CAAC,MAAM;gBACtB,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;gBACxB,MAAM;YACR,KAAK,YAAY,CAAC,SAAS;gBACzB,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC;gBAC3B,MAAM;QACV,CAAC;IACH,CAAC;IAED;;OAEG;IACK,eAAe,CAAC,KAAY;QAClC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM;YAAE,OAAO,KAAK,CAAC;QACtD,OAAO,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,IAAI,CACpC,UAAU,CAAC,EAAE,CAAC,KAAK,YAAY,UAAU,CAC1C,CAAC;IACJ,CAAC;IAED;;OAEG;IACK,SAAS,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAE7C,KAAK,CAAC,WAAW;QACvB,IAAI,OAAO,GAAe,GAAG,EAAE,GAAE,CAAC,CAAC;QACnC,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC;QACpC,IAAI,CAAC,SAAS,GAAG,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE;YAC3C,OAAO,GAAG,OAAO,CAAC;QACpB,CAAC,CAAC,CAAC;QACH,MAAM,YAAY,CAAC;QACnB,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,WAAW;QAChB,MAAM,KAAK,GAAG,IAAI,GAAG,EAA+B,CAAC;QACrD,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAC7C,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;QACtC,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,GAAG,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;;AAGH;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAwD;IACzF,MAAM,OAAO,GAAG,IAAI,cAAc,CAAC,MAAM,CAAC,CAAC;IAE3C,OAAO,UACL,MAAW,EACX,WAAmB,EACnB,UAAsC;QAEtC,MAAM,cAAc,GAAG,UAAU,CAAC,KAAM,CAAC;QAEzC,UAAU,CAAC,KAAK,GAAG,KAAK,WAAsB,GAAG,IAAW;YAC1D,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QACjE,CAAM,CAAC;QAEP,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAAwD;IAExD,MAAM,OAAO,GAAG,IAAI,cAAc,CAAC,MAAM,CAAC,CAAC;IAC3C,OAAO,CAAI,EAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;AAC1D,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resilience Patterns for TypeScript Services
|
|
3
|
+
*
|
|
4
|
+
* Provides circuit breaker, retry, bulkhead, rate limiting, and timeout patterns
|
|
5
|
+
* for building resilient microservices.
|
|
6
|
+
*
|
|
7
|
+
* @module @autonoma/service-utils/resilience
|
|
8
|
+
*/
|
|
9
|
+
export * from './circuit-breaker.js';
|
|
10
|
+
export * from './retry.js';
|
|
11
|
+
export * from './bulkhead.js';
|
|
12
|
+
export * from './rate-limiter.js';
|
|
13
|
+
export * from './timeout.js';
|
|
14
|
+
export * from './types.js';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resilience/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resilience Patterns for TypeScript Services
|
|
3
|
+
*
|
|
4
|
+
* Provides circuit breaker, retry, bulkhead, rate limiting, and timeout patterns
|
|
5
|
+
* for building resilient microservices.
|
|
6
|
+
*
|
|
7
|
+
* @module @autonoma/service-utils/resilience
|
|
8
|
+
*/
|
|
9
|
+
export * from './circuit-breaker.js';
|
|
10
|
+
export * from './retry.js';
|
|
11
|
+
export * from './bulkhead.js';
|
|
12
|
+
export * from './rate-limiter.js';
|
|
13
|
+
export * from './timeout.js';
|
|
14
|
+
export * from './types.js';
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resilience/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter Pattern
|
|
3
|
+
*
|
|
4
|
+
* Controls the rate of requests to prevent overload.
|
|
5
|
+
* Uses a sliding window algorithm for accurate rate limiting.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const limiter = new RateLimiter({
|
|
10
|
+
* name: 'trading-api',
|
|
11
|
+
* windowMs: 1000,
|
|
12
|
+
* maxRequests: 10
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* try {
|
|
16
|
+
* await limiter.acquire();
|
|
17
|
+
* const result = await executeTrade();
|
|
18
|
+
* } catch (error) {
|
|
19
|
+
* if (error instanceof RateLimitExceededError) {
|
|
20
|
+
* // Wait and retry
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import { RateLimiterConfig } from './types.js';
|
|
26
|
+
/**
|
|
27
|
+
* Rate Limiter using token bucket algorithm
|
|
28
|
+
*/
|
|
29
|
+
export declare class RateLimiter {
|
|
30
|
+
private static instances;
|
|
31
|
+
private readonly config;
|
|
32
|
+
private buckets;
|
|
33
|
+
private stats;
|
|
34
|
+
constructor(config: Partial<RateLimiterConfig> & {
|
|
35
|
+
name: string;
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Acquire a token (throws if rate limit exceeded)
|
|
39
|
+
*/
|
|
40
|
+
acquire(context?: any): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Try to acquire a token (returns false if rate limit exceeded)
|
|
43
|
+
*/
|
|
44
|
+
tryAcquire(context?: any): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Execute a function with rate limiting
|
|
47
|
+
*/
|
|
48
|
+
execute<T>(fn: () => Promise<T>, context?: any): Promise<T>;
|
|
49
|
+
/**
|
|
50
|
+
* Get remaining tokens for a key
|
|
51
|
+
*/
|
|
52
|
+
getRemainingTokens(context?: any): number;
|
|
53
|
+
/**
|
|
54
|
+
* Get rate limiter statistics
|
|
55
|
+
*/
|
|
56
|
+
getStats(): {
|
|
57
|
+
totalRequests: number;
|
|
58
|
+
allowedRequests: number;
|
|
59
|
+
rejectedRequests: number;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Reset rate limiter
|
|
63
|
+
*/
|
|
64
|
+
reset(context?: any): void;
|
|
65
|
+
/**
|
|
66
|
+
* Get or create bucket for a key
|
|
67
|
+
*/
|
|
68
|
+
private getOrCreateBucket;
|
|
69
|
+
/**
|
|
70
|
+
* Refill bucket based on elapsed time
|
|
71
|
+
*/
|
|
72
|
+
private refillBucket;
|
|
73
|
+
/**
|
|
74
|
+
* Calculate when next token will be available
|
|
75
|
+
*/
|
|
76
|
+
private calculateRetryAfter;
|
|
77
|
+
/**
|
|
78
|
+
* Get key for rate limiting
|
|
79
|
+
*/
|
|
80
|
+
private getKey;
|
|
81
|
+
/**
|
|
82
|
+
* Get stats for all rate limiters
|
|
83
|
+
*/
|
|
84
|
+
static getAllStats(): Map<string, ReturnType<RateLimiter['getStats']>>;
|
|
85
|
+
/**
|
|
86
|
+
* Get a rate limiter by name
|
|
87
|
+
*/
|
|
88
|
+
static get(name: string): RateLimiter | undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Decorator to apply rate limiting to a method
|
|
92
|
+
*/
|
|
93
|
+
export declare function withRateLimit(config: Partial<RateLimiterConfig> & {
|
|
94
|
+
name: string;
|
|
95
|
+
}): <T extends (...args: any[]) => Promise<any>>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T>;
|
|
96
|
+
/**
|
|
97
|
+
* Create a rate limiter wrapper function
|
|
98
|
+
*/
|
|
99
|
+
export declare function createRateLimiter(config: Partial<RateLimiterConfig> & {
|
|
100
|
+
name: string;
|
|
101
|
+
}): <T>(fn: () => Promise<T>, context?: any) => Promise<T>;
|
|
102
|
+
/**
|
|
103
|
+
* Rate limiter presets for common use cases
|
|
104
|
+
*/
|
|
105
|
+
export declare const rateLimiterPresets: {
|
|
106
|
+
/** Standard API rate limiting */
|
|
107
|
+
standard: Partial<RateLimiterConfig>;
|
|
108
|
+
/** Strict rate limiting for trading operations */
|
|
109
|
+
trading: Partial<RateLimiterConfig>;
|
|
110
|
+
/** Relaxed rate limiting for read operations */
|
|
111
|
+
relaxed: Partial<RateLimiterConfig>;
|
|
112
|
+
/** Per-user rate limiting */
|
|
113
|
+
perUser: Partial<RateLimiterConfig>;
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/resilience/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,iBAAiB,EAA0B,MAAM,YAAY,CAAC;AAcvE;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAC,SAAS,CAAuC;IAE/D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuC;IAC9D,OAAO,CAAC,OAAO,CAAuC;IAEtD,OAAO,CAAC,KAAK,CAIX;gBAEU,MAAM,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;IAKjE;;OAEG;IACG,OAAO,CAAC,OAAO,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB3C;;OAEG;IACH,UAAU,CAAC,OAAO,CAAC,EAAE,GAAG,GAAG,OAAO;IAkBlC;;OAEG;IACG,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC;IAWjE;;OAEG;IACH,kBAAkB,CAAC,OAAO,CAAC,EAAE,GAAG,GAAG,MAAM;IASzC;;OAEG;IACH,QAAQ;;;;;IAIR;;OAEG;IACH,KAAK,CAAC,OAAO,CAAC,EAAE,GAAG,GAAG,IAAI;IAS1B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAYzB;;OAEG;IACH,OAAO,CAAC,YAAY;IAWpB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAO3B;;OAEG;IACH,OAAO,CAAC,MAAM;IA2Bd;;OAEG;IACH,MAAM,CAAC,WAAW,IAAI,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC;IAQtE;;OAEG;IACH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;CAGlD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,IAGhE,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,EACzD,QAAQ,GAAG,EACX,aAAa,MAAM,EACnB,YAAY,uBAAuB,CAAC,CAAC,CAAC,gCAUzC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GACpD,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,CAGxD;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB;IAC7B,iCAAiC;cAI5B,OAAO,CAAC,iBAAiB,CAAC;IAE/B,kDAAkD;aAI7C,OAAO,CAAC,iBAAiB,CAAC;IAE/B,gDAAgD;aAI3C,OAAO,CAAC,iBAAiB,CAAC;IAE/B,6BAA6B;aAKxB,OAAO,CAAC,iBAAiB,CAAC;CAChC,CAAC"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter Pattern
|
|
3
|
+
*
|
|
4
|
+
* Controls the rate of requests to prevent overload.
|
|
5
|
+
* Uses a sliding window algorithm for accurate rate limiting.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const limiter = new RateLimiter({
|
|
10
|
+
* name: 'trading-api',
|
|
11
|
+
* windowMs: 1000,
|
|
12
|
+
* maxRequests: 10
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* try {
|
|
16
|
+
* await limiter.acquire();
|
|
17
|
+
* const result = await executeTrade();
|
|
18
|
+
* } catch (error) {
|
|
19
|
+
* if (error instanceof RateLimitExceededError) {
|
|
20
|
+
* // Wait and retry
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import { RateLimitExceededError } from './types.js';
|
|
26
|
+
const DEFAULT_CONFIG = {
|
|
27
|
+
windowMs: 60000,
|
|
28
|
+
maxRequests: 100,
|
|
29
|
+
skipSuccessfulRequests: false,
|
|
30
|
+
skipFailedRequests: false
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Rate Limiter using token bucket algorithm
|
|
34
|
+
*/
|
|
35
|
+
export class RateLimiter {
|
|
36
|
+
static instances = new Map();
|
|
37
|
+
config;
|
|
38
|
+
buckets = new Map();
|
|
39
|
+
stats = {
|
|
40
|
+
totalRequests: 0,
|
|
41
|
+
allowedRequests: 0,
|
|
42
|
+
rejectedRequests: 0
|
|
43
|
+
};
|
|
44
|
+
constructor(config) {
|
|
45
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
46
|
+
RateLimiter.instances.set(this.config.name, this);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Acquire a token (throws if rate limit exceeded)
|
|
50
|
+
*/
|
|
51
|
+
async acquire(context) {
|
|
52
|
+
const key = this.getKey(context);
|
|
53
|
+
this.stats.totalRequests++;
|
|
54
|
+
const bucket = this.getOrCreateBucket(key);
|
|
55
|
+
this.refillBucket(bucket);
|
|
56
|
+
if (bucket.tokens < 1) {
|
|
57
|
+
this.stats.rejectedRequests++;
|
|
58
|
+
this.config.onLimitReached?.(key);
|
|
59
|
+
// Calculate retry after
|
|
60
|
+
const retryAfter = this.calculateRetryAfter(bucket);
|
|
61
|
+
throw new RateLimitExceededError(key, retryAfter);
|
|
62
|
+
}
|
|
63
|
+
bucket.tokens -= 1;
|
|
64
|
+
this.stats.allowedRequests++;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Try to acquire a token (returns false if rate limit exceeded)
|
|
68
|
+
*/
|
|
69
|
+
tryAcquire(context) {
|
|
70
|
+
try {
|
|
71
|
+
const key = this.getKey(context);
|
|
72
|
+
const bucket = this.getOrCreateBucket(key);
|
|
73
|
+
this.refillBucket(bucket);
|
|
74
|
+
if (bucket.tokens < 1) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
bucket.tokens -= 1;
|
|
78
|
+
this.stats.allowedRequests++;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Execute a function with rate limiting
|
|
87
|
+
*/
|
|
88
|
+
async execute(fn, context) {
|
|
89
|
+
await this.acquire(context);
|
|
90
|
+
try {
|
|
91
|
+
const result = await fn();
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get remaining tokens for a key
|
|
100
|
+
*/
|
|
101
|
+
getRemainingTokens(context) {
|
|
102
|
+
const key = this.getKey(context);
|
|
103
|
+
const bucket = this.buckets.get(key);
|
|
104
|
+
if (!bucket)
|
|
105
|
+
return this.config.maxRequests;
|
|
106
|
+
this.refillBucket(bucket);
|
|
107
|
+
return Math.floor(bucket.tokens);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get rate limiter statistics
|
|
111
|
+
*/
|
|
112
|
+
getStats() {
|
|
113
|
+
return { ...this.stats };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Reset rate limiter
|
|
117
|
+
*/
|
|
118
|
+
reset(context) {
|
|
119
|
+
if (context) {
|
|
120
|
+
const key = this.getKey(context);
|
|
121
|
+
this.buckets.delete(key);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.buckets.clear();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get or create bucket for a key
|
|
129
|
+
*/
|
|
130
|
+
getOrCreateBucket(key) {
|
|
131
|
+
let bucket = this.buckets.get(key);
|
|
132
|
+
if (!bucket) {
|
|
133
|
+
bucket = {
|
|
134
|
+
tokens: this.config.maxRequests,
|
|
135
|
+
lastRefill: Date.now()
|
|
136
|
+
};
|
|
137
|
+
this.buckets.set(key, bucket);
|
|
138
|
+
}
|
|
139
|
+
return bucket;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Refill bucket based on elapsed time
|
|
143
|
+
*/
|
|
144
|
+
refillBucket(bucket) {
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
const elapsed = now - bucket.lastRefill;
|
|
147
|
+
// Calculate tokens to add based on elapsed time
|
|
148
|
+
const tokensToAdd = (elapsed / this.config.windowMs) * this.config.maxRequests;
|
|
149
|
+
bucket.tokens = Math.min(this.config.maxRequests, bucket.tokens + tokensToAdd);
|
|
150
|
+
bucket.lastRefill = now;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Calculate when next token will be available
|
|
154
|
+
*/
|
|
155
|
+
calculateRetryAfter(bucket) {
|
|
156
|
+
// Time until one token is refilled
|
|
157
|
+
const tokenRefillTime = this.config.windowMs / this.config.maxRequests;
|
|
158
|
+
const tokenDeficit = 1 - bucket.tokens;
|
|
159
|
+
return Math.ceil(tokenDeficit * tokenRefillTime);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get key for rate limiting
|
|
163
|
+
*/
|
|
164
|
+
getKey(context) {
|
|
165
|
+
if (!context)
|
|
166
|
+
return 'default';
|
|
167
|
+
if (this.config.keyGenerator) {
|
|
168
|
+
return this.config.keyGenerator(context);
|
|
169
|
+
}
|
|
170
|
+
if (typeof context === 'string')
|
|
171
|
+
return context;
|
|
172
|
+
if (typeof context === 'number')
|
|
173
|
+
return String(context);
|
|
174
|
+
// For objects, try to extract common identifiers
|
|
175
|
+
if (typeof context === 'object' && context !== null) {
|
|
176
|
+
// Try common identifier properties
|
|
177
|
+
if (context.id)
|
|
178
|
+
return String(context.id);
|
|
179
|
+
if (context.userId)
|
|
180
|
+
return String(context.userId);
|
|
181
|
+
if (context.key)
|
|
182
|
+
return String(context.key);
|
|
183
|
+
// Safely stringify, handling circular references
|
|
184
|
+
try {
|
|
185
|
+
return JSON.stringify(context);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Fallback to object hash if stringify fails
|
|
189
|
+
return `object_${Object.keys(context).sort().join('_')}`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return 'default';
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get stats for all rate limiters
|
|
196
|
+
*/
|
|
197
|
+
static getAllStats() {
|
|
198
|
+
const stats = new Map();
|
|
199
|
+
for (const [name, limiter] of this.instances) {
|
|
200
|
+
stats.set(name, limiter.getStats());
|
|
201
|
+
}
|
|
202
|
+
return stats;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get a rate limiter by name
|
|
206
|
+
*/
|
|
207
|
+
static get(name) {
|
|
208
|
+
return this.instances.get(name);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Decorator to apply rate limiting to a method
|
|
213
|
+
*/
|
|
214
|
+
export function withRateLimit(config) {
|
|
215
|
+
const limiter = new RateLimiter(config);
|
|
216
|
+
return function (target, propertyKey, descriptor) {
|
|
217
|
+
const originalMethod = descriptor.value;
|
|
218
|
+
descriptor.value = async function (...args) {
|
|
219
|
+
return limiter.execute(() => originalMethod.apply(this, args));
|
|
220
|
+
};
|
|
221
|
+
return descriptor;
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Create a rate limiter wrapper function
|
|
226
|
+
*/
|
|
227
|
+
export function createRateLimiter(config) {
|
|
228
|
+
const limiter = new RateLimiter(config);
|
|
229
|
+
return (fn, context) => limiter.execute(fn, context);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Rate limiter presets for common use cases
|
|
233
|
+
*/
|
|
234
|
+
export const rateLimiterPresets = {
|
|
235
|
+
/** Standard API rate limiting */
|
|
236
|
+
standard: {
|
|
237
|
+
windowMs: 60000,
|
|
238
|
+
maxRequests: 100
|
|
239
|
+
},
|
|
240
|
+
/** Strict rate limiting for trading operations */
|
|
241
|
+
trading: {
|
|
242
|
+
windowMs: 1000,
|
|
243
|
+
maxRequests: 10
|
|
244
|
+
},
|
|
245
|
+
/** Relaxed rate limiting for read operations */
|
|
246
|
+
relaxed: {
|
|
247
|
+
windowMs: 60000,
|
|
248
|
+
maxRequests: 1000
|
|
249
|
+
},
|
|
250
|
+
/** Per-user rate limiting */
|
|
251
|
+
perUser: {
|
|
252
|
+
windowMs: 60000,
|
|
253
|
+
maxRequests: 60,
|
|
254
|
+
keyGenerator: (context) => context.userId || 'anonymous'
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
//# sourceMappingURL=rate-limiter.js.map
|