@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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +490 -0
  3. package/dist/event-emitter.d.ts +32 -0
  4. package/dist/event-emitter.d.ts.map +1 -0
  5. package/dist/event-emitter.js +72 -0
  6. package/dist/event-emitter.js.map +1 -0
  7. package/dist/index.d.ts +47 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +59 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/priority-queue.d.ts +66 -0
  12. package/dist/priority-queue.d.ts.map +1 -0
  13. package/dist/priority-queue.js +147 -0
  14. package/dist/priority-queue.js.map +1 -0
  15. package/dist/rate-limiter.d.ts +135 -0
  16. package/dist/rate-limiter.d.ts.map +1 -0
  17. package/dist/rate-limiter.js +455 -0
  18. package/dist/rate-limiter.js.map +1 -0
  19. package/dist/redis/distributed-rate-limiter.d.ts +149 -0
  20. package/dist/redis/distributed-rate-limiter.d.ts.map +1 -0
  21. package/dist/redis/distributed-rate-limiter.js +423 -0
  22. package/dist/redis/distributed-rate-limiter.js.map +1 -0
  23. package/dist/redis/index.d.ts +8 -0
  24. package/dist/redis/index.d.ts.map +1 -0
  25. package/dist/redis/index.js +11 -0
  26. package/dist/redis/index.js.map +1 -0
  27. package/dist/redis/lua-scripts.d.ts +62 -0
  28. package/dist/redis/lua-scripts.d.ts.map +1 -0
  29. package/dist/redis/lua-scripts.js +229 -0
  30. package/dist/redis/lua-scripts.js.map +1 -0
  31. package/dist/redis/redis-storage.d.ts +134 -0
  32. package/dist/redis/redis-storage.d.ts.map +1 -0
  33. package/dist/redis/redis-storage.js +255 -0
  34. package/dist/redis/redis-storage.js.map +1 -0
  35. package/dist/types.d.ts +207 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +15 -0
  38. package/dist/types.js.map +1 -0
  39. 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"}
@@ -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"}