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/ratelimiter.js
CHANGED
|
@@ -41,6 +41,23 @@ if count >= limit then
|
|
|
41
41
|
end
|
|
42
42
|
return { count, retryAfter }
|
|
43
43
|
`;
|
|
44
|
+
/**
|
|
45
|
+
* Generates a "fail-open" result when Redis is unavailable.
|
|
46
|
+
*
|
|
47
|
+
* **Behavior:**
|
|
48
|
+
* - When Redis errors occur during rate limit operations, the limiter "fails open"
|
|
49
|
+
* (allows the request) to prevent an outage from taking down the whole application.
|
|
50
|
+
* - This helper creates the result structure that would be returned in a fail-open scenario.
|
|
51
|
+
*
|
|
52
|
+
**Returns:**
|
|
53
|
+
* - A {@link RateLimitResult} with `allowed: true` and default values.
|
|
54
|
+
*
|
|
55
|
+
* **Parameters:**
|
|
56
|
+
* - `limit` - The configured maximum request count.
|
|
57
|
+
* - `duration` - The window length in seconds.
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
44
61
|
function failOpenResult(limit, duration) {
|
|
45
62
|
return {
|
|
46
63
|
allowed: true,
|
|
@@ -99,6 +116,31 @@ export class RateLimiter {
|
|
|
99
116
|
this.defaultAlgorithm = options.algorithm ?? 'sliding';
|
|
100
117
|
this.defaultNamespace = options.namespace ?? 'ratelimit';
|
|
101
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Creates a rate limiter bound to a Redis client.
|
|
121
|
+
*
|
|
122
|
+
* **Default Configuration:**
|
|
123
|
+
* - `limit`: `100` requests per window
|
|
124
|
+
* - `duration`: `60` seconds per window
|
|
125
|
+
* - `algorithm`: `'sliding'` (precise rolling window)
|
|
126
|
+
* - `namespace`: `'ratelimit'` key prefix
|
|
127
|
+
*
|
|
128
|
+
* **Example:**
|
|
129
|
+
* ```ts
|
|
130
|
+
* // Rate limit per route, per IP, 100 requests per 60 seconds (sliding window)
|
|
131
|
+
* const limiter = new RateLimiter(client, { limit: 100, duration: 60 });
|
|
132
|
+
*
|
|
133
|
+
* // Fixed window: 10 requests per 1 second
|
|
134
|
+
* const fixed = new RateLimiter(client, { limit: 10, duration: 1, algorithm: 'fixed' });
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* **Parameters:**
|
|
138
|
+
* - `client` - The underlying {@link RedisClientWrapper}. All rate limit operations
|
|
139
|
+
* delegate to this client.
|
|
140
|
+
* - `options` - Default rate limit settings. Overridden per-call via the `consume`
|
|
141
|
+
* and `check` methods.
|
|
142
|
+
* - `logger` - Optional pino-compatible logger. Defaults to `console`.
|
|
143
|
+
*/
|
|
102
144
|
/**
|
|
103
145
|
* Builds the Redis key for a resource + identifier combination.
|
|
104
146
|
*
|
|
@@ -114,6 +156,27 @@ export class RateLimiter {
|
|
|
114
156
|
* // 'ratelimit:/api/login:ip-10.0.0.1'
|
|
115
157
|
* ```
|
|
116
158
|
*/
|
|
159
|
+
/**
|
|
160
|
+
* Builds the Redis key for a resource + identifier combination.
|
|
161
|
+
*
|
|
162
|
+
* **Key Format:**
|
|
163
|
+
* The generated key follows the pattern: `${namespace}:${resource}:${identifier}`
|
|
164
|
+
* For example: `ratelimit:/api/login:ip-10.0.0.1`
|
|
165
|
+
*
|
|
166
|
+
* **Example:**
|
|
167
|
+
* ```ts
|
|
168
|
+
* const key = limiter.makeKey('/api/login', 'ip-10.0.0.1');
|
|
169
|
+
* // key === 'ratelimit:/api/login:ip-10.0.0.1'
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* **Parameters:**
|
|
173
|
+
* - `resource` - The rate-limited resource, e.g. a route `'/api/login'` or
|
|
174
|
+
* a resource name `'email:send'`.
|
|
175
|
+
* - `identifier` - The caller identity, e.g. an IP, user id or API key.
|
|
176
|
+
* - `namespace` - Key prefix. Defaults to the limiter's configured namespace.
|
|
177
|
+
*
|
|
178
|
+
* @returns The full key, e.g. `'ratelimit:/api/login:ip-10.0.0.1'`.
|
|
179
|
+
*/
|
|
117
180
|
makeKey(resource, identifier, namespace = this.defaultNamespace) {
|
|
118
181
|
return `${namespace}:${resource}:${identifier}`;
|
|
119
182
|
}
|
|
@@ -142,6 +205,50 @@ export class RateLimiter {
|
|
|
142
205
|
* }
|
|
143
206
|
* ```
|
|
144
207
|
*/
|
|
208
|
+
/**
|
|
209
|
+
* Consumes one unit of capacity for a resource + identifier and returns the
|
|
210
|
+
* resulting limit state.
|
|
211
|
+
*
|
|
212
|
+
* **Behavior:**
|
|
213
|
+
* - When the limit is reached, the request is not recorded and `allowed` is `false`
|
|
214
|
+
* with `retryAfter` (seconds) and `resetAt` (epoch ms) hints.
|
|
215
|
+
* - Fails open (allows the request) if Redis errors occur, so an outage cannot take
|
|
216
|
+
* down the whole app.
|
|
217
|
+
* - Two algorithm modes are available: `sliding` (default, precise rolling window)
|
|
218
|
+
* and `fixed` (simple counter-based).
|
|
219
|
+
*
|
|
220
|
+
* **Type Parameters:**
|
|
221
|
+
* - The return type is {@link RateLimitResult}.
|
|
222
|
+
*
|
|
223
|
+
* **Returns:**
|
|
224
|
+
* - A {@link RateLimitResult} object containing:
|
|
225
|
+
* - `allowed`: whether the request may proceed
|
|
226
|
+
* - `limit`: the configured max
|
|
227
|
+
* - `used`: requests in current window
|
|
228
|
+
* - `remaining`: left in the window
|
|
229
|
+
* - `resetAt`: epoch ms when window resets
|
|
230
|
+
* - `retryAfter`: seconds to wait (0 when allowed)
|
|
231
|
+
*
|
|
232
|
+
* **Example:**
|
|
233
|
+
* ```ts
|
|
234
|
+
* const result = await limiter.consume('/api/login', 'ip-10.0.0.1');
|
|
235
|
+
* if (!result.allowed) {
|
|
236
|
+
* // HTTP 429, set Retry-After: result.retryAfter
|
|
237
|
+
* res.setHeader('Retry-After', String(result.retryAfter));
|
|
238
|
+
* return res.status(429).json({ error: 'Too many requests' });
|
|
239
|
+
* }
|
|
240
|
+
* // allowed === true, request may proceed
|
|
241
|
+
* ```
|
|
242
|
+
*
|
|
243
|
+
* **Parameters:**
|
|
244
|
+
* - `resource` - The rate-limited resource, e.g. a route `'/api/login'` or
|
|
245
|
+
* a resource name `'email:send'`.
|
|
246
|
+
* - `identifier` - The caller identity, e.g. an IP, user id or API key.
|
|
247
|
+
* - `options` - Per-call overrides for `limit`, `duration`, `algorithm`, and `namespace`.
|
|
248
|
+
*
|
|
249
|
+
* @returns The limit state: `allowed`, `limit`, `used`, `remaining`,
|
|
250
|
+
* `resetAt` (epoch ms), `retryAfter` (seconds).
|
|
251
|
+
*/
|
|
145
252
|
async consume(resource, identifier, options = {}) {
|
|
146
253
|
const limit = options.limit ?? this.defaultLimit;
|
|
147
254
|
const duration = options.duration ?? this.defaultDuration;
|
|
@@ -179,6 +286,37 @@ export class RateLimiter {
|
|
|
179
286
|
* }
|
|
180
287
|
* ```
|
|
181
288
|
*/
|
|
289
|
+
/**
|
|
290
|
+
* Peeks at the current limit state without consuming capacity.
|
|
291
|
+
*
|
|
292
|
+
* **Behavior:**
|
|
293
|
+
* - Useful for pre-flight checks (e.g. showing "limit reached" in a UI before
|
|
294
|
+
* the actual request).
|
|
295
|
+
* - Does not increment the counter; only reads the current state.
|
|
296
|
+
* - Fails open (allows the request) if Redis errors occur.
|
|
297
|
+
*
|
|
298
|
+
* **Type Parameters:**
|
|
299
|
+
* - The return type is {@link RateLimitResult}.
|
|
300
|
+
*
|
|
301
|
+
* **Returns:**
|
|
302
|
+
* - A {@link RateLimitResult} object representing the current state.
|
|
303
|
+
* `used` is not incremented.
|
|
304
|
+
*
|
|
305
|
+
* **Example:**
|
|
306
|
+
* ```ts
|
|
307
|
+
* const state = await limiter.check('/api/search', 'user-1');
|
|
308
|
+
* if (state.remaining === 0) {
|
|
309
|
+
* // disable the search button
|
|
310
|
+
* }
|
|
311
|
+
* ```
|
|
312
|
+
*
|
|
313
|
+
* **Parameters:**
|
|
314
|
+
* - `resource` - The rate-limited resource.
|
|
315
|
+
* - `identifier` - The caller identity.
|
|
316
|
+
* - `options` - Per-call overrides for `limit`, `duration`, `algorithm`, and `namespace`.
|
|
317
|
+
*
|
|
318
|
+
* @returns The current limit state; `used` is not incremented.
|
|
319
|
+
*/
|
|
182
320
|
async check(resource, identifier, options = {}) {
|
|
183
321
|
const limit = options.limit ?? this.defaultLimit;
|
|
184
322
|
const duration = options.duration ?? this.defaultDuration;
|
|
@@ -210,6 +348,30 @@ export class RateLimiter {
|
|
|
210
348
|
* await limiter.reset('/api/export', 'user-7');
|
|
211
349
|
* ```
|
|
212
350
|
*/
|
|
351
|
+
/**
|
|
352
|
+
* Resets the counter for a resource + identifier, granting full capacity again.
|
|
353
|
+
*
|
|
354
|
+
* **Behavior:**
|
|
355
|
+
* - Deletes the rate limit key from Redis, resetting the counter to zero.
|
|
356
|
+
* - After reset, the next request will be allowed (full capacity available).
|
|
357
|
+
*
|
|
358
|
+
* **Returns:**
|
|
359
|
+
* - `true` if a counter existed and was removed.
|
|
360
|
+
* - `false` if no counter existed (key already deleted).
|
|
361
|
+
*
|
|
362
|
+
* **Example:**
|
|
363
|
+
* ```ts
|
|
364
|
+
* // User upgraded to a premium plan, lift their limits
|
|
365
|
+
* await limiter.reset('/api/export', 'user-7');
|
|
366
|
+
* ```
|
|
367
|
+
*
|
|
368
|
+
* **Parameters:**
|
|
369
|
+
* - `resource` - The rate-limited resource.
|
|
370
|
+
* - `identifier` - The caller identity.
|
|
371
|
+
* - `namespace` - Key prefix. Defaults to the limiter's configured namespace.
|
|
372
|
+
*
|
|
373
|
+
* @returns `true` if a counter existed and was removed.
|
|
374
|
+
*/
|
|
213
375
|
async reset(resource, identifier, namespace = this.defaultNamespace) {
|
|
214
376
|
const key = this.makeKey(resource, identifier, namespace);
|
|
215
377
|
try {
|
|
@@ -221,6 +383,25 @@ export class RateLimiter {
|
|
|
221
383
|
return false;
|
|
222
384
|
}
|
|
223
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Consumes one unit of capacity using the fixed-window algorithm.
|
|
388
|
+
*
|
|
389
|
+
* **Behavior:**
|
|
390
|
+
* - Uses Redis `INCR` to increment a counter key.
|
|
391
|
+
* - If the counter was `1` (first request in the window), sets a TTL via `EXPIRE`.
|
|
392
|
+
* - The window resets at fixed boundaries determined by the TTL.
|
|
393
|
+
* - Returns `allowed: true` as long as `count <= limit`.
|
|
394
|
+
*
|
|
395
|
+
* **Returns:**
|
|
396
|
+
* - A {@link RateLimitResult} with the current window state.
|
|
397
|
+
*
|
|
398
|
+
* **Parameters:**
|
|
399
|
+
* - `key` - The Redis key for this resource + identifier combination.
|
|
400
|
+
* - `limit` - The maximum allowed requests within the window.
|
|
401
|
+
* - `duration` - The TTL in seconds for the key (also the window length).
|
|
402
|
+
*
|
|
403
|
+
* @internal
|
|
404
|
+
*/
|
|
224
405
|
async consumeFixed(key, limit, duration) {
|
|
225
406
|
const now = Date.now();
|
|
226
407
|
const count = await this.client.incr(key);
|
|
@@ -239,6 +420,32 @@ export class RateLimiter {
|
|
|
239
420
|
retryAfter: allowed ? 0 : ttlSeconds,
|
|
240
421
|
};
|
|
241
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Consumes one unit of capacity using the sliding-window algorithm.
|
|
425
|
+
*
|
|
426
|
+
* **Behavior:**
|
|
427
|
+
* - Uses an atomic Lua script over a sorted set for a precise rolling window.
|
|
428
|
+
* - Old entries outside the window are purged before counting.
|
|
429
|
+
* - A unique member (timestamp + UUID) is added for each request.
|
|
430
|
+
* - The `PEXPIRE` command ensures the key expires after the window duration.
|
|
431
|
+
* - Returns `allowed: true` as long as the count of entries within the window is < limit.
|
|
432
|
+
*
|
|
433
|
+
* **The Lua script** (see {@link CONSUME_SCRIPT}) performs these operations atomically:
|
|
434
|
+
* 1. Remove entries with scores older than `now - window`
|
|
435
|
+
* 2. Count remaining entries (`ZCARD`)
|
|
436
|
+
* 3. If count >= limit, return `allowed: false` with `retryAfter`
|
|
437
|
+
* 4. Otherwise, add the new entry (`ZADD`) and return `allowed: true`
|
|
438
|
+
*
|
|
439
|
+
* **Returns:**
|
|
440
|
+
* - A {@link RateLimitResult} with the current window state.
|
|
441
|
+
*
|
|
442
|
+
* **Parameters:**
|
|
443
|
+
* - `key` - The Redis key for this resource + identifier combination.
|
|
444
|
+
* - `limit` - The maximum allowed requests within the window.
|
|
445
|
+
* - `duration` - The window length in seconds.
|
|
446
|
+
*
|
|
447
|
+
* @internal
|
|
448
|
+
*/
|
|
242
449
|
async consumeSliding(key, limit, duration) {
|
|
243
450
|
const now = Date.now();
|
|
244
451
|
const member = `${now}:${randomUUID()}`;
|
|
@@ -256,6 +463,24 @@ export class RateLimiter {
|
|
|
256
463
|
retryAfter,
|
|
257
464
|
};
|
|
258
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* Peeks at the current limit using the fixed-window algorithm.
|
|
468
|
+
*
|
|
469
|
+
* **Behavior:**
|
|
470
|
+
* - Reads the current counter value from Redis via `GET`.
|
|
471
|
+
* - If the key does not exist, `used` is `0`.
|
|
472
|
+
* - Returns `allowed: true` when `used < limit`.
|
|
473
|
+
*
|
|
474
|
+
* **Returns:**
|
|
475
|
+
* - A {@link RateLimitResult} with the current window state.
|
|
476
|
+
*
|
|
477
|
+
* **Parameters:**
|
|
478
|
+
* - `key` - The Redis key for this resource + identifier combination.
|
|
479
|
+
* - `limit` - The maximum allowed requests within the window.
|
|
480
|
+
* - `duration` - The TTL/window length in seconds.
|
|
481
|
+
*
|
|
482
|
+
* @internal
|
|
483
|
+
*/
|
|
259
484
|
async checkFixed(key, limit, duration) {
|
|
260
485
|
const now = Date.now();
|
|
261
486
|
const raw = await this.client.get(key);
|
|
@@ -272,6 +497,31 @@ export class RateLimiter {
|
|
|
272
497
|
retryAfter: allowed ? 0 : ttlSeconds,
|
|
273
498
|
};
|
|
274
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* Peeks at the current limit using the sliding-window algorithm.
|
|
502
|
+
*
|
|
503
|
+
* **Behavior:**
|
|
504
|
+
* - Uses an atomic Lua script (see {@link PEEK_SCRIPT}) to count entries within
|
|
505
|
+
* the rolling window without consuming capacity.
|
|
506
|
+
* - Old entries outside the window are purged before counting.
|
|
507
|
+
* - Returns `allowed: true` when the count of entries within the window is < limit.
|
|
508
|
+
*
|
|
509
|
+
* **The Lua script** (see {@link PEEK_SCRIPT}) performs:
|
|
510
|
+
* 1. Remove entries with scores older than `now - window`
|
|
511
|
+
* 2. Count remaining entries (`ZCARD`)
|
|
512
|
+
* 3. Return the count and optional `retryAfter`
|
|
513
|
+
*
|
|
514
|
+
* **Returns:**
|
|
515
|
+
* - A {@link RateLimitResult} with the current window state.
|
|
516
|
+
* `used` is the count of entries in the window; not incremented.
|
|
517
|
+
*
|
|
518
|
+
* **Parameters:**
|
|
519
|
+
* - `key` - The Redis key for this resource + identifier combination.
|
|
520
|
+
* - `limit` - The maximum allowed requests within the window.
|
|
521
|
+
* - `duration` - The window length in seconds.
|
|
522
|
+
*
|
|
523
|
+
* @internal
|
|
524
|
+
*/
|
|
275
525
|
async checkSliding(key, limit, duration) {
|
|
276
526
|
const now = Date.now();
|
|
277
527
|
const result = (await this.client.raw.eval(PEEK_SCRIPT, 1, key, now, duration * 1000, limit));
|
|
@@ -31,6 +31,20 @@ export type SessionManagerOptions = {
|
|
|
31
31
|
/** Injectable clock for tests. */
|
|
32
32
|
now?: () => number;
|
|
33
33
|
};
|
|
34
|
+
export type WithSessionManagerOptions = {
|
|
35
|
+
/** Session configuration (defaults applied; see SessionConfigSchema). */
|
|
36
|
+
config?: PartialSessionConfig;
|
|
37
|
+
/**
|
|
38
|
+
* Encryption key provider. REQUIRED when config.encryption.enabled is
|
|
39
|
+
* true (fail at construction rather than at first write). The provider
|
|
40
|
+
* should be backed by a KMS/vault in production.
|
|
41
|
+
*/
|
|
42
|
+
encryptionKeyProvider?: SessionKeyProvider;
|
|
43
|
+
/** Metrics adapter (no-op without it). */
|
|
44
|
+
metricsAdapter?: SessionMetricsAdapter;
|
|
45
|
+
/** Injectable clock for tests. */
|
|
46
|
+
now?: () => number;
|
|
47
|
+
};
|
|
34
48
|
export declare class SessionManager {
|
|
35
49
|
readonly config: SessionConfig;
|
|
36
50
|
readonly service: SessionService;
|
package/dist/types.js
CHANGED
|
@@ -67,8 +67,6 @@ export const BaseRedisConfigSchema = z
|
|
|
67
67
|
maxFanOutConcurrency: z.number().int().min(1).max(128).default(8),
|
|
68
68
|
maxBatchSize: z.number().int().min(1).max(10_000).default(500),
|
|
69
69
|
lockOptions: DistributedLockOptionsSchema.optional(),
|
|
70
|
-
// defaultTTL: z.number().int().min(0).default(3_600),
|
|
71
|
-
// compressionThreshold: z.number().int().min(1).default(1_024),
|
|
72
70
|
cacheOptions: CacheOptionsSchema.optional(),
|
|
73
71
|
slowCommandThreshold: z.number().int().min(0).default(1_000),
|
|
74
72
|
rateLimit: RateLimitOptionsSchema.optional(),
|
package/package.json
CHANGED