@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,338 @@
|
|
|
1
|
+
const RateLimiterStoreAbstract = require("./RateLimiterStoreAbstract");
|
|
2
|
+
const RateLimiterRes = require("./RateLimiterRes");
|
|
3
|
+
|
|
4
|
+
class RateLimiterSQLite extends RateLimiterStoreAbstract {
|
|
5
|
+
/**
|
|
6
|
+
* Internal store type used to determine the SQLite client in use.
|
|
7
|
+
* It can be one of the following:
|
|
8
|
+
* - `"sqlite3".
|
|
9
|
+
* - `"better-sqlite3".
|
|
10
|
+
*
|
|
11
|
+
* @type {("sqlite3" | "better-sqlite3" | null)}
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
_internalStoreType = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @callback callback
|
|
18
|
+
* @param {Object} err
|
|
19
|
+
*
|
|
20
|
+
* @param {Object} opts
|
|
21
|
+
* @param {callback} cb
|
|
22
|
+
* Defaults {
|
|
23
|
+
* ... see other in RateLimiterStoreAbstract
|
|
24
|
+
* storeClient: sqliteClient, // SQLite database instance (sqlite3, better-sqlite3, or knex instance)
|
|
25
|
+
* storeType: 'sqlite3' | 'better-sqlite3' | 'knex', // Optional, defaults to 'sqlite3'
|
|
26
|
+
* tableName: 'string',
|
|
27
|
+
* tableCreated: boolean,
|
|
28
|
+
* clearExpiredByTimeout: boolean,
|
|
29
|
+
* }
|
|
30
|
+
*/
|
|
31
|
+
constructor(opts, cb = null) {
|
|
32
|
+
super(opts);
|
|
33
|
+
|
|
34
|
+
this.client = opts.storeClient;
|
|
35
|
+
this.storeType = opts.storeType || "sqlite3";
|
|
36
|
+
this.tableName = opts.tableName;
|
|
37
|
+
this.tableCreated = opts.tableCreated || false;
|
|
38
|
+
this.clearExpiredByTimeout = opts.clearExpiredByTimeout;
|
|
39
|
+
|
|
40
|
+
this._validateStoreTypes(cb);
|
|
41
|
+
this._validateStoreClient(cb);
|
|
42
|
+
this._setInternalStoreType(cb);
|
|
43
|
+
this._validateTableName(cb);
|
|
44
|
+
|
|
45
|
+
if (!this.tableCreated) {
|
|
46
|
+
this._createDbAndTable()
|
|
47
|
+
.then(() => {
|
|
48
|
+
this.tableCreated = true;
|
|
49
|
+
if (this.clearExpiredByTimeout) this._clearExpiredHourAgo();
|
|
50
|
+
if (typeof cb === "function") cb();
|
|
51
|
+
})
|
|
52
|
+
.catch((err) => {
|
|
53
|
+
if (typeof cb === "function") cb(err);
|
|
54
|
+
else throw err;
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
if (this.clearExpiredByTimeout) this._clearExpiredHourAgo();
|
|
58
|
+
if (typeof cb === "function") cb();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
_validateStoreTypes(cb) {
|
|
62
|
+
const validStoreTypes = ["sqlite3", "better-sqlite3", "knex"];
|
|
63
|
+
if (!validStoreTypes.includes(this.storeType)) {
|
|
64
|
+
const err = new Error(
|
|
65
|
+
`storeType must be one of: ${validStoreTypes.join(", ")}`
|
|
66
|
+
);
|
|
67
|
+
if (typeof cb === "function") return cb(err);
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
_validateStoreClient(cb) {
|
|
72
|
+
if (this.storeType === "sqlite3") {
|
|
73
|
+
if (typeof this.client.run !== "function") {
|
|
74
|
+
const err = new Error(
|
|
75
|
+
"storeClient must be an instance of sqlite3.Database when storeType is 'sqlite3' or no storeType was provided"
|
|
76
|
+
);
|
|
77
|
+
if (typeof cb === "function") return cb(err);
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
} else if (this.storeType === "better-sqlite3") {
|
|
81
|
+
if (
|
|
82
|
+
typeof this.client.prepare !== "function" ||
|
|
83
|
+
typeof this.client.run !== "undefined"
|
|
84
|
+
) {
|
|
85
|
+
const err = new Error(
|
|
86
|
+
"storeClient must be an instance of better-sqlite3.Database when storeType is 'better-sqlite3'"
|
|
87
|
+
);
|
|
88
|
+
if (typeof cb === "function") return cb(err);
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
} else if (this.storeType === "knex") {
|
|
92
|
+
if (typeof this.client.raw !== "function") {
|
|
93
|
+
const err = new Error(
|
|
94
|
+
"storeClient must be an instance of Knex when storeType is 'knex'"
|
|
95
|
+
);
|
|
96
|
+
if (typeof cb === "function") return cb(err);
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
_setInternalStoreType(cb) {
|
|
102
|
+
if (this.storeType === "knex") {
|
|
103
|
+
const knexClientType = this.client.client.config.client;
|
|
104
|
+
if (knexClientType === "sqlite3") {
|
|
105
|
+
this._internalStoreType = "sqlite3";
|
|
106
|
+
} else if (knexClientType === "better-sqlite3") {
|
|
107
|
+
this._internalStoreType = "better-sqlite3";
|
|
108
|
+
} else {
|
|
109
|
+
const err = new Error(
|
|
110
|
+
"Knex must be configured with 'sqlite3' or 'better-sqlite3' for RateLimiterSQLite"
|
|
111
|
+
);
|
|
112
|
+
if (typeof cb === "function") return cb(err);
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
this._internalStoreType = this.storeType;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
_validateTableName(cb) {
|
|
120
|
+
if (!/^[A-Za-z0-9_]*$/.test(this.tableName)) {
|
|
121
|
+
const err = new Error("Table name must contain only letters and numbers");
|
|
122
|
+
if (typeof cb === "function") return cb(err);
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Acquires the database connection based on the storeType.
|
|
129
|
+
* @returns {Promise<Object>} The database client or connection
|
|
130
|
+
*/
|
|
131
|
+
async _getConnection() {
|
|
132
|
+
if (this.storeType === "knex") {
|
|
133
|
+
return this.client.client.acquireConnection(); // Acquire raw connection from knex pool
|
|
134
|
+
}
|
|
135
|
+
return this.client; // For sqlite3 and better-sqlite3, return the client directly
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Releases the database connection if necessary.
|
|
140
|
+
* @param {Object} conn The database client or connection
|
|
141
|
+
*/
|
|
142
|
+
_releaseConnection(conn) {
|
|
143
|
+
if (this.storeType === "knex") {
|
|
144
|
+
this.client.client.releaseConnection(conn);
|
|
145
|
+
}
|
|
146
|
+
// No release needed for direct sqlite3 or better-sqlite3 clients
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async _createDbAndTable() {
|
|
150
|
+
const conn = await this._getConnection();
|
|
151
|
+
try {
|
|
152
|
+
switch (this._internalStoreType) {
|
|
153
|
+
case "sqlite3":
|
|
154
|
+
await new Promise((resolve, reject) => {
|
|
155
|
+
conn.run(this._getCreateTableSQL(), (err) =>
|
|
156
|
+
err ? reject(err) : resolve()
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
break;
|
|
160
|
+
case "better-sqlite3":
|
|
161
|
+
conn.prepare(this._getCreateTableSQL()).run();
|
|
162
|
+
break;
|
|
163
|
+
default:
|
|
164
|
+
throw new Error("Unsupported internalStoreType");
|
|
165
|
+
}
|
|
166
|
+
} finally {
|
|
167
|
+
this._releaseConnection(conn);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
_getCreateTableSQL() {
|
|
172
|
+
return `CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
173
|
+
key TEXT PRIMARY KEY,
|
|
174
|
+
points INTEGER NOT NULL DEFAULT 0,
|
|
175
|
+
expire INTEGER
|
|
176
|
+
)`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
_clearExpiredHourAgo() {
|
|
180
|
+
if (this._clearExpiredTimeoutId) clearTimeout(this._clearExpiredTimeoutId);
|
|
181
|
+
this._clearExpiredTimeoutId = setTimeout(() => {
|
|
182
|
+
this.clearExpired(Date.now() - 3600000) // 1 hour ago
|
|
183
|
+
.then(() => this._clearExpiredHourAgo());
|
|
184
|
+
}, 300000); // Every 5 minutes
|
|
185
|
+
this._clearExpiredTimeoutId.unref();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async clearExpired(nowMs) {
|
|
189
|
+
const sql = `DELETE FROM ${this.tableName} WHERE expire < ?`;
|
|
190
|
+
const conn = await this._getConnection();
|
|
191
|
+
try {
|
|
192
|
+
switch (this._internalStoreType) {
|
|
193
|
+
case "sqlite3":
|
|
194
|
+
await new Promise((resolve, reject) => {
|
|
195
|
+
conn.run(sql, [nowMs], (err) => (err ? reject(err) : resolve()));
|
|
196
|
+
});
|
|
197
|
+
break;
|
|
198
|
+
case "better-sqlite3":
|
|
199
|
+
conn.prepare(sql).run(nowMs);
|
|
200
|
+
break;
|
|
201
|
+
default:
|
|
202
|
+
throw new Error("Unsupported internalStoreType");
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
this._releaseConnection(conn);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_getRateLimiterRes(rlKey, changedPoints, result) {
|
|
210
|
+
const res = new RateLimiterRes();
|
|
211
|
+
res.isFirstInDuration = changedPoints === result.points;
|
|
212
|
+
res.consumedPoints = res.isFirstInDuration ? changedPoints : result.points;
|
|
213
|
+
res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
|
|
214
|
+
res.msBeforeNext = result.expire
|
|
215
|
+
? Math.max(result.expire - Date.now(), 0)
|
|
216
|
+
: -1;
|
|
217
|
+
return res;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async _upsertTransactionSQLite3(conn, upsertQuery, upsertParams) {
|
|
221
|
+
return await new Promise((resolve, reject) => {
|
|
222
|
+
conn.serialize(() => {
|
|
223
|
+
conn.run("SAVEPOINT rate_limiter_trx;", (err) => {
|
|
224
|
+
if (err) return reject(err);
|
|
225
|
+
conn.get(upsertQuery, upsertParams, (err, row) => {
|
|
226
|
+
if (err) {
|
|
227
|
+
conn.run("ROLLBACK TO SAVEPOINT rate_limiter_trx;", () =>
|
|
228
|
+
reject(err)
|
|
229
|
+
);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
conn.run("RELEASE SAVEPOINT rate_limiter_trx;", () => resolve(row));
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async _upsertTransactionBetterSQLite3(conn, upsertQuery, upsertParams) {
|
|
240
|
+
return conn.transaction(() =>
|
|
241
|
+
conn.prepare(upsertQuery).get(...upsertParams)
|
|
242
|
+
)();
|
|
243
|
+
}
|
|
244
|
+
async _upsertTransaction(rlKey, points, msDuration, forceExpire) {
|
|
245
|
+
const dateNow = Date.now();
|
|
246
|
+
const newExpire = msDuration > 0 ? dateNow + msDuration : null;
|
|
247
|
+
const upsertQuery = forceExpire
|
|
248
|
+
? `INSERT OR REPLACE INTO ${this.tableName} (key, points, expire) VALUES (?, ?, ?) RETURNING points, expire`
|
|
249
|
+
: `INSERT INTO ${this.tableName} (key, points, expire)
|
|
250
|
+
VALUES (?, ?, ?)
|
|
251
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
252
|
+
points = CASE WHEN expire IS NULL OR expire > ? THEN points + excluded.points ELSE excluded.points END,
|
|
253
|
+
expire = CASE WHEN expire IS NULL OR expire > ? THEN expire ELSE excluded.expire END
|
|
254
|
+
RETURNING points, expire`;
|
|
255
|
+
const upsertParams = forceExpire
|
|
256
|
+
? [rlKey, points, newExpire]
|
|
257
|
+
: [rlKey, points, newExpire, dateNow, dateNow];
|
|
258
|
+
|
|
259
|
+
const conn = await this._getConnection();
|
|
260
|
+
try {
|
|
261
|
+
switch (this._internalStoreType) {
|
|
262
|
+
case "sqlite3":
|
|
263
|
+
return this._upsertTransactionSQLite3(
|
|
264
|
+
conn,
|
|
265
|
+
upsertQuery,
|
|
266
|
+
upsertParams
|
|
267
|
+
);
|
|
268
|
+
case "better-sqlite3":
|
|
269
|
+
return this._upsertTransactionBetterSQLite3(
|
|
270
|
+
conn,
|
|
271
|
+
upsertQuery,
|
|
272
|
+
upsertParams
|
|
273
|
+
);
|
|
274
|
+
default:
|
|
275
|
+
throw new Error("Unsupported internalStoreType");
|
|
276
|
+
}
|
|
277
|
+
} finally {
|
|
278
|
+
this._releaseConnection(conn);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_upsert(rlKey, points, msDuration, forceExpire = false) {
|
|
283
|
+
if (!this.tableCreated) {
|
|
284
|
+
return Promise.reject(new Error("Table is not created yet"));
|
|
285
|
+
}
|
|
286
|
+
return this._upsertTransaction(rlKey, points, msDuration, forceExpire);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async _get(rlKey) {
|
|
290
|
+
const sql = `SELECT points, expire FROM ${this.tableName} WHERE key = ? AND (expire > ? OR expire IS NULL)`;
|
|
291
|
+
const now = Date.now();
|
|
292
|
+
const conn = await this._getConnection();
|
|
293
|
+
try {
|
|
294
|
+
switch (this._internalStoreType) {
|
|
295
|
+
case "sqlite3":
|
|
296
|
+
return await new Promise((resolve, reject) => {
|
|
297
|
+
conn.get(sql, [rlKey, now], (err, row) =>
|
|
298
|
+
err ? reject(err) : resolve(row || null)
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
case "better-sqlite3":
|
|
302
|
+
return conn.prepare(sql).get(rlKey, now) || null;
|
|
303
|
+
default:
|
|
304
|
+
throw new Error("Unsupported internalStoreType");
|
|
305
|
+
}
|
|
306
|
+
} finally {
|
|
307
|
+
this._releaseConnection(conn);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async _delete(rlKey) {
|
|
312
|
+
if (!this.tableCreated) {
|
|
313
|
+
return Promise.reject(new Error("Table is not created yet"));
|
|
314
|
+
}
|
|
315
|
+
const sql = `DELETE FROM ${this.tableName} WHERE key = ?`;
|
|
316
|
+
const conn = await this._getConnection();
|
|
317
|
+
try {
|
|
318
|
+
switch (this._internalStoreType) {
|
|
319
|
+
case "sqlite3":
|
|
320
|
+
return await new Promise((resolve, reject) => {
|
|
321
|
+
conn.run(sql, [rlKey], function (err) {
|
|
322
|
+
if (err) reject(err);
|
|
323
|
+
else resolve(this.changes > 0);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
case "better-sqlite3":
|
|
327
|
+
const result = conn.prepare(sql).run(rlKey);
|
|
328
|
+
return result.changes > 0;
|
|
329
|
+
default:
|
|
330
|
+
throw new Error("Unsupported internalStoreType");
|
|
331
|
+
}
|
|
332
|
+
} finally {
|
|
333
|
+
this._releaseConnection(conn);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = RateLimiterSQLite;
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
const RateLimiterAbstract = require('./RateLimiterAbstract');
|
|
2
|
+
const BlockedKeys = require('./component/BlockedKeys');
|
|
3
|
+
const RateLimiterRes = require('./RateLimiterRes');
|
|
4
|
+
const RateLimiterInsuredAbstract = require('./RateLimiterInsuredAbstract');
|
|
5
|
+
|
|
6
|
+
module.exports = class RateLimiterStoreAbstract extends RateLimiterInsuredAbstract {
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* @param opts Object Defaults {
|
|
10
|
+
* ... see other in RateLimiterAbstract
|
|
11
|
+
*
|
|
12
|
+
* inMemoryBlockOnConsumed: 40, // Number of points when key is blocked
|
|
13
|
+
* inMemoryBlockDuration: 10, // Block duration in seconds
|
|
14
|
+
* insuranceLimiter: RateLimiterAbstract
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
constructor(opts = {}) {
|
|
18
|
+
super(opts);
|
|
19
|
+
|
|
20
|
+
this.inMemoryBlockOnConsumed = opts.inMemoryBlockOnConsumed;
|
|
21
|
+
this.inMemoryBlockDuration = opts.inMemoryBlockDuration;
|
|
22
|
+
this._inMemoryBlockedKeys = new BlockedKeys();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get client() {
|
|
26
|
+
return this._client;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set client(value) {
|
|
30
|
+
if (typeof value === 'undefined') {
|
|
31
|
+
throw new Error('storeClient is not set');
|
|
32
|
+
}
|
|
33
|
+
this._client = value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Have to be launched after consume
|
|
38
|
+
* It blocks key and execute evenly depending on result from store
|
|
39
|
+
*
|
|
40
|
+
* It uses _getRateLimiterRes function to prepare RateLimiterRes from store result
|
|
41
|
+
*
|
|
42
|
+
* @param resolve
|
|
43
|
+
* @param reject
|
|
44
|
+
* @param rlKey
|
|
45
|
+
* @param changedPoints
|
|
46
|
+
* @param storeResult
|
|
47
|
+
* @param {Object} options
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
_afterConsume(resolve, reject, rlKey, changedPoints, storeResult, options = {}) {
|
|
51
|
+
const res = this._getRateLimiterRes(rlKey, changedPoints, storeResult);
|
|
52
|
+
|
|
53
|
+
if (this.inMemoryBlockOnConsumed > 0 && !(this.inMemoryBlockDuration > 0)
|
|
54
|
+
&& res.consumedPoints >= this.inMemoryBlockOnConsumed
|
|
55
|
+
) {
|
|
56
|
+
this._inMemoryBlockedKeys.addMs(rlKey, res.msBeforeNext);
|
|
57
|
+
if (res.consumedPoints > this.points) {
|
|
58
|
+
return reject(res);
|
|
59
|
+
} else {
|
|
60
|
+
return resolve(res)
|
|
61
|
+
}
|
|
62
|
+
} else if (res.consumedPoints > this.points) {
|
|
63
|
+
let blockPromise = Promise.resolve();
|
|
64
|
+
// Block only first time when consumed more than points
|
|
65
|
+
if (this.blockDuration > 0 && res.consumedPoints <= (this.points + changedPoints)) {
|
|
66
|
+
res.msBeforeNext = this.msBlockDuration;
|
|
67
|
+
blockPromise = this._block(rlKey, res.consumedPoints, this.msBlockDuration, options);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (this.inMemoryBlockOnConsumed > 0 && res.consumedPoints >= this.inMemoryBlockOnConsumed) {
|
|
71
|
+
// Block key for this.inMemoryBlockDuration seconds
|
|
72
|
+
this._inMemoryBlockedKeys.add(rlKey, this.inMemoryBlockDuration);
|
|
73
|
+
res.msBeforeNext = this.msInMemoryBlockDuration;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
blockPromise
|
|
77
|
+
.then(() => {
|
|
78
|
+
reject(res);
|
|
79
|
+
})
|
|
80
|
+
.catch((err) => {
|
|
81
|
+
reject(err);
|
|
82
|
+
});
|
|
83
|
+
} else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) {
|
|
84
|
+
let delay = Math.ceil(res.msBeforeNext / (res.remainingPoints + 2));
|
|
85
|
+
if (delay < this.execEvenlyMinDelayMs) {
|
|
86
|
+
delay = res.consumedPoints * this.execEvenlyMinDelayMs;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setTimeout(resolve, delay, res);
|
|
90
|
+
} else {
|
|
91
|
+
resolve(res);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getInMemoryBlockMsBeforeExpire(rlKey) {
|
|
96
|
+
if (this.inMemoryBlockOnConsumed > 0) {
|
|
97
|
+
return this._inMemoryBlockedKeys.msBeforeExpire(rlKey);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get inMemoryBlockOnConsumed() {
|
|
104
|
+
return this._inMemoryBlockOnConsumed;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
set inMemoryBlockOnConsumed(value) {
|
|
108
|
+
this._inMemoryBlockOnConsumed = value ? parseInt(value) : 0;
|
|
109
|
+
if (this.inMemoryBlockOnConsumed > 0 && this.points > this.inMemoryBlockOnConsumed) {
|
|
110
|
+
throw new Error('inMemoryBlockOnConsumed option must be greater or equal "points" option');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get inMemoryBlockDuration() {
|
|
115
|
+
return this._inMemoryBlockDuration;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
set inMemoryBlockDuration(value) {
|
|
119
|
+
this._inMemoryBlockDuration = value ? parseInt(value) : 0;
|
|
120
|
+
if (this.inMemoryBlockDuration > 0 && this.inMemoryBlockOnConsumed === 0) {
|
|
121
|
+
throw new Error('inMemoryBlockOnConsumed option must be set up');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
get msInMemoryBlockDuration() {
|
|
126
|
+
return this._inMemoryBlockDuration * 1000;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Block any key for secDuration seconds
|
|
131
|
+
*
|
|
132
|
+
* @param key
|
|
133
|
+
* @param secDuration
|
|
134
|
+
* @param {Object} options
|
|
135
|
+
*
|
|
136
|
+
* @return Promise<RateLimiterRes>
|
|
137
|
+
*/
|
|
138
|
+
block(key, secDuration, options = {}) {
|
|
139
|
+
const msDuration = secDuration * 1000;
|
|
140
|
+
return this._block(this.getKey(key), this.points + 1, msDuration, options);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Set points by key for any duration
|
|
145
|
+
*
|
|
146
|
+
* @param key
|
|
147
|
+
* @param points
|
|
148
|
+
* @param secDuration
|
|
149
|
+
* @param {Object} options
|
|
150
|
+
*
|
|
151
|
+
* @return Promise<RateLimiterRes>
|
|
152
|
+
*/
|
|
153
|
+
set(key, points, secDuration, options = {}) {
|
|
154
|
+
const msDuration = (secDuration >= 0 ? secDuration : this.duration) * 1000;
|
|
155
|
+
return this._block(this.getKey(key), points, msDuration, options);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
*
|
|
160
|
+
* @param key
|
|
161
|
+
* @param pointsToConsume
|
|
162
|
+
* @param {Object} options
|
|
163
|
+
* @returns Promise<RateLimiterRes>
|
|
164
|
+
*/
|
|
165
|
+
_consume(key, pointsToConsume = 1, options = {}) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const rlKey = this.getKey(key);
|
|
168
|
+
|
|
169
|
+
const inMemoryBlockMsBeforeExpire = this.getInMemoryBlockMsBeforeExpire(rlKey);
|
|
170
|
+
if (inMemoryBlockMsBeforeExpire > 0) {
|
|
171
|
+
return reject(new RateLimiterRes(0, inMemoryBlockMsBeforeExpire));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this._upsert(rlKey, pointsToConsume, this._getKeySecDuration(options) * 1000, false, options)
|
|
175
|
+
.then((res) => {
|
|
176
|
+
this._afterConsume(resolve, reject, rlKey, pointsToConsume, res);
|
|
177
|
+
})
|
|
178
|
+
.catch((err) => reject(err));
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
*
|
|
184
|
+
* @param key
|
|
185
|
+
* @param points
|
|
186
|
+
* @param {Object} options
|
|
187
|
+
* @returns Promise<RateLimiterRes>
|
|
188
|
+
*/
|
|
189
|
+
_penalty(key, points = 1, options = {}) {
|
|
190
|
+
const rlKey = this.getKey(key);
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
this._upsert(rlKey, points, this._getKeySecDuration(options) * 1000, false, options)
|
|
193
|
+
.then((res) => {
|
|
194
|
+
resolve(this._getRateLimiterRes(rlKey, points, res));
|
|
195
|
+
})
|
|
196
|
+
.catch((res) => reject(res));
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
*
|
|
202
|
+
* @param key
|
|
203
|
+
* @param points
|
|
204
|
+
* @param {Object} options
|
|
205
|
+
* @returns Promise<RateLimiterRes>
|
|
206
|
+
*/
|
|
207
|
+
_reward(key, points = 1, options = {}) {
|
|
208
|
+
const rlKey = this.getKey(key);
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
this._upsert(rlKey, -points, this._getKeySecDuration(options) * 1000, false, options)
|
|
211
|
+
.then((res) => {
|
|
212
|
+
resolve(this._getRateLimiterRes(rlKey, -points, res));
|
|
213
|
+
})
|
|
214
|
+
.catch((res) => reject(res));
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
*
|
|
220
|
+
* @param key
|
|
221
|
+
* @param {Object} options
|
|
222
|
+
* @returns Promise<RateLimiterRes>|null
|
|
223
|
+
*/
|
|
224
|
+
get(key, options = {}) {
|
|
225
|
+
const rlKey = this.getKey(key);
|
|
226
|
+
return new Promise((resolve, reject) => {
|
|
227
|
+
this._get(rlKey, options)
|
|
228
|
+
.then((res) => {
|
|
229
|
+
if (res === null || typeof res === 'undefined') {
|
|
230
|
+
resolve(null);
|
|
231
|
+
} else {
|
|
232
|
+
resolve(this._getRateLimiterRes(rlKey, 0, res));
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
.catch((err) => {
|
|
236
|
+
this._handleError(err, 'get', resolve, reject, [key, options]);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
*
|
|
243
|
+
* @param key
|
|
244
|
+
* @param {Object} options
|
|
245
|
+
* @returns Promise<boolean>
|
|
246
|
+
*/
|
|
247
|
+
delete(key, options = {}) {
|
|
248
|
+
const rlKey = this.getKey(key);
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
this._delete(rlKey, options)
|
|
251
|
+
.then((res) => {
|
|
252
|
+
this._inMemoryBlockedKeys.delete(rlKey);
|
|
253
|
+
resolve(res);
|
|
254
|
+
})
|
|
255
|
+
.catch((err) => {
|
|
256
|
+
this._handleError(err, 'delete', resolve, reject, [key, options]);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Cleanup keys no-matter expired or not.
|
|
263
|
+
*/
|
|
264
|
+
deleteInMemoryBlockedAll() {
|
|
265
|
+
this._inMemoryBlockedKeys.delete();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get RateLimiterRes object filled depending on storeResult, which specific for exact store
|
|
270
|
+
*
|
|
271
|
+
* @param rlKey
|
|
272
|
+
* @param changedPoints
|
|
273
|
+
* @param storeResult
|
|
274
|
+
* @private
|
|
275
|
+
*/
|
|
276
|
+
_getRateLimiterRes(rlKey, changedPoints, storeResult) { // eslint-disable-line no-unused-vars
|
|
277
|
+
throw new Error("You have to implement the method '_getRateLimiterRes'!");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Block key for this.msBlockDuration milliseconds
|
|
282
|
+
* Usually, it just prolongs lifetime of key
|
|
283
|
+
*
|
|
284
|
+
* @param rlKey
|
|
285
|
+
* @param initPoints
|
|
286
|
+
* @param msDuration
|
|
287
|
+
* @param {Object} options
|
|
288
|
+
*
|
|
289
|
+
* @return Promise<any>
|
|
290
|
+
*/
|
|
291
|
+
_block(rlKey, initPoints, msDuration, options = {}) {
|
|
292
|
+
return new Promise((resolve, reject) => {
|
|
293
|
+
this._upsert(rlKey, initPoints, msDuration, true, options)
|
|
294
|
+
.then(() => {
|
|
295
|
+
resolve(new RateLimiterRes(0, msDuration > 0 ? msDuration : -1, initPoints));
|
|
296
|
+
})
|
|
297
|
+
.catch((err) => {
|
|
298
|
+
this._handleError(err, 'block', resolve, reject, [this.parseKey(rlKey), msDuration / 1000, options]);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Have to be implemented in every limiter
|
|
305
|
+
* Resolve with raw result from Store OR null if rlKey is not set
|
|
306
|
+
* or Reject with error
|
|
307
|
+
*
|
|
308
|
+
* @param rlKey
|
|
309
|
+
* @param {Object} options
|
|
310
|
+
* @private
|
|
311
|
+
*
|
|
312
|
+
* @return Promise<any>
|
|
313
|
+
*/
|
|
314
|
+
_get(rlKey, options = {}) { // eslint-disable-line no-unused-vars
|
|
315
|
+
throw new Error("You have to implement the method '_get'!");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Have to be implemented
|
|
320
|
+
* Resolve with true OR false if rlKey doesn't exist
|
|
321
|
+
* or Reject with error
|
|
322
|
+
*
|
|
323
|
+
* @param rlKey
|
|
324
|
+
* @param {Object} options
|
|
325
|
+
* @private
|
|
326
|
+
*
|
|
327
|
+
* @return Promise<any>
|
|
328
|
+
*/
|
|
329
|
+
_delete(rlKey, options = {}) { // eslint-disable-line no-unused-vars
|
|
330
|
+
throw new Error("You have to implement the method '_delete'!");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Have to be implemented
|
|
335
|
+
* Resolve with object used for {@link _getRateLimiterRes} to generate {@link RateLimiterRes}
|
|
336
|
+
*
|
|
337
|
+
* @param {string} rlKey
|
|
338
|
+
* @param {number} points
|
|
339
|
+
* @param {number} msDuration
|
|
340
|
+
* @param {boolean} forceExpire
|
|
341
|
+
* @param {Object} options
|
|
342
|
+
* @abstract
|
|
343
|
+
*
|
|
344
|
+
* @return Promise<Object>
|
|
345
|
+
*/
|
|
346
|
+
_upsert(rlKey, points, msDuration, forceExpire = false, options = {}) {
|
|
347
|
+
throw new Error("You have to implement the method '_upsert'!");
|
|
348
|
+
}
|
|
349
|
+
};
|