express-adaptive-rate-limiter 1.0.0 → 2.0.1

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 (51) hide show
  1. package/README.md +434 -17
  2. package/dist/algorithms/baseAlgorithm.js +1 -1
  3. package/dist/algorithms/fixedWindow.js +1 -1
  4. package/dist/algorithms/leakyBucket.js +1 -1
  5. package/dist/algorithms/slidingWindowCounter.js +1 -1
  6. package/dist/algorithms/slidingWindowLog.js +1 -1
  7. package/dist/algorithms/tokenBucket.js +1 -1
  8. package/dist/core/blockedKeys.js +1 -0
  9. package/dist/core/coreMiddleware.js +1 -0
  10. package/dist/core/limiters/clusterLimiter.js +1 -0
  11. package/dist/core/limiters/dynamoDbLimiter.js +1 -0
  12. package/dist/core/limiters/etcdLimiter.js +1 -0
  13. package/dist/core/limiters/memcachedLimiter.js +1 -0
  14. package/dist/core/limiters/memoryLimiter.js +1 -0
  15. package/dist/core/limiters/mongoLimiter.js +1 -0
  16. package/dist/core/limiters/mongooseLimiter.js +1 -0
  17. package/dist/core/limiters/mySqlLimiter.js +12 -0
  18. package/dist/core/limiters/nodeClusterLimiter.js +1 -0
  19. package/dist/core/limiters/postgresLimiter.js +13 -0
  20. package/dist/core/limiters/prismaLimiter.js +24 -0
  21. package/dist/core/limiters/redisLimiter.js +16 -0
  22. package/dist/core/limiters/sqliteLimiter.js +13 -0
  23. package/dist/core/limiters/valkeyLimiter.js +16 -0
  24. package/dist/core/rateLimiter.js +1 -0
  25. package/dist/core/rateLimiterRes.js +1 -0
  26. package/dist/core/rateLimiterStore.js +1 -0
  27. package/dist/core/wrappers/blackAndWhiteLimiter.js +1 -0
  28. package/dist/core/wrappers/burstyRateLimiter.js +1 -0
  29. package/dist/core/wrappers/rateLimiterQueue.js +1 -0
  30. package/dist/core/wrappers/rateLimiterUnion.js +1 -0
  31. package/dist/core/wrappers/timeoutLimiter.js +1 -0
  32. package/dist/index.js +1 -1
  33. package/dist/middleware/expressMiddleware.js +1 -1
  34. package/dist/middleware/headers.js +1 -1
  35. package/dist/rateLimit.js +1 -1
  36. package/dist/rateLimitManager.js +1 -1
  37. package/dist/stores/baseStore.js +1 -1
  38. package/dist/stores/luaScripts.js +379 -1
  39. package/dist/stores/memoryStore.js +1 -1
  40. package/dist/stores/redisStore.js +1 -1
  41. package/dist/types/index.d.ts +525 -1
  42. package/dist/utils/errors.js +1 -1
  43. package/dist/utils/graphQLParser.js +1 -1
  44. package/dist/utils/ipUtils.js +1 -1
  45. package/dist/utils/keyGenerator.js +1 -1
  46. package/dist/utils/redisClient.js +1 -0
  47. package/dist/utils/retryAfter.js +1 -1
  48. package/dist/utils/skipUtils.js +1 -1
  49. package/dist/utils/time.js +1 -1
  50. package/dist/utils/validation.js +1 -1
  51. package/package.json +72 -11
package/README.md CHANGED
@@ -1,8 +1,36 @@
1
1
  # express-adaptive-rate-limiter
2
2
 
3
- Production-ready, multi-algorithm rate limiting middleware for Express.js.
3
+ Production-ready rate limiting for Node.js.
4
4
 
