ioredis-toolkit 0.0.4 → 0.0.6
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/README.md +772 -316
- package/dist/cache.d.ts +502 -6
- package/dist/cache.js +501 -5
- package/dist/client.d.ts +107 -2
- package/dist/client.js +107 -3
- package/dist/health.d.ts +141 -0
- package/dist/health.js +133 -0
- package/dist/lock.d.ts +55 -1
- package/dist/lock.js +12 -22
- package/dist/pubsub.d.ts +259 -7
- package/dist/pubsub.js +259 -7
- package/dist/ratelimiter.d.ts +279 -1
- package/dist/ratelimiter.js +250 -0
- package/dist/session/session-manager.d.ts +14 -0
- package/dist/types.js +0 -2
- package/package.json +1 -1
package/dist/pubsub.js
CHANGED
|
@@ -4,21 +4,50 @@ import { defaultLogger } from './logger.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Redis Pub/Sub with a dedicated publisher and subscriber connection.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* **Description:**
|
|
8
|
+
* Provides a higher-level interface over native Redis Pub/Sub with the following features:
|
|
9
|
+
* - Dedicated publisher connection (configured at construction)
|
|
10
|
+
* - Dedicated subscriber connection (opened via {@link connectSubscriber})
|
|
11
|
+
* - JSON serialization on publish, auto-parsing on delivery
|
|
12
|
+
* - Pattern subscription support (`psubscribe`, `punsubscribe`)
|
|
13
|
+
* - Extends Node.js `EventEmitter` for event-based handling
|
|
14
|
+
* - Emits `'error'` events on subscriber failures
|
|
9
15
|
*
|
|
10
|
-
*
|
|
16
|
+
* **Type Parameters:**
|
|
17
|
+
* - `T` - The type of message payload. When publishing non-string values, they are
|
|
18
|
+
* JSON-serialized. When subscribing, messages are auto-parsed from JSON when possible.
|
|
11
19
|
*
|
|
12
|
-
*
|
|
20
|
+
* **Mode Support:**
|
|
21
|
+
* - Standalone, Sentinel, and Cluster modes are all supported.
|
|
22
|
+
* - The subscriber connection is created per the configured mode.
|
|
23
|
+
*
|
|
24
|
+
* **Example:**
|
|
13
25
|
* ```ts
|
|
14
26
|
* const pubsub = new PubSub(client);
|
|
15
|
-
* await pubsub.connectSubscriber(
|
|
27
|
+
* await pubsub.connectSubscriber({ mode: 'standalone', host: 'localhost', port: 6379 });
|
|
16
28
|
*
|
|
29
|
+
* // Subscribe to a channel
|
|
17
30
|
* await pubsub.subscribe('orders:created', (message) => {
|
|
18
|
-
* console.log(message); // { id: 1 }
|
|
31
|
+
* console.log(message); // { id: 1 } - JSON-parsed
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // Publish a message
|
|
35
|
+
* const receivers = await pubsub.publish('orders:created', { id: 1 });
|
|
36
|
+
* // receivers === number of subscribers that received the message
|
|
37
|
+
*
|
|
38
|
+
* // Pattern subscription
|
|
39
|
+
* await pubsub.psubscribe('orders:*', ({ channel, message }) => {
|
|
40
|
+
* console.log(channel, message);
|
|
19
41
|
* });
|
|
20
|
-
* await pubsub.publish('orders:created', { id: 1 });
|
|
21
42
|
* ```
|
|
43
|
+
*
|
|
44
|
+
* **Event Emissions:**
|
|
45
|
+
* - `message`: Emitted when a message is received on a subscribed channel.
|
|
46
|
+
* Payload: `{ channel: string, message: string }`
|
|
47
|
+
* - `pmessage`: Emitted when a pattern message is received.
|
|
48
|
+
* Payload: `{ pattern: string, channel: string, message: string }`
|
|
49
|
+
* - `error`: Emitted on subscriber errors.
|
|
50
|
+
* Payload: `Error`
|
|
22
51
|
*/
|
|
23
52
|
export class PubSub extends EventEmitter {
|
|
24
53
|
publisher;
|
|
@@ -26,6 +55,18 @@ export class PubSub extends EventEmitter {
|
|
|
26
55
|
logger;
|
|
27
56
|
subscriptions = new Map();
|
|
28
57
|
patternSubscriptions = new Map();
|
|
58
|
+
/**
|
|
59
|
+
* Creates a pub/sub instance. Publishing works immediately; subscribing
|
|
60
|
+
* requires calling {@link connectSubscriber} first.
|
|
61
|
+
*
|
|
62
|
+
* @param publisher - A {@link RedisClientWrapper} used for publishing.
|
|
63
|
+
* @param logger - Optional pino-compatible logger; defaults to `console`.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const pubsub = new PubSub(client);
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
29
70
|
/**
|
|
30
71
|
* Creates a pub/sub instance. Publishing works immediately; subscribing
|
|
31
72
|
* requires calling {@link connectSubscriber} first.
|
|
@@ -56,12 +97,46 @@ export class PubSub extends EventEmitter {
|
|
|
56
97
|
* await pubsub.connectSubscriber({ mode: 'standalone', host: 'localhost', port: 6379 });
|
|
57
98
|
* ```
|
|
58
99
|
*/
|
|
100
|
+
/**
|
|
101
|
+
* Opens a dedicated subscriber connection.
|
|
102
|
+
*
|
|
103
|
+
* **Behavior:**
|
|
104
|
+
* - Idempotent: a second call is a no-op while a subscriber is connected.
|
|
105
|
+
* - Subscriber errors are emitted as `'error'` events on the instance.
|
|
106
|
+
*
|
|
107
|
+
* **Parameters:**
|
|
108
|
+
* - `config` - Redis config for the subscriber connection (any mode: standalone, sentinel, or cluster).
|
|
109
|
+
*
|
|
110
|
+
* **Example:**
|
|
111
|
+
* ```ts
|
|
112
|
+
* // Connect with standalone configuration
|
|
113
|
+
* await pubsub.connectSubscriber({ mode: 'standalone', host: 'localhost', port: 6379 });
|
|
114
|
+
*
|
|
115
|
+
* // Connect with cluster configuration
|
|
116
|
+
* await pubsub.connectSubscriber({
|
|
117
|
+
* mode: 'cluster',
|
|
118
|
+
* clusterNodes: [{ host: 'redis1', port: 7000 }, { host: 'redis2', port: 7001 }],
|
|
119
|
+
* });
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* @returns `Promise<void>` that resolves when the subscriber connection is established.
|
|
123
|
+
*/
|
|
59
124
|
async connectSubscriber(config) {
|
|
60
125
|
if (this.subscriber)
|
|
61
126
|
return;
|
|
62
127
|
this.subscriber = new RedisClientWrapper(config, this.logger);
|
|
63
128
|
this.setupSubscriber();
|
|
64
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Sets up event listeners on the subscriber connection.
|
|
132
|
+
*
|
|
133
|
+
* **Behavior:**
|
|
134
|
+
* - Listens for `message` events and dispatches to {@link handleMessage}.
|
|
135
|
+
* - Listens for `pmessage` events (pattern subscriptions) and dispatches to {@link handlePatternMessage}.
|
|
136
|
+
* - Listens for `error` events and emits them on the instance.
|
|
137
|
+
*
|
|
138
|
+
* @internal
|
|
139
|
+
*/
|
|
65
140
|
setupSubscriber() {
|
|
66
141
|
if (!this.subscriber)
|
|
67
142
|
return;
|
|
@@ -77,6 +152,20 @@ export class PubSub extends EventEmitter {
|
|
|
77
152
|
this.emit('error', error);
|
|
78
153
|
});
|
|
79
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Handles a standard message event from the subscriber.
|
|
157
|
+
*
|
|
158
|
+
* **Behavior:**
|
|
159
|
+
* - Parses the message payload as JSON when possible.
|
|
160
|
+
* - Invokes all registered handlers for the channel.
|
|
161
|
+
* - Catches and logs handler errors without crashing.
|
|
162
|
+
*
|
|
163
|
+
* **Parameters:**
|
|
164
|
+
* - `channel` - The Redis channel name.
|
|
165
|
+
* - `message` - The raw message string from Redis (JSON-encoded).
|
|
166
|
+
*
|
|
167
|
+
* @internal
|
|
168
|
+
*/
|
|
80
169
|
handleMessage(channel, message) {
|
|
81
170
|
const handlers = this.subscriptions.get(channel);
|
|
82
171
|
if (!handlers)
|
|
@@ -95,6 +184,22 @@ export class PubSub extends EventEmitter {
|
|
|
95
184
|
}
|
|
96
185
|
}
|
|
97
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Handles a pattern message event from the subscriber.
|
|
189
|
+
*
|
|
190
|
+
* **Behavior:**
|
|
191
|
+
* - Parses the message payload as JSON when possible.
|
|
192
|
+
* - Invokes all registered handlers for the pattern.
|
|
193
|
+
* - Each handler receives an object with `channel` and `message` properties.
|
|
194
|
+
* - Catches and logs handler errors without crashing.
|
|
195
|
+
*
|
|
196
|
+
* **Parameters:**
|
|
197
|
+
* - `pattern` - The pattern that matched.
|
|
198
|
+
* - `channel` - The specific channel that was matched.
|
|
199
|
+
* - `message` - The raw message string from Redis (JSON-encoded).
|
|
200
|
+
*
|
|
201
|
+
* @internal
|
|
202
|
+
*/
|
|
98
203
|
handlePatternMessage(pattern, channel, message) {
|
|
99
204
|
const handlers = this.patternSubscriptions.get(pattern);
|
|
100
205
|
if (!handlers)
|
|
@@ -127,6 +232,32 @@ export class PubSub extends EventEmitter {
|
|
|
127
232
|
* const receivers = await pubsub.publish('orders:created', { id: 1, total: 99 });
|
|
128
233
|
* ```
|
|
129
234
|
*/
|
|
235
|
+
/**
|
|
236
|
+
* Publishes a message to a channel.
|
|
237
|
+
*
|
|
238
|
+
* **Behavior:**
|
|
239
|
+
* - Non-string values are JSON-serialized automatically.
|
|
240
|
+
* - The raw message is published to the Redis channel.
|
|
241
|
+
* - Returns the number of subscribers that received the message.
|
|
242
|
+
*
|
|
243
|
+
* **Type Parameters:**
|
|
244
|
+
* - `T` - The type of the message payload. Non-string values are JSON-serialized.
|
|
245
|
+
*
|
|
246
|
+
* **Returns:**
|
|
247
|
+
* - The number of subscribers that received the message.
|
|
248
|
+
*
|
|
249
|
+
* **Example:**
|
|
250
|
+
* ```ts
|
|
251
|
+
* const receivers = await pubsub.publish('orders:created', { id: 1, total: 99 });
|
|
252
|
+
* // receivers === number of subscribed clients
|
|
253
|
+
* ```
|
|
254
|
+
*
|
|
255
|
+
* **Parameters:**
|
|
256
|
+
* - `channel` - The channel name.
|
|
257
|
+
* - `message` - The message payload (string or any JSON-serializable value).
|
|
258
|
+
*
|
|
259
|
+
* @returns The number of subscribers that received the message.
|
|
260
|
+
*/
|
|
130
261
|
async publish(channel, message) {
|
|
131
262
|
const raw = typeof message === 'string' ? message : JSON.stringify(message);
|
|
132
263
|
return this.publisher.raw.publish(channel, raw);
|
|
@@ -148,6 +279,34 @@ export class PubSub extends EventEmitter {
|
|
|
148
279
|
* });
|
|
149
280
|
* ```
|
|
150
281
|
*/
|
|
282
|
+
/**
|
|
283
|
+
* Subscribes a handler to a channel.
|
|
284
|
+
*
|
|
285
|
+
* **Behavior:**
|
|
286
|
+
* - Multiple handlers per channel are supported; the channel is subscribed on
|
|
287
|
+
* Redis only once.
|
|
288
|
+
* - Delivered payloads are JSON-parsed when possible.
|
|
289
|
+
* - Throws an error if the subscriber connection is not open.
|
|
290
|
+
*
|
|
291
|
+
* **Type Parameters:**
|
|
292
|
+
* - `T` - The type of the message data received in the handler.
|
|
293
|
+
*
|
|
294
|
+
* **Returns:**
|
|
295
|
+
* - `Promise<void>` that resolves when the subscription is established.
|
|
296
|
+
*
|
|
297
|
+
* **Example:**
|
|
298
|
+
* ```ts
|
|
299
|
+
* await pubsub.subscribe('orders:created', (order) => {
|
|
300
|
+
* console.log(order.id);
|
|
301
|
+
* });
|
|
302
|
+
* ```
|
|
303
|
+
*
|
|
304
|
+
* **Parameters:**
|
|
305
|
+
* - `channel` - The channel name.
|
|
306
|
+
* - `handler` - Callback receiving the (parsed) message.
|
|
307
|
+
*
|
|
308
|
+
* @throws `Error` if the subscriber connection is not open.
|
|
309
|
+
*/
|
|
151
310
|
async subscribe(channel, handler) {
|
|
152
311
|
if (!this.subscriber) {
|
|
153
312
|
throw new Error('Subscriber not connected');
|
|
@@ -175,6 +334,28 @@ export class PubSub extends EventEmitter {
|
|
|
175
334
|
* await pubsub.unsubscribe('orders:created'); // remove everything
|
|
176
335
|
* ```
|
|
177
336
|
*/
|
|
337
|
+
/**
|
|
338
|
+
* Removes a handler (or all handlers) from a channel.
|
|
339
|
+
*
|
|
340
|
+
* **Behavior:**
|
|
341
|
+
* - The Redis subscription is dropped once the last handler for the channel is
|
|
342
|
+
* removed.
|
|
343
|
+
* - Without a handler, the whole channel is unsubscribed.
|
|
344
|
+
*
|
|
345
|
+
* **Returns:**
|
|
346
|
+
* - `Promise<void>` that resolves when the unsubscription is complete.
|
|
347
|
+
*
|
|
348
|
+
* **Example:**
|
|
349
|
+
* ```ts
|
|
350
|
+
* await pubsub.unsubscribe('orders:created', myHandler);
|
|
351
|
+
* await pubsub.unsubscribe('orders:created'); // remove everything
|
|
352
|
+
* ```
|
|
353
|
+
*
|
|
354
|
+
* **Parameters:**
|
|
355
|
+
* - `channel` - The channel name.
|
|
356
|
+
* - `handler` - Optional specific handler to remove; when omitted all
|
|
357
|
+
* handlers for the channel are removed.
|
|
358
|
+
*/
|
|
178
359
|
async unsubscribe(channel, handler) {
|
|
179
360
|
if (!this.subscriber)
|
|
180
361
|
return;
|
|
@@ -207,6 +388,32 @@ export class PubSub extends EventEmitter {
|
|
|
207
388
|
* });
|
|
208
389
|
* ```
|
|
209
390
|
*/
|
|
391
|
+
/**
|
|
392
|
+
* Subscribes a handler to all channels matching a glob pattern.
|
|
393
|
+
*
|
|
394
|
+
* **Behavior:**
|
|
395
|
+
* - Pattern handlers receive `{ channel, message }` (message JSON-parsed).
|
|
396
|
+
* - The Redis subscription is set up once per pattern.
|
|
397
|
+
*
|
|
398
|
+
* **Type Parameters:**
|
|
399
|
+
* - `T` - The type of the message data received in the handler.
|
|
400
|
+
*
|
|
401
|
+
* **Returns:**
|
|
402
|
+
* - `Promise<void>` that resolves when the pattern subscription is established.
|
|
403
|
+
*
|
|
404
|
+
* **Example:**
|
|
405
|
+
* ```ts
|
|
406
|
+
* await pubsub.psubscribe('orders:*', ({ channel, message }) => {
|
|
407
|
+
* console.log(channel, message);
|
|
408
|
+
* });
|
|
409
|
+
* ```
|
|
410
|
+
*
|
|
411
|
+
* **Parameters:**
|
|
412
|
+
* - `pattern` - Glob pattern, e.g. `'orders:*'`.
|
|
413
|
+
* - `handler` - Callback receiving `{ channel, message }`.
|
|
414
|
+
*
|
|
415
|
+
* @throws `Error` if the subscriber connection is not open.
|
|
416
|
+
*/
|
|
210
417
|
async psubscribe(pattern, handler) {
|
|
211
418
|
if (!this.subscriber) {
|
|
212
419
|
throw new Error('Subscriber not connected');
|
|
@@ -230,6 +437,23 @@ export class PubSub extends EventEmitter {
|
|
|
230
437
|
* await pubsub.punsubscribe('orders:*');
|
|
231
438
|
* ```
|
|
232
439
|
*/
|
|
440
|
+
/**
|
|
441
|
+
* Removes a handler (or all handlers) from a pattern subscription.
|
|
442
|
+
*
|
|
443
|
+
* **Returns:**
|
|
444
|
+
* - `Promise<void>` that resolves when the punsubscription is complete.
|
|
445
|
+
*
|
|
446
|
+
* **Example:**
|
|
447
|
+
* ```ts
|
|
448
|
+
* await pubsub.punsubscribe('orders:*', myHandler);
|
|
449
|
+
* await pubsub.punsubscribe('orders:*');
|
|
450
|
+
* ```
|
|
451
|
+
*
|
|
452
|
+
* **Parameters:**
|
|
453
|
+
* - `pattern` - The glob pattern.
|
|
454
|
+
* - `handler` - Optional specific handler to remove; when omitted all
|
|
455
|
+
* handlers for the pattern are removed.
|
|
456
|
+
*/
|
|
233
457
|
async punsubscribe(pattern, handler) {
|
|
234
458
|
if (!this.subscriber)
|
|
235
459
|
return;
|
|
@@ -256,6 +480,20 @@ export class PubSub extends EventEmitter {
|
|
|
256
480
|
* await pubsub.close();
|
|
257
481
|
* ```
|
|
258
482
|
*/
|
|
483
|
+
/**
|
|
484
|
+
* Closes the subscriber connection and clears all subscriptions.
|
|
485
|
+
*
|
|
486
|
+
* **Behavior:**
|
|
487
|
+
* - The publisher client is not closed (it is owned by the caller).
|
|
488
|
+
* - All subscriptions are cleared from memory.
|
|
489
|
+
*
|
|
490
|
+
* **Example:**
|
|
491
|
+
* ```ts
|
|
492
|
+
* await pubsub.close();
|
|
493
|
+
* ```
|
|
494
|
+
*
|
|
495
|
+
* @returns `Promise<void>` that resolves when the subscriber is closed.
|
|
496
|
+
*/
|
|
259
497
|
async close() {
|
|
260
498
|
if (this.subscriber) {
|
|
261
499
|
await this.subscriber.close();
|
|
@@ -275,6 +513,20 @@ export class PubSub extends EventEmitter {
|
|
|
275
513
|
* // { subscriptions: 2, patternSubscriptions: 1, connected: true }
|
|
276
514
|
* ```
|
|
277
515
|
*/
|
|
516
|
+
/**
|
|
517
|
+
* Returns subscription statistics.
|
|
518
|
+
*
|
|
519
|
+
* **Returns:**
|
|
520
|
+
* - A {@link PubSubStats} object with the current subscription state.
|
|
521
|
+
*
|
|
522
|
+
* **Example:**
|
|
523
|
+
* ```ts
|
|
524
|
+
* const stats = pubsub.getStats();
|
|
525
|
+
* // { subscriptions: 2, patternSubscriptions: 1, connected: true }
|
|
526
|
+
* ```
|
|
527
|
+
*
|
|
528
|
+
* @returns `{ subscriptions, patternSubscriptions, connected }`.
|
|
529
|
+
*/
|
|
278
530
|
getStats() {
|
|
279
531
|
return {
|
|
280
532
|
subscriptions: this.subscriptions.size,
|
package/dist/ratelimiter.d.ts
CHANGED
|
@@ -6,10 +6,32 @@ import { RateLimitOptionsInput } from './types.js';
|
|
|
6
6
|
* - `fixed` - fixed window via `INCR`/`EXPIRE` (simple, cheapest)
|
|
7
7
|
* - `sliding` - sliding window via an atomic Lua script over a sorted set (smoothest)
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* Window algorithm used by the rate limiter.
|
|
11
|
+
*
|
|
12
|
+
* - `fixed` - Fixed window via `INCR`/`EXPIRE` (simple, cheapest).
|
|
13
|
+
* - `sliding` - Sliding window via an atomic Lua script over a sorted set (smoothest,
|
|
14
|
+
* precise rolling window).
|
|
15
|
+
*/
|
|
9
16
|
export type RateLimitAlgorithm = 'fixed' | 'sliding';
|
|
10
17
|
/**
|
|
11
18
|
* Options for a rate limiter instance or an individual call.
|
|
12
19
|
*/
|
|
20
|
+
/**
|
|
21
|
+
* Options for a rate limiter instance or an individual call.
|
|
22
|
+
*
|
|
23
|
+
* **Algorithm Details:**
|
|
24
|
+
* - `sliding` (default): Uses a sorted set with a Lua script for a precise rolling window.
|
|
25
|
+
* New entries are added with the current timestamp, and old entries outside the window
|
|
26
|
+
* are purged before counting. This provides the smoothest rate limiting experience.
|
|
27
|
+
* - `fixed`: Uses a simple counter with `INCR`/`EXPIRE`. The window resets at fixed
|
|
28
|
+
* boundaries (e.g., every 60 seconds from the start). This is the cheapest algorithm
|
|
29
|
+
* but has slightly less precise rate limiting.
|
|
30
|
+
*
|
|
31
|
+
* **Key Naming:**
|
|
32
|
+
* Keys are namespaced as `ratelimit:{namespace}:{resource}:{identifier}` so each
|
|
33
|
+
* resource + identifier pair is tracked independently.
|
|
34
|
+
*/
|
|
13
35
|
export interface RateLimitOptions {
|
|
14
36
|
/** Maximum allowed requests within `duration`. Default: `100`. */
|
|
15
37
|
limit?: number;
|
|
@@ -23,6 +45,29 @@ export interface RateLimitOptions {
|
|
|
23
45
|
/**
|
|
24
46
|
* Result of a rate limit `consume`/`check` call.
|
|
25
47
|
*/
|
|
48
|
+
/**
|
|
49
|
+
* Result of a rate limit `consume`/`check` call.
|
|
50
|
+
*
|
|
51
|
+
* **Fields:**
|
|
52
|
+
* - `allowed`: `true` when the request is within the rate limit and may proceed.
|
|
53
|
+
* - `limit`: The configured maximum number of requests within the window.
|
|
54
|
+
* - `used`: The number of requests already counted in the current window.
|
|
55
|
+
* - `remaining`: The number of requests still available (`limit - used`, floored at `0`).
|
|
56
|
+
* - `resetAt`: Epoch milliseconds when the current window resets. `0` when the request
|
|
57
|
+
* is allowed (indicating the window is still open).
|
|
58
|
+
* - `retryAfter`: Seconds to wait before retrying the request. `0` when the request
|
|
59
|
+
* is allowed.
|
|
60
|
+
*
|
|
61
|
+
* **Example:**
|
|
62
|
+
* ```ts
|
|
63
|
+
* const result = await limiter.consume('/api/login', 'ip-10.0.0.1');
|
|
64
|
+
* if (!result.allowed) {
|
|
65
|
+
* // result.retryAfter tells you how many seconds to wait before retrying
|
|
66
|
+
* console.log(`Retry after ${result.retryAfter}s`);
|
|
67
|
+
* console.log(`Window resets at ${new Date(result.resetAt)}`);
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
26
71
|
export interface RateLimitResult {
|
|
27
72
|
/** `true` when the request is within the limit. */
|
|
28
73
|
allowed: boolean;
|
|
@@ -32,7 +77,7 @@ export interface RateLimitResult {
|
|
|
32
77
|
used: number;
|
|
33
78
|
/** Requests still available (`limit - used`, floored at `0`). */
|
|
34
79
|
remaining: number;
|
|
35
|
-
/** Epoch milliseconds when the window resets
|
|
80
|
+
/** Epoch milliseconds when the window resets. */
|
|
36
81
|
resetAt: number;
|
|
37
82
|
/** Seconds to wait before retrying; `0` when allowed. */
|
|
38
83
|
retryAfter: number;
|
|
@@ -78,6 +123,31 @@ export declare class RateLimiter {
|
|
|
78
123
|
* ```
|
|
79
124
|
*/
|
|
80
125
|
constructor(client: RedisClientWrapper, options?: RateLimitOptionsInput, logger?: LoggerLike);
|
|
126
|
+
/**
|
|
127
|
+
* Creates a rate limiter bound to a Redis client.
|
|
128
|
+
*
|
|
129
|
+
* **Default Configuration:**
|
|
130
|
+
* - `limit`: `100` requests per window
|
|
131
|
+
* - `duration`: `60` seconds per window
|
|
132
|
+
* - `algorithm`: `'sliding'` (precise rolling window)
|
|
133
|
+
* - `namespace`: `'ratelimit'` key prefix
|
|
134
|
+
*
|
|
135
|
+
* **Example:**
|
|
136
|
+
* ```ts
|
|
137
|
+
* // Rate limit per route, per IP, 100 requests per 60 seconds (sliding window)
|
|
138
|
+
* const limiter = new RateLimiter(client, { limit: 100, duration: 60 });
|
|
139
|
+
*
|
|
140
|
+
* // Fixed window: 10 requests per 1 second
|
|
141
|
+
* const fixed = new RateLimiter(client, { limit: 10, duration: 1, algorithm: 'fixed' });
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* **Parameters:**
|
|
145
|
+
* - `client` - The underlying {@link RedisClientWrapper}. All rate limit operations
|
|
146
|
+
* delegate to this client.
|
|
147
|
+
* - `options` - Default rate limit settings. Overridden per-call via the `consume`
|
|
148
|
+
* and `check` methods.
|
|
149
|
+
* - `logger` - Optional pino-compatible logger. Defaults to `console`.
|
|
150
|
+
*/
|
|
81
151
|
/**
|
|
82
152
|
* Builds the Redis key for a resource + identifier combination.
|
|
83
153
|
*
|
|
@@ -93,6 +163,27 @@ export declare class RateLimiter {
|
|
|
93
163
|
* // 'ratelimit:/api/login:ip-10.0.0.1'
|
|
94
164
|
* ```
|
|
95
165
|
*/
|
|
166
|
+
/**
|
|
167
|
+
* Builds the Redis key for a resource + identifier combination.
|
|
168
|
+
*
|
|
169
|
+
* **Key Format:**
|
|
170
|
+
* The generated key follows the pattern: `${namespace}:${resource}:${identifier}`
|
|
171
|
+
* For example: `ratelimit:/api/login:ip-10.0.0.1`
|
|
172
|
+
*
|
|
173
|
+
* **Example:**
|
|
174
|
+
* ```ts
|
|
175
|
+
* const key = limiter.makeKey('/api/login', 'ip-10.0.0.1');
|
|
176
|
+
* // key === 'ratelimit:/api/login:ip-10.0.0.1'
|
|
177
|
+
* ```
|
|
178
|
+
*
|
|
179
|
+
* **Parameters:**
|
|
180
|
+
* - `resource` - The rate-limited resource, e.g. a route `'/api/login'` or
|
|
181
|
+
* a resource name `'email:send'`.
|
|
182
|
+
* - `identifier` - The caller identity, e.g. an IP, user id or API key.
|
|
183
|
+
* - `namespace` - Key prefix. Defaults to the limiter's configured namespace.
|
|
184
|
+
*
|
|
185
|
+
* @returns The full key, e.g. `'ratelimit:/api/login:ip-10.0.0.1'`.
|
|
186
|
+
*/
|
|
96
187
|
makeKey(resource: string, identifier: string, namespace?: string): string;
|
|
97
188
|
/**
|
|
98
189
|
* Consumes one unit of capacity for a resource + identifier and returns the
|
|
@@ -119,6 +210,50 @@ export declare class RateLimiter {
|
|
|
119
210
|
* }
|
|
120
211
|
* ```
|
|
121
212
|
*/
|
|
213
|
+
/**
|
|
214
|
+
* Consumes one unit of capacity for a resource + identifier and returns the
|
|
215
|
+
* resulting limit state.
|
|
216
|
+
*
|
|
217
|
+
* **Behavior:**
|
|
218
|
+
* - When the limit is reached, the request is not recorded and `allowed` is `false`
|
|
219
|
+
* with `retryAfter` (seconds) and `resetAt` (epoch ms) hints.
|
|
220
|
+
* - Fails open (allows the request) if Redis errors occur, so an outage cannot take
|
|
221
|
+
* down the whole app.
|
|
222
|
+
* - Two algorithm modes are available: `sliding` (default, precise rolling window)
|
|
223
|
+
* and `fixed` (simple counter-based).
|
|
224
|
+
*
|
|
225
|
+
* **Type Parameters:**
|
|
226
|
+
* - The return type is {@link RateLimitResult}.
|
|
227
|
+
*
|
|
228
|
+
* **Returns:**
|
|
229
|
+
* - A {@link RateLimitResult} object containing:
|
|
230
|
+
* - `allowed`: whether the request may proceed
|
|
231
|
+
* - `limit`: the configured max
|
|
232
|
+
* - `used`: requests in current window
|
|
233
|
+
* - `remaining`: left in the window
|
|
234
|
+
* - `resetAt`: epoch ms when window resets
|
|
235
|
+
* - `retryAfter`: seconds to wait (0 when allowed)
|
|
236
|
+
*
|
|
237
|
+
* **Example:**
|
|
238
|
+
* ```ts
|
|
239
|
+
* const result = await limiter.consume('/api/login', 'ip-10.0.0.1');
|
|
240
|
+
* if (!result.allowed) {
|
|
241
|
+
* // HTTP 429, set Retry-After: result.retryAfter
|
|
242
|
+
* res.setHeader('Retry-After', String(result.retryAfter));
|
|
243
|
+
* return res.status(429).json({ error: 'Too many requests' });
|
|
244
|
+
* }
|
|
245
|
+
* // allowed === true, request may proceed
|
|
246
|
+
* ```
|
|
247
|
+
*
|
|
248
|
+
* **Parameters:**
|
|
249
|
+
* - `resource` - The rate-limited resource, e.g. a route `'/api/login'` or
|
|
250
|
+
* a resource name `'email:send'`.
|
|
251
|
+
* - `identifier` - The caller identity, e.g. an IP, user id or API key.
|
|
252
|
+
* - `options` - Per-call overrides for `limit`, `duration`, `algorithm`, and `namespace`.
|
|
253
|
+
*
|
|
254
|
+
* @returns The limit state: `allowed`, `limit`, `used`, `remaining`,
|
|
255
|
+
* `resetAt` (epoch ms), `retryAfter` (seconds).
|
|
256
|
+
*/
|
|
122
257
|
consume(resource: string, identifier: string, options?: RateLimitOptions): Promise<RateLimitResult>;
|
|
123
258
|
/**
|
|
124
259
|
* Peeks at the current limit state without consuming capacity.
|
|
@@ -140,6 +275,37 @@ export declare class RateLimiter {
|
|
|
140
275
|
* }
|
|
141
276
|
* ```
|
|
142
277
|
*/
|
|
278
|
+
/**
|
|
279
|
+
* Peeks at the current limit state without consuming capacity.
|
|
280
|
+
*
|
|
281
|
+
* **Behavior:**
|
|
282
|
+
* - Useful for pre-flight checks (e.g. showing "limit reached" in a UI before
|
|
283
|
+
* the actual request).
|
|
284
|
+
* - Does not increment the counter; only reads the current state.
|
|
285
|
+
* - Fails open (allows the request) if Redis errors occur.
|
|
286
|
+
*
|
|
287
|
+
* **Type Parameters:**
|
|
288
|
+
* - The return type is {@link RateLimitResult}.
|
|
289
|
+
*
|
|
290
|
+
* **Returns:**
|
|
291
|
+
* - A {@link RateLimitResult} object representing the current state.
|
|
292
|
+
* `used` is not incremented.
|
|
293
|
+
*
|
|
294
|
+
* **Example:**
|
|
295
|
+
* ```ts
|
|
296
|
+
* const state = await limiter.check('/api/search', 'user-1');
|
|
297
|
+
* if (state.remaining === 0) {
|
|
298
|
+
* // disable the search button
|
|
299
|
+
* }
|
|
300
|
+
* ```
|
|
301
|
+
*
|
|
302
|
+
* **Parameters:**
|
|
303
|
+
* - `resource` - The rate-limited resource.
|
|
304
|
+
* - `identifier` - The caller identity.
|
|
305
|
+
* - `options` - Per-call overrides for `limit`, `duration`, `algorithm`, and `namespace`.
|
|
306
|
+
*
|
|
307
|
+
* @returns The current limit state; `used` is not incremented.
|
|
308
|
+
*/
|
|
143
309
|
check(resource: string, identifier: string, options?: RateLimitOptions): Promise<RateLimitResult>;
|
|
144
310
|
/**
|
|
145
311
|
* Resets the counter for a resource + identifier, granting full capacity again.
|
|
@@ -155,9 +321,121 @@ export declare class RateLimiter {
|
|
|
155
321
|
* await limiter.reset('/api/export', 'user-7');
|
|
156
322
|
* ```
|
|
157
323
|
*/
|
|
324
|
+
/**
|
|
325
|
+
* Resets the counter for a resource + identifier, granting full capacity again.
|
|
326
|
+
*
|
|
327
|
+
* **Behavior:**
|
|
328
|
+
* - Deletes the rate limit key from Redis, resetting the counter to zero.
|
|
329
|
+
* - After reset, the next request will be allowed (full capacity available).
|
|
330
|
+
*
|
|
331
|
+
* **Returns:**
|
|
332
|
+
* - `true` if a counter existed and was removed.
|
|
333
|
+
* - `false` if no counter existed (key already deleted).
|
|
334
|
+
*
|
|
335
|
+
* **Example:**
|
|
336
|
+
* ```ts
|
|
337
|
+
* // User upgraded to a premium plan, lift their limits
|
|
338
|
+
* await limiter.reset('/api/export', 'user-7');
|
|
339
|
+
* ```
|
|
340
|
+
*
|
|
341
|
+
* **Parameters:**
|
|
342
|
+
* - `resource` - The rate-limited resource.
|
|
343
|
+
* - `identifier` - The caller identity.
|
|
344
|
+
* - `namespace` - Key prefix. Defaults to the limiter's configured namespace.
|
|
345
|
+
*
|
|
346
|
+
* @returns `true` if a counter existed and was removed.
|
|
347
|
+
*/
|
|
158
348
|
reset(resource: string, identifier: string, namespace?: string): Promise<boolean>;
|
|
349
|
+
/**
|
|
350
|
+
* Consumes one unit of capacity using the fixed-window algorithm.
|
|
351
|
+
*
|
|
352
|
+
* **Behavior:**
|
|
353
|
+
* - Uses Redis `INCR` to increment a counter key.
|
|
354
|
+
* - If the counter was `1` (first request in the window), sets a TTL via `EXPIRE`.
|
|
355
|
+
* - The window resets at fixed boundaries determined by the TTL.
|
|
356
|
+
* - Returns `allowed: true` as long as `count <= limit`.
|
|
357
|
+
*
|
|
358
|
+
* **Returns:**
|
|
359
|
+
* - A {@link RateLimitResult} with the current window state.
|
|
360
|
+
*
|
|
361
|
+
* **Parameters:**
|
|
362
|
+
* - `key` - The Redis key for this resource + identifier combination.
|
|
363
|
+
* - `limit` - The maximum allowed requests within the window.
|
|
364
|
+
* - `duration` - The TTL in seconds for the key (also the window length).
|
|
365
|
+
*
|
|
366
|
+
* @internal
|
|
367
|
+
*/
|
|
159
368
|
private consumeFixed;
|
|
369
|
+
/**
|
|
370
|
+
* Consumes one unit of capacity using the sliding-window algorithm.
|
|
371
|
+
*
|
|
372
|
+
* **Behavior:**
|
|
373
|
+
* - Uses an atomic Lua script over a sorted set for a precise rolling window.
|
|
374
|
+
* - Old entries outside the window are purged before counting.
|
|
375
|
+
* - A unique member (timestamp + UUID) is added for each request.
|
|
376
|
+
* - The `PEXPIRE` command ensures the key expires after the window duration.
|
|
377
|
+
* - Returns `allowed: true` as long as the count of entries within the window is < limit.
|
|
378
|
+
*
|
|
379
|
+
* **The Lua script** (see {@link CONSUME_SCRIPT}) performs these operations atomically:
|
|
380
|
+
* 1. Remove entries with scores older than `now - window`
|
|
381
|
+
* 2. Count remaining entries (`ZCARD`)
|
|
382
|
+
* 3. If count >= limit, return `allowed: false` with `retryAfter`
|
|
383
|
+
* 4. Otherwise, add the new entry (`ZADD`) and return `allowed: true`
|
|
384
|
+
*
|
|
385
|
+
* **Returns:**
|
|
386
|
+
* - A {@link RateLimitResult} with the current window state.
|
|
387
|
+
*
|
|
388
|
+
* **Parameters:**
|
|
389
|
+
* - `key` - The Redis key for this resource + identifier combination.
|
|
390
|
+
* - `limit` - The maximum allowed requests within the window.
|
|
391
|
+
* - `duration` - The window length in seconds.
|
|
392
|
+
*
|
|
393
|
+
* @internal
|
|
394
|
+
*/
|
|
160
395
|
private consumeSliding;
|
|
396
|
+
/**
|
|
397
|
+
* Peeks at the current limit using the fixed-window algorithm.
|
|
398
|
+
*
|
|
399
|
+
* **Behavior:**
|
|
400
|
+
* - Reads the current counter value from Redis via `GET`.
|
|
401
|
+
* - If the key does not exist, `used` is `0`.
|
|
402
|
+
* - Returns `allowed: true` when `used < limit`.
|
|
403
|
+
*
|
|
404
|
+
* **Returns:**
|
|
405
|
+
* - A {@link RateLimitResult} with the current window state.
|
|
406
|
+
*
|
|
407
|
+
* **Parameters:**
|
|
408
|
+
* - `key` - The Redis key for this resource + identifier combination.
|
|
409
|
+
* - `limit` - The maximum allowed requests within the window.
|
|
410
|
+
* - `duration` - The TTL/window length in seconds.
|
|
411
|
+
*
|
|
412
|
+
* @internal
|
|
413
|
+
*/
|
|
161
414
|
private checkFixed;
|
|
415
|
+
/**
|
|
416
|
+
* Peeks at the current limit using the sliding-window algorithm.
|
|
417
|
+
*
|
|
418
|
+
* **Behavior:**
|
|
419
|
+
* - Uses an atomic Lua script (see {@link PEEK_SCRIPT}) to count entries within
|
|
420
|
+
* the rolling window without consuming capacity.
|
|
421
|
+
* - Old entries outside the window are purged before counting.
|
|
422
|
+
* - Returns `allowed: true` when the count of entries within the window is < limit.
|
|
423
|
+
*
|
|
424
|
+
* **The Lua script** (see {@link PEEK_SCRIPT}) performs:
|
|
425
|
+
* 1. Remove entries with scores older than `now - window`
|
|
426
|
+
* 2. Count remaining entries (`ZCARD`)
|
|
427
|
+
* 3. Return the count and optional `retryAfter`
|
|
428
|
+
*
|
|
429
|
+
* **Returns:**
|
|
430
|
+
* - A {@link RateLimitResult} with the current window state.
|
|
431
|
+
* `used` is the count of entries in the window; not incremented.
|
|
432
|
+
*
|
|
433
|
+
* **Parameters:**
|
|
434
|
+
* - `key` - The Redis key for this resource + identifier combination.
|
|
435
|
+
* - `limit` - The maximum allowed requests within the window.
|
|
436
|
+
* - `duration` - The window length in seconds.
|
|
437
|
+
*
|
|
438
|
+
* @internal
|
|
439
|
+
*/
|
|
162
440
|
private checkSliding;
|
|
163
441
|
}
|