@taukirsheikh/rate-limiter 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +490 -0
- package/dist/event-emitter.d.ts +32 -0
- package/dist/event-emitter.d.ts.map +1 -0
- package/dist/event-emitter.js +72 -0
- package/dist/event-emitter.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/priority-queue.d.ts +66 -0
- package/dist/priority-queue.d.ts.map +1 -0
- package/dist/priority-queue.js +147 -0
- package/dist/priority-queue.js.map +1 -0
- package/dist/rate-limiter.d.ts +135 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +455 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/redis/distributed-rate-limiter.d.ts +149 -0
- package/dist/redis/distributed-rate-limiter.d.ts.map +1 -0
- package/dist/redis/distributed-rate-limiter.js +423 -0
- package/dist/redis/distributed-rate-limiter.js.map +1 -0
- package/dist/redis/index.d.ts +8 -0
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +11 -0
- package/dist/redis/index.js.map +1 -0
- package/dist/redis/lua-scripts.d.ts +62 -0
- package/dist/redis/lua-scripts.d.ts.map +1 -0
- package/dist/redis/lua-scripts.js +229 -0
- package/dist/redis/lua-scripts.js.map +1 -0
- package/dist/redis/redis-storage.d.ts +134 -0
- package/dist/redis/redis-storage.d.ts.map +1 -0
- package/dist/redis/redis-storage.js +255 -0
- package/dist/redis/redis-storage.js.map +1 -0
- package/dist/types.d.ts +207 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mohammed Taukir Sheikh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# @custom/rate-limiter
|
|
2
|
+
|
|
3
|
+
A powerful rate limiter for Node.js with TypeScript support and Redis clustering. Similar to [Bottleneck](https://www.npmjs.com/package/bottleneck) but built from scratch.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚦 **Concurrency Control** - Limit max concurrent jobs
|
|
8
|
+
- ⏱️ **Rate Limiting** - Min time between jobs, max per interval
|
|
9
|
+
- 🪣 **Reservoir (Token Bucket)** - Finite pool with automatic refill
|
|
10
|
+
- ⭐ **Priority Queues** - Higher priority jobs execute first
|
|
11
|
+
- 🔄 **Retry Support** - Automatic retries with exponential backoff
|
|
12
|
+
- ❌ **Cancellation** - Cancel by ID or AbortController
|
|
13
|
+
- 📡 **Event Hooks** - Lifecycle events for monitoring
|
|
14
|
+
- 📊 **Statistics** - Track wait times, execution times, success/failure
|
|
15
|
+
- 🎁 **Function Wrapping** - Easily wrap existing async functions
|
|
16
|
+
- 🌐 **Redis Clustering** - Distributed rate limiting across multiple servers
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @custom/rate-limiter
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { RateLimiter, Priority } from '@custom/rate-limiter';
|
|
28
|
+
|
|
29
|
+
// Create a limiter
|
|
30
|
+
const limiter = new RateLimiter({
|
|
31
|
+
maxConcurrent: 5, // Max 5 concurrent requests
|
|
32
|
+
minTime: 100, // 100ms minimum between requests
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Schedule a job
|
|
36
|
+
const result = await limiter.schedule(async () => {
|
|
37
|
+
return await fetch('https://api.example.com/data');
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Configuration Options
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
interface RateLimiterOptions {
|
|
45
|
+
// Concurrency
|
|
46
|
+
maxConcurrent?: number; // Max concurrent jobs (default: Infinity)
|
|
47
|
+
|
|
48
|
+
// Rate Limiting
|
|
49
|
+
minTime?: number; // Min ms between job starts (default: 0)
|
|
50
|
+
maxPerInterval?: number; // Max jobs per interval (default: Infinity)
|
|
51
|
+
interval?: number; // Interval duration in ms (default: 1000)
|
|
52
|
+
|
|
53
|
+
// Reservoir (Token Bucket)
|
|
54
|
+
reservoir?: number; // Initial reservoir size
|
|
55
|
+
reservoirRefreshInterval?: number; // Refill interval in ms
|
|
56
|
+
reservoirRefreshAmount?: number; // Amount to refill
|
|
57
|
+
|
|
58
|
+
// Queue Management
|
|
59
|
+
highWater?: number; // Max queue size
|
|
60
|
+
strategy?: 'leak' | 'overflow' | 'block'; // Overflow strategy
|
|
61
|
+
|
|
62
|
+
// Retry
|
|
63
|
+
retryCount?: number; // Auto-retry count (default: 0)
|
|
64
|
+
retryDelay?: number | ((attempt, error) => number); // Retry delay
|
|
65
|
+
|
|
66
|
+
// Other
|
|
67
|
+
timeout?: number; // Job timeout in ms
|
|
68
|
+
id?: string; // Limiter instance ID
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Examples
|
|
73
|
+
|
|
74
|
+
### Basic Rate Limiting
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const limiter = new RateLimiter({
|
|
78
|
+
maxConcurrent: 2,
|
|
79
|
+
minTime: 100,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const results = await Promise.all([
|
|
83
|
+
limiter.schedule(() => fetchUser(1)),
|
|
84
|
+
limiter.schedule(() => fetchUser(2)),
|
|
85
|
+
limiter.schedule(() => fetchUser(3)),
|
|
86
|
+
]);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Priority Queuing
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { Priority } from '@custom/rate-limiter';
|
|
93
|
+
|
|
94
|
+
// Critical jobs run first
|
|
95
|
+
await limiter.schedule(
|
|
96
|
+
{ priority: Priority.CRITICAL },
|
|
97
|
+
() => handleUrgentRequest()
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Low priority runs when queue is free
|
|
101
|
+
await limiter.schedule(
|
|
102
|
+
{ priority: Priority.LOW },
|
|
103
|
+
() => backgroundSync()
|
|
104
|
+
);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Reservoir (Token Bucket)
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const limiter = new RateLimiter({
|
|
111
|
+
reservoir: 10, // Start with 10 tokens
|
|
112
|
+
reservoirRefreshInterval: 60000, // Refill every minute
|
|
113
|
+
reservoirRefreshAmount: 10, // Refill to 10 tokens
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Each request uses 1 token
|
|
117
|
+
// After 10 requests, must wait for refill
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Wrap Existing Functions
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const rateLimitedFetch = limiter.wrap(
|
|
124
|
+
async (url: string) => {
|
|
125
|
+
const response = await fetch(url);
|
|
126
|
+
return response.json();
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Use like a normal function
|
|
131
|
+
const data = await rateLimitedFetch('/api/users');
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Retry with Exponential Backoff
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const limiter = new RateLimiter({
|
|
138
|
+
retryCount: 3,
|
|
139
|
+
retryDelay: (attempt, error) => Math.pow(2, attempt) * 1000,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Will retry up to 3 times: 2s, 4s, 8s delays
|
|
143
|
+
await limiter.schedule(() => unreliableApiCall());
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Cancellation
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// Cancel by ID
|
|
150
|
+
limiter.schedule({ id: 'my-job' }, async () => { ... });
|
|
151
|
+
limiter.cancel('my-job');
|
|
152
|
+
|
|
153
|
+
// Cancel with AbortController
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
limiter.schedule(
|
|
156
|
+
{ signal: controller.signal },
|
|
157
|
+
async () => { ... }
|
|
158
|
+
);
|
|
159
|
+
controller.abort();
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Event Monitoring
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
limiter.on('executing', ({ job, running }) => {
|
|
166
|
+
console.log(`Starting ${job.id}, ${running} jobs running`);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
limiter.on('done', ({ job, duration }) => {
|
|
170
|
+
console.log(`Completed ${job.id} in ${duration}ms`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
limiter.on('failed', ({ job, error, willRetry }) => {
|
|
174
|
+
console.log(`Failed ${job.id}: ${error.message}`);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
limiter.on('depleted', () => {
|
|
178
|
+
console.log('Reservoir empty, waiting for refill');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
limiter.on('idle', () => {
|
|
182
|
+
console.log('All jobs complete');
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Statistics
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
const stats = limiter.getStats();
|
|
190
|
+
console.log({
|
|
191
|
+
running: stats.running,
|
|
192
|
+
queued: stats.queued,
|
|
193
|
+
done: stats.done,
|
|
194
|
+
failed: stats.failed,
|
|
195
|
+
avgWaitTime: stats.avgWaitTime,
|
|
196
|
+
avgExecutionTime: stats.avgExecutionTime,
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 🌐 Distributed Rate Limiting with Redis
|
|
203
|
+
|
|
204
|
+
For multi-server deployments, use `DistributedRateLimiter` to coordinate rate limits across all instances using Redis.
|
|
205
|
+
|
|
206
|
+
### Quick Start (Redis)
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import { DistributedRateLimiter } from '@custom/rate-limiter';
|
|
210
|
+
|
|
211
|
+
const limiter = new DistributedRateLimiter({
|
|
212
|
+
id: 'api-limiter', // Shared ID across all servers
|
|
213
|
+
maxConcurrent: 10, // 10 concurrent across ALL instances
|
|
214
|
+
minTime: 100,
|
|
215
|
+
redis: {
|
|
216
|
+
host: 'localhost',
|
|
217
|
+
port: 6379,
|
|
218
|
+
keyPrefix: 'myapp:ratelimit',
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Wait for Redis connection
|
|
223
|
+
await limiter.ready();
|
|
224
|
+
|
|
225
|
+
// Use like normal RateLimiter
|
|
226
|
+
const result = await limiter.schedule(async () => {
|
|
227
|
+
return await fetch('https://api.example.com/data');
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Redis Configuration
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
interface RedisConnectionOptions {
|
|
235
|
+
// Connection
|
|
236
|
+
url?: string; // Redis URL (redis://...)
|
|
237
|
+
host?: string; // Redis host (default: localhost)
|
|
238
|
+
port?: number; // Redis port (default: 6379)
|
|
239
|
+
password?: string; // Redis password
|
|
240
|
+
db?: number; // Database number
|
|
241
|
+
|
|
242
|
+
// Clustering
|
|
243
|
+
cluster?: boolean; // Use Redis Cluster
|
|
244
|
+
clusterNodes?: Array<{host: string; port: number}>;
|
|
245
|
+
|
|
246
|
+
// Namespacing
|
|
247
|
+
keyPrefix?: string; // Key prefix (default: 'ratelimit')
|
|
248
|
+
|
|
249
|
+
// Advanced
|
|
250
|
+
client?: Redis | Cluster; // Existing ioredis client
|
|
251
|
+
redisOptions?: RedisOptions; // Additional ioredis options
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Multi-Server Example
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// Server 1
|
|
259
|
+
const limiter1 = new DistributedRateLimiter({
|
|
260
|
+
id: 'shared-limiter', // Same ID = shared limits
|
|
261
|
+
maxConcurrent: 5,
|
|
262
|
+
redis: { host: 'redis.example.com' },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Server 2 (different machine)
|
|
266
|
+
const limiter2 = new DistributedRateLimiter({
|
|
267
|
+
id: 'shared-limiter', // Same ID!
|
|
268
|
+
maxConcurrent: 5,
|
|
269
|
+
redis: { host: 'redis.example.com' },
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Both servers share the 5 concurrent slot limit
|
|
273
|
+
// If Server 1 has 3 running, Server 2 can only run 2
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Distributed Reservoir
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
const limiter = new DistributedRateLimiter({
|
|
280
|
+
id: 'token-bucket',
|
|
281
|
+
reservoir: 100, // 100 tokens shared across all servers
|
|
282
|
+
reservoirRefreshInterval: 60000, // Refill every minute
|
|
283
|
+
reservoirRefreshAmount: 100,
|
|
284
|
+
redis: { host: 'localhost' },
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// All instances share the 100 token pool
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Redis Cluster Support
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
const limiter = new DistributedRateLimiter({
|
|
294
|
+
id: 'cluster-limiter',
|
|
295
|
+
maxConcurrent: 50,
|
|
296
|
+
redis: {
|
|
297
|
+
cluster: true,
|
|
298
|
+
clusterNodes: [
|
|
299
|
+
{ host: 'redis-1.example.com', port: 6379 },
|
|
300
|
+
{ host: 'redis-2.example.com', port: 6379 },
|
|
301
|
+
{ host: 'redis-3.example.com', port: 6379 },
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Using Existing Redis Client
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
import Redis from 'ioredis';
|
|
311
|
+
|
|
312
|
+
const redis = new Redis({ host: 'localhost', port: 6379 });
|
|
313
|
+
|
|
314
|
+
const limiter = new DistributedRateLimiter({
|
|
315
|
+
id: 'shared-client',
|
|
316
|
+
maxConcurrent: 10,
|
|
317
|
+
redis: {
|
|
318
|
+
client: redis, // Use existing client
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Distributed Options
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
interface DistributedRateLimiterOptions extends RateLimiterOptions {
|
|
327
|
+
redis: RedisConnectionOptions;
|
|
328
|
+
|
|
329
|
+
// Polling interval when waiting for slot (default: 50ms)
|
|
330
|
+
pollInterval?: number;
|
|
331
|
+
|
|
332
|
+
// Heartbeat interval to keep state alive (default: 30000ms)
|
|
333
|
+
heartbeatInterval?: number;
|
|
334
|
+
|
|
335
|
+
// Clear Redis state on start - useful for testing (default: false)
|
|
336
|
+
clearOnStart?: boolean;
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### How It Works
|
|
341
|
+
|
|
342
|
+
The distributed limiter uses **Lua scripts** for atomic operations:
|
|
343
|
+
|
|
344
|
+
1. **Acquire Slot**: Atomically checks concurrency, rate limits, and reservoir
|
|
345
|
+
2. **Release Slot**: Atomically decrements running count and updates stats
|
|
346
|
+
3. **All state lives in Redis**: Running count, interval counters, reservoir
|
|
347
|
+
|
|
348
|
+
This ensures that even with multiple servers hitting Redis simultaneously, the rate limits are enforced correctly without race conditions.
|
|
349
|
+
|
|
350
|
+
### State Persistence
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
// State persists in Redis even when servers restart
|
|
354
|
+
const state = await limiter.getState();
|
|
355
|
+
console.log({
|
|
356
|
+
running: state.running, // Currently running (across all servers)
|
|
357
|
+
done: state.done, // Total completed
|
|
358
|
+
failed: state.failed, // Total failed
|
|
359
|
+
reservoir: state.reservoir, // Current reservoir level
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Graceful Degradation
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
try {
|
|
367
|
+
const limiter = new DistributedRateLimiter({
|
|
368
|
+
id: 'api-limiter',
|
|
369
|
+
redis: { host: 'redis.example.com' },
|
|
370
|
+
});
|
|
371
|
+
await limiter.ready();
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.log('Redis unavailable, falling back to local limiter');
|
|
374
|
+
// Fall back to non-distributed RateLimiter
|
|
375
|
+
const limiter = new RateLimiter({ maxConcurrent: 5 });
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## API Reference
|
|
382
|
+
|
|
383
|
+
### `RateLimiter`
|
|
384
|
+
|
|
385
|
+
#### Methods
|
|
386
|
+
|
|
387
|
+
| Method | Description |
|
|
388
|
+
|--------|-------------|
|
|
389
|
+
| `schedule(fn)` | Schedule a job for execution |
|
|
390
|
+
| `schedule(options, fn)` | Schedule with options |
|
|
391
|
+
| `wrap(fn, options?)` | Create a rate-limited version of a function |
|
|
392
|
+
| `scheduleAll(jobs)` | Schedule multiple jobs |
|
|
393
|
+
| `pause()` | Pause processing |
|
|
394
|
+
| `resume()` | Resume processing |
|
|
395
|
+
| `stop()` | Stop and reject all pending jobs |
|
|
396
|
+
| `cancel(jobId)` | Cancel a specific job |
|
|
397
|
+
| `waitForIdle()` | Wait for all jobs to complete |
|
|
398
|
+
| `getState()` | Get current limiter state |
|
|
399
|
+
| `getStats()` | Get detailed statistics |
|
|
400
|
+
| `getQueued()` | Get queued jobs |
|
|
401
|
+
| `updateReservoir(value)` | Set reservoir value |
|
|
402
|
+
| `incrementReservoir(amount)` | Add to reservoir |
|
|
403
|
+
| `isIdle()` | Check if limiter is idle |
|
|
404
|
+
|
|
405
|
+
#### Events
|
|
406
|
+
|
|
407
|
+
| Event | Data | Description |
|
|
408
|
+
|-------|------|-------------|
|
|
409
|
+
| `queued` | `{ job, position }` | Job added to queue |
|
|
410
|
+
| `executing` | `{ job, queued, running }` | Job started |
|
|
411
|
+
| `done` | `{ job, result, duration }` | Job completed |
|
|
412
|
+
| `failed` | `{ job, error, willRetry }` | Job failed |
|
|
413
|
+
| `retry` | `{ job, attempt, error }` | Job being retried |
|
|
414
|
+
| `dropped` | `{ job, reason }` | Job dropped (overflow) |
|
|
415
|
+
| `depleted` | - | Reservoir empty |
|
|
416
|
+
| `idle` | - | All jobs complete |
|
|
417
|
+
| `error` | `Error` | Error occurred |
|
|
418
|
+
|
|
419
|
+
### `JobOptions`
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
interface JobOptions {
|
|
423
|
+
priority?: number; // Lower = higher priority (default: 5)
|
|
424
|
+
weight?: number; // Concurrent slots used (default: 1)
|
|
425
|
+
id?: string; // Unique job ID
|
|
426
|
+
timeout?: number; // Job-specific timeout
|
|
427
|
+
retryCount?: number; // Job-specific retry count
|
|
428
|
+
signal?: AbortSignal; // For cancellation
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### `Priority` Enum
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
enum Priority {
|
|
436
|
+
CRITICAL = 0,
|
|
437
|
+
HIGH = 3,
|
|
438
|
+
NORMAL = 5,
|
|
439
|
+
LOW = 7,
|
|
440
|
+
IDLE = 9,
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### `DistributedRateLimiter`
|
|
445
|
+
|
|
446
|
+
Same methods as `RateLimiter`, plus:
|
|
447
|
+
|
|
448
|
+
| Method | Description |
|
|
449
|
+
|--------|-------------|
|
|
450
|
+
| `ready()` | Wait for Redis connection (must call before use) |
|
|
451
|
+
| `getStorage()` | Get underlying RedisStorage instance |
|
|
452
|
+
| `clear()` | Clear all state from Redis |
|
|
453
|
+
|
|
454
|
+
**Note:** `getState()` and `getStats()` are async for the distributed limiter.
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## Running the Examples
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
cd rate-limiter
|
|
462
|
+
npm install
|
|
463
|
+
npm run example # Local rate limiter examples
|
|
464
|
+
npm run example:redis # Distributed examples (requires Redis)
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Running Tests
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
npm test # Local rate limiter tests
|
|
471
|
+
npm test:redis # Distributed tests (requires Redis)
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Starting Redis for Tests
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
# Docker
|
|
478
|
+
docker run -d -p 6379:6379 redis:alpine
|
|
479
|
+
|
|
480
|
+
# macOS
|
|
481
|
+
brew services start redis
|
|
482
|
+
|
|
483
|
+
# Linux
|
|
484
|
+
sudo systemctl start redis
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## License
|
|
488
|
+
|
|
489
|
+
MIT
|
|
490
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { EventListener, RateLimiterEvents } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Type-safe event emitter for the rate limiter
|
|
4
|
+
*/
|
|
5
|
+
export declare class TypedEventEmitter {
|
|
6
|
+
private listeners;
|
|
7
|
+
/**
|
|
8
|
+
* Subscribe to an event
|
|
9
|
+
*/
|
|
10
|
+
on<K extends keyof RateLimiterEvents>(event: K, listener: EventListener<RateLimiterEvents[K]>): () => void;
|
|
11
|
+
/**
|
|
12
|
+
* Subscribe to an event once
|
|
13
|
+
*/
|
|
14
|
+
once<K extends keyof RateLimiterEvents>(event: K, listener: EventListener<RateLimiterEvents[K]>): () => void;
|
|
15
|
+
/**
|
|
16
|
+
* Unsubscribe from an event
|
|
17
|
+
*/
|
|
18
|
+
off<K extends keyof RateLimiterEvents>(event: K, listener: EventListener<RateLimiterEvents[K]>): void;
|
|
19
|
+
/**
|
|
20
|
+
* Emit an event to all listeners
|
|
21
|
+
*/
|
|
22
|
+
protected emit<K extends keyof RateLimiterEvents>(event: K, data: RateLimiterEvents[K]): void;
|
|
23
|
+
/**
|
|
24
|
+
* Remove all listeners for an event (or all events)
|
|
25
|
+
*/
|
|
26
|
+
removeAllListeners(event?: keyof RateLimiterEvents): void;
|
|
27
|
+
/**
|
|
28
|
+
* Get listener count for an event
|
|
29
|
+
*/
|
|
30
|
+
listenerCount(event: keyof RateLimiterEvents): number;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=event-emitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-emitter.d.ts","sourceRoot":"","sources":["../src/event-emitter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEnE;;GAEG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,SAAS,CAAmE;IAEpF;;OAEG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAClC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,GAC5C,MAAM,IAAI;IAUb;;OAEG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,iBAAiB,EACpC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,GAC5C,MAAM,IAAI;IASb;;OAEG;IACH,GAAG,CAAC,CAAC,SAAS,MAAM,iBAAiB,EACnC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,GAC5C,IAAI;IAIP;;OAEG;IACH,SAAS,CAAC,IAAI,CAAC,CAAC,SAAS,MAAM,iBAAiB,EAC9C,KAAK,EAAE,CAAC,EACR,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GACzB,IAAI;IAcP;;OAEG;IACH,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,iBAAiB,GAAG,IAAI;IAQzD;;OAEG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,iBAAiB,GAAG,MAAM;CAGtD"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TypedEventEmitter = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Type-safe event emitter for the rate limiter
|
|
6
|
+
*/
|
|
7
|
+
class TypedEventEmitter {
|
|
8
|
+
listeners = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Subscribe to an event
|
|
11
|
+
*/
|
|
12
|
+
on(event, listener) {
|
|
13
|
+
if (!this.listeners.has(event)) {
|
|
14
|
+
this.listeners.set(event, new Set());
|
|
15
|
+
}
|
|
16
|
+
this.listeners.get(event).add(listener);
|
|
17
|
+
// Return unsubscribe function
|
|
18
|
+
return () => this.off(event, listener);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Subscribe to an event once
|
|
22
|
+
*/
|
|
23
|
+
once(event, listener) {
|
|
24
|
+
const wrapper = ((data) => {
|
|
25
|
+
this.off(event, wrapper);
|
|
26
|
+
listener(data);
|
|
27
|
+
});
|
|
28
|
+
return this.on(event, wrapper);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Unsubscribe from an event
|
|
32
|
+
*/
|
|
33
|
+
off(event, listener) {
|
|
34
|
+
this.listeners.get(event)?.delete(listener);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Emit an event to all listeners
|
|
38
|
+
*/
|
|
39
|
+
emit(event, data) {
|
|
40
|
+
const eventListeners = this.listeners.get(event);
|
|
41
|
+
if (eventListeners) {
|
|
42
|
+
for (const listener of eventListeners) {
|
|
43
|
+
try {
|
|
44
|
+
listener(data);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
// Don't let listener errors break the emitter
|
|
48
|
+
console.error(`Error in event listener for "${String(event)}":`, error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Remove all listeners for an event (or all events)
|
|
55
|
+
*/
|
|
56
|
+
removeAllListeners(event) {
|
|
57
|
+
if (event) {
|
|
58
|
+
this.listeners.delete(event);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this.listeners.clear();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get listener count for an event
|
|
66
|
+
*/
|
|
67
|
+
listenerCount(event) {
|
|
68
|
+
return this.listeners.get(event)?.size ?? 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.TypedEventEmitter = TypedEventEmitter;
|
|
72
|
+
//# sourceMappingURL=event-emitter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-emitter.js","sourceRoot":"","sources":["../src/event-emitter.ts"],"names":[],"mappings":";;;AAEA;;GAEG;AACH,MAAa,iBAAiB;IACpB,SAAS,GAAG,IAAI,GAAG,EAAwD,CAAC;IAEpF;;OAEG;IACH,EAAE,CACA,KAAQ,EACR,QAA6C;QAE7C,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;QACvC,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAE,CAAC,GAAG,CAAC,QAAkC,CAAC,CAAC;QAEnE,8BAA8B;QAC9B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED;;OAEG;IACH,IAAI,CACF,KAAQ,EACR,QAA6C;QAE7C,MAAM,OAAO,GAAG,CAAC,CAAC,IAA0B,EAAE,EAAE;YAC9C,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YACzB,QAAQ,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC,CAAwC,CAAC;QAE1C,OAAO,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC;IAED;;OAEG;IACH,GAAG,CACD,KAAQ,EACR,QAA6C;QAE7C,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,QAAkC,CAAC,CAAC;IACxE,CAAC;IAED;;OAEG;IACO,IAAI,CACZ,KAAQ,EACR,IAA0B;QAE1B,MAAM,cAAc,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACjD,IAAI,cAAc,EAAE,CAAC;YACnB,KAAK,MAAM,QAAQ,IAAI,cAAc,EAAE,CAAC;gBACtC,IAAI,CAAC;oBACH,QAAQ,CAAC,IAAI,CAAC,CAAC;gBACjB,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,8CAA8C;oBAC9C,OAAO,CAAC,KAAK,CAAC,gCAAgC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBAC1E,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACH,kBAAkB,CAAC,KAA+B;QAChD,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC/B,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QACzB,CAAC;IACH,CAAC;IAED;;OAEG;IACH,aAAa,CAAC,KAA8B;QAC1C,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC;IAC9C,CAAC;CACF;AAjFD,8CAiFC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @custom/rate-limiter
|
|
3
|
+
*
|
|
4
|
+
* A powerful rate limiter with:
|
|
5
|
+
* - Concurrency control
|
|
6
|
+
* - Rate limiting (minTime, maxPerInterval)
|
|
7
|
+
* - Priority queues
|
|
8
|
+
* - Reservoir (token bucket) pattern
|
|
9
|
+
* - Task scheduling
|
|
10
|
+
* - Event hooks
|
|
11
|
+
* - Retry support
|
|
12
|
+
* - Abort signal support
|
|
13
|
+
* - Redis clustering for distributed systems
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { RateLimiter, Priority } from '@custom/rate-limiter';
|
|
18
|
+
*
|
|
19
|
+
* const limiter = new RateLimiter({
|
|
20
|
+
* maxConcurrent: 5,
|
|
21
|
+
* minTime: 100,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* const result = await limiter.schedule(async () => {
|
|
25
|
+
* return await fetch('/api/data');
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @example Distributed with Redis
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { DistributedRateLimiter } from '@custom/rate-limiter';
|
|
32
|
+
*
|
|
33
|
+
* const limiter = new DistributedRateLimiter({
|
|
34
|
+
* maxConcurrent: 10,
|
|
35
|
+
* redis: { host: 'localhost', port: 6379 },
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* await limiter.ready();
|
|
39
|
+
* const result = await limiter.schedule(async () => fetchData());
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export { RateLimiter } from './rate-limiter.js';
|
|
43
|
+
export { PriorityQueue } from './priority-queue.js';
|
|
44
|
+
export { TypedEventEmitter } from './event-emitter.js';
|
|
45
|
+
export { DistributedRateLimiter, RedisStorage, type DistributedRateLimiterOptions, type RedisConnectionOptions, type AcquireResult, type DistributedState, } from './redis/index.js';
|
|
46
|
+
export { Priority, type RateLimiterOptions, type JobOptions, type Job, type LimiterState, type Stats, type RateLimiterEvents, type EventListener, type OverflowStrategy, } from './types.js';
|
|
47
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAGvD,OAAO,EACL,sBAAsB,EACtB,YAAY,EACZ,KAAK,6BAA6B,EAClC,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,QAAQ,EACR,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,KAAK,GAAG,EACR,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACtB,MAAM,YAAY,CAAC"}
|