5
- Supports **Fixed Window**, **Sliding Window Log**, **Sliding Window Counter**, **Token Bucket**, and **Leaky Bucket** algorithms with **Memory** and **Redis** backends. Built-in atomic Lua scripts for Redis, automatic fail-open/fail-closed, configurable RFC-compliant standard headers (IETF draft-6 and draft-7), custom key generators, GraphQL root field detection, dynamic request costs, powerful multi-criteria skip rules, and full TypeScript support.
5
+ The package ships **two tiers**:
6
+
7
+ - **Core tier** — framework-agnostic limiters with a `consume()` / `get()` / `set()` / `block()` API, backed by **Memory, Redis, Mongo, Mongoose, MySQL, Postgres, SQLite, Prisma, Memcached, DynamoDB, Valkey, etcd** and **Redis Cluster**.
8
+ - **Express tier** — middleware built on **Fixed Window, Sliding Window Log, Sliding Window Counter, Token Bucket** and **Leaky Bucket** algorithms, with atomic Redis Lua scripts, fail-open/fail-closed handling, RFC-compliant standard headers, GraphQL root field detection, dynamic request costs, multi-criteria skip rules and full TypeScript support.
9
+
10
+ Every backend in the core tier is atomic: the counter increment and the window creation happen in a single round trip, so several processes can share one limit safely.
11
+
12
+ ---
13
+
14
+ ## What's new in 2.0.1
15
+
16
+ - **Full Redis Cluster support**:
17
+ - **Cross-Slot (`CROSSSLOT`) Protection**: Safe atomic key deletion (`delMany`) via pipeline or per-slot execution preventing Redis Cluster `CROSSSLOT` failures.
18
+ - **Multi-Master Cluster Scanning**: `scanKeys` and `store.clear()` query every cluster master node (`nodes('master')` in `ioredis`, `masters` in `node-redis`) to locate and clear keys across all shards without missing any nodes.
19
+ - **Automatic Hash Tagging**: `hashTag: true` automatically wraps keys in `{keyPrefix:key}` so all operations for a given rate limit target map to the exact same hash slot on the cluster.
20
+ - **Cluster Limiter Parity**: Full drop-in compatibility with `RateLimiterRedisCluster`, `ClusterLimiter`, and `RedisClusterLimiter`, inheriting `rejectIfRedisNotReady` and `parseKey` key unwrapping.
21
+ - **Direct Express Tier Integration**: Configure `rateLimit({ store: 'redis-cluster' })` or `rateLimit({ store: 'cluster' })` with automated cluster client creation and node discovery.
22
+
23
+ ---
24
+
25
+ ## What's new in 2.0.0
26
+
27
+ **New: the framework-agnostic core tier.** 13 storage backends (Memory, Redis, Valkey, Redis Cluster, Mongo, Mongoose, DynamoDB, MySQL, Postgres, SQLite, Prisma, Memcached, etcd) with a unified points-based API, plus the `Queue`, `Union`, `Bursty`, `BlackAndWhite` and `Timeout` wrappers. See [Core Tier](#core-tier-framework-agnostic-limiters).
28
+
29
+ **Behavior change: the fixed window is anchored on the first request of a key.** In 1.0.0 the window was aligned to the wall clock, so a client that straddled a minute boundary could consume two full windows in a few milliseconds and exceed its configured limit. The window now starts with the first request of a key and lasts `windowMs`. `RateLimit-Reset` now reports the real time until the window ends instead of the next clock boundary.
30
+
31
+ **Performance.** The memory store evicts the least recently used key in O(1) instead of scanning the whole keyspace, IP whitelists and header-based skip rules are compiled once and cached, and Redis key listing uses `SCAN` instead of the blocking `KEYS`.
32
+
33
+ **Redis client support.** A single adapter now drives `ioredis`, `node-redis` v3 and `node-redis` v4, detected at runtime, so the same code works with every supported client.
6
34
 
7
35
  ---
8
36
 
@@ -14,23 +42,27 @@ npm install express-adaptive-rate-limiter
14
42
 
15
43
  ### Optional Peer Dependencies
16
44
 
17
- Install only what you need:
18
-
19
- - **Redis backend**:
20
- ```bash
21
- npm install ioredis
22
- # OR
23
- npm install redis
24
- ```
25
- - **GraphQL root field key generation**:
26
- ```bash
27
- npm install graphql
28
- ```
45
+ Install only the driver for the backend you use:
46
+
47
+ - **Redis**: `npm install ioredis` or `npm install redis`
48
+ - **MongoDB**: `npm install mongodb`
49
+ - **Mongoose**: `npm install mongoose`
50
+ - **MySQL**: `npm install mysql2`
51
+ - **PostgreSQL**: `npm install pg`
52
+ - **SQLite**: `npm install better-sqlite3` (Node 22.5+ can use the built-in `node:sqlite`)
53
+ - **Prisma**: `npm install @prisma/client`
54
+ - **Memcached**: `npm install memjs` or `npm install memcached`
55
+ - **DynamoDB**: `npm install @aws-sdk/client-dynamodb`
56
+ - **Valkey**: `npm install iovalkey` or `npm install @valkey/valkey-glide`
57
+ - **etcd**: `npm install etcd3`
58
+ - **GraphQL root field key generation**: `npm install graphql`
29
59
 
30
60
  ---
31
61
 
32
62
  ## Quick Start
33
63
 
64
+ ### Express middleware
65
+
34
66
  ```javascript
35
67
  import express from 'express';
36
68
  import { rateLimit } from 'express-adaptive-rate-limiter';
@@ -53,13 +85,42 @@ app.get('/', (req, res) => {
53
85
  app.listen(3000);
54
86
  ```
55
87
 
88
+ ### Framework-agnostic core limiter
89
+
90
+ ```javascript
91
+ import { MemoryLimiter } from 'express-adaptive-rate-limiter';
92
+
93
+ const limiter = new MemoryLimiter({ points: 100, duration: 60 });
94
+
95
+ const res = await limiter.consume('user-42');
96
+ console.log(res.remainingPoints, res.msBeforeNext);
97
+ ```
98
+
99
+ ### Core limiter behind Express
100
+
101
+ Any core limiter can be turned into Express middleware:
102
+
103
+ ```javascript
104
+ import express from 'express';
105
+ import { MongoLimiter, createCoreMiddleware } from 'express-adaptive-rate-limiter';
106
+
107
+ const app = express();
108
+
109
+ app.use(
110
+ createCoreMiddleware(
111
+ new MongoLimiter({ points: 100, duration: 60, dbName: 'my-app' }),
112
+ { standardHeaders: true }
113
+ )
114
+ );
115
+ ```
116
+
56
117
  ---
57
118
 
58
119
  ## Supported Algorithms
59
120
 
60
121
  | Algorithm | Identifier | Best For | Memory | Redis | Atomic |
61
122
  | --------------------------- | ------------------------ | -------------------------------------------- | :----: | :------: | :----: |
62
- | **Fixed Window Counter** | `fixed-window` | Low-overhead, simple reset boundaries | ✅ | ✅ (Lua) | ✅ |
123
+ | **Fixed Window Counter** | `fixed-window` | Low-overhead, window anchored on first hit | ✅ | ✅ (Lua) | ✅ |
63
124
  | **Sliding Window Log** | `sliding-window-log` | 100% strict precision rolling window | ✅ | ✅ (Lua) | ✅ |
64
125
  | **Sliding Window Counter** | `sliding-window-counter` | Smooth rolling limits with O(1) memory | ✅ | ✅ (Lua) | ✅ |
65
126
  | **Token Bucket** | `token-bucket` | Bursts allowed with steady background refill | ✅ | ✅ (Lua) | ✅ |
@@ -68,6 +129,313 @@ app.listen(3000);
68
129
 
69
130
  ---
70
131
 
132
+ ## Core Tier: Framework-Agnostic Limiters
133
+
134
+ The core tier is a high-performance points-based limiter. A limiter holds a counter per key; the counter is reset when its window expires.
135
+
136
+ ### The API
137
+
138
+ Every core limiter exposes the same surface:
139
+
140
+ | Method | Description |
141
+ | -------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
142
+ | `consume(key, points?, options?)` | Consume points. Resolves with a `RateLimiterRes`, or **rejects** with one when the limit is exceeded. |
143
+ | `penalty(key, points?, options?)` | Increase the consumed points without moving the window. |
144
+ | `reward(key, points?, options?)` | Decrease the consumed points without moving the window. |
145
+ | `get(key, options?)` | Read the current state, or `null` when the key is unknown. |
146
+ | `set(key, points, secDuration?, options?)` | Overwrite the counter and restart the window. |
147
+ | `block(key, secDuration, options?)` | Block a key for a number of seconds. |
148
+ | `delete(key, options?)` | Reset a key in the store and clear in-memory blocks. Returns whether it existed. |
149
+ | `deleteInMemoryBlocked(key)` | Remove only the local in-process block for a specific key. |
150
+ | `deleteInMemoryBlockedAll()` | Remove all active in-process blocks. |
151
+ | `getKey(key)` / `parseKey(rlKey)` | Prefix handling for the storage key. |
152
+
153
+ `RateLimiterRes` carries the following fields:
154
+
155
+ ```javascript
156
+ {
157
+ remainingPoints: 99, // points still available
158
+ msBeforeNext: 59832, // ms until the window resets (-1 = never)
159
+ consumedPoints: 1, // points consumed in this window
160
+ isFirstInDuration: true // whether this call opened the window
161
+ }
162
+ ```
163
+
164
+ ### Common options
165
+
166
+ | Option | Type | Default | Description |
167
+ | ------------------------ | ----------------- | ------- | ------------------------------------------------------------------------- |
168
+ | `points` | `number` | `60` | Maximum points consumable within the window. `limit` is an alias. |
169
+ | `duration` | `number` | `60` | Window length in **seconds**. `0` means points are never reset. |
170
+ | `windowMs` | `number` | — | Alias of `duration`, in milliseconds. |
171
+ | `blockDuration` | `number` | `0` | Seconds a key stays blocked once it exceeds its limit. |
172
+ | `execEvenly` | `boolean` | `false` | Spread allowed requests evenly across the window. |
173
+ | `execEvenlyMinDelayMs` | `number` | derived | Minimum delay between two evenly spread executions. |
174
+ | `keyPrefix` | `string` | `'rl:'` | Prefix applied to every stored key. |
175
+ | `insuranceLimiter` | `RateLimiter` | `null` | Fallback limiter used when the primary store fails. |
176
+ | `inMemoryBlockOnConsumed`| `number` | `0` | Block a key in process memory once this many points are consumed. |
177
+ | `inMemoryBlockDuration` | `number` | `0` | Seconds the in-memory block lasts. |
178
+ | `clearExpiredByTimeout` | `boolean` | `false` | Automatically purge expired rows from SQL/Mongo tables periodically. |
179
+ | `clearExpiredTimeoutMs` | `number` | `300000`| Interval for expired key purge timer (default 5 minutes). |
180
+
181
+ ### Backends & Class Aliases
182
+
183
+ Both modern descriptive names and classic class aliases are exported for convenience:
184
+
185
+ | Descriptive Limiter | Class Alias | Driver | Atomic mechanism |
186
+ | ------------------- | ----------------------------- | ----------------------------------------- | ------------------------------------ |
187
+ | `MemoryLimiter` | `RateLimiterMemory` | none | in-process Map with O(1) eviction |
188
+ | `RedisLimiter` | `RateLimiterRedis` | `ioredis` or `redis` | Lua script |
189
+ | `RateLimiterCluster`| `RateLimiterCluster` | `node:cluster` IPC | Master-Worker IPC messaging |
190
+ | `RedisClusterLimiter`| `ClusterLimiter` / `RateLimiterRedisCluster` | ioredis Cluster / node-redis cluster | Lua script, hash-tagged `{slot}` |
191
+ | `ValkeyLimiter` | `RateLimiterValkey` | `iovalkey` or `@valkey/valkey-glide` | Lua script |
192
+ | `MongoLimiter` | `RateLimiterMongo` | `mongodb` | `findOneAndUpdate` with `$inc` |
193
+ | `MongooseLimiter` | `RateLimiterMongoose` | `mongoose` | `findOneAndUpdate` with `$inc` |
194
+ | `DynamoDbLimiter` | `RateLimiterDynamo` / `DynamoDb` | `@aws-sdk/client-dynamodb` | `UpdateItem` with `ADD` |
195
+ | `MySqlLimiter` | `RateLimiterMySQL` | `mysql2` | `INSERT ... ON DUPLICATE KEY UPDATE` |
196
+ | `PostgresLimiter` | `RateLimiterPostgres` | `pg` | `INSERT ... ON CONFLICT DO UPDATE` |
197
+ | `SqliteLimiter` | `RateLimiterSQLite` | `better-sqlite3` or built-in `node:sqlite`| `INSERT ... ON CONFLICT DO UPDATE` |
198
+ | `PrismaLimiter` | `RateLimiterPrisma` | `@prisma/client` | raw upsert, Postgres or MySQL |
199
+ | `MemcachedLimiter` | `RateLimiterMemcache` / `Memcached` | `memjs` / `memcached` | `add` for creation, `incr`/`decr` |
200
+ | `EtcdLimiter` | `RateLimiterEtcd` | `etcd3` | compare-and-swap transaction |
201
+
202
+ Each backend accepts its driver through `storeClient` (with `client` as an alias).
203
+
204
+ ### Node.js Cluster Rate Limiting (`RateLimiterCluster` & `RateLimiterClusterMaster`)
205
+
206
+ Rate-limit across multiple CPU cores without Redis using Node's native Inter-Process Communication (IPC):
207
+
208
+ ```javascript
209
+ import cluster from 'node:cluster';
210
+ import { RateLimiterClusterMaster, RateLimiterCluster } from 'express-adaptive-rate-limiter';
211
+
212
+ if (cluster.isPrimary || cluster.isMaster) {
213
+ // Master process initializes the central coordinator
214
+ new RateLimiterClusterMaster();
215
+
216
+ // Fork worker processes
217
+ for (let i = 0; i < 4; i++) {
218
+ cluster.fork();
219
+ }
220
+ } else {
221
+ // Worker process rate limiter
222
+ const limiter = new RateLimiterCluster({
223
+ keyPrefix: 'cluster_rl',
224
+ points: 10,
225
+ duration: 1,
226
+ timeoutMs: 3000 // failsafe timeout
227
+ });
228
+
229
+ const res = await limiter.consume('user-ip');
230
+ console.log(res.remainingPoints);
231
+ }
232
+ ```
233
+
234
+ ### Redis Cluster Rate Limiting (`ClusterLimiter` & `store: 'redis-cluster'`)
235
+
236
+ Rate limiting across a distributed Redis Cluster requires careful handling of key routing. Redis Cluster partitions keys across 16,384 hash slots. Multi-key operations or atomic scripts touching related keys will fail with `CROSSSLOT Keys in request don't hash to the same slot` unless configured with **hash tags (`{...}`)**.
237
+
238
+ `express-adaptive-rate-limiter` provides **first-class Redis Cluster support** across both the Express middleware and Core tiers:
239
+ - **Automatic Hash Tagging**: Wraps keys in `{keyPrefix:key}` by default so multi-key operations land on the exact same hash slot.
240
+ - **Cross-Slot Safe Cleanups**: Key deletions (`delMany`) use Redis pipelines or individual per-slot deletions, preventing `CROSSSLOT` errors.
241
+ - **Cluster-Wide Master Scanning**: `store.clear()` queries all cluster master nodes (`cluster.nodes('master')` in `ioredis`, `cluster.masters` in `node-redis`) to ensure no dangling keys remain across shards.
242
+ - **Client Flexibility**: Works with both `ioredis.Cluster` and `@redis/client` (`node-redis`) cluster mode.
243
+
244
+ #### 1. Express Middleware with Redis Cluster
245
+
246
+ ##### Using `ioredis.Cluster`:
247
+
248
+ ```javascript
249
+ import express from 'express';
250
+ import Redis from 'ioredis';
251
+ import { rateLimit } from 'express-adaptive-rate-limiter';
252
+
253
+ const app = express();
254
+
255
+ // Initialize ioredis Cluster client
256
+ const clusterClient = new Redis.Cluster(
257
+ [
258
+ { host: '10.0.0.1', port: 6379 },
259
+ { host: '10.0.0.2', port: 6379 },
260
+ { host: '10.0.0.3', port: 6379 }
261
+ ],
262
+ {
263
+ redisOptions: {
264
+ enableOfflineQueue: false // recommended: fail fast on network partitions
265
+ }
266
+ }
267
+ );
268
+
269
+ // Configure Express rate limiter with Redis Cluster
270
+ const clusterLimiter = rateLimit({
271
+ store: 'redis-cluster', // or 'cluster'
272
+ client: clusterClient,
273
+ algorithm: 'sliding-window-counter', // any of the 5 algorithms
274
+ limit: 100, // 100 requests
275
+ windowMs: 60 * 1000, // per 1 minute
276
+ hashTag: true, // wraps storage keys in {prefix:key} (default: true)
277
+ failOpen: true // allow traffic if cluster is unreachable
278
+ });
279
+
280
+ app.use(clusterLimiter);
281
+
282
+ app.get('/api/resource', (req, res) => {
283
+ res.json({ message: 'Within cluster rate limit', rateLimit: req.rateLimit });
284
+ });
285
+ ```
286
+
287
+ ##### Using `node-redis` (`createCluster`):
288
+
289
+ ```javascript
290
+ import express from 'express';
291
+ import { createCluster } from 'redis';
292
+ import { rateLimit } from 'express-adaptive-rate-limiter';
293
+
294
+ const app = express();
295
+
296
+ const cluster = createCluster({
297
+ rootNodes: [{ url: 'redis://10.0.0.1:6379' }, { url: 'redis://10.0.0.2:6379' }]
298
+ });
299
+
300
+ await cluster.connect();
301
+
302
+ app.use(
303
+ rateLimit({
304
+ store: 'redis-cluster',
305
+ client: cluster,
306
+ limit: 60,
307
+ windowMs: 60000
308
+ })
309
+ );
310
+ ```
311
+
312
+ ##### Auto-Connecting Cluster (without manual client instantiation):
313
+
314
+ You can also pass cluster nodes directly to `rateLimit` or `RedisStore`:
315
+
316
+ ```javascript
317
+ import { rateLimit } from 'express-adaptive-rate-limiter';
318
+
319
+ app.use(
320
+ rateLimit({
321
+ store: 'redis-cluster',
322
+ cluster: [
323
+ { host: '10.0.0.1', port: 6379 },
324
+ { host: '10.0.0.2', port: 6379 }
325
+ ],
326
+ limit: 100,
327
+ windowMs: 60000
328
+ })
329
+ );
330
+ ```
331
+
332
+ ---
333
+
334
+ #### 2. Framework-Agnostic Core Tier (`ClusterLimiter` / `RateLimiterRedisCluster`)
335
+
336
+ For non-Express applications, microservices, GraphQL resolvers, or background queues, use `ClusterLimiter` (also exported as `RedisClusterLimiter` and `RateLimiterRedisCluster` for 100% `rate-limiter-flexible` drop-in parity):
337
+
338
+ ```javascript
339
+ import Redis from 'ioredis';
340
+ import { ClusterLimiter } from 'express-adaptive-rate-limiter';
341
+
342
+ const redisCluster = new Redis.Cluster([
343
+ { host: '10.0.0.1', port: 6379 },
344
+ { host: '10.0.0.2', port: 6379 }
345
+ ]);
346
+
347
+ const limiter = new ClusterLimiter({
348
+ storeClient: redisCluster,
349
+ points: 10, // 10 points
350
+ duration: 1, // per 1 second
351
+ keyPrefix: 'cluster_api',
352
+ hashTag: true, // wraps keys in {cluster_api:key}
353
+ rejectIfRedisNotReady: true // reject immediately if cluster is not ready
354
+ });
355
+
356
+ // Consume points
357
+ try {
358
+ const res = await limiter.consume('user_42', 1);
359
+ console.log('Points remaining:', res.remainingPoints);
360
+ } catch (rejRes) {
361
+ if (rejRes instanceof Error) {
362
+ console.error('Cluster storage error:', rejRes);
363
+ } else {
364
+ console.warn(`Rate limit exceeded! Retry after ${rejRes.msBeforeNext}ms`);
365
+ }
366
+ }
367
+
368
+ // Penalty and reward
369
+ await limiter.penalty('user_42', 2);
370
+ await limiter.reward('user_42', 1);
371
+
372
+ // Block a client across the cluster
373
+ await limiter.block('suspicious_user', 3600); // blocked for 1 hour
374
+
375
+ // Read state without consuming
376
+ const state = await limiter.get('user_42');
377
+ console.log(state?.consumedPoints);
378
+ ```
379
+
380
+ #### Redis Cluster Options Reference
381
+
382
+ | Option | Type | Default | Description |
383
+ | --- | --- | --- | --- |
384
+ | `storeClient` / `client` | `object` | `null` | Active `ioredis.Cluster` or `node-redis` cluster instance. |
385
+ | `cluster` / `clusterNodes` | `Array` | `null` | Array of cluster node definitions to automatically instantiate. |
386
+ | `hashTag` | `boolean` | `true` | Wraps keys in `{...}` hash tags so multi-key operations land in the same slot. |
387
+ | `rejectIfRedisNotReady` | `boolean` | `false` | Throws `RedisConnectionError` if cluster status is not ready. |
388
+ | `failOpen` | `boolean` | `false` | When true, errors from the store fail open instead of failing closed. |
389
+
390
+ ### In-Memory State Dump & Restore (`dump()` & `restore()`)
391
+
392
+ Save in-memory state on shutdown for zero-downtime restarts:
393
+
394
+ ```javascript
395
+ import fs from 'node:fs';
396
+ import { RateLimiterMemory } from 'express-adaptive-rate-limiter';
397
+
398
+ const limiter = new RateLimiterMemory({ points: 10, duration: 60 });
399
+
400
+ // On graceful shutdown
401
+ const snapshot = limiter.dump();
402
+ fs.writeFileSync('./rate-limit-state.json', JSON.stringify(snapshot));
403
+
404
+ // On application startup
405
+ if (fs.existsSync('./rate-limit-state.json')) {
406
+ const data = JSON.parse(fs.readFileSync('./rate-limit-state.json', 'utf8'));
407
+ const report = limiter.restore(data);
408
+ console.log(`Restored ${report.restored} keys, ${report.expired} expired`);
409
+ }
410
+ ```
411
+
412
+ ### Wrappers
413
+
414
+ | Wrapper | Purpose |
415
+ | ---------------------------------------- | ---------------------------------------------------------------- |
416
+ | `RateLimiterQueue` | Queues requests instead of rejecting them when the limit is full.|
417
+ | `RateLimiterUnion` | Requires every wrapped limiter to allow the request. |
418
+ | `BurstyRateLimiter` | A steady limiter with a burst fallback. |
419
+ | `BlackAndWhiteLimiter` / `RateLimiterBlackAndWhite` | Black and white lists in front of a limiter. |
420
+ | `TimeoutLimiter` | Fails a call that takes longer than `timeoutMs`. |
421
+
422
+ ```javascript
423
+ import { MemoryLimiter, RateLimiterUnion, BlackAndWhiteLimiter } from 'express-adaptive-rate-limiter';
424
+
425
+ const perIp = new MemoryLimiter({ points: 100, duration: 60 });
426
+ const perAccount = new MemoryLimiter({ points: 10, duration: 60, keyPrefix: 'acct' });
427
+
428
+ const both = new RateLimiterUnion(perIp, perAccount);
429
+
430
+ const guarded = new BlackAndWhiteLimiter({
431
+ limiter: both,
432
+ whiteList: ['10.0.0.0/8'],
433
+ blackList: ['203.0.113.5']
434
+ });
435
+ ```
436
+
437
+ ---
438
+
71
439
  ## Configuration Reference
72
440
 
73
441
  ### 1. Common Options
@@ -107,7 +475,7 @@ All options below can be passed to `rateLimit(options)`:
107
475
 
108
476
  #### Fixed Window (`algorithm: 'fixed-window'`)
109
477
 
110
- Divides time into fixed discrete windows (e.g. 1 minute). Resets count at each window boundary.
478
+ Counts requests in a window that starts with the **first request of a key** and lasts `windowMs`. The window never slides, so a client can never consume two windows worth of quota in quick succession.
111
479
 
112
480
  | Option | Type | Default | Description |
113
481
  | --------------- | -------- | ------- | ------------------------------------------- |
@@ -229,6 +597,8 @@ rateLimit({
229
597
 
230
598
  ### 4. Storage Backends
231
599
 
600
+ The Express tier uses the algorithm stores (`MemoryStore`, `RedisStore`) described below. The **core tier** ships its own set of backends — see [Core Tier](#core-tier-framework-agnostic-limiters) for the full list and their options.
601
+
232
602
  #### MemoryStore Options
233
603
 
234
604
  ```javascript
@@ -604,6 +974,8 @@ Retry-After: 45
604
974
  | `validateIpRangeV4` | `(ip, whitelist?) => boolean` | Tests IP against whitelist or `Platform.WHITELIST_IPS`. |
605
975
  | `validateSecondaryIps` | `(req, whitelist?) => boolean` | Validates `x-real-ip` or `x-forwarded-for` when `x-xcld: true`. |
606
976
  | `evaluateSkipRule` | `(rule, req, res, context?) => Promise<boolean>` | Evaluates skip condition across headers, origins, IPs, keys. |
977
+ | `createCoreMiddleware` | `(limiter, options?) => middleware` | Turns a core limiter into Express middleware. |
978
+ | `detectRedisClientKind` | `(client) => string` | Reports which Redis client family is in use. |
607
979
 
608
980
  ---
609
981
 
@@ -616,6 +988,7 @@ All error classes inherit from `RateLimiterError`:
616
988
  - `RedisConnectionError`: Thrown when Redis connection, command, or script execution fails.
617
989
  - `QueueFullError`: Thrown in Leaky Bucket shaping mode when `maxQueueSize` is exceeded.
618
990
  - `QueueTimeoutError`: Thrown in Leaky Bucket shaping mode when a queued request times out (`queueTimeoutMs`).
991
+ - `RateLimiterQueueError`: Thrown by `RateLimiterQueue` when the queue is full or a queued request expires.
619
992
 
620
993
  ---
621
994
 
@@ -651,6 +1024,50 @@ app.get('/info', (req, res) => {
651
1024
  });
652
1025
  ```
653
1026
 
1027
+ ## Performance Benchmarks
1028
+
1029
+ Run the benchmark suite with `npm run bench`:
1030
+
1031
+ ```
1032
+ ===============================================================================================
1033
+ Express Adaptive Rate Limiter - Performance Benchmark Suite
1034
+ ===============================================================================================
1035
+ | Algorithm & Store | Reqs | Throughput | Mean | p50 | p95 | p99 |
1036
+ -----------------------------------------------------------------------------------------------
1037
+ | Fixed Window (Memory) | 10000 | 254,535 req/s | 0.078 ms | 0.049 ms | 0.172 ms | 0.654 ms |
1038
+ | Sliding Window Counter (Memory) | 10000 | 264,659 req/s | 0.075 ms | 0.060 ms | 0.116 ms | 0.786 ms |
1039
+ | Token Bucket (Memory) | 10000 | 321,866 req/s | 0.061 ms | 0.047 ms | 0.087 ms | 0.555 ms |
1040
+ | Leaky Bucket Policing (Memory) | 10000 | 188,326 req/s | 0.105 ms | 0.079 ms | 0.246 ms | 0.749 ms |
1041
+ | Sliding Window Log (Memory) | 10000 | 121,287 req/s | 0.164 ms | 0.127 ms | 0.227 ms | 0.826 ms |
1042
+ | Core Points Limiter (Memory) | 10000 | 205,303 req/s | 0.095 ms | 0.070 ms | 0.190 ms | 0.660 ms |
1043
+ | Core Points Limiter (Redis) | 1000 | 1,200+ req/s| 0.810 ms | 0.720 ms | 1.450 ms | 2.100 ms |
1044
+ ===============================================================================================
1045
+ ```
1046
+
1047
+ Key performance highlights:
1048
+ - **Zero-overhead memory tier**: 250,000–320,000 requests/sec with p50 latency under 0.06 ms.
1049
+ - **Atomic Redis scripts**: Single round-trip Lua scripts for atomic increments and TTL sets without race conditions.
1050
+ - **In-memory blocking shield**: Protects Redis or SQL databases against high-frequency brute-force/DoS attacks by caching blocks in local process memory (`inMemoryBlockOnConsumed`).
1051
+ - **O(1) LRU eviction**: High memory ceilings without garbage collection pauses.
1052
+
1053
+ ---
1054
+
1055
+ ## Issues & Support
1056
+
1057
+ If you encounter any bug, issue, or have any questions:
1058
+ - **npm Package Page**: You can report issues or reach out via the package page on npm:
1059
+ 👉 **[npm: express-adaptive-rate-limiter](https://www.npmjs.com/package/express-adaptive-rate-limiter)**
1060
+ - **Developer Profile**: You can also reach out directly through the maintainer's profile:
1061
+ 👉 **[GitHub Profile: @ravinddeveloper](https://github.com/ravinddeveloper)**
1062
+
1063
+ Our team actively investigates and resolves all reported issues. When reporting a bug or requesting assistance, please include:
1064
+ - Node.js version (`node -v`)
1065
+ - Package version (`2.0.1`)
1066
+ - Storage driver & version (e.g., `ioredis@5.4.2` or `redis@4.7.0`)
1067
+ - A minimal reproducible example or stack trace
1068
+
1069
+ ---
1070
+
654
1071
  ## License
655
1072
 
656
- MIT © Express Adaptive Rate Limiter Team
1073
+ MIT © [RAVI N D](https://github.com/ravinddeveloper)
@@ -1 +1 @@
1
- function _0x47a5(_0x51c212,_0x165fdc){_0x51c212=_0x51c212-301;const _0x1095cc=_0x1095();let _0x47a50a=_0x1095cc[_0x51c212];if(_0x47a5["ycBEGA"]===undefined){var _0x5450e5=function(_0x15ad9d){const _0x26983d="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=";let _0x429b31="",_0x3ba478="";for(let _0x5b6f0d=0,_0x4a0a50,_0x31250c,_0x51469f=0;_0x31250c=_0x15ad9d["charAt"](_0x51469f++);~_0x31250c&&(_0x4a0a50=_0x5b6f0d%4?_0x4a0a50*64+_0x31250c:_0x31250c,_0x5b6f0d++%4)?_0x429b31+=String["fromCharCode"](255&_0x4a0a50>>(-2*_0x5b6f0d&6)):0){_0x31250c=_0x26983d["indexOf"](_0x31250c)}for(let _0x3a614b=0,_0x10e72a=_0x429b31["length"];_0x3a614b<_0x10e72a;_0x3a614b++){_0x3ba478+="%"+("00"+_0x429b31["charCodeAt"](_0x3a614b)["toString"](16))["slice"](-2)}return decodeURIComponent(_0x3ba478)};_0x47a5["SFwwOi"]=_0x5450e5,_0x47a5["byooEB"]={},_0x47a5["ycBEGA"]=!![]}const _0x26a2e1=_0x1095cc[0];_0x47a5["ohdXjQ"]!==_0x26a2e1&&(_0x47a5["byooEB"]={},_0x47a5["ohdXjQ"]=_0x26a2e1);const _0xe7b0f6=_0x47a5["byooEB"][_0x51c212];return _0xe7b0f6===undefined?(_0x47a50a=_0x47a5["SFwwOi"](_0x47a50a),_0x47a5["byooEB"][_0x51c212]=_0x47a50a):_0x47a50a=_0xe7b0f6,_0x47a50a}function _0x1095(){const _0x34fc2b=["mte2v3zPwufh","ntG1nxDmqLnIwG","y2XVC2u","mJyXntDcsMzoyxC","C3rVCMu","zNvUy3rPB24","ndGZodyWtvHPD0rw","Bwf4","z2v0","Bg9JA3m","nJbJre13u0C","mJy1mJq0tKDWzuLx","y2XLyxi","CNvUu2nYAxb0","B3b0Aw9UCW","qwXNB3jPDgHTlMnVBNn1BwuOksbTDxn0igjLigLTCgXLBwvUDgvK","CMvZB2X2zq","ntzfDvDjswy","CMvZzxq","BM9YBwfSAxPLuMvZDwX0","D2L0AeXVy2S","qwXNB3jPDgHTlMDLDfn0yxrLkcKGBxvZDcbIzsbPBxbSzw1LBNrLza","y29UC3vTzq","zgvSzxrL","mNzwwNDcra","AxnszwrPCW","mtmYode0ueX2CvrK","mtaYyMzLt01W","mte4mtC5wwfLAw1Y","mtG5mZCZB3z3tLH0"];_0x1095=function(){return _0x34fc2b};return _0x1095()}const _0x37c172=_0x47a5;(function(_0x14a922,_0x43c0bc){const _0x2fdb6e=_0x47a5,_0x521e73=_0x14a922();while(!![]){try{const _0x226c5d=parseInt(_0x2fdb6e(315))/1*(-parseInt(_0x2fdb6e(310))/2)+-parseInt(_0x2fdb6e(319))/3*(-parseInt(_0x2fdb6e(316))/4)+-parseInt(_0x2fdb6e(317))/5*(-parseInt(_0x2fdb6e(313))/6)+parseInt(_0x2fdb6e(327))/7+-parseInt(_0x2fdb6e(303))/8*(parseInt(_0x2fdb6e(314))/9)+parseInt(_0x2fdb6e(322))/10+parseInt(_0x2fdb6e(312))/11*(parseInt(_0x2fdb6e(326))/12);if(_0x226c5d===_0x43c0bc)break;else _0x521e73["push"](_0x521e73["shift"]())}catch(_0xbdae4e){_0x521e73["push"](_0x521e73["shift"]())}}})(_0x1095,138116);export class BaseAlgorithm{constructor(_0x3ba478,_0x5b6f0d){const _0x3df90c=_0x47a5;this[_0x3df90c(330)]=_0x3ba478,this["store"]=_0x5b6f0d,this[_0x3df90c(325)]=new Map}[_0x37c172(311)](){const _0x315ca3=_0x37c172;return this[_0x315ca3(320)]&&typeof this["store"][_0x315ca3(329)]==="function"}[_0x37c172(305)](_0x4a0a50,_0x31250c,_0x51469f,_0x3a614b,_0x10e72a,_0x4a2703){const _0x5951c2=_0x37c172,_0x1521cd=Number(_0x4a2703),_0x46285f=new Date(_0x1521cd);return{"allowed":Boolean(_0x4a0a50),"limit":Number(_0x31250c),"remaining":Math[_0x5951c2(323)](0,Number(_0x51469f)),"consumed":Math[_0x5951c2(323)](0,Number(_0x3a614b)),"retryAfter":Math[_0x5951c2(323)](0,Number(_0x10e72a)),"resetTime":_0x46285f}}async[_0x37c172(306)](_0x185eef,_0x3cce41){const _0x3593c2=_0x37c172,_0x3a01ba=this[_0x3593c2(325)][_0x3593c2(324)](_0x185eef)||Promise[_0x3593c2(302)]();let _0x2d29cc;const _0x24eddc=new Promise(_0x42393e=>{_0x2d29cc=_0x42393e});this[_0x3593c2(325)]["set"](_0x185eef,_0x24eddc);try{return await _0x3a01ba,await _0x3cce41()}finally{_0x2d29cc(),this[_0x3593c2(325)][_0x3593c2(324)](_0x185eef)===_0x24eddc&&this[_0x3593c2(325)]["delete"](_0x185eef)}}async[_0x37c172(308)](_0x351ec0,_0x44e810=1){const _0x517adb=_0x37c172;throw new Error(_0x517adb(301))}async[_0x37c172(304)](_0x593c85){const _0x42dc88=_0x37c172;return this["store"][_0x42dc88(309)](_0x593c85)}async["getState"](_0x3574a1){const _0x43a0cb=_0x37c172;throw new Error(_0x43a0cb(307))}async[_0x37c172(318)](){const _0x4d7bb1=_0x37c172;this[_0x4d7bb1(325)][_0x4d7bb1(328)](),this[_0x4d7bb1(320)]&&typeof this[_0x4d7bb1(320)]["close"]===_0x4d7bb1(321)&&(await this[_0x4d7bb1(320)][_0x4d7bb1(318)]())}}export default BaseAlgorithm;
1
+ export class BaseAlgorithm{constructor(options,store){this.options=options;this.store=store;this.locks=new Map();}isRedis(){return this.store&&typeof this.store.runScript==='function';}normalizeResult(allowed,limit,remaining,consumed,retryAfter,resetTime){const resetMs=Number(resetTime);const resetDate=new Date(resetMs);return{allowed:Boolean(allowed),limit:Number(limit),remaining:Math.max(0,Number(remaining)),consumed:Math.max(0,Number(consumed)),retryAfter:Math.max(0,Number(retryAfter)),resetTime:resetDate};}async withLock(key,fn){const prev=this.locks.get(key)||Promise.resolve();let release;const current=new Promise(resolve=>{release=resolve;});this.locks.set(key,current);try{await prev;return await fn();}finally{release();if(this.locks.get(key)===current){this.locks.delete(key);}}}async consume(_key,_cost=1){throw new Error('Algorithm.consume() must be implemented');}async reset(_key){return this.store.delete(_key);}async getState(_key){throw new Error('Algorithm.getState() must be implemented');}async close(){this.locks.clear();if(this.store&&typeof this.store.close==='function'){await this.store.close();}}}export default BaseAlgorithm;
@@ -1 +1 @@
1
- const _0x13a647=_0x4d5c;(function(_0x2bfa8a,_0x1f3479){const _0x39a8ce=_0x4d5c,_0xc6a873=_0x2bfa8a();while(!![]){try{const _0xf6108b=-parseInt(_0x39a8ce(331))/1*(-parseInt(_0x39a8ce(340))/2)+-parseInt(_0x39a8ce(330))/3+-parseInt(_0x39a8ce(344))/4*(-parseInt(_0x39a8ce(338))/5)+parseInt(_0x39a8ce(334))/6+parseInt(_0x39a8ce(346))/7+-parseInt(_0x39a8ce(343))/8*(-parseInt(_0x39a8ce(351))/9)+-parseInt(_0x39a8ce(350))/10;if(_0xf6108b===_0x1f3479)break;else _0xc6a873["push"](_0xc6a873["shift"]())}catch(_0x551ae7){_0xc6a873["push"](_0xc6a873["shift"]())}}})(_0xfef5,129657);import{BaseAlgorithm}from"./baseAlgorithm.js";function _0x4d5c(_0x392f3c,_0x16a0ed){_0x392f3c=_0x392f3c-326;const _0xfef5ee=_0xfef5();let _0x4d5c43=_0xfef5ee[_0x392f3c];if(_0x4d5c["DZEkIy"]===undefined){var _0x562ee9=function(_0x3fc304){const _0x2d1aa1="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=";let _0x23fc1c="",_0x2de4e6="";for(let _0x1cebe7=0,_0x57da16,_0x28c4c9,_0x2aef9d=0;_0x28c4c9=_0x3fc304["charAt"](_0x2aef9d++);~_0x28c4c9&&(_0x57da16=_0x1cebe7%4?_0x57da16*64+_0x28c4c9:_0x28c4c9,_0x1cebe7++%4)?_0x23fc1c+=String["fromCharCode"](255&_0x57da16>>(-2*_0x1cebe7&6)):0){_0x28c4c9=_0x2d1aa1["indexOf"](_0x28c4c9)}for(let _0x21e75f=0,_0x4ca84f=_0x23fc1c["length"];_0x21e75f<_0x4ca84f;_0x21e75f++){_0x2de4e6+="%"+("00"+_0x23fc1c["charCodeAt"](_0x21e75f)["toString"](16))["slice"](-2)}return decodeURIComponent(_0x2de4e6)};_0x4d5c["QxeXAH"]=_0x562ee9,_0x4d5c["dvXTXH"]={},_0x4d5c["DZEkIy"]=!![]}const _0x320e66=_0xfef5ee[0];_0x4d5c["xFWTTS"]!==_0x320e66&&(_0x4d5c["dvXTXH"]={},_0x4d5c["xFWTTS"]=_0x320e66);const _0x373829=_0x4d5c["dvXTXH"][_0x392f3c];return _0x373829===undefined?(_0x4d5c43=_0x4d5c["QxeXAH"](_0x4d5c43),_0x4d5c["dvXTXH"][_0x392f3c]=_0x4d5c43):_0x4d5c43=_0x373829,_0x4d5c43}export class FixedWindow extends BaseAlgorithm{constructor(_0x2de4e6,_0x1cebe7){const _0x1d86a6=_0x4d5c;super(_0x2de4e6,_0x1cebe7),this[_0x1d86a6(329)]=_0x2de4e6["limit"],this[_0x1d86a6(333)]=_0x2de4e6[_0x1d86a6(333)]}async[_0x13a647(352)](_0x57da16,_0x28c4c9=1){const _0x5353ba=_0x13a647;if(this[_0x5353ba(337)]()){const _0x2aef9d=typeof this[_0x5353ba(341)][_0x5353ba(348)]==="function"?this[_0x5353ba(341)][_0x5353ba(348)]():0,_0x21e75f=await this[_0x5353ba(341)]["runScript"]("fixed-window",[_0x57da16],[this[_0x5353ba(329)],this[_0x5353ba(333)],_0x28c4c9,_0x2aef9d]);return this[_0x5353ba(332)](_0x21e75f[0]===1,_0x21e75f[1],_0x21e75f[2],_0x21e75f[3],_0x21e75f[4],_0x21e75f[5])}return this["withLock"](_0x57da16,async()=>{const _0x3173c1=_0x5353ba,_0x4ca84f=Date[_0x3173c1(342)](),_0x53a8a4=Math[_0x3173c1(326)](_0x4ca84f/this[_0x3173c1(333)])*this[_0x3173c1(333)],_0x23ead2=_0x53a8a4+this[_0x3173c1(333)],_0x2912f4=Math[_0x3173c1(354)](1,_0x23ead2-_0x4ca84f),_0x20a85d=await this[_0x3173c1(341)]["get"](_0x57da16);let _0x8809a1=0;_0x20a85d&&_0x20a85d[_0x3173c1(335)]===_0x53a8a4&&(_0x8809a1=_0x20a85d["count"]);if(_0x8809a1+_0x28c4c9<=this[_0x3173c1(329)]){const _0x531598=_0x8809a1+_0x28c4c9;return await this[_0x3173c1(341)][_0x3173c1(327)](_0x57da16,{"count":_0x531598,"windowStart":_0x53a8a4,"resetTime":_0x23ead2},_0x2912f4),this[_0x3173c1(332)](!![],this["limit"],this["limit"]-_0x531598,_0x531598,0,_0x23ead2)}await this[_0x3173c1(341)][_0x3173c1(327)](_0x57da16,{"count":_0x8809a1,"windowStart":_0x53a8a4,"resetTime":_0x23ead2},_0x2912f4);const _0x51728a=Math[_0x3173c1(354)](1,Math[_0x3173c1(349)](_0x2912f4/1000)),_0x18c063=Math[_0x3173c1(354)](0,this["limit"]-_0x8809a1);return this[_0x3173c1(332)](![],this[_0x3173c1(329)],_0x18c063,_0x8809a1,_0x51728a,_0x23ead2)})}async[_0x13a647(328)](_0x56981c){const _0x15c559=_0x13a647;if(this[_0x15c559(337)]()){const _0x266fac=typeof this["store"]["getNow"]===_0x15c559(347)?this[_0x15c559(341)][_0x15c559(348)]():0,_0xac99c3=await this[_0x15c559(341)][_0x15c559(345)](_0x15c559(353),[_0x56981c],[this[_0x15c559(329)],this[_0x15c559(333)],0,_0x266fac]);return this[_0x15c559(332)](_0xac99c3[0]===1,_0xac99c3[1],_0xac99c3[2],_0xac99c3[3],_0xac99c3[4],_0xac99c3[5])}const _0x2c259e=Date[_0x15c559(342)](),_0x55ff26=Math["floor"](_0x2c259e/this[_0x15c559(333)])*this[_0x15c559(333)],_0x542986=_0x55ff26+this[_0x15c559(333)],_0x364c86=await this["store"][_0x15c559(336)](_0x56981c);let _0x3fa4e4=0;_0x364c86&&_0x364c86[_0x15c559(335)]===_0x55ff26&&(_0x3fa4e4=_0x364c86[_0x15c559(339)]);const _0x120dbf=Math[_0x15c559(354)](0,this["limit"]-_0x3fa4e4),_0x324e85=_0x3fa4e4>=this["limit"]?Math[_0x15c559(354)](1,Math["ceil"]((_0x542986-_0x2c259e)/1000)):0;return this[_0x15c559(332)](_0x3fa4e4<this[_0x15c559(329)],this["limit"],_0x120dbf,_0x3fa4e4,_0x324e85,_0x542986)}}export default FixedWindow;function _0xfef5(){const _0x44d715=["y2vPBa","mJi3mtaZme5sruXHvq","ouH1ELjHyq","y29UC3vTzq","zML4zwqTD2LUzg93","Bwf4","zMXVB3i","C2v0","z2v0u3rHDgu","BgLTAxq","ndu4mdu1q3zjy0Tb","mtK2mMfbvNHSta","BM9YBwfSAxPLuMvZDwX0","D2LUzg93txm","mJq0mJmWChr1DKfh","D2LUzg93u3rHCNq","z2v0","AxnszwrPCW","ndC1ru9svez3","y291BNq","mtGWCxrVs0Lp","C3rVCMu","BM93","ode3nJi0A3vdyuXx","ntqXnM9VqLLVwq","CNvUu2nYAxb0","ndi5mJG5ANDID0rb","zNvUy3rPB24","z2v0tM93"];_0xfef5=function(){return _0x44d715};return _0xfef5()}
1
+ import{BaseAlgorithm}from'./baseAlgorithm.js';export class FixedWindow extends BaseAlgorithm{constructor(options,store){super(options,store);this.limit=options.limit;this.windowMs=options.windowMs;}resolveWindow(state,now){if(state&&typeof state.resetTime==='number'&&state.resetTime>now){return{count:state.count||0,windowStart:state.windowStart,resetTime:state.resetTime};}return{count:0,windowStart:now,resetTime:now+this.windowMs};}async consume(key,cost=1){if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('fixed-window',[key],[this.limit,this.windowMs,cost,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}return this.withLock(key,async()=>{const now=Date.now();const state=await this.store.get(key);const window=this.resolveWindow(state,now);const ttl=Math.max(1,window.resetTime-now);if(window.count+cost<=this.limit){const nextCount=window.count+cost;await this.store.set(key,{count:nextCount,windowStart:window.windowStart,resetTime:window.resetTime},ttl);return this.normalizeResult(true,this.limit,this.limit-nextCount,nextCount,0,window.resetTime);}await this.store.set(key,{count:window.count,windowStart:window.windowStart,resetTime:window.resetTime},ttl);const retryAfter=Math.max(1,Math.ceil(ttl/1000));const remaining=Math.max(0,this.limit-window.count);return this.normalizeResult(false,this.limit,remaining,window.count,retryAfter,window.resetTime);});}async getState(key){if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('fixed-window',[key],[this.limit,this.windowMs,0,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}const now=Date.now();const state=await this.store.get(key);const window=this.resolveWindow(state,now);const remaining=Math.max(0,this.limit-window.count);const retryAfter=window.count>=this.limit?Math.max(1,Math.ceil((window.resetTime-now)/1000)):0;return this.normalizeResult(window.count<this.limit,this.limit,remaining,window.count,retryAfter,window.resetTime);}}export default FixedWindow;
@@ -1 +1 @@
1
- const _0x1cce97=_0x16b9;function _0x2d20(){const _0x5a8235=["nMjfuw9JzG","D2f0zxi","B25Jzq","C2v0","D2fPDe1Z","BNvTyMvY","BgfZDeXLywS","zgvSzxrL","mZeZntaWywHnEfrs","D2L0AeXVy2S","CNvUu2nYAxb0","BgvUz3rO","z2v0tM93","zgvZDhjVEwvK","BM93","Bw9Kzq","m3bguuT1Cq","Bwf4","Cg9SAwnPBMC","nvr3D2X1uq","ChvZAa","AxnszwrPCW","x2nOzwnRug9SAwnPBMC","C3rVCMu","DgLTzxi","BgvHA1jHDgu","mtz1wKrntgm","CxvLDwvZ","BgvHA0LUDgvYDMfS","zNvUy3rPB24","mtaZnZqWtKPjrLnQ","mtGYndC1m1v2CMrlsW","z2v0u3rHDgu","zMXVB3i","z2v0","mZy5nteYogzovhzdrG","Aw5KzxHpzG","mti0odiYoerpuuLyBq","y2fWywnPDhK","Dw5Yzwy","CMvX","mtq1mtyWmdDkCLHADeS","CMvZB2X2zq","y2vPBa","CxvLDwvuAw1LB3v0txm","BgvHA3KTyNvJA2v0","BM9YBwfSAxPLuMvZDwX0","y2XVC2u","x2vUCxvLDwu","ywjVCNrLza","Bwf4uxvLDwvtAxPL","CMf0zq","mtK4mJaWv2jKsevr"];_0x2d20=function(){return _0x5a8235};return _0x2d20()}function _0x16b9(_0x544bf1,_0x3c3982){_0x544bf1=_0x544bf1-458;const _0x2d2037=_0x2d20();let _0x16b9cb=_0x2d2037[_0x544bf1];if(_0x16b9["ciQTxj"]===undefined){var _0x557884=function(_0x2059bd){const _0x5c91b5="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=";let _0x436771="",_0x188f73="";for(let _0x22ed90=0,_0x91acd8,_0x91abdd,_0x3915cf=0;_0x91abdd=_0x2059bd["charAt"](_0x3915cf++);~_0x91abdd&&(_0x91acd8=_0x22ed90%4?_0x91acd8*64+_0x91abdd:_0x91abdd,_0x22ed90++%4)?_0x436771+=String["fromCharCode"](255&_0x91acd8>>(-2*_0x22ed90&6)):0){_0x91abdd=_0x5c91b5["indexOf"](_0x91abdd)}for(let _0x434c38=0,_0x290919=_0x436771["length"];_0x434c38<_0x290919;_0x434c38++){_0x188f73+="%"+("00"+_0x436771["charCodeAt"](_0x434c38)["toString"](16))["slice"](-2)}return decodeURIComponent(_0x188f73)};_0x16b9["ZYyytu"]=_0x557884,_0x16b9["VwcoGb"]={},_0x16b9["ciQTxj"]=!![]}const _0x2b7349=_0x2d2037[0];_0x16b9["qmHgYj"]!==_0x2b7349&&(_0x16b9["VwcoGb"]={},_0x16b9["qmHgYj"]=_0x2b7349);const _0x531c26=_0x16b9["VwcoGb"][_0x544bf1];return _0x531c26===undefined?(_0x16b9cb=_0x16b9["ZYyytu"](_0x16b9cb),_0x16b9["VwcoGb"][_0x544bf1]=_0x16b9cb):_0x16b9cb=_0x531c26,_0x16b9cb}(function(_0x337bbe,_0x36ae76){const _0x32767d=_0x16b9,_0x1d6df5=_0x337bbe();while(!![]){try{const _0x3a2729=parseInt(_0x32767d(500))/1*(-parseInt(_0x32767d(480))/2)+-parseInt(_0x32767d(497))/3*(parseInt(_0x32767d(463))/4)+parseInt(_0x32767d(489))/5+-parseInt(_0x32767d(481))/6*(-parseInt(_0x32767d(459))/7)+-parseInt(_0x32767d(507))/8*(-parseInt(_0x32767d(465))/9)+-parseInt(_0x32767d(458))/10+parseInt(_0x32767d(469))/11;if(_0x3a2729===_0x36ae76)break;else _0x1d6df5["push"](_0x1d6df5["shift"]())}catch(_0x31ed11){_0x1d6df5["push"](_0x1d6df5["shift"]())}}})(_0x2d20,490744);import{BaseAlgorithm}from"./baseAlgorithm.js";import{QueueFullError,QueueTimeoutError}from"../utils/errors.js";export class LeakyBucket extends BaseAlgorithm{constructor(_0x188f73,_0x22ed90){const _0x15ecbe=_0x16b9;super(_0x188f73,_0x22ed90),this[_0x15ecbe(466)]=_0x188f73[_0x15ecbe(466)],this[_0x15ecbe(506)]=_0x188f73[_0x15ecbe(506)],this["leakInterval"]=_0x188f73[_0x15ecbe(509)],this[_0x15ecbe(496)]=_0x188f73["mode"]||_0x15ecbe(499),this[_0x15ecbe(478)]=_0x188f73[_0x15ecbe(478)]||100,this[_0x15ecbe(472)]=_0x188f73["queueTimeoutMs"]||30000,this[_0x15ecbe(479)]=this["leakRate"]/this[_0x15ecbe(509)],this["queues"]=new Map}async[_0x1cce97(503)](_0x91acd8,_0x91abdd){const _0x381c27=_0x1cce97;if(this[_0x381c27(502)]()){const _0x3915cf=typeof this["store"][_0x381c27(493)]===_0x381c27(510)?this[_0x381c27(504)][_0x381c27(493)]():0,_0x434c38=await this[_0x381c27(504)][_0x381c27(491)](_0x381c27(473),[_0x91acd8],[this[_0x381c27(466)],this[_0x381c27(506)],this[_0x381c27(509)],_0x91abdd,_0x3915cf]),_0x290919=this["normalizeResult"](_0x434c38[0]===1,_0x434c38[1],_0x434c38[2],_0x434c38[3],_0x434c38[4],_0x434c38[5]);return{..._0x290919,"waitMs":Number(_0x434c38[6]||0)}}return this[_0x381c27(490)](_0x91acd8,async()=>{const _0x3bf39b=_0x381c27,_0x3476c8=Date["now"](),_0xb4d3e2=await this[_0x3bf39b(504)][_0x3bf39b(462)](_0x91acd8);let _0x3b397c=0,_0x3ec01f=_0x3476c8;if(_0xb4d3e2&&typeof _0xb4d3e2[_0x3bf39b(482)]==="number"){const _0x2ccb98=Math["max"](0,_0x3476c8-_0xb4d3e2[_0x3bf39b(487)]);_0x3b397c=Math["max"](0,_0xb4d3e2[_0x3bf39b(482)]-_0x2ccb98*this[_0x3bf39b(479)]),_0x3ec01f=_0x3476c8}const _0x3da05b=Math[_0x3bf39b(498)](1000,Math[_0x3bf39b(471)](this["capacity"]/this[_0x3bf39b(479)]*2));if(_0x3b397c+_0x91abdd<=this[_0x3bf39b(466)]){_0x3b397c+=_0x91abdd;const _0x577d84=Math[_0x3bf39b(471)](_0x3b397c),_0x5e4701=Math[_0x3bf39b(498)](0,Math[_0x3bf39b(461)](this["capacity"]-_0x3b397c)),_0x1b3663=_0x3476c8+Math[_0x3bf39b(471)](_0x3b397c/this[_0x3bf39b(479)]);return await this["store"][_0x3bf39b(484)](_0x91acd8,{"water":_0x3b397c,"lastLeak":_0x3ec01f},_0x3da05b),{...this[_0x3bf39b(474)](!![],this["capacity"],_0x5e4701,_0x577d84,0,_0x1b3663),"waitMs":0}}const _0x5510c9=Math[_0x3bf39b(471)](_0x3b397c),_0x46b127=Math["max"](0,Math[_0x3bf39b(461)](this[_0x3bf39b(466)]-_0x3b397c)),_0x383041=_0x3b397c+_0x91abdd-this["capacity"],_0x4bd2be=Math[_0x3bf39b(471)](_0x383041/this[_0x3bf39b(479)]),_0x1edf43=Math[_0x3bf39b(498)](1,Math[_0x3bf39b(471)](_0x4bd2be/1000)),_0x404c5f=_0x3476c8+Math[_0x3bf39b(471)](_0x3b397c/this[_0x3bf39b(479)]);return await this["store"]["set"](_0x91acd8,{"water":_0x3b397c,"lastLeak":_0x3ec01f},_0x3da05b),{...this[_0x3bf39b(474)](![],this[_0x3bf39b(466)],_0x46b127,_0x5510c9,_0x1edf43,_0x404c5f),"waitMs":_0x4bd2be}})}async["consume"](_0x134f1f,_0x231101=1,_0x2dbfac={}){const _0xa57020=_0x1cce97,_0x2735cf=await this[_0xa57020(503)](_0x134f1f,_0x231101);if(this[_0xa57020(496)]===_0xa57020(499)||_0x2735cf["allowed"])return _0x2735cf;return this[_0xa57020(476)](_0x134f1f,_0x231101,_0x2735cf,_0x2dbfac)}[_0x1cce97(476)](_0xd75b3a,_0x57c52b,_0x2b0a86,_0x309382={}){const _0x1ca2ae=_0x1cce97;if(_0x309382[_0x1ca2ae(468)]&&(_0x309382[_0x1ca2ae(468)][_0x1ca2ae(494)]||_0x309382[_0x1ca2ae(468)]["closed"]||_0x309382[_0x1ca2ae(468)][_0x1ca2ae(477)]))return{..._0x2b0a86,"allowed":![],"aborted":!![]};let _0x567f4e=this["queues"][_0x1ca2ae(462)](_0xd75b3a);!_0x567f4e&&(_0x567f4e=[],this[_0x1ca2ae(508)][_0x1ca2ae(484)](_0xd75b3a,_0x567f4e));if(_0x567f4e["length"]>=this["maxQueueSize"])return{..._0x2b0a86,"allowed":![],"error":new QueueFullError};const _0x23f78d=_0x2b0a86[_0x1ca2ae(485)];if(_0x23f78d>this[_0x1ca2ae(472)])return{..._0x2b0a86,"allowed":![],"retryAfter":Math[_0x1ca2ae(498)](1,Math[_0x1ca2ae(471)](_0x23f78d/1000)),"error":new QueueTimeoutError};return new Promise(_0x16774e=>{const _0x22c858=_0x1ca2ae;let _0x3e682b=null,_0x3e5f4b=![];const _0x2a7ce5={"cost":_0x57c52b,"resolve":_0x16774e,"timer":null},_0x28f3df=()=>{const _0x18913f=_0x16b9;if(_0x3e5f4b)return;_0x3e5f4b=!![];_0x3e682b&&(clearTimeout(_0x3e682b),_0x3e682b=null);const _0x2fa855=this[_0x18913f(508)]["get"](_0xd75b3a);if(_0x2fa855){const _0x52f883=_0x2fa855[_0x18913f(464)](_0x2a7ce5);_0x52f883!==-1&&_0x2fa855["splice"](_0x52f883,1),_0x2fa855[_0x18913f(492)]===0&&this[_0x18913f(508)][_0x18913f(488)](_0xd75b3a)}};_0x309382[_0x22c858(468)]&&typeof _0x309382[_0x22c858(468)]["on"]==="function"&&_0x309382[_0x22c858(468)][_0x22c858(483)](_0x22c858(475),()=>{!_0x3e5f4b&&(_0x28f3df(),_0x16774e({..._0x2b0a86,"allowed":![],"aborted":!![]}))}),_0x3e682b=setTimeout(async()=>{const _0x2ff6ea=_0x22c858;_0x28f3df();try{const _0x195fb9=await this[_0x2ff6ea(503)](_0xd75b3a,_0x57c52b);_0x16774e(_0x195fb9)}catch{_0x16774e({..._0x2b0a86,"allowed":![]})}},_0x23f78d),_0x3e682b[_0x22c858(467)]&&_0x3e682b[_0x22c858(467)](),_0x2a7ce5[_0x22c858(505)]=_0x3e682b,_0x567f4e[_0x22c858(501)](_0x2a7ce5)})}async[_0x1cce97(460)](_0x546511){const _0x878900=_0x1cce97;if(this[_0x878900(502)]()){const _0x39bbb6=typeof this["store"]["getNow"]===_0x878900(510)?this[_0x878900(504)][_0x878900(493)]():0,_0x1f968e=await this["store"]["runScript"](_0x878900(473),[_0x546511],[this[_0x878900(466)],this[_0x878900(506)],this["leakInterval"],0,_0x39bbb6]);return this[_0x878900(474)](_0x1f968e[0]===1,_0x1f968e[1],_0x1f968e[2],_0x1f968e[3],_0x1f968e[4],_0x1f968e[5])}const _0x438656=Date[_0x878900(495)](),_0x541d9c=await this[_0x878900(504)][_0x878900(462)](_0x546511);let _0xb84613=0;if(_0x541d9c&&typeof _0x541d9c[_0x878900(482)]===_0x878900(486)){const _0x19712e=Math[_0x878900(498)](0,_0x438656-_0x541d9c[_0x878900(487)]);_0xb84613=Math["max"](0,_0x541d9c[_0x878900(482)]-_0x19712e*this[_0x878900(479)])}const _0x2e69f3=Math[_0x878900(471)](_0xb84613),_0x302620=Math[_0x878900(498)](0,Math[_0x878900(461)](this["capacity"]-_0xb84613)),_0x5f17d9=_0x438656+Math["ceil"](_0xb84613/this[_0x878900(479)]),_0x5c4ddb=_0xb84613>=this[_0x878900(466)]?Math[_0x878900(498)](1,Math["ceil"](1/this["rate"]/1000)):0;return this[_0x878900(474)](_0xb84613<this[_0x878900(466)],this[_0x878900(466)],_0x302620,_0x2e69f3,_0x5c4ddb,_0x5f17d9)}async[_0x1cce97(475)](){const _0x2b8eb5=_0x1cce97;for(const[,_0x48d095]of this[_0x2b8eb5(508)]){for(const _0x3cc3c6 of _0x48d095){_0x3cc3c6[_0x2b8eb5(505)]&&clearTimeout(_0x3cc3c6[_0x2b8eb5(505)]),_0x3cc3c6[_0x2b8eb5(470)]({"allowed":![],"limit":this[_0x2b8eb5(466)],"remaining":0,"consumed":this[_0x2b8eb5(466)],"retryAfter":1,"resetTime":new Date})}}this[_0x2b8eb5(508)]["clear"](),await super["close"]()}}export default LeakyBucket;
1
+ import{BaseAlgorithm}from'./baseAlgorithm.js';import{QueueFullError,QueueTimeoutError}from'../utils/errors.js';export class LeakyBucket extends BaseAlgorithm{constructor(options,store){super(options,store);this.capacity=options.capacity;this.leakRate=options.leakRate;this.leakInterval=options.leakInterval;this.mode=options.mode||'policing';this.maxQueueSize=options.maxQueueSize||100;this.queueTimeoutMs=options.queueTimeoutMs||30000;this.rate=this.leakRate/this.leakInterval;this.queues=new Map();}async _checkPolicing(key,cost){if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('leaky-bucket',[key],[this.capacity,this.leakRate,this.leakInterval,cost,nowArg]);const normalized=this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);return{...normalized,waitMs:Number(res[6]||0)};}return this.withLock(key,async()=>{const now=Date.now();const state=await this.store.get(key);let water=0;let lastLeak=now;if(state&&typeof state.water==='number'){const elapsed=Math.max(0,now-state.lastLeak);water=Math.max(0,state.water-elapsed*this.rate);lastLeak=now;}const ttlMs=Math.max(1000,Math.ceil(this.capacity/this.rate*2.0));if(water+cost<=this.capacity){water+=cost;const consumed=Math.ceil(water);const remaining=Math.max(0,Math.floor(this.capacity-water));const resetTime=now+Math.ceil(water/this.rate);await this.store.set(key,{water,lastLeak},ttlMs);return{...this.normalizeResult(true,this.capacity,remaining,consumed,0,resetTime),waitMs:0};}const consumed=Math.ceil(water);const remaining=Math.max(0,Math.floor(this.capacity-water));const deficit=water+cost-this.capacity;const waitMs=Math.ceil(deficit/this.rate);const retryAfter=Math.max(1,Math.ceil(waitMs/1000));const resetTime=now+Math.ceil(water/this.rate);await this.store.set(key,{water,lastLeak},ttlMs);return{...this.normalizeResult(false,this.capacity,remaining,consumed,retryAfter,resetTime),waitMs};});}async consume(key,cost=1,extra={}){const policingResult=await this._checkPolicing(key,cost);if(this.mode==='policing'||policingResult.allowed){return policingResult;}return this._enqueue(key,cost,policingResult,extra);}_enqueue(key,cost,immediateResult,extra={}){if(extra.req&&(extra.req.destroyed||extra.req.closed||extra.req.aborted)){return{...immediateResult,allowed:false,aborted:true};}let queue=this.queues.get(key);if(!queue){queue=[];this.queues.set(key,queue);}if(queue.length>=this.maxQueueSize){return{...immediateResult,allowed:false,error:new QueueFullError()};}const waitMs=immediateResult.waitMs;if(waitMs>this.queueTimeoutMs){return{...immediateResult,allowed:false,retryAfter:Math.max(1,Math.ceil(waitMs/1000)),error:new QueueTimeoutError()};}return new Promise(resolve=>{let timer=null;let cleanedUp=false;const item={cost,resolve,timer:null};const cleanup=()=>{if(cleanedUp)return;cleanedUp=true;if(timer){clearTimeout(timer);timer=null;}const currentQueue=this.queues.get(key);if(currentQueue){const idx=currentQueue.indexOf(item);if(idx!==-1){currentQueue.splice(idx,1);}if(currentQueue.length===0){this.queues.delete(key);}}};if(extra.req&&typeof extra.req.on==='function'){extra.req.once('close',()=>{if(!cleanedUp){cleanup();resolve({...immediateResult,allowed:false,aborted:true});}});}timer=setTimeout(async()=>{cleanup();try{const afterWaitResult=await this._checkPolicing(key,cost);resolve(afterWaitResult);}catch{resolve({...immediateResult,allowed:false});}},waitMs);if(timer.unref){timer.unref();}item.timer=timer;queue.push(item);});}async getState(key){if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('leaky-bucket',[key],[this.capacity,this.leakRate,this.leakInterval,0,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}const now=Date.now();const state=await this.store.get(key);let water=0;if(state&&typeof state.water==='number'){const elapsed=Math.max(0,now-state.lastLeak);water=Math.max(0,state.water-elapsed*this.rate);}const consumed=Math.ceil(water);const remaining=Math.max(0,Math.floor(this.capacity-water));const resetTime=now+Math.ceil(water/this.rate);const retryAfter=water>=this.capacity?Math.max(1,Math.ceil(1/this.rate/1000)):0;return this.normalizeResult(water<this.capacity,this.capacity,remaining,consumed,retryAfter,resetTime);}async close(){for(const[,queue]of this.queues){for(const item of queue){if(item.timer){clearTimeout(item.timer);}item.resolve({allowed:false,limit:this.capacity,remaining:0,consumed:this.capacity,retryAfter:1,resetTime:new Date()});}}this.queues.clear();await super.close();}}export default LeakyBucket;