@async-kit/ratelimitx 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +385 -0
- package/dist/index.d.mts +167 -0
- package/dist/index.d.ts +167 -0
- package/dist/index.js +284 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +278 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +48 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="https://capsule-render.vercel.app/api?type=rect&color=gradient&customColorList=9&height=120§ion=header&text=ratelimitx&fontSize=50&fontColor=fff&animation=fadeIn&desc=%40async-kit%2Fratelimitx&descAlignY=75&descAlign=50" width="100%"/>
|
|
4
|
+
|
|
5
|
+
<br/>
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@async-kit/ratelimitx)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](../../LICENSE)
|
|
10
|
+
[](https://bundlephobia.com/package/@async-kit/ratelimitx)
|
|
11
|
+
[](https://nodejs.org/)
|
|
12
|
+
[](#compatibility)
|
|
13
|
+
|
|
14
|
+
**Multi-algorithm rate limiter — Token Bucket, Sliding Window, Fixed Window, and Composite enforcement with AbortSignal support.**
|
|
15
|
+
|
|
16
|
+
*Four algorithms, one interface. Zero dependencies.*
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @async-kit/ratelimitx
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { TokenBucket, SlidingWindow, FixedWindow, CompositeLimiter } from '@async-kit/ratelimitx';
|
|
32
|
+
|
|
33
|
+
// Token Bucket — burst-friendly
|
|
34
|
+
const bucket = new TokenBucket({ capacity: 10, refillRate: 2, refillInterval: 1000 });
|
|
35
|
+
await bucket.consume(); // waits until a token is available
|
|
36
|
+
|
|
37
|
+
// Sliding Window — strict per-window limit
|
|
38
|
+
const window = new SlidingWindow({ windowMs: 60_000, maxRequests: 100 });
|
|
39
|
+
await window.waitAndAcquire();
|
|
40
|
+
|
|
41
|
+
// Fixed Window — simplest, resets on schedule
|
|
42
|
+
const fw = new FixedWindow({ windowMs: 60_000, maxRequests: 100 });
|
|
43
|
+
fw.acquire(); // throws RateLimitError if over limit
|
|
44
|
+
|
|
45
|
+
// Composite — enforce multiple tiers simultaneously
|
|
46
|
+
const limiter = new CompositeLimiter([
|
|
47
|
+
new TokenBucket({ capacity: 10, refillRate: 10, refillInterval: 1000 }),
|
|
48
|
+
new SlidingWindow({ windowMs: 60_000, maxRequests: 500 }),
|
|
49
|
+
]);
|
|
50
|
+
await limiter.waitAndAcquire();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## API
|
|
54
|
+
|
|
55
|
+
### `TokenBucket`
|
|
56
|
+
|
|
57
|
+
Tokens accumulate at `refillRate` per `refillInterval` up to `capacity`. Burst-friendly.
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const bucket = new TokenBucket({ capacity: 10, refillRate: 2, refillInterval: 1000 });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Method | Description |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `.tryConsume(count?)` | Non-blocking; `true` if tokens available |
|
|
66
|
+
| `.consume(count?, signal?)` | **Async — waits** until tokens available; only throws if `count > capacity` |
|
|
67
|
+
| `.acquireOrThrow(count?)` | Throws `RateLimitError` immediately if tokens unavailable |
|
|
68
|
+
| `.tryAcquire()` | Alias for `tryConsume()` |
|
|
69
|
+
| `.acquire()` | Alias for `acquireOrThrow()` |
|
|
70
|
+
| `.waitAndAcquire(signal?)` | Alias for `consume(1, signal)` |
|
|
71
|
+
| `.reset()` | Refill to capacity, reset clock |
|
|
72
|
+
| `.setCapacity(n)` | Hot-resize; clamps current tokens if lower |
|
|
73
|
+
| `.available` | Current token count (after refill) |
|
|
74
|
+
|
|
75
|
+
### `SlidingWindow`
|
|
76
|
+
|
|
77
|
+
Tracks exact timestamps in a **ring buffer** (`Float64Array`). O(k) prune, no burst.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const win = new SlidingWindow({ windowMs: 60_000, maxRequests: 100 });
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Method | Description |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `.tryAcquire()` | Non-blocking; `true` if a slot is available |
|
|
86
|
+
| `.acquire()` | Throws `RateLimitError` immediately if at limit |
|
|
87
|
+
| `.waitAndAcquire(signal?)` | Async — waits until the oldest request expires |
|
|
88
|
+
| `.currentCount` | Active request count in the current window |
|
|
89
|
+
|
|
90
|
+
### `FixedWindow`
|
|
91
|
+
|
|
92
|
+
Counts requests in a fixed time bucket that resets every `windowMs`.
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const fw = new FixedWindow({
|
|
96
|
+
windowMs: 60_000,
|
|
97
|
+
maxRequests: 100,
|
|
98
|
+
onWindowReset: (t) => console.log('Reset at', t),
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
| Method | Description |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `.tryAcquire()` | Non-blocking; `true` if under limit |
|
|
105
|
+
| `.acquire()` | Throws `RateLimitError` if at limit |
|
|
106
|
+
| `.waitAndAcquire(signal?)` | Async — waits until the window resets |
|
|
107
|
+
| `.reset()` | Manually reset count (useful in tests) |
|
|
108
|
+
| `.currentCount` | Requests in current window |
|
|
109
|
+
| `.windowResetMs` | Ms remaining until the window resets |
|
|
110
|
+
|
|
111
|
+
### `CompositeLimiter`
|
|
112
|
+
|
|
113
|
+
Enforces **all** provided limiters simultaneously. Useful for multi-tier API limits.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const limiter = new CompositeLimiter([
|
|
117
|
+
new TokenBucket({ capacity: 10, refillRate: 10, refillInterval: 1000 }),
|
|
118
|
+
new SlidingWindow({ windowMs: 60_000, maxRequests: 500 }),
|
|
119
|
+
new FixedWindow({ windowMs: 3_600_000, maxRequests: 5000 }),
|
|
120
|
+
]);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
| Method | Description |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `.tryAcquire()` | `true` only if **all** limiters pass |
|
|
126
|
+
| `.acquire()` | Throws on the first limiter that rejects |
|
|
127
|
+
| `.waitAndAcquire(signal?)` | Async — waits until all limiters have capacity |
|
|
128
|
+
|
|
129
|
+
### `RateLimitError`
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
try {
|
|
133
|
+
limiter.acquire();
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (err instanceof RateLimitError) {
|
|
136
|
+
console.log(err.algorithm); // 'token-bucket' | 'sliding-window' | 'fixed-window'
|
|
137
|
+
console.log(err.retryAfterMs); // ms to wait before retrying
|
|
138
|
+
console.log(err.limit); // configured limit
|
|
139
|
+
console.log(err.current); // current usage
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## `Limiter` Interface
|
|
145
|
+
|
|
146
|
+
All three classes implement `Limiter`, making them interchangeable:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
interface Limiter {
|
|
150
|
+
tryAcquire(): boolean;
|
|
151
|
+
acquire(): void;
|
|
152
|
+
waitAndAcquire(signal?: AbortSignal): Promise<void>;
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Algorithm Comparison
|
|
157
|
+
|
|
158
|
+
| Algorithm | Burst | Memory | Boundary spike | Best For |
|
|
159
|
+
|---|---|---|---|---|
|
|
160
|
+
| Token Bucket | ✅ Yes | O(1) | No | API quotas, outbound throttling |
|
|
161
|
+
| Sliding Window | ❌ No | O(maxRequests) | No | Strict per-window enforcement |
|
|
162
|
+
| Fixed Window | ✅ At boundary | O(1) | Yes | Simple quotas, easy reasoning |
|
|
163
|
+
| CompositeLimiter | Depends | Combined | Depends | Multi-tier API limits |
|
|
164
|
+
|
|
165
|
+
## Examples
|
|
166
|
+
|
|
167
|
+
### Express middleware — per-IP sliding window
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import express from 'express';
|
|
171
|
+
import { SlidingWindow, RateLimitError } from '@async-kit/ratelimitx';
|
|
172
|
+
|
|
173
|
+
const app = express();
|
|
174
|
+
const limiters = new Map<string, SlidingWindow>();
|
|
175
|
+
|
|
176
|
+
app.use((req, res, next) => {
|
|
177
|
+
const ip = req.ip ?? 'unknown';
|
|
178
|
+
if (!limiters.has(ip)) {
|
|
179
|
+
limiters.set(ip, new SlidingWindow({ windowMs: 60_000, maxRequests: 100 }));
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
limiters.get(ip)!.acquire();
|
|
183
|
+
next();
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (err instanceof RateLimitError) {
|
|
186
|
+
res.set('Retry-After', String(Math.ceil(err.retryAfterMs / 1000)));
|
|
187
|
+
res.set('X-RateLimit-Limit', String(err.limit));
|
|
188
|
+
res.set('X-RateLimit-Remaining', '0');
|
|
189
|
+
res.status(429).json({ error: 'Too Many Requests', retryAfterMs: err.retryAfterMs });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Outbound API quota — token bucket for GitHub API
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { TokenBucket } from '@async-kit/ratelimitx';
|
|
199
|
+
import { Octokit } from 'octokit';
|
|
200
|
+
|
|
201
|
+
// GitHub: 5 000 authenticated requests / hour ≈ 1.38 / sec
|
|
202
|
+
const github = new TokenBucket({
|
|
203
|
+
capacity: 30, // burst: up to 30 back-to-back
|
|
204
|
+
refillRate: 1,
|
|
205
|
+
refillInterval: 720, // 720 ms ≈ 1.38 tokens/sec
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const octokit = new Octokit({ auth: process.env.GH_TOKEN });
|
|
209
|
+
|
|
210
|
+
async function githubRequest<T>(fn: () => Promise<T>): Promise<T> {
|
|
211
|
+
// Block until a token is available (never drops requests)
|
|
212
|
+
await github.waitAndAcquire();
|
|
213
|
+
return fn();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Safe to call in a tight loop — will naturally pace itself
|
|
217
|
+
const repos = await githubRequest(() =>
|
|
218
|
+
octokit.rest.repos.listForOrg({ org: 'my-org', per_page: 100 })
|
|
219
|
+
);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Multi-tier composite limit (per-second + per-minute + per-hour)
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { TokenBucket, SlidingWindow, FixedWindow, CompositeLimiter } from '@async-kit/ratelimitx';
|
|
226
|
+
|
|
227
|
+
// Model a typical SaaS API tier:
|
|
228
|
+
// • burst 10 back-to-back
|
|
229
|
+
// • 60 / min steady-state
|
|
230
|
+
// • 1 000 / hr hard quota
|
|
231
|
+
const apiLimiter = new CompositeLimiter([
|
|
232
|
+
new TokenBucket({ capacity: 10, refillRate: 10, refillInterval: 1_000 }),
|
|
233
|
+
new SlidingWindow({ windowMs: 60_000, maxRequests: 60 }),
|
|
234
|
+
new FixedWindow ({ windowMs: 3_600_000, maxRequests: 1_000,
|
|
235
|
+
onWindowReset: (t) => console.log('Hourly quota reset at', new Date(t).toISOString()),
|
|
236
|
+
}),
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
async function callSaasApi(endpoint: string) {
|
|
240
|
+
await apiLimiter.waitAndAcquire();
|
|
241
|
+
return fetch(endpoint).then(r => r.json());
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Combine with retryx for automatic backoff
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { SlidingWindow, RateLimitError } from '@async-kit/ratelimitx';
|
|
249
|
+
import { retry } from '@async-kit/retryx';
|
|
250
|
+
|
|
251
|
+
const limiter = new SlidingWindow({ windowMs: 1_000, maxRequests: 10 });
|
|
252
|
+
|
|
253
|
+
const result = await retry(
|
|
254
|
+
() => { limiter.acquire(); return callApi(); },
|
|
255
|
+
{
|
|
256
|
+
maxAttempts: 60,
|
|
257
|
+
retryIf: (err) => err instanceof RateLimitError,
|
|
258
|
+
onRetry: (_n, err) => {
|
|
259
|
+
const wait = (err as RateLimitError).retryAfterMs;
|
|
260
|
+
console.log(`Rate limited — retrying in ${wait}ms`);
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Async queue consumer with `waitAndAcquire`
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import { TokenBucket } from '@async-kit/ratelimitx';
|
|
270
|
+
|
|
271
|
+
const bucket = new TokenBucket({ capacity: 5, refillRate: 5, refillInterval: 1_000 });
|
|
272
|
+
|
|
273
|
+
// Consumer loop — processes at most 5 items/sec no matter how fast items arrive
|
|
274
|
+
async function processQueue(queue: AsyncIterable<Job>) {
|
|
275
|
+
for await (const job of queue) {
|
|
276
|
+
await bucket.waitAndAcquire(); // blocks here when the bucket is empty
|
|
277
|
+
void processJob(job); // fire without awaiting
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Fixed window with reset callback for quota UI
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import { FixedWindow } from '@async-kit/ratelimitx';
|
|
286
|
+
|
|
287
|
+
let remaining = 100;
|
|
288
|
+
|
|
289
|
+
const fw = new FixedWindow({
|
|
290
|
+
windowMs: 60_000,
|
|
291
|
+
maxRequests: 100,
|
|
292
|
+
onWindowReset: () => { remaining = 100; },
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
function tryRequest(): { allowed: boolean; remaining: number; resetIn: number } {
|
|
296
|
+
const allowed = fw.tryAcquire();
|
|
297
|
+
if (allowed) remaining--;
|
|
298
|
+
return { allowed, remaining, resetIn: fw.windowResetMs };
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Dynamic capacity — hot-resize for plan upgrades
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import { TokenBucket } from '@async-kit/ratelimitx';
|
|
306
|
+
|
|
307
|
+
const bucket = new TokenBucket({ capacity: 10, refillRate: 10, refillInterval: 1_000 });
|
|
308
|
+
|
|
309
|
+
// User upgrades from Free (10/s) to Pro (100/s) — no restart needed
|
|
310
|
+
async function onPlanUpgrade(userId: string, newPlan: 'free' | 'pro') {
|
|
311
|
+
const newCapacity = newPlan === 'pro' ? 100 : 10;
|
|
312
|
+
bucket.setCapacity(newCapacity);
|
|
313
|
+
console.log(`User ${userId} upgraded to ${newPlan} — new capacity: ${newCapacity}`);
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Custom `Limiter` implementation
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
import type { Limiter } from '@async-kit/ratelimitx';
|
|
321
|
+
import { CompositeLimiter } from '@async-kit/ratelimitx';
|
|
322
|
+
|
|
323
|
+
// Roll your own limiter and plug it into CompositeLimiter
|
|
324
|
+
class DailyQuotaLimiter implements Limiter {
|
|
325
|
+
private used = 0;
|
|
326
|
+
constructor(private readonly limit: number) {}
|
|
327
|
+
|
|
328
|
+
tryAcquire(): boolean {
|
|
329
|
+
if (this.used < this.limit) { this.used++; return true; }
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
acquire(): void {
|
|
334
|
+
if (!this.tryAcquire()) throw new Error(`Daily quota of ${this.limit} exhausted`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async waitAndAcquire(): Promise<void> {
|
|
338
|
+
// Block until midnight UTC
|
|
339
|
+
const ms = msUntilMidnightUTC();
|
|
340
|
+
await new Promise(r => setTimeout(r, ms));
|
|
341
|
+
this.used = 0;
|
|
342
|
+
this.used++;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const composite = new CompositeLimiter([
|
|
347
|
+
new SlidingWindow({ windowMs: 1_000, maxRequests: 10 }),
|
|
348
|
+
new DailyQuotaLimiter(10_000),
|
|
349
|
+
]);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Types
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import type {
|
|
356
|
+
TokenBucketOptions,
|
|
357
|
+
SlidingWindowOptions,
|
|
358
|
+
FixedWindowOptions,
|
|
359
|
+
Limiter,
|
|
360
|
+
RateLimitAlgorithm,
|
|
361
|
+
} from '@async-kit/ratelimitx';
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Compatibility
|
|
365
|
+
|
|
366
|
+
| Environment | Support | Notes |
|
|
367
|
+
|---|---|---|
|
|
368
|
+
| **Node.js** | ≥ 18 | Recommended ≥ 24 for best performance |
|
|
369
|
+
| **Deno** | ✅ | Via npm specifier (`npm:@async-kit/ratelimitx`) |
|
|
370
|
+
| **Bun** | ✅ | Full support |
|
|
371
|
+
| **Chrome** | ≥ 80 | ESM via bundler or native import |
|
|
372
|
+
| **Firefox** | ≥ 75 | ESM via bundler or native import |
|
|
373
|
+
| **Safari** | ≥ 13.1 | ESM via bundler or native import |
|
|
374
|
+
| **Edge** | ≥ 80 | ESM via bundler or native import |
|
|
375
|
+
| **React Native** | ✅ | Via Metro bundler |
|
|
376
|
+
| **Cloudflare Workers** | ✅ | ESM, `AbortSignal` natively supported |
|
|
377
|
+
| **Vercel Edge Runtime** | ✅ | ESM, no `process` / `fs` dependencies |
|
|
378
|
+
|
|
379
|
+
**No Node.js built-ins are used.** The package relies only on standard JavaScript (`Promise`, `setTimeout`, `clearTimeout`, `Float64Array`, `AbortSignal`, `DOMException`) — universally available in modern runtimes and browsers.
|
|
380
|
+
|
|
381
|
+
> **`Float64Array`** (used by `SlidingWindow`'s ring buffer) is part of the ECMAScript spec and available in every JavaScript environment including old browsers and edge workers.
|
|
382
|
+
|
|
383
|
+
## License
|
|
384
|
+
|
|
385
|
+
MIT © async-kit contributors · Part of the [async-kit](../../README.md) ecosystem
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/** Constructor options for `TokenBucket`. */
|
|
2
|
+
interface TokenBucketOptions {
|
|
3
|
+
/** Maximum token capacity (burst size). */
|
|
4
|
+
capacity: number;
|
|
5
|
+
/** Tokens added per `refillInterval`. */
|
|
6
|
+
refillRate: number;
|
|
7
|
+
/** Interval in ms at which tokens are added. Default: `1000`. */
|
|
8
|
+
refillInterval?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Constructor options for `SlidingWindow`. */
|
|
11
|
+
interface SlidingWindowOptions {
|
|
12
|
+
/** Rolling window duration in ms. */
|
|
13
|
+
windowMs: number;
|
|
14
|
+
/** Maximum requests allowed per window. */
|
|
15
|
+
maxRequests: number;
|
|
16
|
+
}
|
|
17
|
+
/** Constructor options for `FixedWindow`. */
|
|
18
|
+
interface FixedWindowOptions {
|
|
19
|
+
/** Window duration in ms. */
|
|
20
|
+
windowMs: number;
|
|
21
|
+
/** Maximum requests per window. */
|
|
22
|
+
maxRequests: number;
|
|
23
|
+
/** Called each time the window resets. */
|
|
24
|
+
onWindowReset?: (windowStart: number) => void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Common interface implemented by all three rate-limiter classes
|
|
28
|
+
* and accepted by `CompositeLimiter`.
|
|
29
|
+
*/
|
|
30
|
+
interface Limiter {
|
|
31
|
+
tryAcquire(): boolean;
|
|
32
|
+
acquire(): void;
|
|
33
|
+
waitAndAcquire(signal?: AbortSignal): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/** Algorithm tag used in `RateLimitError`. */
|
|
36
|
+
type RateLimitAlgorithm = 'token-bucket' | 'sliding-window' | 'fixed-window';
|
|
37
|
+
|
|
38
|
+
declare class RateLimitError extends Error {
|
|
39
|
+
readonly retryAfterMs: number;
|
|
40
|
+
readonly algorithm: 'token-bucket' | 'sliding-window' | 'fixed-window';
|
|
41
|
+
readonly limit: number;
|
|
42
|
+
readonly current: number;
|
|
43
|
+
constructor(message: string, retryAfterMs: number, algorithm: 'token-bucket' | 'sliding-window' | 'fixed-window', limit: number, current: number);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Token Bucket rate limiter.
|
|
47
|
+
*
|
|
48
|
+
* Tokens accumulate over time up to `capacity`. Burst-friendly — allows up
|
|
49
|
+
* to `capacity` back-to-back calls as long as the bucket is full.
|
|
50
|
+
*
|
|
51
|
+
* - `tryConsume()` — non-throwing, returns `true` if tokens available.
|
|
52
|
+
* - `consume()` — async, **blocks** until tokens are available.
|
|
53
|
+
* - `available` — current token count (after refill).
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* const bucket = new TokenBucket({ capacity: 10, refillRate: 2, refillInterval: 1000 });
|
|
57
|
+
* await bucket.consume(); // waits until a token is available
|
|
58
|
+
*/
|
|
59
|
+
declare class TokenBucket {
|
|
60
|
+
private tokens;
|
|
61
|
+
private lastRefillTime;
|
|
62
|
+
private capacity;
|
|
63
|
+
private readonly refillRate;
|
|
64
|
+
private readonly refillInterval;
|
|
65
|
+
constructor(options: TokenBucketOptions);
|
|
66
|
+
private refill;
|
|
67
|
+
get available(): number;
|
|
68
|
+
tryConsume(count?: number): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Waits until `count` tokens are available, then consumes them.
|
|
71
|
+
* Throws `RateLimitError` only if the request can never be satisfied
|
|
72
|
+
* (i.e. `count > capacity`). Cancellable via `AbortSignal`.
|
|
73
|
+
*/
|
|
74
|
+
consume(count?: number, signal?: AbortSignal): Promise<void>;
|
|
75
|
+
/** Alias for `acquireOrThrow()` — satisfies the `Limiter` interface. */
|
|
76
|
+
acquire(count?: number): void;
|
|
77
|
+
/** Satisfies the `Limiter` interface — alias for `tryConsume()`. */
|
|
78
|
+
tryAcquire(): boolean;
|
|
79
|
+
/** Satisfies the `Limiter` interface — alias for `consume()`. */
|
|
80
|
+
waitAndAcquire(signal?: AbortSignal): Promise<void>;
|
|
81
|
+
/** Throws immediately if tokens are unavailable (non-blocking equivalent of `consume`). */
|
|
82
|
+
acquireOrThrow(count?: number): void;
|
|
83
|
+
/** Refill to full capacity and reset the refill clock. */
|
|
84
|
+
reset(): void;
|
|
85
|
+
/** Hot-resize capacity. Clamps current tokens if the new capacity is lower. */
|
|
86
|
+
setCapacity(capacity: number): void;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Sliding Window rate limiter.
|
|
90
|
+
*
|
|
91
|
+
* Tracks exact request timestamps in a ring buffer. Strict enforcement —
|
|
92
|
+
* no burst beyond `maxRequests` regardless of spacing.
|
|
93
|
+
*
|
|
94
|
+
* - `tryAcquire()` — non-throwing.
|
|
95
|
+
* - `acquire()` — throws `RateLimitError` immediately.
|
|
96
|
+
* - `waitAndAcquire()` — async, blocks until a slot is available.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* const window = new SlidingWindow({ windowMs: 60_000, maxRequests: 100 });
|
|
100
|
+
* await window.waitAndAcquire(); // queues caller until a slot opens
|
|
101
|
+
*/
|
|
102
|
+
declare class SlidingWindow {
|
|
103
|
+
private readonly ring;
|
|
104
|
+
private head;
|
|
105
|
+
private count;
|
|
106
|
+
private readonly windowMs;
|
|
107
|
+
private readonly maxRequests;
|
|
108
|
+
constructor(options: SlidingWindowOptions);
|
|
109
|
+
private prune;
|
|
110
|
+
get currentCount(): number;
|
|
111
|
+
tryAcquire(): boolean;
|
|
112
|
+
acquire(): void;
|
|
113
|
+
/** Blocks until a slot is available, then acquires it. Cancellable via AbortSignal. */
|
|
114
|
+
waitAndAcquire(signal?: AbortSignal): Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Fixed Window rate limiter.
|
|
118
|
+
*
|
|
119
|
+
* Simplest algorithm — counts requests in a fixed time bucket that resets
|
|
120
|
+
* every `windowMs`. Easiest to reason about; susceptible to boundary spikes.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* const fw = new FixedWindow({ windowMs: 60_000, maxRequests: 100 });
|
|
124
|
+
* fw.acquire(); // throws if over limit in current window
|
|
125
|
+
*/
|
|
126
|
+
declare class FixedWindow {
|
|
127
|
+
private count;
|
|
128
|
+
private windowStart;
|
|
129
|
+
private readonly windowMs;
|
|
130
|
+
private readonly maxRequests;
|
|
131
|
+
private readonly onWindowReset?;
|
|
132
|
+
constructor(options: FixedWindowOptions);
|
|
133
|
+
private tick;
|
|
134
|
+
get currentCount(): number;
|
|
135
|
+
/** Ms until the current window resets. */
|
|
136
|
+
get windowResetMs(): number;
|
|
137
|
+
tryAcquire(): boolean;
|
|
138
|
+
acquire(): void;
|
|
139
|
+
waitAndAcquire(signal?: AbortSignal): Promise<void>;
|
|
140
|
+
/** Manually reset the window (useful in tests). */
|
|
141
|
+
reset(): void;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Composite rate limiter — enforces multiple limits simultaneously.
|
|
145
|
+
*
|
|
146
|
+
* All limiters must pass for a call to be allowed. Useful when APIs enforce
|
|
147
|
+
* multiple tiers (e.g. 10/second AND 500/minute AND 5000/hour).
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* const limiter = new CompositeLimiter([
|
|
151
|
+
* new TokenBucket({ capacity: 10, refillRate: 10, refillInterval: 1000 }),
|
|
152
|
+
* new SlidingWindow({ windowMs: 60_000, maxRequests: 500 }),
|
|
153
|
+
* ]);
|
|
154
|
+
* await limiter.waitAndAcquire();
|
|
155
|
+
*/
|
|
156
|
+
declare class CompositeLimiter {
|
|
157
|
+
private readonly limiters;
|
|
158
|
+
constructor(limiters: Limiter[]);
|
|
159
|
+
/** Returns `true` only if ALL limiters can acquire. Rolls back on partial failure. */
|
|
160
|
+
tryAcquire(): boolean;
|
|
161
|
+
/** Throws `RateLimitError` with the most restrictive `retryAfterMs`. */
|
|
162
|
+
acquire(): void;
|
|
163
|
+
/** Waits for ALL limiters to have capacity, then acquires atomically. */
|
|
164
|
+
waitAndAcquire(signal?: AbortSignal): Promise<void>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { CompositeLimiter, FixedWindow, type FixedWindowOptions, type Limiter, type RateLimitAlgorithm, RateLimitError, SlidingWindow, type SlidingWindowOptions, TokenBucket, type TokenBucketOptions };
|