rate-limiter-flexible 6.2.0 → 7.0.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 +9 -7
- package/index.js +3 -1
- package/lib/ExpressBruteFlexible.js +12 -0
- package/lib/RateLimiterSQLite.js +8 -6
- package/lib/RateLimiterValkeyGlide.js +273 -0
- package/lib/constants.js +2 -0
- package/lib/index.d.ts +96 -0
- package/package.json +13 -3
- package/.editorconfig +0 -13
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[![node version][node-image]][node-url]
|
|
4
4
|
[](https://github.com/denoland/deno)
|
|
5
5
|
|
|
6
|
-
[node-image]: https://img.shields.io/badge/node.js-%3E=
|
|
6
|
+
[node-image]: https://img.shields.io/badge/node.js-%3E=_20.0-green.svg?style=flat-square
|
|
7
7
|
[node-url]: http://nodejs.org/download/
|
|
8
8
|
|
|
9
9
|
<img src="img/rlflx-logo-small.png" width="50" alt="Logo"/>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
**rate-limiter-flexible** counts and limits the number of actions by key and protects from DDoS and brute force attacks at any scale.
|
|
14
14
|
|
|
15
|
-
It works with
|
|
15
|
+
It works with _Valkey_, _Redis_, _Prisma_, _DynamoDB_, process _Memory_, _Cluster_ or _PM2_, _Memcached_, _MongoDB_, _MySQL_, _SQLite_, and _PostgreSQL_.
|
|
16
16
|
|
|
17
17
|
Memory limiter also works in the browser.
|
|
18
18
|
|
|
@@ -24,7 +24,9 @@ Memory limiter also works in the browser.
|
|
|
24
24
|
|
|
25
25
|
**Ready for growth.** It provides a unified API for all limiters. Whenever your application grows, it is ready. Prepare your limiters in minutes.
|
|
26
26
|
|
|
27
|
-
**Friendly.** No matter which node package you prefer: `redis` or `ioredis`, `
|
|
27
|
+
**Friendly.** No matter which node package you prefer: [`valkey-glide`](https://www.npmjs.com/package/@valkey/valkey-glide) or [`iovalkey`](https://www.npmjs.com/package/iovalkey), `redis` or `ioredis`, `sequelize`/`typeorm` or `knex`, `memcached`, native driver or `mongoose`. It works with all of them.
|
|
28
|
+
|
|
29
|
+
**Safe for using with valkey cluster.** [`valkey-glide`](https://www.npmjs.com/package/@valkey/valkey-glide) implementation, [RateLimiterValkeyGlide](https://github.com/animir/node-rate-limiter-flexible/wiki/RateLimiterValkeyGlide), is being tested against `valkey` cluster to ensure the rate limiter is agnostic to the server type, it able to avoid race conditions in high traffic along with sharded cluster, and to ensure compatibility and high performance.
|
|
28
30
|
|
|
29
31
|
**In-memory blocks.** Avoid extra requests to store with [inMemoryBlockOnConsumed](https://github.com/animir/node-rate-limiter-flexible/wiki/Options#inmemoryblockonconsumed).
|
|
30
32
|
|
|
@@ -95,7 +97,7 @@ const headers = {
|
|
|
95
97
|
"Retry-After": rateLimiterRes.msBeforeNext / 1000,
|
|
96
98
|
"X-RateLimit-Limit": opts.points,
|
|
97
99
|
"X-RateLimit-Remaining": rateLimiterRes.remainingPoints,
|
|
98
|
-
"X-RateLimit-Reset":
|
|
100
|
+
"X-RateLimit-Reset": Math.ceil((Date.now() + rateLimiterRes.msBeforeNext) / 1000)
|
|
99
101
|
}
|
|
100
102
|
```
|
|
101
103
|
|
|
@@ -137,18 +139,18 @@ Some copy/paste examples on Wiki:
|
|
|
137
139
|
|
|
138
140
|
* [Options](https://github.com/animir/node-rate-limiter-flexible/wiki/Options)
|
|
139
141
|
* [API methods](https://github.com/animir/node-rate-limiter-flexible/wiki/API-methods)
|
|
142
|
+
* [Valkey](https://github.com/animir/node-rate-limiter-flexible/wiki/Valkey)
|
|
140
143
|
* [Redis](https://github.com/animir/node-rate-limiter-flexible/wiki/Redis)
|
|
141
144
|
* [Memory](https://github.com/animir/node-rate-limiter-flexible/wiki/Memory)
|
|
142
145
|
* [DynamoDb](https://github.com/animir/node-rate-limiter-flexible/wiki/DynamoDB)
|
|
143
146
|
* [Prisma](https://github.com/animir/node-rate-limiter-flexible/wiki/Prisma)
|
|
144
|
-
* [Valkey](https://github.com/animir/node-rate-limiter-flexible/wiki/Valkey)
|
|
145
147
|
* [BurstyRateLimiter](https://github.com/animir/node-rate-limiter-flexible/wiki/BurstyRateLimiter) Traffic burst support
|
|
146
148
|
* [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))
|
|
147
149
|
* [MySQL](https://github.com/animir/node-rate-limiter-flexible/wiki/MySQL) (support Sequelize and Knex)
|
|
148
150
|
* [Postgres](https://github.com/animir/node-rate-limiter-flexible/wiki/PostgreSQL) (support Sequelize, TypeORM and Knex)
|
|
149
151
|
* [SQLite](https://github.com/animir/node-rate-limiter-flexible/wiki/SQLite)
|
|
150
152
|
* [RateLimiterCluster](https://github.com/animir/node-rate-limiter-flexible/wiki/Cluster) ([PM2 cluster docs read here](https://github.com/animir/node-rate-limiter-flexible/wiki/PM2-cluster))
|
|
151
|
-
* [
|
|
153
|
+
* [Memcached](https://github.com/animir/node-rate-limiter-flexible/wiki/Memcache)
|
|
152
154
|
* [RateLimiterUnion](https://github.com/animir/node-rate-limiter-flexible/wiki/RateLimiterUnion) Combine 2 or more limiters to act as single
|
|
153
155
|
* [RLWrapperBlackAndWhite](https://github.com/animir/node-rate-limiter-flexible/wiki/Black-and-White-lists) Black and White lists
|
|
154
156
|
* [RateLimiterQueue](https://github.com/animir/node-rate-limiter-flexible/wiki/RateLimiterQueue) Rate limiter with FIFO queue
|
|
@@ -177,7 +179,7 @@ See [releases](https://github.com/animir/node-rate-limiter-flexible/releases) fo
|
|
|
177
179
|
|
|
178
180
|
`Required for store limiters`
|
|
179
181
|
|
|
180
|
-
Must be `redis`, `ioredis`, `memcached`, `mongodb`, `pg`, `mysql2`, `mysql` or any other related pool or connection.
|
|
182
|
+
Must be `@valkey/valkey-glide`, `iovalkey`, `redis`, `ioredis`, `memcached`, `mongodb`, `pg`, `mysql2`, `mysql` or any other related pool or connection.
|
|
181
183
|
|
|
182
184
|
### Other options on Wiki:
|
|
183
185
|
* [keyPrefix](https://github.com/animir/node-rate-limiter-flexible/wiki/Options#keyprefix) Make keys unique among different limiters.
|
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@ const RateLimiterRedis = require('./lib/RateLimiterRedis');
|
|
|
2
2
|
const RateLimiterMongo = require('./lib/RateLimiterMongo');
|
|
3
3
|
const RateLimiterMySQL = require('./lib/RateLimiterMySQL');
|
|
4
4
|
const RateLimiterPostgres = require('./lib/RateLimiterPostgres');
|
|
5
|
-
const {RateLimiterClusterMaster, RateLimiterClusterMasterPM2, RateLimiterCluster} = require('./lib/RateLimiterCluster');
|
|
5
|
+
const { RateLimiterClusterMaster, RateLimiterClusterMasterPM2, RateLimiterCluster } = require('./lib/RateLimiterCluster');
|
|
6
6
|
const RateLimiterMemory = require('./lib/RateLimiterMemory');
|
|
7
7
|
const RateLimiterMemcache = require('./lib/RateLimiterMemcache');
|
|
8
8
|
const RLWrapperBlackAndWhite = require('./lib/RLWrapperBlackAndWhite');
|
|
@@ -13,6 +13,7 @@ const RateLimiterRes = require('./lib/RateLimiterRes');
|
|
|
13
13
|
const RateLimiterDynamo = require('./lib/RateLimiterDynamo');
|
|
14
14
|
const RateLimiterPrisma = require('./lib/RateLimiterPrisma');
|
|
15
15
|
const RateLimiterValkey = require('./lib/RateLimiterValkey');
|
|
16
|
+
const RateLimiterValkeyGlide = require('./lib/RateLimiterValkeyGlide');
|
|
16
17
|
const RateLimiterSQLite = require('./lib/RateLimiterSQLite');
|
|
17
18
|
|
|
18
19
|
module.exports = {
|
|
@@ -33,5 +34,6 @@ module.exports = {
|
|
|
33
34
|
RateLimiterDynamo,
|
|
34
35
|
RateLimiterPrisma,
|
|
35
36
|
RateLimiterValkey,
|
|
37
|
+
RateLimiterValkeyGlide,
|
|
36
38
|
RateLimiterSQLite,
|
|
37
39
|
};
|
|
@@ -11,6 +11,8 @@ const {
|
|
|
11
11
|
RateLimiterMySQL,
|
|
12
12
|
RateLimiterPostgres,
|
|
13
13
|
RateLimiterRedis,
|
|
14
|
+
RateLimiterValkey,
|
|
15
|
+
RateLimiterValkeyGlide,
|
|
14
16
|
} = require('../index');
|
|
15
17
|
|
|
16
18
|
function getDelayMs(count, delays, maxWait) {
|
|
@@ -119,6 +121,16 @@ ExpressBruteFlexible.prototype.getMiddleware = function (options) {
|
|
|
119
121
|
this.blockLimiter = new RateLimiterPostgres(blockLimiterOptions);
|
|
120
122
|
this.counterLimiter = new RateLimiterPostgres(counterLimiterOptions);
|
|
121
123
|
break;
|
|
124
|
+
case 'valkey-glide':
|
|
125
|
+
this.freeLimiter = new RateLimiterValkeyGlide(freeLimiterOptions);
|
|
126
|
+
this.blockLimiter = new RateLimiterValkeyGlide(blockLimiterOptions);
|
|
127
|
+
this.counterLimiter = new RateLimiterValkeyGlide(counterLimiterOptions);
|
|
128
|
+
break;
|
|
129
|
+
case 'valkey':
|
|
130
|
+
this.freeLimiter = new RateLimiterValkey(freeLimiterOptions);
|
|
131
|
+
this.blockLimiter = new RateLimiterValkey(blockLimiterOptions);
|
|
132
|
+
this.counterLimiter = new RateLimiterValkey(counterLimiterOptions);
|
|
133
|
+
break;
|
|
122
134
|
case 'redis':
|
|
123
135
|
this.freeLimiter = new RateLimiterRedis(freeLimiterOptions);
|
|
124
136
|
this.blockLimiter = new RateLimiterRedis(blockLimiterOptions);
|
package/lib/RateLimiterSQLite.js
CHANGED
|
@@ -217,8 +217,7 @@ class RateLimiterSQLite extends RateLimiterStoreAbstract {
|
|
|
217
217
|
return res;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
async _upsertTransactionSQLite3(upsertQuery, upsertParams) {
|
|
221
|
-
const conn = await this._getConnection();
|
|
220
|
+
async _upsertTransactionSQLite3(conn, upsertQuery, upsertParams) {
|
|
222
221
|
return await new Promise((resolve, reject) => {
|
|
223
222
|
conn.serialize(() => {
|
|
224
223
|
conn.run("SAVEPOINT rate_limiter_trx;", (err) => {
|
|
@@ -237,9 +236,7 @@ class RateLimiterSQLite extends RateLimiterStoreAbstract {
|
|
|
237
236
|
});
|
|
238
237
|
}
|
|
239
238
|
|
|
240
|
-
async _upsertTransactionBetterSQLite3(upsertQuery, upsertParams) {
|
|
241
|
-
const conn = await this._getConnection();
|
|
242
|
-
|
|
239
|
+
async _upsertTransactionBetterSQLite3(conn, upsertQuery, upsertParams) {
|
|
243
240
|
return conn.transaction(() =>
|
|
244
241
|
conn.prepare(upsertQuery).get(...upsertParams)
|
|
245
242
|
)();
|
|
@@ -263,9 +260,14 @@ class RateLimiterSQLite extends RateLimiterStoreAbstract {
|
|
|
263
260
|
try {
|
|
264
261
|
switch (this._internalStoreType) {
|
|
265
262
|
case "sqlite3":
|
|
266
|
-
return this._upsertTransactionSQLite3(
|
|
263
|
+
return this._upsertTransactionSQLite3(
|
|
264
|
+
conn,
|
|
265
|
+
upsertQuery,
|
|
266
|
+
upsertParams
|
|
267
|
+
);
|
|
267
268
|
case "better-sqlite3":
|
|
268
269
|
return this._upsertTransactionBetterSQLite3(
|
|
270
|
+
conn,
|
|
269
271
|
upsertQuery,
|
|
270
272
|
upsertParams
|
|
271
273
|
);
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
|
|
3
|
+
const RateLimiterRes = require('./RateLimiterRes');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('@valkey/valkey-glide').GlideClient} GlideClient
|
|
7
|
+
* @typedef {import('@valkey/valkey-glide').GlideClusterClient} GlideClusterClient
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const DEFAULT_LIBRARY_NAME = 'ratelimiterflexible';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_VALKEY_SCRIPT = `local key = KEYS[1]
|
|
13
|
+
local pointsToConsume = tonumber(ARGV[1])
|
|
14
|
+
if tonumber(ARGV[2]) > 0 then
|
|
15
|
+
server.call('set', key, "0", 'EX', ARGV[2], 'NX')
|
|
16
|
+
local consumed = server.call('incrby', key, pointsToConsume)
|
|
17
|
+
local pttl = server.call('pttl', key)
|
|
18
|
+
return {consumed, pttl}
|
|
19
|
+
end
|
|
20
|
+
local consumed = server.call('incrby', key, pointsToConsume)
|
|
21
|
+
local pttl = server.call('pttl', key)
|
|
22
|
+
return {consumed, pttl}`;
|
|
23
|
+
|
|
24
|
+
const GET_VALKEY_SCRIPT = `local key = KEYS[1]
|
|
25
|
+
local value = server.call('get', key)
|
|
26
|
+
if value == nil then
|
|
27
|
+
return value
|
|
28
|
+
end
|
|
29
|
+
local pttl = server.call('pttl', key)
|
|
30
|
+
return {tonumber(value), pttl}`;
|
|
31
|
+
|
|
32
|
+
class RateLimiterValkeyGlide extends RateLimiterStoreAbstract {
|
|
33
|
+
/**
|
|
34
|
+
* Constructor for RateLimiterValkeyGlide
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} opts - Configuration options
|
|
37
|
+
* @param {GlideClient|GlideClusterClient} opts.storeClient - Valkey Glide client instance (required)
|
|
38
|
+
* @param {number} [opts.points=4] - Maximum number of points that can be consumed over duration
|
|
39
|
+
* @param {number} [opts.duration=1] - Duration in seconds before points are reset
|
|
40
|
+
* @param {number} [opts.blockDuration=0] - Duration in seconds that a key will be blocked for if consumed more than points
|
|
41
|
+
* @param {boolean} [opts.rejectIfValkeyNotReady=false] - Whether to reject requests if Valkey is not ready
|
|
42
|
+
* @param {boolean} [opts.execEvenly=false] - Delay actions to distribute them evenly over duration
|
|
43
|
+
* @param {number} [opts.execEvenlyMinDelayMs] - Minimum delay between actions when execEvenly is true
|
|
44
|
+
* @param {string} [opts.customFunction] - Custom Lua script for rate limiting logic
|
|
45
|
+
* @param {number} [opts.inMemoryBlockOnConsumed] - Points threshold for in-memory blocking
|
|
46
|
+
* @param {number} [opts.inMemoryBlockDuration] - Duration in seconds for in-memory blocking
|
|
47
|
+
* @param {string} [opts.customFunctionLibName] - Custom name for the function library, defaults to 'ratelimiter'.
|
|
48
|
+
* The name is used to identify the library of the lua function. An custom name should be used only if you
|
|
49
|
+
* you want to use different libraries for different rate limiters, otherwise it is not needed.
|
|
50
|
+
* @param {RateLimiterAbstract} [opts.insuranceLimiter] - Backup limiter to use when the primary client fails
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const rateLimiter = new RateLimiterValkeyGlide({
|
|
54
|
+
* storeClient: glideClient,
|
|
55
|
+
* points: 5,
|
|
56
|
+
* duration: 1
|
|
57
|
+
* });
|
|
58
|
+
*
|
|
59
|
+
* @example <caption>With custom Lua function</caption>
|
|
60
|
+
* const customScript = `local key = KEYS[1]
|
|
61
|
+
* local pointsToConsume = tonumber(ARGV[1]) or 0
|
|
62
|
+
* local secDuration = tonumber(ARGV[2]) or 0
|
|
63
|
+
*
|
|
64
|
+
* -- Custom implementation
|
|
65
|
+
* -- ...
|
|
66
|
+
*
|
|
67
|
+
* -- Must return exactly two values: [consumed_points, ttl_in_ms]
|
|
68
|
+
* return {consumed, ttl}`
|
|
69
|
+
*
|
|
70
|
+
* const rateLimiter = new RateLimiterValkeyGlide({
|
|
71
|
+
* storeClient: glideClient,
|
|
72
|
+
* points: 5,
|
|
73
|
+
* customFunction: customScript
|
|
74
|
+
* });
|
|
75
|
+
*
|
|
76
|
+
* @example <caption>With insurance limiter</caption>
|
|
77
|
+
* const rateLimiter = new RateLimiterValkeyGlide({
|
|
78
|
+
* storeClient: primaryGlideClient,
|
|
79
|
+
* points: 5,
|
|
80
|
+
* duration: 2,
|
|
81
|
+
* insuranceLimiter: new RateLimiterMemory({
|
|
82
|
+
* points: 5,
|
|
83
|
+
* duration: 2
|
|
84
|
+
* })
|
|
85
|
+
* });
|
|
86
|
+
*
|
|
87
|
+
* @description
|
|
88
|
+
* When providing a custom Lua script via `opts.customFunction`, it must:
|
|
89
|
+
*
|
|
90
|
+
* 1. Accept parameters:
|
|
91
|
+
* - KEYS[1]: The key being rate limited
|
|
92
|
+
* - ARGV[1]: Points to consume (as string, use tonumber() to convert)
|
|
93
|
+
* - ARGV[2]: Duration in seconds (as string, use tonumber() to convert)
|
|
94
|
+
*
|
|
95
|
+
* 2. Return an array with exactly two elements:
|
|
96
|
+
* - [0]: Consumed points (number)
|
|
97
|
+
* - [1]: TTL in milliseconds (number)
|
|
98
|
+
*
|
|
99
|
+
* 3. Handle scenarios:
|
|
100
|
+
* - New key creation: Initialize with expiry for fixed windows
|
|
101
|
+
* - Key updates: Increment existing counters
|
|
102
|
+
*/
|
|
103
|
+
constructor(opts) {
|
|
104
|
+
super(opts);
|
|
105
|
+
this.client = opts.storeClient;
|
|
106
|
+
this._scriptLoaded = false;
|
|
107
|
+
this._getScriptLoaded = false;
|
|
108
|
+
this._rejectIfValkeyNotReady = !!opts.rejectIfValkeyNotReady;
|
|
109
|
+
this._luaScript = opts.customFunction || DEFAULT_VALKEY_SCRIPT;
|
|
110
|
+
this._libraryName = opts.customFunctionLibName || DEFAULT_LIBRARY_NAME;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Ensure scripts are loaded in the Valkey server
|
|
115
|
+
* @returns {Promise<boolean>} True if scripts are loaded
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
async _loadScripts() {
|
|
119
|
+
if (this._scriptLoaded && this._getScriptLoaded) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
if (!this.client) {
|
|
123
|
+
throw new Error('Valkey client is not set');
|
|
124
|
+
}
|
|
125
|
+
const promises = [];
|
|
126
|
+
if (!this._scriptLoaded) {
|
|
127
|
+
const script = Buffer.from(`#!lua name=${this._libraryName}
|
|
128
|
+
local function consume(KEYS, ARGV)
|
|
129
|
+
${this._luaScript.trim()}
|
|
130
|
+
end
|
|
131
|
+
server.register_function('consume', consume)`);
|
|
132
|
+
promises.push(this.client.functionLoad(script, { replace: true }));
|
|
133
|
+
} else promises.push(Promise.resolve(this._libraryName));
|
|
134
|
+
|
|
135
|
+
if (!this._getScriptLoaded) {
|
|
136
|
+
const script = Buffer.from(`#!lua name=ratelimiter_get
|
|
137
|
+
local function getValue(KEYS, ARGV)
|
|
138
|
+
${GET_VALKEY_SCRIPT.trim()}
|
|
139
|
+
end
|
|
140
|
+
server.register_function('getValue', getValue)`);
|
|
141
|
+
promises.push(this.client.functionLoad(script, { replace: true }));
|
|
142
|
+
} else promises.push(Promise.resolve('ratelimiter_get'));
|
|
143
|
+
|
|
144
|
+
const results = await Promise.all(promises);
|
|
145
|
+
this._scriptLoaded = results[0] === this._libraryName;
|
|
146
|
+
this._getScriptLoaded = results[1] === 'ratelimiter_get';
|
|
147
|
+
|
|
148
|
+
if ((!this._scriptLoaded || !this._getScriptLoaded)) {
|
|
149
|
+
throw new Error('Valkey connection is not ready, scripts not loaded');
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Update or insert the rate limiter record
|
|
156
|
+
*
|
|
157
|
+
* @param {string} rlKey - The rate limiter key
|
|
158
|
+
* @param {number} pointsToConsume - Points to be consumed
|
|
159
|
+
* @param {number} msDuration - Duration in milliseconds
|
|
160
|
+
* @param {boolean} [forceExpire=false] - Whether to force expiration
|
|
161
|
+
* @param {Object} [options={}] - Additional options
|
|
162
|
+
* @returns {Promise<Array>} Array containing consumed points and TTL
|
|
163
|
+
* @private
|
|
164
|
+
*/
|
|
165
|
+
async _upsert(rlKey, pointsToConsume, msDuration, forceExpire = false, options = {}) {
|
|
166
|
+
await this._loadScripts();
|
|
167
|
+
const secDuration = Math.floor(msDuration / 1000);
|
|
168
|
+
if (forceExpire) {
|
|
169
|
+
if (secDuration > 0) {
|
|
170
|
+
await this.client.set(
|
|
171
|
+
rlKey,
|
|
172
|
+
String(pointsToConsume),
|
|
173
|
+
{ expiry: { type: 'EX', count: secDuration } },
|
|
174
|
+
);
|
|
175
|
+
return [pointsToConsume, secDuration * 1000];
|
|
176
|
+
}
|
|
177
|
+
await this.client.set(rlKey, String(pointsToConsume));
|
|
178
|
+
return [pointsToConsume, -1];
|
|
179
|
+
}
|
|
180
|
+
const result = await this.client.fcall(
|
|
181
|
+
'consume',
|
|
182
|
+
[rlKey],
|
|
183
|
+
[String(pointsToConsume), String(secDuration)],
|
|
184
|
+
);
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get the rate limiter record
|
|
190
|
+
*
|
|
191
|
+
* @param {string} rlKey - The rate limiter key
|
|
192
|
+
* @param {Object} [options={}] - Additional options
|
|
193
|
+
* @returns {Promise<Array|null>} Array containing consumed points and TTL, or null if not found
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
async _get(rlKey, options = {}) {
|
|
197
|
+
await this._loadScripts();
|
|
198
|
+
const res = await this.client.fcall('getValue', [rlKey], []);
|
|
199
|
+
return res.length > 0 ? res : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Delete the rate limiter record
|
|
204
|
+
*
|
|
205
|
+
* @param {string} rlKey - The rate limiter key
|
|
206
|
+
* @param {Object} [options={}] - Additional options
|
|
207
|
+
* @returns {Promise<boolean>} True if successful, false otherwise
|
|
208
|
+
* @private
|
|
209
|
+
*/
|
|
210
|
+
async _delete(rlKey, options = {}) {
|
|
211
|
+
const result = await this.client.del([rlKey]);
|
|
212
|
+
return result > 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Convert raw result to RateLimiterRes object
|
|
217
|
+
*
|
|
218
|
+
* @param {string} rlKey - The rate limiter key
|
|
219
|
+
* @param {number} changedPoints - Points changed in this operation
|
|
220
|
+
* @param {Array|null} result - Result from Valkey operation
|
|
221
|
+
* @returns {RateLimiterRes|null} RateLimiterRes object or null if result is null
|
|
222
|
+
* @private
|
|
223
|
+
*/
|
|
224
|
+
_getRateLimiterRes(rlKey, changedPoints, result) {
|
|
225
|
+
if (result === null) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const res = new RateLimiterRes();
|
|
229
|
+
const [consumedPointsStr, pttl] = result;
|
|
230
|
+
const consumedPoints = Number(consumedPointsStr);
|
|
231
|
+
|
|
232
|
+
// Handle consumed points
|
|
233
|
+
res.isFirstInDuration = consumedPoints === changedPoints;
|
|
234
|
+
res.consumedPoints = consumedPoints;
|
|
235
|
+
res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
|
|
236
|
+
res.msBeforeNext = pttl;
|
|
237
|
+
return res;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Close the rate limiter and release resources
|
|
242
|
+
* Note: The method won't going to close the Valkey client, as it may be shared with other instances.
|
|
243
|
+
* @returns {Promise<void>} Promise that resolves when the rate limiter is closed
|
|
244
|
+
*/
|
|
245
|
+
async close() {
|
|
246
|
+
if (this._scriptLoaded) {
|
|
247
|
+
await this.client.functionDelete(this._libraryName);
|
|
248
|
+
this._scriptLoaded = false;
|
|
249
|
+
}
|
|
250
|
+
if (this._getScriptLoaded) {
|
|
251
|
+
await this.client.functionDelete('ratelimiter_get');
|
|
252
|
+
this._getScriptLoaded = false;
|
|
253
|
+
}
|
|
254
|
+
if (this.insuranceLimiter) {
|
|
255
|
+
try {
|
|
256
|
+
await this.insuranceLimiter.close();
|
|
257
|
+
} catch (e) {
|
|
258
|
+
// We can't assume that insuranceLimiter is a Valkey client or any
|
|
259
|
+
// other insuranceLimiter type which implement close method.
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Clear instance properties to let garbage collector free memory
|
|
263
|
+
this.client = null;
|
|
264
|
+
this._scriptLoaded = false;
|
|
265
|
+
this._getScriptLoaded = false;
|
|
266
|
+
this._rejectIfValkeyNotReady = false;
|
|
267
|
+
this._luaScript = null;
|
|
268
|
+
this._libraryName = null;
|
|
269
|
+
this.insuranceLimiter = null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = RateLimiterValkeyGlide;
|
package/lib/constants.js
CHANGED
package/lib/index.d.ts
CHANGED
|
@@ -417,3 +417,99 @@ interface IRateLimiterDynamoOptions extends IRateLimiterStoreOptions {
|
|
|
417
417
|
export class RateLimiterDynamo extends RateLimiterStoreAbstract {
|
|
418
418
|
constructor(opts: IRateLimiterDynamoOptions, cb?: ICallbackReady);
|
|
419
419
|
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Options for RateLimiterValkeyGlide
|
|
423
|
+
*/
|
|
424
|
+
interface IRateLimiterValkeyGlideOptions extends IRateLimiterStoreOptions {
|
|
425
|
+
/**
|
|
426
|
+
* Valkey Glide client instance (GlideClient or GlideClusterClient)
|
|
427
|
+
*/
|
|
428
|
+
storeClient: any; // GlideClient | GlideClusterClient;
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Whether to reject requests if Valkey is not ready
|
|
432
|
+
* @default false
|
|
433
|
+
*/
|
|
434
|
+
rejectIfValkeyNotReady?: boolean;
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Custom Lua script for rate limiting logic.
|
|
438
|
+
* Must accept parameters:
|
|
439
|
+
* - KEYS[1]: The key being rate limited
|
|
440
|
+
* - ARGV[1]: Points to consume (as string, use tonumber() to convert)
|
|
441
|
+
* - ARGV[2]: Duration in seconds (as string, use tonumber() to convert)
|
|
442
|
+
*
|
|
443
|
+
* Must return an array with exactly two elements:
|
|
444
|
+
* - [0]: Consumed points (number)
|
|
445
|
+
* - [1]: TTL in milliseconds (number)
|
|
446
|
+
*/
|
|
447
|
+
customFunction?: string;
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Custom name for the function library, defaults to 'ratelimiter'.
|
|
451
|
+
* The name is used to identify the library of the Lua function.
|
|
452
|
+
* A custom name should be used only if you want to use different
|
|
453
|
+
* libraries for different rate limiters.
|
|
454
|
+
* @default 'ratelimiter'
|
|
455
|
+
*/
|
|
456
|
+
customFunctionLibName?: string;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Rate limiter that uses Valkey Glide client for storage
|
|
461
|
+
*/
|
|
462
|
+
export class RateLimiterValkeyGlide extends RateLimiterStoreAbstract {
|
|
463
|
+
/**
|
|
464
|
+
* Creates a new instance of RateLimiterValkeyGlide
|
|
465
|
+
*
|
|
466
|
+
* @param opts Configuration options
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* ```typescript
|
|
470
|
+
* // Basic usage
|
|
471
|
+
* const rateLimiter = new RateLimiterValkeyGlide({
|
|
472
|
+
* storeClient: glideClient,
|
|
473
|
+
* points: 5,
|
|
474
|
+
* duration: 1
|
|
475
|
+
* });
|
|
476
|
+
*
|
|
477
|
+
* // With custom Lua function
|
|
478
|
+
* const customScript = `local key = KEYS[1]
|
|
479
|
+
* local pointsToConsume = tonumber(ARGV[1]) or 0
|
|
480
|
+
* local secDuration = tonumber(ARGV[2]) or 0
|
|
481
|
+
*
|
|
482
|
+
* -- Custom implementation
|
|
483
|
+
* -- ...
|
|
484
|
+
*
|
|
485
|
+
* -- Must return exactly two values: [consumed_points, ttl_in_ms]
|
|
486
|
+
* return {consumed, ttl}`;
|
|
487
|
+
*
|
|
488
|
+
* const rateLimiter = new RateLimiterValkeyGlide({
|
|
489
|
+
* storeClient: glideClient,
|
|
490
|
+
* points: 5,
|
|
491
|
+
* customFunction: customScript
|
|
492
|
+
* });
|
|
493
|
+
*
|
|
494
|
+
* // With insurance limiter
|
|
495
|
+
* const rateLimiter = new RateLimiterValkeyGlide({
|
|
496
|
+
* storeClient: primaryGlideClient,
|
|
497
|
+
* points: 5,
|
|
498
|
+
* duration: 2,
|
|
499
|
+
* insuranceLimiter: new RateLimiterMemory({
|
|
500
|
+
* points: 5,
|
|
501
|
+
* duration: 2
|
|
502
|
+
* })
|
|
503
|
+
* });
|
|
504
|
+
* ```
|
|
505
|
+
*/
|
|
506
|
+
constructor(opts: IRateLimiterValkeyGlideOptions);
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Close the rate limiter and release resources
|
|
510
|
+
* Note: The method won't close the Valkey client, as it may be shared with other instances.
|
|
511
|
+
*
|
|
512
|
+
* @returns Promise that resolves when the rate limiter is closed
|
|
513
|
+
*/
|
|
514
|
+
close(): Promise<void>;
|
|
515
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rate-limiter-flexible",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Node.js rate limiter by key and protection from DDoS and Brute-Force attacks in process Memory, Redis, MongoDb, Memcached, MySQL, PostgreSQL, Cluster or PM",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dc:up": "docker-compose -f docker-compose.yml up -d",
|
|
8
8
|
"dc:down": "docker-compose -f docker-compose.yml down",
|
|
9
|
+
"valkey-cluster:up": "docker-compose -f docker-compose.valkey-cluster.yml up -d",
|
|
10
|
+
"valkey-cluster:down": "docker-compose -f docker-compose.valkey-cluster.yml down -v",
|
|
11
|
+
"test:valkey-cluster": "VALKEY_CLUSTER_PORT=7001 mocha test/RateLimiterValkeyGlide.test.js -- -g 'RateLimiterValkeyGlide with cluster client'",
|
|
9
12
|
"prisma:postgres": "prisma generate --schema=./test/RateLimiterPrisma/Postgres/schema.prisma && prisma db push --schema=./test/RateLimiterPrisma/Postgres/schema.prisma",
|
|
10
13
|
"test": "npm run prisma:postgres && nyc --reporter=html --reporter=text mocha",
|
|
11
14
|
"debug-test": "mocha --inspect-brk lib/**/**.test.js",
|
|
@@ -33,7 +36,12 @@
|
|
|
33
36
|
"prisma",
|
|
34
37
|
"koa",
|
|
35
38
|
"express",
|
|
36
|
-
"hapi"
|
|
39
|
+
"hapi",
|
|
40
|
+
"valkey",
|
|
41
|
+
"valkey-glide",
|
|
42
|
+
"GLIDE",
|
|
43
|
+
"cluster",
|
|
44
|
+
"memcached"
|
|
37
45
|
],
|
|
38
46
|
"author": "animir <animirr@gmail.com>",
|
|
39
47
|
"license": "ISC",
|
|
@@ -56,6 +64,7 @@
|
|
|
56
64
|
"ioredis": "^5.3.2",
|
|
57
65
|
"iovalkey": "^0.3.1",
|
|
58
66
|
"istanbul": "^1.1.0-alpha.1",
|
|
67
|
+
"knex": "^3.1.0",
|
|
59
68
|
"memcached-mock": "^0.1.0",
|
|
60
69
|
"mocha": "^10.2.0",
|
|
61
70
|
"nyc": "^15.1.0",
|
|
@@ -63,7 +72,8 @@
|
|
|
63
72
|
"redis": "^4.6.8",
|
|
64
73
|
"redis-mock": "^0.48.0",
|
|
65
74
|
"sinon": "^17.0.1",
|
|
66
|
-
"sqlite3": "^5.1.7"
|
|
75
|
+
"sqlite3": "^5.1.7",
|
|
76
|
+
"@valkey/valkey-glide": "^1.3.1"
|
|
67
77
|
},
|
|
68
78
|
"browser": {
|
|
69
79
|
"cluster": false,
|
package/.editorconfig
DELETED