@stimulcross/rate-limiter 0.0.1
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/.editorconfig +21 -0
- package/.github/workflows/node.yml +87 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.megaignore +8 -0
- package/.prettierignore +3 -0
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/commitlint.config.js +8 -0
- package/eslint.config.js +65 -0
- package/lint-staged.config.js +4 -0
- package/package.json +89 -0
- package/prettier.config.cjs +1 -0
- package/src/core/cancellable.ts +4 -0
- package/src/core/clock.ts +9 -0
- package/src/core/decision.ts +27 -0
- package/src/core/rate-limit-policy.ts +15 -0
- package/src/core/rate-limiter-status.ts +14 -0
- package/src/core/rate-limiter.ts +37 -0
- package/src/core/state-storage.ts +51 -0
- package/src/enums/rate-limit-error-code.ts +29 -0
- package/src/errors/custom.error.ts +14 -0
- package/src/errors/invalid-cost.error.ts +33 -0
- package/src/errors/rate-limit.error.ts +91 -0
- package/src/errors/rate-limiter-destroyed.error.ts +8 -0
- package/src/index.ts +11 -0
- package/src/interfaces/rate-limiter-options.ts +84 -0
- package/src/interfaces/rate-limiter-queue-options.ts +45 -0
- package/src/interfaces/rate-limiter-run-options.ts +58 -0
- package/src/limiters/abstract-rate-limiter.ts +206 -0
- package/src/limiters/composite.policy.ts +102 -0
- package/src/limiters/fixed-window/fixed-window.limiter.ts +121 -0
- package/src/limiters/fixed-window/fixed-window.options.ts +29 -0
- package/src/limiters/fixed-window/fixed-window.policy.ts +159 -0
- package/src/limiters/fixed-window/fixed-window.state.ts +10 -0
- package/src/limiters/fixed-window/fixed-window.status.ts +46 -0
- package/src/limiters/fixed-window/index.ts +4 -0
- package/src/limiters/generic-cell/generic-cell.limiter.ts +108 -0
- package/src/limiters/generic-cell/generic-cell.options.ts +23 -0
- package/src/limiters/generic-cell/generic-cell.policy.ts +115 -0
- package/src/limiters/generic-cell/generic-cell.state.ts +8 -0
- package/src/limiters/generic-cell/generic-cell.status.ts +54 -0
- package/src/limiters/generic-cell/index.ts +4 -0
- package/src/limiters/http-response-based/http-limit-info.extractor.ts +20 -0
- package/src/limiters/http-response-based/http-limit.info.ts +41 -0
- package/src/limiters/http-response-based/http-response-based-limiter.options.ts +18 -0
- package/src/limiters/http-response-based/http-response-based-limiter.state.ts +13 -0
- package/src/limiters/http-response-based/http-response-based-limiter.status.ts +74 -0
- package/src/limiters/http-response-based/http-response-based.limiter.ts +512 -0
- package/src/limiters/http-response-based/index.ts +6 -0
- package/src/limiters/leaky-bucket/index.ts +4 -0
- package/src/limiters/leaky-bucket/leaky-bucket.limiter.ts +105 -0
- package/src/limiters/leaky-bucket/leaky-bucket.options.ts +23 -0
- package/src/limiters/leaky-bucket/leaky-bucket.policy.ts +134 -0
- package/src/limiters/leaky-bucket/leaky-bucket.state.ts +9 -0
- package/src/limiters/leaky-bucket/leaky-bucket.status.ts +36 -0
- package/src/limiters/sliding-window-counter/index.ts +7 -0
- package/src/limiters/sliding-window-counter/sliding-window-counter.limiter.ts +76 -0
- package/src/limiters/sliding-window-counter/sliding-window-counter.options.ts +20 -0
- package/src/limiters/sliding-window-counter/sliding-window-counter.policy.ts +167 -0
- package/src/limiters/sliding-window-counter/sliding-window-counter.state.ts +10 -0
- package/src/limiters/sliding-window-counter/sliding-window-counter.status.ts +53 -0
- package/src/limiters/sliding-window-log/index.ts +4 -0
- package/src/limiters/sliding-window-log/sliding-window-log.limiter.ts +65 -0
- package/src/limiters/sliding-window-log/sliding-window-log.options.ts +20 -0
- package/src/limiters/sliding-window-log/sliding-window-log.policy.ts +166 -0
- package/src/limiters/sliding-window-log/sliding-window-log.state.ts +19 -0
- package/src/limiters/sliding-window-log/sliding-window-log.status.ts +44 -0
- package/src/limiters/token-bucket/index.ts +4 -0
- package/src/limiters/token-bucket/token-bucket.limiter.ts +110 -0
- package/src/limiters/token-bucket/token-bucket.options.ts +17 -0
- package/src/limiters/token-bucket/token-bucket.policy.ts +155 -0
- package/src/limiters/token-bucket/token-bucket.state.ts +10 -0
- package/src/limiters/token-bucket/token-bucket.status.ts +36 -0
- package/src/runtime/default-clock.ts +8 -0
- package/src/runtime/execution-tickets.ts +34 -0
- package/src/runtime/in-memory-state-store.ts +135 -0
- package/src/runtime/rate-limiter.executor.ts +286 -0
- package/src/runtime/semaphore.ts +31 -0
- package/src/runtime/task.ts +141 -0
- package/src/types/limit-behavior.ts +8 -0
- package/src/utils/generate-random-string.ts +16 -0
- package/src/utils/promise-with-resolvers.ts +23 -0
- package/src/utils/sanitize-error.ts +4 -0
- package/src/utils/sanitize-priority.ts +22 -0
- package/src/utils/validate-cost.ts +16 -0
- package/tests/integration/limiters/fixed-window.limiter.spec.ts +371 -0
- package/tests/integration/limiters/generic-cell.limiter.spec.ts +361 -0
- package/tests/integration/limiters/http-response-based.limiter.spec.ts +833 -0
- package/tests/integration/limiters/leaky-bucket.spec.ts +357 -0
- package/tests/integration/limiters/sliding-window-counter.limiter.spec.ts +175 -0
- package/tests/integration/limiters/sliding-window-log.spec.ts +185 -0
- package/tests/integration/limiters/token-bucket.limiter.spec.ts +363 -0
- package/tests/tsconfig.json +4 -0
- package/tests/unit/policies/composite.policy.spec.ts +244 -0
- package/tests/unit/policies/fixed-window.policy.spec.ts +260 -0
- package/tests/unit/policies/generic-cell.policy.spec.ts +178 -0
- package/tests/unit/policies/leaky-bucket.policy.spec.ts +215 -0
- package/tests/unit/policies/sliding-window-counter.policy.spec.ts +209 -0
- package/tests/unit/policies/sliding-window-log.policy.spec.ts +285 -0
- package/tests/unit/policies/token-bucket.policy.spec.ts +371 -0
- package/tests/unit/runtime/execution-tickets.spec.ts +121 -0
- package/tests/unit/runtime/in-memory-state-store.spec.ts +238 -0
- package/tests/unit/runtime/rate-limiter.executor.spec.ts +353 -0
- package/tests/unit/runtime/semaphore.spec.ts +98 -0
- package/tests/unit/runtime/task.spec.ts +182 -0
- package/tests/unit/utils/generate-random-string.spec.ts +51 -0
- package/tests/unit/utils/promise-with-resolvers.spec.ts +57 -0
- package/tests/unit/utils/sanitize-priority.spec.ts +46 -0
- package/tests/unit/utils/validate-cost.spec.ts +48 -0
- package/tsconfig.json +14 -0
- package/vitest.config.js +22 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { type Clock, RateLimiterDestroyedError, RateLimitErrorCode } from '../../../src/index.js';
|
|
3
|
+
import { GenericCellLimiter, type GenericCellState } from '../../../src/limiters/generic-cell/index.js';
|
|
4
|
+
import { InMemoryStateStore } from '../../../src/runtime/in-memory-state-store.js';
|
|
5
|
+
|
|
6
|
+
describe('GenericCellLimiter (Integration)', () => {
|
|
7
|
+
let clock: Clock;
|
|
8
|
+
let store: InMemoryStateStore<GenericCellState>;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.useFakeTimers();
|
|
12
|
+
vi.setSystemTime(10_000);
|
|
13
|
+
|
|
14
|
+
clock = { now: () => Date.now() };
|
|
15
|
+
store = new InMemoryStateStore(clock);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.clearAllTimers();
|
|
20
|
+
vi.useRealTimers();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('Reject mode (immediate decisions)', () => {
|
|
24
|
+
it('should allow requests within the burst limit immediately', async () => {
|
|
25
|
+
const limiter = new GenericCellLimiter({
|
|
26
|
+
limitBehavior: 'reject',
|
|
27
|
+
burst: 2,
|
|
28
|
+
intervalMs: 100,
|
|
29
|
+
clock,
|
|
30
|
+
store,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const res1 = await limiter.run(async () => 'A');
|
|
34
|
+
const res2 = await limiter.run(async () => 'B');
|
|
35
|
+
|
|
36
|
+
expect(res1).toBe('A');
|
|
37
|
+
expect(res2).toBe('B');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should reject requests that exceed the burst limit if behavior is "reject"', async () => {
|
|
41
|
+
const limiter = new GenericCellLimiter({
|
|
42
|
+
limitBehavior: 'reject',
|
|
43
|
+
burst: 1,
|
|
44
|
+
intervalMs: 100,
|
|
45
|
+
clock,
|
|
46
|
+
store,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
void limiter.run(() => 'A');
|
|
50
|
+
|
|
51
|
+
const promise = limiter.run(() => 'B');
|
|
52
|
+
await expect(promise).rejects.toMatchObject({ code: RateLimitErrorCode.LimitExceeded });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should consume multiple units of capacity if cost > 1', async () => {
|
|
56
|
+
const limiter = new GenericCellLimiter({
|
|
57
|
+
limitBehavior: 'reject',
|
|
58
|
+
burst: 5,
|
|
59
|
+
intervalMs: 100,
|
|
60
|
+
clock,
|
|
61
|
+
store,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await expect(limiter.run(() => 'A', { cost: 3 })).resolves.toBe('A');
|
|
65
|
+
await expect(limiter.run(() => 'B', { cost: 3 })).rejects.toMatchObject({
|
|
66
|
+
code: RateLimitErrorCode.LimitExceeded,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should allow new requests after the interval has passed', async () => {
|
|
71
|
+
const limiter = new GenericCellLimiter({
|
|
72
|
+
limitBehavior: 'reject',
|
|
73
|
+
burst: 1,
|
|
74
|
+
intervalMs: 100,
|
|
75
|
+
clock,
|
|
76
|
+
store,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await limiter.run(() => 'A');
|
|
80
|
+
await expect(limiter.run(() => 'B')).rejects.toMatchObject({ code: RateLimitErrorCode.LimitExceeded });
|
|
81
|
+
|
|
82
|
+
// next TAT
|
|
83
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
84
|
+
|
|
85
|
+
await expect(limiter.run(() => 'C')).resolves.toBe('C');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('Enqueue mode (queueing & scheduling)', () => {
|
|
90
|
+
it('should delay request and execute it when the interval permits', async () => {
|
|
91
|
+
const limiter = new GenericCellLimiter({
|
|
92
|
+
limitBehavior: 'enqueue',
|
|
93
|
+
burst: 1,
|
|
94
|
+
intervalMs: 100,
|
|
95
|
+
clock,
|
|
96
|
+
store,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
void limiter.run(() => 'A');
|
|
100
|
+
|
|
101
|
+
const bSpy = vi.fn().mockReturnValue('B');
|
|
102
|
+
// delay 100ms
|
|
103
|
+
const pB = limiter.run(bSpy);
|
|
104
|
+
|
|
105
|
+
expect(bSpy).not.toHaveBeenCalled();
|
|
106
|
+
|
|
107
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
108
|
+
|
|
109
|
+
const result: unknown = await pB;
|
|
110
|
+
expect(bSpy).toHaveBeenCalledOnce();
|
|
111
|
+
expect(result).toBe('B');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should maintain order and timings for multiple queued requests', async () => {
|
|
115
|
+
const limiter = new GenericCellLimiter({
|
|
116
|
+
limitBehavior: 'enqueue',
|
|
117
|
+
burst: 1,
|
|
118
|
+
intervalMs: 100,
|
|
119
|
+
clock,
|
|
120
|
+
store,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
void limiter.run(() => {});
|
|
124
|
+
|
|
125
|
+
const results: string[] = [];
|
|
126
|
+
|
|
127
|
+
// delay 100ms
|
|
128
|
+
const pA = limiter.run(() => results.push('A'));
|
|
129
|
+
// delay 200ms
|
|
130
|
+
const pB = limiter.run(() => results.push('B'));
|
|
131
|
+
// delay 300ms
|
|
132
|
+
const pC = limiter.run(() => results.push('C'));
|
|
133
|
+
|
|
134
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
135
|
+
await pA;
|
|
136
|
+
expect(results).toEqual(['A']);
|
|
137
|
+
|
|
138
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
139
|
+
await pB;
|
|
140
|
+
expect(results).toEqual(['A', 'B']);
|
|
141
|
+
|
|
142
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
143
|
+
await pC;
|
|
144
|
+
expect(results).toEqual(['A', 'B', 'C']);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should reject with QueueOverflow if executor queue exceeds maxSize', async () => {
|
|
148
|
+
const limiter = new GenericCellLimiter({
|
|
149
|
+
limitBehavior: 'enqueue',
|
|
150
|
+
burst: 1,
|
|
151
|
+
intervalMs: 100,
|
|
152
|
+
queue: { capacity: 1 },
|
|
153
|
+
clock,
|
|
154
|
+
store,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// exhausts burst
|
|
158
|
+
const pA = limiter.run(() => 'A');
|
|
159
|
+
// enqueued
|
|
160
|
+
const pB = limiter.run(() => 'B');
|
|
161
|
+
|
|
162
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
163
|
+
|
|
164
|
+
// overflow
|
|
165
|
+
const pC = limiter.run(() => 'C');
|
|
166
|
+
|
|
167
|
+
await expect(pC).rejects.toMatchObject({ code: RateLimitErrorCode.QueueOverflow });
|
|
168
|
+
|
|
169
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
170
|
+
await expect(pA).resolves.toBe('A');
|
|
171
|
+
|
|
172
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
173
|
+
await expect(pB).resolves.toBe('B');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should execute queued tasks based on priority order', async () => {
|
|
177
|
+
const limiter = new GenericCellLimiter({
|
|
178
|
+
limitBehavior: 'enqueue',
|
|
179
|
+
burst: 1,
|
|
180
|
+
intervalMs: 100,
|
|
181
|
+
clock,
|
|
182
|
+
store,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await limiter.run(() => 'A');
|
|
186
|
+
|
|
187
|
+
const order: string[] = [];
|
|
188
|
+
|
|
189
|
+
void limiter.run(() => order.push('Lowest'), { priority: 1 });
|
|
190
|
+
void limiter.run(() => order.push('Highest'), { priority: 5 });
|
|
191
|
+
|
|
192
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
193
|
+
|
|
194
|
+
expect(order).toEqual(['Highest', 'Lowest']);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should enqueue the task and reject it with Expired when TTL is reached', async () => {
|
|
198
|
+
const limiter = new GenericCellLimiter({
|
|
199
|
+
limitBehavior: 'enqueue',
|
|
200
|
+
burst: 1,
|
|
201
|
+
intervalMs: 100,
|
|
202
|
+
queue: { maxWaitMs: 150 },
|
|
203
|
+
clock,
|
|
204
|
+
store,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const pA = limiter.run(() => 'A');
|
|
208
|
+
// delay 100ms
|
|
209
|
+
const pB = limiter.run(() => 'B');
|
|
210
|
+
// delay 200ms, expires after 150ms
|
|
211
|
+
const spyC = vi.fn().mockReturnValue('C');
|
|
212
|
+
const pC = limiter.run(spyC);
|
|
213
|
+
pC.catch(() => {});
|
|
214
|
+
|
|
215
|
+
await expect(pA).resolves.toBe('A');
|
|
216
|
+
|
|
217
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
218
|
+
await expect(pB).resolves.toBe('B');
|
|
219
|
+
expect(spyC).not.toHaveBeenCalled();
|
|
220
|
+
|
|
221
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
222
|
+
await expect(pC).rejects.toMatchObject({ code: RateLimitErrorCode.Expired });
|
|
223
|
+
expect(spyC).not.toHaveBeenCalled();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should free up the canceled ticket for new requests', async () => {
|
|
227
|
+
const limiter = new GenericCellLimiter({
|
|
228
|
+
limitBehavior: 'enqueue',
|
|
229
|
+
burst: 1,
|
|
230
|
+
intervalMs: 100,
|
|
231
|
+
clock,
|
|
232
|
+
store,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const pA = limiter.run(() => 'A');
|
|
236
|
+
|
|
237
|
+
const controllerB = new AbortController();
|
|
238
|
+
// delay 100ms
|
|
239
|
+
const pB = limiter.run(() => 'B', { signal: controllerB.signal });
|
|
240
|
+
|
|
241
|
+
const spyC = vi.fn().mockReturnValue('C');
|
|
242
|
+
// delay 200ms
|
|
243
|
+
const pC = limiter.run(spyC);
|
|
244
|
+
|
|
245
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
246
|
+
|
|
247
|
+
// canceling B
|
|
248
|
+
// the 200ms ticket should be freed up, the consumed token should be returned to the bucket
|
|
249
|
+
controllerB.abort();
|
|
250
|
+
await expect(pB).rejects.toMatchObject({ code: RateLimitErrorCode.Cancelled });
|
|
251
|
+
|
|
252
|
+
const spyD = vi.fn().mockReturnValue('D');
|
|
253
|
+
// delay 200ms (it was freed up by canceling B)
|
|
254
|
+
const pD = limiter.run(spyD);
|
|
255
|
+
|
|
256
|
+
await expect(pA).resolves.toBe('A');
|
|
257
|
+
expect(spyC).not.toHaveBeenCalled();
|
|
258
|
+
expect(spyD).not.toHaveBeenCalled();
|
|
259
|
+
|
|
260
|
+
// t = 10_100
|
|
261
|
+
// C takes the 100ms ticket created by B
|
|
262
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
263
|
+
|
|
264
|
+
await expect(pC).resolves.toBe('C');
|
|
265
|
+
expect(spyC).toHaveBeenCalledOnce();
|
|
266
|
+
expect(spyD).not.toHaveBeenCalled();
|
|
267
|
+
|
|
268
|
+
// t = 10_200
|
|
269
|
+
// D takes the 200ms ticket created by itself
|
|
270
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
271
|
+
|
|
272
|
+
await expect(pD).resolves.toBe('D');
|
|
273
|
+
expect(spyD).toHaveBeenCalledOnce();
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('Runtime overrides', () => {
|
|
278
|
+
it('should allow overriding limitBehavior per task', async () => {
|
|
279
|
+
const limiter = new GenericCellLimiter({
|
|
280
|
+
limitBehavior: 'reject',
|
|
281
|
+
burst: 1,
|
|
282
|
+
intervalMs: 100,
|
|
283
|
+
clock,
|
|
284
|
+
store,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await limiter.run(() => 'A');
|
|
288
|
+
const pB = limiter.run(() => 'B', { limitBehavior: 'enqueue' });
|
|
289
|
+
|
|
290
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
291
|
+
await expect(pB).resolves.toBe('B');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should override max wait time for a specific task and expire it independently', async () => {
|
|
295
|
+
const limiter = new GenericCellLimiter({
|
|
296
|
+
limitBehavior: 'enqueue',
|
|
297
|
+
burst: 1,
|
|
298
|
+
intervalMs: 100,
|
|
299
|
+
queue: { maxWaitMs: 5000 },
|
|
300
|
+
clock,
|
|
301
|
+
store,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
void limiter.run(() => 'A');
|
|
305
|
+
|
|
306
|
+
// delay 100ms
|
|
307
|
+
const pB = limiter.run(() => 'B');
|
|
308
|
+
|
|
309
|
+
// delay 200ms
|
|
310
|
+
// expires after 150ms, so it should be rejected
|
|
311
|
+
const pC = limiter.run(() => 'C', { maxWaitMs: 150 });
|
|
312
|
+
pC.catch(() => {});
|
|
313
|
+
|
|
314
|
+
// delay 300ms
|
|
315
|
+
const pD = limiter.run(() => 'D', { maxWaitMs: 400 });
|
|
316
|
+
|
|
317
|
+
// t=10_100
|
|
318
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
319
|
+
await expect(pB).resolves.toBe('B');
|
|
320
|
+
|
|
321
|
+
// t=10_150
|
|
322
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
323
|
+
await expect(pC).rejects.toMatchObject({ code: RateLimitErrorCode.Expired });
|
|
324
|
+
|
|
325
|
+
// t=10_300
|
|
326
|
+
await vi.advanceTimersByTimeAsync(150);
|
|
327
|
+
await expect(pD).resolves.toBe('D');
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('State lifecycle', () => {
|
|
332
|
+
it('should reset limits and clear queued tasks on clear()', async () => {
|
|
333
|
+
const limiter = new GenericCellLimiter({
|
|
334
|
+
limitBehavior: 'reject',
|
|
335
|
+
burst: 1,
|
|
336
|
+
intervalMs: 100,
|
|
337
|
+
clock,
|
|
338
|
+
store,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
await limiter.run(() => 'A');
|
|
342
|
+
|
|
343
|
+
await limiter.clear();
|
|
344
|
+
|
|
345
|
+
await expect(limiter.run(() => 'B')).resolves.toBe('B');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should throw RateLimitError(Destroyed) after destroy() is called', async () => {
|
|
349
|
+
const limiter = new GenericCellLimiter({
|
|
350
|
+
burst: 5,
|
|
351
|
+
intervalMs: 100,
|
|
352
|
+
clock,
|
|
353
|
+
store,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
await limiter.destroy();
|
|
357
|
+
|
|
358
|
+
await expect(limiter.run(() => 'A')).rejects.toThrow(RateLimiterDestroyedError);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|