@depup/rate-limiter-flexible 9.1.1-depup.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/LICENSE.md +7 -0
- package/README.md +25 -0
- package/changes.json +5 -0
- package/index.js +55 -0
- package/lib/BurstyRateLimiter.js +78 -0
- package/lib/ExpressBruteFlexible.js +359 -0
- package/lib/RLWrapperBlackAndWhite.js +195 -0
- package/lib/RLWrapperTimeouts.js +82 -0
- package/lib/RateLimiterAbstract.js +125 -0
- package/lib/RateLimiterCluster.js +367 -0
- package/lib/RateLimiterDrizzle.js +174 -0
- package/lib/RateLimiterDrizzleNonAtomic.js +175 -0
- package/lib/RateLimiterDynamo.js +401 -0
- package/lib/RateLimiterEtcd.js +63 -0
- package/lib/RateLimiterEtcdNonAtomic.js +80 -0
- package/lib/RateLimiterInsuredAbstract.js +112 -0
- package/lib/RateLimiterMemcache.js +150 -0
- package/lib/RateLimiterMemory.js +106 -0
- package/lib/RateLimiterMongo.js +261 -0
- package/lib/RateLimiterMySQL.js +400 -0
- package/lib/RateLimiterPostgres.js +351 -0
- package/lib/RateLimiterPrisma.js +127 -0
- package/lib/RateLimiterQueue.js +131 -0
- package/lib/RateLimiterRedis.js +209 -0
- package/lib/RateLimiterRedisNonAtomic.js +195 -0
- package/lib/RateLimiterRes.js +64 -0
- package/lib/RateLimiterSQLite.js +338 -0
- package/lib/RateLimiterStoreAbstract.js +349 -0
- package/lib/RateLimiterUnion.js +51 -0
- package/lib/RateLimiterValkey.js +117 -0
- package/lib/RateLimiterValkeyGlide.js +273 -0
- package/lib/component/BlockedKeys/BlockedKeys.js +75 -0
- package/lib/component/BlockedKeys/index.js +3 -0
- package/lib/component/MemoryStorage/MemoryStorage.js +83 -0
- package/lib/component/MemoryStorage/Record.js +40 -0
- package/lib/component/MemoryStorage/index.js +3 -0
- package/lib/component/RateLimiterEtcdTransactionFailedError.js +10 -0
- package/lib/component/RateLimiterQueueError.js +13 -0
- package/lib/component/RateLimiterSetupError.js +10 -0
- package/lib/constants.js +21 -0
- package/package.json +100 -0
- package/types.d.ts +581 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const RateLimiterAbstract = require('./RateLimiterAbstract');
|
|
2
|
+
|
|
3
|
+
module.exports = class RateLimiterUnion {
|
|
4
|
+
constructor(...limiters) {
|
|
5
|
+
if (limiters.length < 1) {
|
|
6
|
+
throw new Error('RateLimiterUnion: at least one limiter have to be passed');
|
|
7
|
+
}
|
|
8
|
+
limiters.forEach((limiter) => {
|
|
9
|
+
if (!(limiter instanceof RateLimiterAbstract)) {
|
|
10
|
+
throw new Error('RateLimiterUnion: all limiters have to be instance of RateLimiterAbstract');
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
this._limiters = limiters;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
consume(key, points = 1) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const promises = [];
|
|
20
|
+
this._limiters.forEach((limiter) => {
|
|
21
|
+
promises.push(limiter.consume(key, points).catch(rej => ({ rejected: true, rej })));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
Promise.all(promises)
|
|
25
|
+
.then((res) => {
|
|
26
|
+
const resObj = {};
|
|
27
|
+
let rejected = false;
|
|
28
|
+
|
|
29
|
+
res.forEach((item) => {
|
|
30
|
+
if (item.rejected === true) {
|
|
31
|
+
rejected = true;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < res.length; i++) {
|
|
36
|
+
if (rejected && res[i].rejected === true) {
|
|
37
|
+
resObj[this._limiters[i].keyPrefix] = res[i].rej;
|
|
38
|
+
} else if (!rejected) {
|
|
39
|
+
resObj[this._limiters[i].keyPrefix] = res[i];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (rejected) {
|
|
44
|
+
reject(resObj);
|
|
45
|
+
} else {
|
|
46
|
+
resolve(resObj);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
|
|
2
|
+
const RateLimiterRes = require('./RateLimiterRes');
|
|
3
|
+
|
|
4
|
+
const incrTtlLuaScript = `
|
|
5
|
+
server.call('set', KEYS[1], 0, 'EX', ARGV[2], 'NX')
|
|
6
|
+
local consumed = server.call('incrby', KEYS[1], ARGV[1])
|
|
7
|
+
local ttl = server.call('pttl', KEYS[1])
|
|
8
|
+
return {consumed, ttl}
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
class RateLimiterValkey extends RateLimiterStoreAbstract {
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} opts
|
|
15
|
+
* Defaults {
|
|
16
|
+
* ... see other in RateLimiterStoreAbstract
|
|
17
|
+
*
|
|
18
|
+
* storeClient: ValkeyClient
|
|
19
|
+
* rejectIfValkeyNotReady: boolean = false - reject / invoke insuranceLimiter immediately when valkey connection is not "ready"
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
constructor(opts) {
|
|
23
|
+
super(opts);
|
|
24
|
+
this.client = opts.storeClient;
|
|
25
|
+
|
|
26
|
+
this._rejectIfValkeyNotReady = !!opts.rejectIfValkeyNotReady;
|
|
27
|
+
this._incrTtlLuaScript = opts.customIncrTtlLuaScript || incrTtlLuaScript;
|
|
28
|
+
|
|
29
|
+
this.client.defineCommand('rlflxIncr', {
|
|
30
|
+
numberOfKeys: 1,
|
|
31
|
+
lua: this._incrTtlLuaScript,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Prevent actual valkey call if valkey connection is not ready
|
|
37
|
+
* @return {boolean}
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
_isValkeyReady() {
|
|
41
|
+
if (!this._rejectIfValkeyNotReady) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return this.client.status === 'ready';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_getRateLimiterRes(rlKey, changedPoints, result) {
|
|
49
|
+
let consumed;
|
|
50
|
+
let resTtlMs;
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(result[0])) {
|
|
53
|
+
[[, consumed], [, resTtlMs]] = result;
|
|
54
|
+
} else {
|
|
55
|
+
[consumed, resTtlMs] = result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const res = new RateLimiterRes();
|
|
59
|
+
res.consumedPoints = +consumed;
|
|
60
|
+
res.isFirstInDuration = res.consumedPoints === changedPoints;
|
|
61
|
+
res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
|
|
62
|
+
res.msBeforeNext = resTtlMs;
|
|
63
|
+
|
|
64
|
+
return res;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_upsert(rlKey, points, msDuration, forceExpire = false) {
|
|
68
|
+
if (!this._isValkeyReady()) {
|
|
69
|
+
throw new Error('Valkey connection is not ready');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const secDuration = Math.floor(msDuration / 1000);
|
|
73
|
+
|
|
74
|
+
if (forceExpire) {
|
|
75
|
+
const multi = this.client.multi();
|
|
76
|
+
|
|
77
|
+
if (secDuration > 0) {
|
|
78
|
+
multi.set(rlKey, points, 'EX', secDuration);
|
|
79
|
+
} else {
|
|
80
|
+
multi.set(rlKey, points);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return multi.pttl(rlKey).exec();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (secDuration > 0) {
|
|
87
|
+
return this.client.rlflxIncr([rlKey, String(points), String(secDuration), String(this.points), String(this.duration)]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return this.client.multi().incrby(rlKey, points).pttl(rlKey).exec();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_get(rlKey) {
|
|
94
|
+
if (!this._isValkeyReady()) {
|
|
95
|
+
throw new Error('Valkey connection is not ready');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return this.client
|
|
99
|
+
.multi()
|
|
100
|
+
.get(rlKey)
|
|
101
|
+
.pttl(rlKey)
|
|
102
|
+
.exec()
|
|
103
|
+
.then((result) => {
|
|
104
|
+
const [[, points]] = result;
|
|
105
|
+
if (points === null) return null;
|
|
106
|
+
return result;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_delete(rlKey) {
|
|
111
|
+
return this.client
|
|
112
|
+
.del(rlKey)
|
|
113
|
+
.then(result => result > 0);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = RateLimiterValkey;
|
|
@@ -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;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module.exports = class BlockedKeys {
|
|
2
|
+
constructor() {
|
|
3
|
+
this._keys = {}; // {'key': 1526279430331}
|
|
4
|
+
this._addedKeysAmount = 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
collectExpired() {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
|
|
10
|
+
Object.keys(this._keys).forEach((key) => {
|
|
11
|
+
if (this._keys[key] <= now) {
|
|
12
|
+
delete this._keys[key];
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
this._addedKeysAmount = Object.keys(this._keys).length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Add new blocked key
|
|
21
|
+
*
|
|
22
|
+
* @param key String
|
|
23
|
+
* @param sec Number
|
|
24
|
+
*/
|
|
25
|
+
add(key, sec) {
|
|
26
|
+
this.addMs(key, sec * 1000);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Add new blocked key for ms
|
|
31
|
+
*
|
|
32
|
+
* @param key String
|
|
33
|
+
* @param ms Number
|
|
34
|
+
*/
|
|
35
|
+
addMs(key, ms) {
|
|
36
|
+
this._keys[key] = Date.now() + ms;
|
|
37
|
+
this._addedKeysAmount++;
|
|
38
|
+
if (this._addedKeysAmount > 999) {
|
|
39
|
+
this.collectExpired();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 0 means not blocked
|
|
45
|
+
*
|
|
46
|
+
* @param key
|
|
47
|
+
* @returns {number}
|
|
48
|
+
*/
|
|
49
|
+
msBeforeExpire(key) {
|
|
50
|
+
const expire = this._keys[key];
|
|
51
|
+
|
|
52
|
+
if (expire && expire >= Date.now()) {
|
|
53
|
+
this.collectExpired();
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
return expire >= now ? expire - now : 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* If key is not given, delete all data in memory
|
|
63
|
+
*
|
|
64
|
+
* @param {string|undefined} key
|
|
65
|
+
*/
|
|
66
|
+
delete(key) {
|
|
67
|
+
if (key) {
|
|
68
|
+
delete this._keys[key];
|
|
69
|
+
} else {
|
|
70
|
+
Object.keys(this._keys).forEach((key) => {
|
|
71
|
+
delete this._keys[key];
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const Record = require('./Record');
|
|
2
|
+
const RateLimiterRes = require('../../RateLimiterRes');
|
|
3
|
+
|
|
4
|
+
module.exports = class MemoryStorage {
|
|
5
|
+
constructor() {
|
|
6
|
+
/**
|
|
7
|
+
* @type {Object.<string, Record>}
|
|
8
|
+
* @private
|
|
9
|
+
*/
|
|
10
|
+
this._storage = {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
incrby(key, value, durationSec) {
|
|
14
|
+
if (this._storage[key]) {
|
|
15
|
+
const msBeforeExpires = this._storage[key].expiresAt
|
|
16
|
+
? this._storage[key].expiresAt.getTime() - new Date().getTime()
|
|
17
|
+
: -1;
|
|
18
|
+
if (!this._storage[key].expiresAt || msBeforeExpires > 0) {
|
|
19
|
+
// Change value
|
|
20
|
+
this._storage[key].value = this._storage[key].value + value;
|
|
21
|
+
|
|
22
|
+
return new RateLimiterRes(0, msBeforeExpires, this._storage[key].value, false);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return this.set(key, value, durationSec);
|
|
26
|
+
}
|
|
27
|
+
return this.set(key, value, durationSec);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
set(key, value, durationSec) {
|
|
31
|
+
const durationMs = durationSec * 1000;
|
|
32
|
+
|
|
33
|
+
if (this._storage[key] && this._storage[key].timeoutId) {
|
|
34
|
+
clearTimeout(this._storage[key].timeoutId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this._storage[key] = new Record(
|
|
38
|
+
value,
|
|
39
|
+
durationMs > 0 ? new Date(Date.now() + durationMs) : null
|
|
40
|
+
);
|
|
41
|
+
if (durationMs > 0) {
|
|
42
|
+
this._storage[key].timeoutId = setTimeout(() => {
|
|
43
|
+
delete this._storage[key];
|
|
44
|
+
}, durationMs);
|
|
45
|
+
if (this._storage[key].timeoutId.unref) {
|
|
46
|
+
this._storage[key].timeoutId.unref();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return new RateLimiterRes(0, durationMs === 0 ? -1 : durationMs, this._storage[key].value, true);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
*
|
|
55
|
+
* @param key
|
|
56
|
+
* @returns {*}
|
|
57
|
+
*/
|
|
58
|
+
get(key) {
|
|
59
|
+
if (this._storage[key]) {
|
|
60
|
+
const msBeforeExpires = this._storage[key].expiresAt
|
|
61
|
+
? this._storage[key].expiresAt.getTime() - new Date().getTime()
|
|
62
|
+
: -1;
|
|
63
|
+
return new RateLimiterRes(0, msBeforeExpires, this._storage[key].value, false);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
*
|
|
70
|
+
* @param key
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
delete(key) {
|
|
74
|
+
if (this._storage[key]) {
|
|
75
|
+
if (this._storage[key].timeoutId) {
|
|
76
|
+
clearTimeout(this._storage[key].timeoutId);
|
|
77
|
+
}
|
|
78
|
+
delete this._storage[key];
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module.exports = class Record {
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* @param value int
|
|
5
|
+
* @param expiresAt Date|int
|
|
6
|
+
* @param timeoutId
|
|
7
|
+
*/
|
|
8
|
+
constructor(value, expiresAt, timeoutId = null) {
|
|
9
|
+
this.value = value;
|
|
10
|
+
this.expiresAt = expiresAt;
|
|
11
|
+
this.timeoutId = timeoutId;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get value() {
|
|
15
|
+
return this._value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
set value(value) {
|
|
19
|
+
this._value = parseInt(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get expiresAt() {
|
|
23
|
+
return this._expiresAt;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
set expiresAt(value) {
|
|
27
|
+
if (!(value instanceof Date) && Number.isInteger(value)) {
|
|
28
|
+
value = new Date(value);
|
|
29
|
+
}
|
|
30
|
+
this._expiresAt = value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get timeoutId() {
|
|
34
|
+
return this._timeoutId;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
set timeoutId(value) {
|
|
38
|
+
this._timeoutId = value;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module.exports = class RateLimiterEtcdTransactionFailedError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super();
|
|
4
|
+
if (Error.captureStackTrace) {
|
|
5
|
+
Error.captureStackTrace(this, this.constructor);
|
|
6
|
+
}
|
|
7
|
+
this.name = 'RateLimiterEtcdTransactionFailedError';
|
|
8
|
+
this.message = message;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module.exports = class RateLimiterQueueError extends Error {
|
|
2
|
+
constructor(message, extra) {
|
|
3
|
+
super();
|
|
4
|
+
if (Error.captureStackTrace) {
|
|
5
|
+
Error.captureStackTrace(this, this.constructor);
|
|
6
|
+
}
|
|
7
|
+
this.name = 'CustomError';
|
|
8
|
+
this.message = message;
|
|
9
|
+
if (extra) {
|
|
10
|
+
this.extra = extra;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
};
|