rate-limiter-flexible 11.0.2 → 11.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/README.md CHANGED
@@ -136,7 +136,7 @@ Copy/paste examples on Wiki:
136
136
  * [DynamoDb](https://github.com/animir/node-rate-limiter-flexible/wiki/DynamoDB)
137
137
  * [Etcd](https://github.com/animir/node-rate-limiter-flexible/wiki/Etcd) Atomic and non-atomic counters.
138
138
  * [Memcached](https://github.com/animir/node-rate-limiter-flexible/wiki/Memcache)
139
- * [Memory](https://github.com/animir/node-rate-limiter-flexible/wiki/Memory)
139
+ * [Memory](https://github.com/animir/node-rate-limiter-flexible/wiki/Memory) Supports dump() and restore() for graceful restarts and green/blue deployment
140
140
  * [Mongo](https://github.com/animir/node-rate-limiter-flexible/wiki/Mongo) (with [sharding support](https://github.com/animir/node-rate-limiter-flexible/wiki/Mongo#mongodb-sharding-options))
141
141
  * [MySQL](https://github.com/animir/node-rate-limiter-flexible/wiki/MySQL) (support Sequelize and Knex)
142
142
  * [Postgres](https://github.com/animir/node-rate-limiter-flexible/wiki/PostgreSQL) (support Sequelize, TypeORM and Knex)
@@ -102,6 +102,96 @@ class RateLimiterMemory extends RateLimiterAbstract {
102
102
  delete(key) {
103
103
  return Promise.resolve(this._memoryStorage.delete(this.getKey(key)));
104
104
  }
105
+
106
+ dump() {
107
+ const storage = [];
108
+ for (const [key, record] of this._memoryStorage._storage) {
109
+ storage.push({
110
+ key: this.parseKey(key),
111
+ value: record.value,
112
+ expiresAt: record.expiresAt,
113
+ });
114
+ }
115
+
116
+ return {
117
+ version: 1,
118
+ dumpedAt: Date.now(),
119
+ storage,
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Restores rate-limiter state from a previously dumped snapshot.
125
+ *
126
+ * Each entry is classified into one of three buckets:
127
+ * - **invalid** – the item is not an object or has a malformed schema (bad key, value or expiry type).
128
+ * - **expired** – the item has logically expired based on current timestamp.
129
+ * - **restored** – the item was valid and successfully loaded into storage.
130
+ *
131
+ * @param {Object} data - Snapshot produced by {@link dump}.
132
+ * @param {boolean} [detailResponse=false] - When `true`, each bucket also
133
+ * carries a `keys` array listing every affected key.
134
+ * @returns {{ invalid, expired, restored } | undefined} Summary counts (and
135
+ * optionally keys). Returns `undefined` when `data` fails schema checks.
136
+ */
137
+ restore(data, detailResponse = false) {
138
+ if (!data || typeof data !== 'object' || data.version !== 1) {
139
+ return undefined;
140
+ }
141
+
142
+ const response = detailResponse
143
+ ? {
144
+ invalid: { count: 0, keys: [] },
145
+ expired: { count: 0, keys: [] },
146
+ restored: { count: 0, keys: [] },
147
+ }
148
+ : { invalid: 0, expired: 0, restored: 0 };
149
+
150
+ /**
151
+ * Increments the named bucket and, in detail mode, appends the key.
152
+ * @param {'invalid'|'expired'|'restored'} bucket
153
+ * @param {string|number} key
154
+ */
155
+ const record = (bucket, key) => {
156
+ if (detailResponse) {
157
+ response[bucket].count += 1;
158
+ response[bucket].keys.push(key);
159
+ } else {
160
+ response[bucket] += 1;
161
+ }
162
+ };
163
+
164
+ if (!Array.isArray(data.storage)) {
165
+ return response;
166
+ }
167
+
168
+ for (const item of data.storage) {
169
+ if (!item || typeof item !== 'object') {
170
+ //in array if we don't get object we push N/A
171
+ record('invalid', 'N/A');
172
+ continue;
173
+ }
174
+
175
+ const isValidKey = typeof item.key === 'string' || typeof item.key === 'number';
176
+ const isValidValue = Number.isFinite(item.value);
177
+ const isValidExpiry = item.expiresAt === null || Number.isFinite(item.expiresAt);
178
+
179
+ if (!isValidKey || !isValidValue || !isValidExpiry) {
180
+ record('invalid', item.key);
181
+ continue;
182
+ }
183
+
184
+ if (item.expiresAt !== null && item.expiresAt <= Date.now()) {
185
+ record('expired', item.key);
186
+ continue;
187
+ }
188
+
189
+ this._memoryStorage._restoreRecord(this.getKey(item.key), item.value, item.expiresAt);
190
+ record('restored', item.key);
191
+ }
192
+
193
+ return response;
194
+ }
105
195
  }
106
196
 
107
197
  module.exports = RateLimiterMemory;
@@ -86,4 +86,45 @@ module.exports = class MemoryStorage {
86
86
  }
87
87
  return false;
88
88
  }
89
+
90
+ /**
91
+ * @internal
92
+ * @param key
93
+ * @param value
94
+ * @param expiresAt
95
+ */
96
+ _restoreRecord(key, value, expiresAt) {
97
+ const now = Date.now();
98
+ const hasExpiresAt = expiresAt !== null;
99
+
100
+ if (hasExpiresAt && !Number.isFinite(expiresAt)) {
101
+ return;
102
+ }
103
+
104
+ if (hasExpiresAt && expiresAt <= now) {
105
+ return;
106
+ }
107
+
108
+ const durationMs = hasExpiresAt ? expiresAt - now : 0;
109
+
110
+ const existingRecord = this._storage.get(key);
111
+ if (existingRecord && existingRecord.timeoutId) {
112
+ clearTimeout(existingRecord.timeoutId);
113
+ }
114
+
115
+ const record = new Record(
116
+ value,
117
+ expiresAt
118
+ );
119
+ this._storage.set(key, record);
120
+
121
+ if (durationMs > 0) {
122
+ record.timeoutId = setTimeout(() => {
123
+ this._storage.delete(key);
124
+ }, durationMs);
125
+ if (record.timeoutId.unref) {
126
+ record.timeoutId.unref();
127
+ }
128
+ }
129
+ }
89
130
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rate-limiter-flexible",
3
- "version": "11.0.2",
3
+ "version": "11.1.0",
4
4
  "description": "Node.js atomic and non-atomic counters, rate limiting tools, protection from DoS and brute-force attacks at scale",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/types.d.ts CHANGED
@@ -318,7 +318,7 @@ interface IRateLimiterRedisOptions extends IRateLimiterStoreOptions {
318
318
  }
319
319
 
320
320
  interface IRateLimiterValkeyOptions extends IRateLimiterStoreOptions {
321
- customIncrTtlLuaScript?: string;
321
+ customIncrTtlLuaScript?: string;
322
322
  }
323
323
 
324
324
  interface ICallbackReady {
@@ -334,8 +334,43 @@ interface IRLWrapperBlackAndWhiteOptions {
334
334
  runActionAnyway?: boolean;
335
335
  }
336
336
 
337
+ export interface IRateLimiterMemoryRecord {
338
+ key: string | number;
339
+ value: number;
340
+ expiresAt: number | null;
341
+ }
342
+
343
+ export interface IRateLimiterMemoryExport {
344
+ version: number;
345
+ dumpedAt: number;
346
+ storage: IRateLimiterMemoryRecord[];
347
+ }
348
+
349
+ /** Common structure for restore results. */
350
+ export interface IRateLimiterRestoreResult<T> {
351
+ invalid: T;
352
+ expired: T;
353
+ restored: T;
354
+ }
355
+
356
+ /** Returned by {@link RateLimiterMemory.restore} when `detailResponse` is `false` (default). */
357
+ export type IRateLimiterRestoreResponse = IRateLimiterRestoreResult<number>;
358
+
359
+ export interface IRateLimiterRestoreBucketDetail {
360
+ count: number;
361
+ keys: Array<string | number>;
362
+ }
363
+
364
+ /** Returned by {@link RateLimiterMemory.restore} when `detailResponse` is `true`. */
365
+ export type IRateLimiterRestoreDetailResponse = IRateLimiterRestoreResult<IRateLimiterRestoreBucketDetail>;
366
+
337
367
  export class RateLimiterMemory extends RateLimiterAbstract {
338
368
  constructor(opts: IRateLimiterOptions);
369
+
370
+ dump(): IRateLimiterMemoryExport;
371
+ restore(data: Omit<IRateLimiterMemoryExport, 'dumpedAt'>, detailResponse: true): IRateLimiterRestoreDetailResponse | undefined;
372
+ restore(data: Omit<IRateLimiterMemoryExport, 'dumpedAt'>, detailResponse?: false): IRateLimiterRestoreResponse | undefined;
373
+ restore(data: Omit<IRateLimiterMemoryExport, 'dumpedAt'>, detailResponse?: boolean): IRateLimiterRestoreResponse | IRateLimiterRestoreDetailResponse | undefined;
339
374
  }
340
375
 
341
376
  export class RateLimiterCluster extends RateLimiterAbstract {
@@ -359,7 +394,7 @@ export class RateLimiterRedisNonAtomic extends RateLimiterStoreAbstract {
359
394
  }
360
395
 
361
396
  export class RateLimiterValkey extends RateLimiterStoreAbstract {
362
- constructor(opts: IRateLimiterValkeyOptions);
397
+ constructor(opts: IRateLimiterValkeyOptions);
363
398
  }
364
399
 
365
400
  export interface IRateLimiterMongoFunctionOptions {
@@ -424,19 +459,19 @@ export class RateLimiterPostgres extends RateLimiterStoreAbstract {
424
459
  }
425
460
 
426
461
  export class RateLimiterSQLite extends RateLimiterStoreAbstract {
427
- constructor(opts: IRateLimiterStoreNoAutoExpiryOptions, cb?: ICallbackReady);
462
+ constructor(opts: IRateLimiterStoreNoAutoExpiryOptions, cb?: ICallbackReady);
428
463
  }
429
464
 
430
465
  export class RateLimiterPrisma extends RateLimiterStoreAbstract {
431
- constructor(opts: IRateLimiterStoreNoAutoExpiryOptions, cb?: ICallbackReady);
466
+ constructor(opts: IRateLimiterStoreNoAutoExpiryOptions, cb?: ICallbackReady);
432
467
  }
433
468
 
434
469
  export class RateLimiterDrizzle extends RateLimiterStoreAbstract {
435
- constructor(opts: IRateLimiterStoreNoAutoExpiryOptionsAndSchema, cb?: ICallbackReady);
470
+ constructor(opts: IRateLimiterStoreNoAutoExpiryOptionsAndSchema, cb?: ICallbackReady);
436
471
  }
437
472
 
438
473
  export class RateLimiterDrizzleNonAtomic extends RateLimiterStoreAbstract {
439
- constructor(opts: IRateLimiterStoreNoAutoExpiryOptionsAndSchema, cb?: ICallbackReady);
474
+ constructor(opts: IRateLimiterStoreNoAutoExpiryOptionsAndSchema, cb?: ICallbackReady);
440
475
  }
441
476
 
442
477
  export class RateLimiterMemcache extends RateLimiterStoreAbstract { }