@hazeljs/resilience 0.2.0-beta.41
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 +192 -0
- package/README.md +95 -0
- package/dist/__tests__/bulkhead-timeout.test.d.ts +2 -0
- package/dist/__tests__/bulkhead-timeout.test.d.ts.map +1 -0
- package/dist/__tests__/bulkhead-timeout.test.js +74 -0
- package/dist/__tests__/circuit-breaker.test.d.ts +2 -0
- package/dist/__tests__/circuit-breaker.test.d.ts.map +1 -0
- package/dist/__tests__/circuit-breaker.test.js +160 -0
- package/dist/__tests__/decorators.test.d.ts +2 -0
- package/dist/__tests__/decorators.test.d.ts.map +1 -0
- package/dist/__tests__/decorators.test.js +288 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.js +50 -0
- package/dist/__tests__/metrics.test.d.ts +2 -0
- package/dist/__tests__/metrics.test.d.ts.map +1 -0
- package/dist/__tests__/metrics.test.js +83 -0
- package/dist/__tests__/rate-limiter.test.d.ts +2 -0
- package/dist/__tests__/rate-limiter.test.d.ts.map +1 -0
- package/dist/__tests__/rate-limiter.test.js +143 -0
- package/dist/__tests__/retry-policy.test.d.ts +2 -0
- package/dist/__tests__/retry-policy.test.d.ts.map +1 -0
- package/dist/__tests__/retry-policy.test.js +84 -0
- package/dist/__tests__/sliding-window.test.d.ts +2 -0
- package/dist/__tests__/sliding-window.test.d.ts.map +1 -0
- package/dist/__tests__/sliding-window.test.js +93 -0
- package/dist/bulkhead/bulkhead.d.ts +34 -0
- package/dist/bulkhead/bulkhead.d.ts.map +1 -0
- package/dist/bulkhead/bulkhead.js +97 -0
- package/dist/circuit-breaker/circuit-breaker-registry.d.ts +38 -0
- package/dist/circuit-breaker/circuit-breaker-registry.d.ts.map +1 -0
- package/dist/circuit-breaker/circuit-breaker-registry.js +61 -0
- package/dist/circuit-breaker/circuit-breaker.d.ts +51 -0
- package/dist/circuit-breaker/circuit-breaker.d.ts.map +1 -0
- package/dist/circuit-breaker/circuit-breaker.js +182 -0
- package/dist/circuit-breaker/sliding-window.d.ts +49 -0
- package/dist/circuit-breaker/sliding-window.d.ts.map +1 -0
- package/dist/circuit-breaker/sliding-window.js +89 -0
- package/dist/decorators/index.d.ts +51 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +133 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/metrics/metrics-collector.d.ts +69 -0
- package/dist/metrics/metrics-collector.d.ts.map +1 -0
- package/dist/metrics/metrics-collector.js +180 -0
- package/dist/rate-limiter/rate-limiter.d.ts +72 -0
- package/dist/rate-limiter/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter/rate-limiter.js +147 -0
- package/dist/retry/retry-policy.d.ts +19 -0
- package/dist/retry/retry-policy.d.ts.map +1 -0
- package/dist/retry/retry-policy.js +87 -0
- package/dist/timeout/timeout.d.ts +23 -0
- package/dist/timeout/timeout.d.ts.map +1 -0
- package/dist/timeout/timeout.js +55 -0
- package/dist/types/index.d.ts +135 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +61 -0
- package/package.json +63 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
require("reflect-metadata");
|
|
13
|
+
const decorators_1 = require("../decorators");
|
|
14
|
+
const index_1 = require("../index");
|
|
15
|
+
const circuit_breaker_registry_1 = require("../circuit-breaker/circuit-breaker-registry");
|
|
16
|
+
const types_1 = require("../types");
|
|
17
|
+
describe('CircuitBreaker decorator', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
circuit_breaker_registry_1.CircuitBreakerRegistry.clear();
|
|
20
|
+
});
|
|
21
|
+
it('should wrap method with circuit breaker', async () => {
|
|
22
|
+
class TestService {
|
|
23
|
+
async success() {
|
|
24
|
+
return 'ok';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
__decorate([
|
|
28
|
+
(0, decorators_1.CircuitBreaker)({ failureThreshold: 5, resetTimeout: 100 }),
|
|
29
|
+
__metadata("design:type", Function),
|
|
30
|
+
__metadata("design:paramtypes", []),
|
|
31
|
+
__metadata("design:returntype", Promise)
|
|
32
|
+
], TestService.prototype, "success", null);
|
|
33
|
+
const svc = new TestService();
|
|
34
|
+
const result = await svc.success();
|
|
35
|
+
expect(result).toBe('ok');
|
|
36
|
+
});
|
|
37
|
+
it('should use fallback when configured and primary fails', async () => {
|
|
38
|
+
class TestService {
|
|
39
|
+
async primary() {
|
|
40
|
+
throw new Error('primary failed');
|
|
41
|
+
}
|
|
42
|
+
fallbackMethod() {
|
|
43
|
+
return 'fallback';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
__decorate([
|
|
47
|
+
(0, decorators_1.CircuitBreaker)({ failureThreshold: 2, resetTimeout: 100, fallback: 'fallbackMethod' }),
|
|
48
|
+
__metadata("design:type", Function),
|
|
49
|
+
__metadata("design:paramtypes", []),
|
|
50
|
+
__metadata("design:returntype", Promise)
|
|
51
|
+
], TestService.prototype, "primary", null);
|
|
52
|
+
const svc = new TestService();
|
|
53
|
+
const result = await svc.primary();
|
|
54
|
+
expect(result).toBe('fallback');
|
|
55
|
+
});
|
|
56
|
+
it('should use @Fallback decorator when primary fails', async () => {
|
|
57
|
+
class TestService {
|
|
58
|
+
async primary() {
|
|
59
|
+
throw new Error('primary failed');
|
|
60
|
+
}
|
|
61
|
+
async primaryFallback() {
|
|
62
|
+
return 'fallback';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
__decorate([
|
|
66
|
+
(0, decorators_1.CircuitBreaker)({ failureThreshold: 2, resetTimeout: 100 }),
|
|
67
|
+
__metadata("design:type", Function),
|
|
68
|
+
__metadata("design:paramtypes", []),
|
|
69
|
+
__metadata("design:returntype", Promise)
|
|
70
|
+
], TestService.prototype, "primary", null);
|
|
71
|
+
__decorate([
|
|
72
|
+
(0, decorators_1.Fallback)('primary'),
|
|
73
|
+
__metadata("design:type", Function),
|
|
74
|
+
__metadata("design:paramtypes", []),
|
|
75
|
+
__metadata("design:returntype", Promise)
|
|
76
|
+
], TestService.prototype, "primaryFallback", null);
|
|
77
|
+
const svc = new TestService();
|
|
78
|
+
const result = await svc.primary();
|
|
79
|
+
expect(result).toBe('fallback');
|
|
80
|
+
});
|
|
81
|
+
it('should throw when no fallback and primary fails', async () => {
|
|
82
|
+
class TestService {
|
|
83
|
+
async primary() {
|
|
84
|
+
throw new Error('primary failed');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
__decorate([
|
|
88
|
+
(0, decorators_1.CircuitBreaker)({ failureThreshold: 2, resetTimeout: 100 }),
|
|
89
|
+
__metadata("design:type", Function),
|
|
90
|
+
__metadata("design:paramtypes", []),
|
|
91
|
+
__metadata("design:returntype", Promise)
|
|
92
|
+
], TestService.prototype, "primary", null);
|
|
93
|
+
const svc = new TestService();
|
|
94
|
+
await expect(svc.primary()).rejects.toThrow('primary failed');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('Retry decorator', () => {
|
|
98
|
+
it('should retry on failure and eventually succeed', async () => {
|
|
99
|
+
let attempts = 0;
|
|
100
|
+
class TestService {
|
|
101
|
+
async flaky() {
|
|
102
|
+
attempts++;
|
|
103
|
+
if (attempts < 3)
|
|
104
|
+
throw new Error('fail');
|
|
105
|
+
return 'ok';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
__decorate([
|
|
109
|
+
(0, decorators_1.Retry)({ maxAttempts: 5, backoff: 'fixed', baseDelay: 10 }),
|
|
110
|
+
__metadata("design:type", Function),
|
|
111
|
+
__metadata("design:paramtypes", []),
|
|
112
|
+
__metadata("design:returntype", Promise)
|
|
113
|
+
], TestService.prototype, "flaky", null);
|
|
114
|
+
const svc = new TestService();
|
|
115
|
+
const result = await svc.flaky();
|
|
116
|
+
expect(result).toBe('ok');
|
|
117
|
+
expect(attempts).toBe(3);
|
|
118
|
+
});
|
|
119
|
+
it('should throw after max attempts exhausted', async () => {
|
|
120
|
+
class TestService {
|
|
121
|
+
async alwaysFail() {
|
|
122
|
+
throw new Error('always fail');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
__decorate([
|
|
126
|
+
(0, decorators_1.Retry)({ maxAttempts: 2, backoff: 'fixed', baseDelay: 10 }),
|
|
127
|
+
__metadata("design:type", Function),
|
|
128
|
+
__metadata("design:paramtypes", []),
|
|
129
|
+
__metadata("design:returntype", Promise)
|
|
130
|
+
], TestService.prototype, "alwaysFail", null);
|
|
131
|
+
const svc = new TestService();
|
|
132
|
+
await expect(svc.alwaysFail()).rejects.toThrow('Retry exhausted');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('Timeout decorator', () => {
|
|
136
|
+
it('should resolve when within timeout', async () => {
|
|
137
|
+
class TestService {
|
|
138
|
+
async fast() {
|
|
139
|
+
return 'ok';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
__decorate([
|
|
143
|
+
(0, decorators_1.Timeout)(500),
|
|
144
|
+
__metadata("design:type", Function),
|
|
145
|
+
__metadata("design:paramtypes", []),
|
|
146
|
+
__metadata("design:returntype", Promise)
|
|
147
|
+
], TestService.prototype, "fast", null);
|
|
148
|
+
const svc = new TestService();
|
|
149
|
+
const result = await svc.fast();
|
|
150
|
+
expect(result).toBe('ok');
|
|
151
|
+
});
|
|
152
|
+
it('should reject when exceeding timeout', async () => {
|
|
153
|
+
class TestService {
|
|
154
|
+
async slow() {
|
|
155
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
156
|
+
return 'slow';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
__decorate([
|
|
160
|
+
(0, decorators_1.Timeout)(50),
|
|
161
|
+
__metadata("design:type", Function),
|
|
162
|
+
__metadata("design:paramtypes", []),
|
|
163
|
+
__metadata("design:returntype", Promise)
|
|
164
|
+
], TestService.prototype, "slow", null);
|
|
165
|
+
const svc = new TestService();
|
|
166
|
+
await expect(svc.slow()).rejects.toThrow();
|
|
167
|
+
});
|
|
168
|
+
it('should accept config object', async () => {
|
|
169
|
+
class TestService {
|
|
170
|
+
async fast() {
|
|
171
|
+
return 'ok';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
__decorate([
|
|
175
|
+
(0, decorators_1.Timeout)({ duration: 500 }),
|
|
176
|
+
__metadata("design:type", Function),
|
|
177
|
+
__metadata("design:paramtypes", []),
|
|
178
|
+
__metadata("design:returntype", Promise)
|
|
179
|
+
], TestService.prototype, "fast", null);
|
|
180
|
+
const svc = new TestService();
|
|
181
|
+
const result = await svc.fast();
|
|
182
|
+
expect(result).toBe('ok');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe('Bulkhead decorator', () => {
|
|
186
|
+
it('should limit concurrent executions', async () => {
|
|
187
|
+
class TestService {
|
|
188
|
+
async limited() {
|
|
189
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
190
|
+
return 'done';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
__decorate([
|
|
194
|
+
(0, decorators_1.Bulkhead)({ maxConcurrent: 1, maxQueue: 0 }),
|
|
195
|
+
__metadata("design:type", Function),
|
|
196
|
+
__metadata("design:paramtypes", []),
|
|
197
|
+
__metadata("design:returntype", Promise)
|
|
198
|
+
], TestService.prototype, "limited", null);
|
|
199
|
+
const svc = new TestService();
|
|
200
|
+
const [a, b] = await Promise.all([
|
|
201
|
+
svc.limited(),
|
|
202
|
+
svc.limited().catch((e) => e.message),
|
|
203
|
+
]);
|
|
204
|
+
expect(a).toBe('done');
|
|
205
|
+
expect(b).toContain('Bulkhead');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
describe('RateLimit decorator', () => {
|
|
209
|
+
it('should allow requests within limit', async () => {
|
|
210
|
+
class TestService {
|
|
211
|
+
async limited() {
|
|
212
|
+
return 'ok';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
__decorate([
|
|
216
|
+
(0, decorators_1.RateLimit)({ strategy: 'sliding-window', max: 5, window: 60000 }),
|
|
217
|
+
__metadata("design:type", Function),
|
|
218
|
+
__metadata("design:paramtypes", []),
|
|
219
|
+
__metadata("design:returntype", Promise)
|
|
220
|
+
], TestService.prototype, "limited", null);
|
|
221
|
+
const svc = new TestService();
|
|
222
|
+
const result = await svc.limited();
|
|
223
|
+
expect(result).toBe('ok');
|
|
224
|
+
});
|
|
225
|
+
it('should throw RateLimitError when limit exceeded', async () => {
|
|
226
|
+
class TestService {
|
|
227
|
+
async limited() {
|
|
228
|
+
return 'ok';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
__decorate([
|
|
232
|
+
(0, decorators_1.RateLimit)({ strategy: 'sliding-window', max: 1, window: 60000 }),
|
|
233
|
+
__metadata("design:type", Function),
|
|
234
|
+
__metadata("design:paramtypes", []),
|
|
235
|
+
__metadata("design:returntype", Promise)
|
|
236
|
+
], TestService.prototype, "limited", null);
|
|
237
|
+
const svc = new TestService();
|
|
238
|
+
await svc.limited();
|
|
239
|
+
await expect(svc.limited()).rejects.toThrow(types_1.RateLimitError);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
describe('Fallback decorator', () => {
|
|
243
|
+
beforeEach(() => {
|
|
244
|
+
circuit_breaker_registry_1.CircuitBreakerRegistry.clear();
|
|
245
|
+
});
|
|
246
|
+
it('should store fallback metadata', async () => {
|
|
247
|
+
class TestService {
|
|
248
|
+
async primary() {
|
|
249
|
+
return 'ok';
|
|
250
|
+
}
|
|
251
|
+
async primaryFallback() {
|
|
252
|
+
return 'fallback';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
__decorate([
|
|
256
|
+
(0, decorators_1.CircuitBreaker)(),
|
|
257
|
+
__metadata("design:type", Function),
|
|
258
|
+
__metadata("design:paramtypes", []),
|
|
259
|
+
__metadata("design:returntype", Promise)
|
|
260
|
+
], TestService.prototype, "primary", null);
|
|
261
|
+
__decorate([
|
|
262
|
+
(0, decorators_1.Fallback)('primary'),
|
|
263
|
+
__metadata("design:type", Function),
|
|
264
|
+
__metadata("design:paramtypes", []),
|
|
265
|
+
__metadata("design:returntype", Promise)
|
|
266
|
+
], TestService.prototype, "primaryFallback", null);
|
|
267
|
+
const svc = new TestService();
|
|
268
|
+
const result = await svc.primary();
|
|
269
|
+
expect(result).toBe('ok');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
describe('Re-exported decorators', () => {
|
|
273
|
+
it('WithCircuitBreaker should equal CircuitBreaker', () => {
|
|
274
|
+
expect(index_1.WithCircuitBreaker).toBe(decorators_1.CircuitBreaker);
|
|
275
|
+
});
|
|
276
|
+
it('WithRetry should equal Retry', () => {
|
|
277
|
+
expect(index_1.WithRetry).toBe(decorators_1.Retry);
|
|
278
|
+
});
|
|
279
|
+
it('WithTimeout should equal Timeout', () => {
|
|
280
|
+
expect(index_1.WithTimeout).toBe(decorators_1.Timeout);
|
|
281
|
+
});
|
|
282
|
+
it('WithBulkhead should equal Bulkhead', () => {
|
|
283
|
+
expect(index_1.WithBulkhead).toBe(decorators_1.Bulkhead);
|
|
284
|
+
});
|
|
285
|
+
it('WithRateLimit should equal RateLimit', () => {
|
|
286
|
+
expect(index_1.WithRateLimit).toBe(decorators_1.RateLimit);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Index exports test - verifies all public API is exported correctly
|
|
5
|
+
*/
|
|
6
|
+
const index_1 = require("../index");
|
|
7
|
+
describe('resilience index exports', () => {
|
|
8
|
+
it('should export circuit breaker components', () => {
|
|
9
|
+
expect(index_1.CircuitBreaker).toBeDefined();
|
|
10
|
+
expect(index_1.CircuitBreakerRegistry).toBeDefined();
|
|
11
|
+
expect(index_1.CountBasedSlidingWindow).toBeDefined();
|
|
12
|
+
expect(index_1.TimeBasedSlidingWindow).toBeDefined();
|
|
13
|
+
expect(index_1.createSlidingWindow).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
it('should export retry policy', () => {
|
|
16
|
+
expect(index_1.RetryPolicy).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
it('should export timeout', () => {
|
|
19
|
+
expect(index_1.Timeout).toBeDefined();
|
|
20
|
+
expect(index_1.withTimeout).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
it('should export bulkhead', () => {
|
|
23
|
+
expect(index_1.Bulkhead).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
it('should export rate limiter', () => {
|
|
26
|
+
expect(index_1.RateLimiter).toBeDefined();
|
|
27
|
+
expect(index_1.TokenBucketLimiter).toBeDefined();
|
|
28
|
+
expect(index_1.SlidingWindowLimiter).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
it('should export metrics', () => {
|
|
31
|
+
expect(index_1.MetricsCollector).toBeDefined();
|
|
32
|
+
expect(index_1.MetricsRegistry).toBeDefined();
|
|
33
|
+
});
|
|
34
|
+
it('should export decorators', () => {
|
|
35
|
+
expect(index_1.WithCircuitBreaker).toBeDefined();
|
|
36
|
+
expect(index_1.WithRetry).toBeDefined();
|
|
37
|
+
expect(index_1.WithTimeout).toBeDefined();
|
|
38
|
+
expect(index_1.WithBulkhead).toBeDefined();
|
|
39
|
+
expect(index_1.Fallback).toBeDefined();
|
|
40
|
+
expect(index_1.WithRateLimit).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
it('should export types and errors', () => {
|
|
43
|
+
expect(index_1.CircuitState).toBeDefined();
|
|
44
|
+
expect(index_1.CircuitBreakerError).toBeDefined();
|
|
45
|
+
expect(index_1.RetryExhaustedError).toBeDefined();
|
|
46
|
+
expect(index_1.TimeoutError).toBeDefined();
|
|
47
|
+
expect(index_1.BulkheadError).toBeDefined();
|
|
48
|
+
expect(index_1.RateLimitError).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/metrics.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const metrics_collector_1 = require("../metrics/metrics-collector");
|
|
4
|
+
describe('MetricsCollector', () => {
|
|
5
|
+
let collector;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
collector = new metrics_collector_1.MetricsCollector(60000);
|
|
8
|
+
});
|
|
9
|
+
it('should start with empty metrics', () => {
|
|
10
|
+
const snapshot = collector.getSnapshot();
|
|
11
|
+
expect(snapshot.totalCalls).toBe(0);
|
|
12
|
+
expect(snapshot.failureRate).toBe(0);
|
|
13
|
+
});
|
|
14
|
+
it('should record successes', () => {
|
|
15
|
+
collector.recordSuccess(100);
|
|
16
|
+
collector.recordSuccess(200);
|
|
17
|
+
const snapshot = collector.getSnapshot();
|
|
18
|
+
expect(snapshot.totalCalls).toBe(2);
|
|
19
|
+
expect(snapshot.successCalls).toBe(2);
|
|
20
|
+
expect(snapshot.failureCalls).toBe(0);
|
|
21
|
+
expect(snapshot.failureRate).toBe(0);
|
|
22
|
+
expect(snapshot.averageResponseTime).toBe(150);
|
|
23
|
+
});
|
|
24
|
+
it('should record failures', () => {
|
|
25
|
+
collector.recordSuccess(100);
|
|
26
|
+
collector.recordFailure(200, 'error');
|
|
27
|
+
const snapshot = collector.getSnapshot();
|
|
28
|
+
expect(snapshot.totalCalls).toBe(2);
|
|
29
|
+
expect(snapshot.successCalls).toBe(1);
|
|
30
|
+
expect(snapshot.failureCalls).toBe(1);
|
|
31
|
+
expect(snapshot.failureRate).toBe(50);
|
|
32
|
+
});
|
|
33
|
+
it('should calculate percentiles', () => {
|
|
34
|
+
// Record various durations
|
|
35
|
+
for (let i = 1; i <= 100; i++) {
|
|
36
|
+
collector.recordSuccess(i);
|
|
37
|
+
}
|
|
38
|
+
const snapshot = collector.getSnapshot();
|
|
39
|
+
expect(snapshot.p50ResponseTime).toBeGreaterThanOrEqual(49);
|
|
40
|
+
expect(snapshot.p50ResponseTime).toBeLessThanOrEqual(51);
|
|
41
|
+
expect(snapshot.p99ResponseTime).toBeGreaterThanOrEqual(98);
|
|
42
|
+
expect(snapshot.minResponseTime).toBe(1);
|
|
43
|
+
expect(snapshot.maxResponseTime).toBe(100);
|
|
44
|
+
});
|
|
45
|
+
it('should evict entries outside the window', async () => {
|
|
46
|
+
const shortWindow = new metrics_collector_1.MetricsCollector(100); // 100ms window
|
|
47
|
+
shortWindow.recordSuccess(50);
|
|
48
|
+
expect(shortWindow.getCallCount()).toBe(1);
|
|
49
|
+
// Wait for entries to expire
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
51
|
+
expect(shortWindow.getCallCount()).toBe(0);
|
|
52
|
+
});
|
|
53
|
+
it('should reset metrics', () => {
|
|
54
|
+
collector.recordSuccess(100);
|
|
55
|
+
collector.recordFailure(200);
|
|
56
|
+
collector.reset();
|
|
57
|
+
expect(collector.getCallCount()).toBe(0);
|
|
58
|
+
expect(collector.getFailureRate()).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('MetricsRegistry', () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
metrics_collector_1.MetricsRegistry.clear();
|
|
64
|
+
});
|
|
65
|
+
it('should create and cache collectors', () => {
|
|
66
|
+
const c1 = metrics_collector_1.MetricsRegistry.getOrCreate('test');
|
|
67
|
+
const c2 = metrics_collector_1.MetricsRegistry.getOrCreate('test');
|
|
68
|
+
expect(c1).toBe(c2);
|
|
69
|
+
});
|
|
70
|
+
it('should return different collectors for different names', () => {
|
|
71
|
+
const c1 = metrics_collector_1.MetricsRegistry.getOrCreate('a');
|
|
72
|
+
const c2 = metrics_collector_1.MetricsRegistry.getOrCreate('b');
|
|
73
|
+
expect(c1).not.toBe(c2);
|
|
74
|
+
});
|
|
75
|
+
it('should get all snapshots', () => {
|
|
76
|
+
metrics_collector_1.MetricsRegistry.getOrCreate('a').recordSuccess(100);
|
|
77
|
+
metrics_collector_1.MetricsRegistry.getOrCreate('b').recordFailure(200);
|
|
78
|
+
const snapshots = metrics_collector_1.MetricsRegistry.getAllSnapshots();
|
|
79
|
+
expect(Object.keys(snapshots)).toHaveLength(2);
|
|
80
|
+
expect(snapshots['a'].totalCalls).toBe(1);
|
|
81
|
+
expect(snapshots['b'].totalCalls).toBe(1);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/rate-limiter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const rate_limiter_1 = require("../rate-limiter/rate-limiter");
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
describe('TokenBucketLimiter', () => {
|
|
6
|
+
it('should allow requests within capacity', () => {
|
|
7
|
+
const limiter = new rate_limiter_1.TokenBucketLimiter(5, 10);
|
|
8
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
9
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
10
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
11
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
12
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
it('should reject when tokens exhausted', () => {
|
|
15
|
+
const limiter = new rate_limiter_1.TokenBucketLimiter(2, 10);
|
|
16
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
17
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
18
|
+
expect(limiter.tryAcquire()).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
it('should refill tokens over time', async () => {
|
|
21
|
+
const limiter = new rate_limiter_1.TokenBucketLimiter(1, 100); // 100 tokens/sec
|
|
22
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
23
|
+
expect(limiter.tryAcquire()).toBe(false);
|
|
24
|
+
await new Promise((r) => setTimeout(r, 15)); // ~1.5 tokens
|
|
25
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
it('getRetryAfterMs should return 0 when token available', () => {
|
|
28
|
+
const limiter = new rate_limiter_1.TokenBucketLimiter(5, 10);
|
|
29
|
+
expect(limiter.getRetryAfterMs()).toBe(0);
|
|
30
|
+
});
|
|
31
|
+
it('getRetryAfterMs should return positive when exhausted', () => {
|
|
32
|
+
const limiter = new rate_limiter_1.TokenBucketLimiter(1, 100);
|
|
33
|
+
limiter.tryAcquire();
|
|
34
|
+
const retryAfter = limiter.getRetryAfterMs();
|
|
35
|
+
expect(retryAfter).toBeGreaterThan(0);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('SlidingWindowLimiter', () => {
|
|
39
|
+
it('should allow requests within max', () => {
|
|
40
|
+
const limiter = new rate_limiter_1.SlidingWindowLimiter(5, 60000);
|
|
41
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
42
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
43
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
44
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
45
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
it('should reject when limit exceeded', () => {
|
|
48
|
+
const limiter = new rate_limiter_1.SlidingWindowLimiter(2, 60000);
|
|
49
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
50
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
51
|
+
expect(limiter.tryAcquire()).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
it('getRetryAfterMs should return 0 when under limit', () => {
|
|
54
|
+
const limiter = new rate_limiter_1.SlidingWindowLimiter(5, 60000);
|
|
55
|
+
limiter.tryAcquire();
|
|
56
|
+
expect(limiter.getRetryAfterMs()).toBe(0);
|
|
57
|
+
});
|
|
58
|
+
it('getRetryAfterMs should return positive when at limit', () => {
|
|
59
|
+
const limiter = new rate_limiter_1.SlidingWindowLimiter(1, 60000);
|
|
60
|
+
limiter.tryAcquire();
|
|
61
|
+
expect(limiter.getRetryAfterMs()).toBeGreaterThan(0);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('RateLimiter', () => {
|
|
65
|
+
describe('token-bucket strategy', () => {
|
|
66
|
+
it('should execute fn when within limit', async () => {
|
|
67
|
+
const limiter = new rate_limiter_1.RateLimiter({
|
|
68
|
+
strategy: 'token-bucket',
|
|
69
|
+
max: 5,
|
|
70
|
+
window: 60000,
|
|
71
|
+
});
|
|
72
|
+
const result = await limiter.execute(() => Promise.resolve('ok'));
|
|
73
|
+
expect(result).toBe('ok');
|
|
74
|
+
});
|
|
75
|
+
it('should throw RateLimitError when limit exceeded', async () => {
|
|
76
|
+
const limiter = new rate_limiter_1.RateLimiter({
|
|
77
|
+
strategy: 'token-bucket',
|
|
78
|
+
max: 1,
|
|
79
|
+
window: 60000,
|
|
80
|
+
});
|
|
81
|
+
await limiter.execute(() => Promise.resolve('first'));
|
|
82
|
+
await expect(limiter.execute(() => Promise.resolve('second'))).rejects.toThrow(types_1.RateLimitError);
|
|
83
|
+
});
|
|
84
|
+
it('should include retryAfterMs in RateLimitError', async () => {
|
|
85
|
+
const limiter = new rate_limiter_1.RateLimiter({
|
|
86
|
+
strategy: 'token-bucket',
|
|
87
|
+
max: 1,
|
|
88
|
+
window: 60000,
|
|
89
|
+
});
|
|
90
|
+
await limiter.execute(() => Promise.resolve());
|
|
91
|
+
try {
|
|
92
|
+
await limiter.execute(() => Promise.resolve());
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
expect(e).toBeInstanceOf(types_1.RateLimitError);
|
|
96
|
+
expect(e.retryAfterMs).toBeGreaterThan(0);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe('sliding-window strategy', () => {
|
|
101
|
+
it('should execute fn when within limit', async () => {
|
|
102
|
+
const limiter = new rate_limiter_1.RateLimiter({
|
|
103
|
+
strategy: 'sliding-window',
|
|
104
|
+
max: 5,
|
|
105
|
+
window: 60000,
|
|
106
|
+
});
|
|
107
|
+
const result = await limiter.execute(() => Promise.resolve('ok'));
|
|
108
|
+
expect(result).toBe('ok');
|
|
109
|
+
});
|
|
110
|
+
it('should throw RateLimitError when limit exceeded', async () => {
|
|
111
|
+
const limiter = new rate_limiter_1.RateLimiter({
|
|
112
|
+
strategy: 'sliding-window',
|
|
113
|
+
max: 1,
|
|
114
|
+
window: 60000,
|
|
115
|
+
});
|
|
116
|
+
await limiter.execute(() => Promise.resolve('first'));
|
|
117
|
+
await expect(limiter.execute(() => Promise.resolve('second'))).rejects.toThrow(types_1.RateLimitError);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
it('tryAcquire should consume token and return boolean', () => {
|
|
121
|
+
const limiter = new rate_limiter_1.RateLimiter({
|
|
122
|
+
strategy: 'sliding-window',
|
|
123
|
+
max: 1,
|
|
124
|
+
window: 60000,
|
|
125
|
+
});
|
|
126
|
+
expect(limiter.tryAcquire()).toBe(true);
|
|
127
|
+
expect(limiter.tryAcquire()).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
it('getRetryAfterMs should return 0 when under limit', () => {
|
|
130
|
+
const limiter = new rate_limiter_1.RateLimiter({
|
|
131
|
+
strategy: 'sliding-window',
|
|
132
|
+
max: 5,
|
|
133
|
+
window: 60000,
|
|
134
|
+
});
|
|
135
|
+
expect(limiter.getRetryAfterMs()).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
it('getStrategy should return configured strategy', () => {
|
|
138
|
+
const tb = new rate_limiter_1.RateLimiter({ strategy: 'token-bucket', max: 10, window: 1000 });
|
|
139
|
+
expect(tb.getStrategy()).toBe('token-bucket');
|
|
140
|
+
const sw = new rate_limiter_1.RateLimiter({ strategy: 'sliding-window', max: 10, window: 1000 });
|
|
141
|
+
expect(sw.getStrategy()).toBe('sliding-window');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry-policy.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/retry-policy.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const retry_policy_1 = require("../retry/retry-policy");
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
describe('RetryPolicy', () => {
|
|
6
|
+
it('should succeed on first try', async () => {
|
|
7
|
+
const policy = new retry_policy_1.RetryPolicy({ maxAttempts: 3 });
|
|
8
|
+
const result = await policy.execute(() => Promise.resolve('ok'));
|
|
9
|
+
expect(result).toBe('ok');
|
|
10
|
+
});
|
|
11
|
+
it('should retry on failure and eventually succeed', async () => {
|
|
12
|
+
let attempts = 0;
|
|
13
|
+
const policy = new retry_policy_1.RetryPolicy({
|
|
14
|
+
maxAttempts: 3,
|
|
15
|
+
baseDelay: 10,
|
|
16
|
+
jitter: false,
|
|
17
|
+
backoff: 'fixed',
|
|
18
|
+
});
|
|
19
|
+
const result = await policy.execute(async () => {
|
|
20
|
+
attempts++;
|
|
21
|
+
if (attempts < 3)
|
|
22
|
+
throw new Error('transient');
|
|
23
|
+
return 'recovered';
|
|
24
|
+
});
|
|
25
|
+
expect(result).toBe('recovered');
|
|
26
|
+
expect(attempts).toBe(3);
|
|
27
|
+
});
|
|
28
|
+
it('should throw RetryExhaustedError after max attempts', async () => {
|
|
29
|
+
const policy = new retry_policy_1.RetryPolicy({
|
|
30
|
+
maxAttempts: 2,
|
|
31
|
+
baseDelay: 10,
|
|
32
|
+
jitter: false,
|
|
33
|
+
backoff: 'fixed',
|
|
34
|
+
});
|
|
35
|
+
await expect(policy.execute(() => Promise.reject(new Error('always-fail')))).rejects.toThrow(types_1.RetryExhaustedError);
|
|
36
|
+
});
|
|
37
|
+
it('should not retry non-retryable errors', async () => {
|
|
38
|
+
let attempts = 0;
|
|
39
|
+
const policy = new retry_policy_1.RetryPolicy({
|
|
40
|
+
maxAttempts: 5,
|
|
41
|
+
baseDelay: 10,
|
|
42
|
+
retryPredicate: (error) => error instanceof Error && error.message !== 'fatal',
|
|
43
|
+
});
|
|
44
|
+
await expect(policy.execute(async () => {
|
|
45
|
+
attempts++;
|
|
46
|
+
throw new Error('fatal');
|
|
47
|
+
})).rejects.toThrow('fatal');
|
|
48
|
+
expect(attempts).toBe(1); // No retries for non-retryable
|
|
49
|
+
});
|
|
50
|
+
it('should call onRetry callback', async () => {
|
|
51
|
+
const retries = [];
|
|
52
|
+
const policy = new retry_policy_1.RetryPolicy({
|
|
53
|
+
maxAttempts: 3,
|
|
54
|
+
baseDelay: 10,
|
|
55
|
+
jitter: false,
|
|
56
|
+
backoff: 'fixed',
|
|
57
|
+
onRetry: (_error, attempt) => retries.push(attempt),
|
|
58
|
+
});
|
|
59
|
+
await policy.execute(() => Promise.reject(new Error('fail'))).catch(() => { });
|
|
60
|
+
expect(retries).toEqual([1, 2, 3]);
|
|
61
|
+
});
|
|
62
|
+
it('should use exponential backoff', async () => {
|
|
63
|
+
const timestamps = [];
|
|
64
|
+
const policy = new retry_policy_1.RetryPolicy({
|
|
65
|
+
maxAttempts: 3,
|
|
66
|
+
baseDelay: 50,
|
|
67
|
+
jitter: false,
|
|
68
|
+
backoff: 'exponential',
|
|
69
|
+
});
|
|
70
|
+
await policy
|
|
71
|
+
.execute(async () => {
|
|
72
|
+
timestamps.push(Date.now());
|
|
73
|
+
throw new Error('fail');
|
|
74
|
+
})
|
|
75
|
+
.catch(() => { });
|
|
76
|
+
// Should have 4 timestamps (initial + 3 retries)
|
|
77
|
+
expect(timestamps.length).toBe(4);
|
|
78
|
+
// Delays should roughly double: ~50ms, ~100ms, ~200ms
|
|
79
|
+
const delay1 = timestamps[1] - timestamps[0];
|
|
80
|
+
const delay2 = timestamps[2] - timestamps[1];
|
|
81
|
+
expect(delay1).toBeGreaterThanOrEqual(30); // ~50ms with some tolerance
|
|
82
|
+
expect(delay2).toBeGreaterThanOrEqual(60); // ~100ms with some tolerance
|
|
83
|
+
});
|
|
84
|
+
});
|