@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,351 @@
|
|
|
1
|
+
const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
|
|
2
|
+
const RateLimiterRes = require('./RateLimiterRes');
|
|
3
|
+
|
|
4
|
+
class RateLimiterPostgres extends RateLimiterStoreAbstract {
|
|
5
|
+
/**
|
|
6
|
+
* @callback callback
|
|
7
|
+
* @param {Object} err
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} opts
|
|
10
|
+
* @param {callback} cb
|
|
11
|
+
* Defaults {
|
|
12
|
+
* ... see other in RateLimiterStoreAbstract
|
|
13
|
+
*
|
|
14
|
+
* storeClient: postgresClient,
|
|
15
|
+
* storeType: 'knex', // required only for Knex instance
|
|
16
|
+
* tableName: 'string',
|
|
17
|
+
* schemaName: 'string', // optional
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
constructor(opts, cb = null) {
|
|
21
|
+
super(opts);
|
|
22
|
+
|
|
23
|
+
this.client = opts.storeClient;
|
|
24
|
+
this.clientType = opts.storeType;
|
|
25
|
+
|
|
26
|
+
this.tableName = opts.tableName;
|
|
27
|
+
this.schemaName = opts.schemaName;
|
|
28
|
+
|
|
29
|
+
this.clearExpiredByTimeout = opts.clearExpiredByTimeout;
|
|
30
|
+
|
|
31
|
+
this.tableCreated = opts.tableCreated;
|
|
32
|
+
if (!this.tableCreated) {
|
|
33
|
+
this._createTable()
|
|
34
|
+
.then(() => {
|
|
35
|
+
this.tableCreated = true;
|
|
36
|
+
if (this.clearExpiredByTimeout) {
|
|
37
|
+
this._clearExpiredHourAgo();
|
|
38
|
+
}
|
|
39
|
+
if (typeof cb === 'function') {
|
|
40
|
+
cb();
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
.catch((err) => {
|
|
44
|
+
if (typeof cb === 'function') {
|
|
45
|
+
cb(err);
|
|
46
|
+
} else {
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
} else {
|
|
51
|
+
if (this.clearExpiredByTimeout) {
|
|
52
|
+
this._clearExpiredHourAgo();
|
|
53
|
+
}
|
|
54
|
+
if (typeof cb === 'function') {
|
|
55
|
+
cb();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_getTableIdentifier() {
|
|
61
|
+
return this.schemaName ? `"${this.schemaName}"."${this.tableName}"` : `"${this.tableName}"`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
clearExpired(expire) {
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
const q = {
|
|
67
|
+
name: 'rlflx-clear-expired',
|
|
68
|
+
text: `DELETE FROM ${this._getTableIdentifier()} WHERE expire < $1`,
|
|
69
|
+
values: [expire],
|
|
70
|
+
};
|
|
71
|
+
this._query(q)
|
|
72
|
+
.then(() => {
|
|
73
|
+
resolve();
|
|
74
|
+
})
|
|
75
|
+
.catch(() => {
|
|
76
|
+
// Deleting expired query is not critical
|
|
77
|
+
resolve();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Delete all rows expired 1 hour ago once per 5 minutes
|
|
84
|
+
*
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
_clearExpiredHourAgo() {
|
|
88
|
+
if (this._clearExpiredTimeoutId) {
|
|
89
|
+
clearTimeout(this._clearExpiredTimeoutId);
|
|
90
|
+
}
|
|
91
|
+
this._clearExpiredTimeoutId = setTimeout(() => {
|
|
92
|
+
this.clearExpired(Date.now() - 3600000) // Never rejected
|
|
93
|
+
.then(() => {
|
|
94
|
+
this._clearExpiredHourAgo();
|
|
95
|
+
});
|
|
96
|
+
}, 300000);
|
|
97
|
+
this._clearExpiredTimeoutId.unref();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
*
|
|
102
|
+
* @return Promise<any>
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
_getConnection() {
|
|
106
|
+
switch (this.clientType) {
|
|
107
|
+
case 'pool':
|
|
108
|
+
return Promise.resolve(this.client);
|
|
109
|
+
case 'sequelize':
|
|
110
|
+
return this._getSequelizeConnectionManager().getConnection();
|
|
111
|
+
case 'knex':
|
|
112
|
+
return this.client.client.acquireConnection();
|
|
113
|
+
case 'typeorm':
|
|
114
|
+
return Promise.resolve(this.client.driver.master);
|
|
115
|
+
default:
|
|
116
|
+
return Promise.resolve(this.client);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_releaseConnection(conn) {
|
|
121
|
+
switch (this.clientType) {
|
|
122
|
+
case 'pool':
|
|
123
|
+
return true;
|
|
124
|
+
case 'sequelize':
|
|
125
|
+
return this._getSequelizeConnectionManager().releaseConnection(conn);
|
|
126
|
+
case 'knex':
|
|
127
|
+
return this.client.client.releaseConnection(conn);
|
|
128
|
+
case 'typeorm':
|
|
129
|
+
return true;
|
|
130
|
+
default:
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_getSequelizeConnectionManager() {
|
|
136
|
+
let connectionManager;
|
|
137
|
+
let accessError;
|
|
138
|
+
try {
|
|
139
|
+
connectionManager = this.client.connectionManager;
|
|
140
|
+
} catch (err) {
|
|
141
|
+
// Accessing connectionManager can throw in Sequelize version 7 and higher.
|
|
142
|
+
accessError = err;
|
|
143
|
+
}
|
|
144
|
+
if (connectionManager) {
|
|
145
|
+
return connectionManager;
|
|
146
|
+
}
|
|
147
|
+
if (this.client.dialect && this.client.dialect.connectionManager) {
|
|
148
|
+
return this.client.dialect.connectionManager;
|
|
149
|
+
}
|
|
150
|
+
// Rethrow the original error if it exists, otherwise throw a generic error
|
|
151
|
+
if (accessError) {
|
|
152
|
+
throw accessError;
|
|
153
|
+
}
|
|
154
|
+
throw new Error('Sequelize connection manager is not available');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
*
|
|
159
|
+
* @returns {Promise<any>}
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
_createTable() {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
this._query({
|
|
165
|
+
text: this._getCreateTableStmt(),
|
|
166
|
+
})
|
|
167
|
+
.then(() => {
|
|
168
|
+
resolve();
|
|
169
|
+
})
|
|
170
|
+
.catch((err) => {
|
|
171
|
+
if (err.code === '23505') {
|
|
172
|
+
// Error: duplicate key value violates unique constraint "pg_type_typname_nsp_index"
|
|
173
|
+
// Postgres doesn't handle concurrent table creation
|
|
174
|
+
// It is supposed, that table is created by another worker
|
|
175
|
+
resolve();
|
|
176
|
+
} else {
|
|
177
|
+
reject(err);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_getCreateTableStmt() {
|
|
184
|
+
return `CREATE TABLE IF NOT EXISTS ${this._getTableIdentifier()} (
|
|
185
|
+
key varchar(255) PRIMARY KEY,
|
|
186
|
+
points integer NOT NULL DEFAULT 0,
|
|
187
|
+
expire bigint
|
|
188
|
+
);`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
get clientType() {
|
|
192
|
+
return this._clientType;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
set clientType(value) {
|
|
196
|
+
const constructorName = this.client.constructor.name;
|
|
197
|
+
|
|
198
|
+
if (typeof value === 'undefined') {
|
|
199
|
+
if (constructorName === 'Client') {
|
|
200
|
+
value = 'client';
|
|
201
|
+
} else if (
|
|
202
|
+
constructorName === 'Pool' ||
|
|
203
|
+
constructorName === 'BoundPool'
|
|
204
|
+
) {
|
|
205
|
+
value = 'pool';
|
|
206
|
+
} else if (constructorName === 'Sequelize') {
|
|
207
|
+
value = 'sequelize';
|
|
208
|
+
} else {
|
|
209
|
+
throw new Error('storeType is not defined');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this._clientType = value.toLowerCase();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
get tableName() {
|
|
217
|
+
return this._tableName;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
set tableName(value) {
|
|
221
|
+
this._tableName = typeof value === 'undefined' ? this.keyPrefix : value;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
get schemaName() {
|
|
225
|
+
return this._schemaName;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
set schemaName(value) {
|
|
229
|
+
this._schemaName = value;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
get tableCreated() {
|
|
233
|
+
return this._tableCreated;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
set tableCreated(value) {
|
|
237
|
+
this._tableCreated = typeof value === 'undefined' ? false : !!value;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
get clearExpiredByTimeout() {
|
|
241
|
+
return this._clearExpiredByTimeout;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
set clearExpiredByTimeout(value) {
|
|
245
|
+
this._clearExpiredByTimeout = typeof value === 'undefined' ? true : Boolean(value);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_getRateLimiterRes(rlKey, changedPoints, result) {
|
|
249
|
+
const res = new RateLimiterRes();
|
|
250
|
+
const row = result.rows[0];
|
|
251
|
+
|
|
252
|
+
res.isFirstInDuration = changedPoints === row.points;
|
|
253
|
+
res.consumedPoints = res.isFirstInDuration ? changedPoints : row.points;
|
|
254
|
+
|
|
255
|
+
res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
|
|
256
|
+
res.msBeforeNext = row.expire
|
|
257
|
+
? Math.max(row.expire - Date.now(), 0)
|
|
258
|
+
: -1;
|
|
259
|
+
|
|
260
|
+
return res;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
_query(q) {
|
|
264
|
+
const prefix = this.tableName.toLowerCase();
|
|
265
|
+
const queryObj = { name: `${prefix}:${q.name}`, text: q.text, values: q.values };
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
this._getConnection()
|
|
268
|
+
.then((conn) => {
|
|
269
|
+
conn.query(queryObj)
|
|
270
|
+
.then((res) => {
|
|
271
|
+
resolve(res);
|
|
272
|
+
this._releaseConnection(conn);
|
|
273
|
+
})
|
|
274
|
+
.catch((err) => {
|
|
275
|
+
reject(err);
|
|
276
|
+
this._releaseConnection(conn);
|
|
277
|
+
});
|
|
278
|
+
})
|
|
279
|
+
.catch((err) => {
|
|
280
|
+
reject(err);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
_upsert(key, points, msDuration, forceExpire = false) {
|
|
286
|
+
if (!this.tableCreated) {
|
|
287
|
+
return Promise.reject(Error('Table is not created yet'));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const newExpire = msDuration > 0 ? Date.now() + msDuration : null;
|
|
291
|
+
const expireQ = forceExpire
|
|
292
|
+
? ' $3 '
|
|
293
|
+
: ` CASE
|
|
294
|
+
WHEN ${this._getTableIdentifier()}.expire <= $4 THEN $3
|
|
295
|
+
ELSE ${this._getTableIdentifier()}.expire
|
|
296
|
+
END `;
|
|
297
|
+
|
|
298
|
+
return this._query({
|
|
299
|
+
name: forceExpire ? 'rlflx-upsert-force' : 'rlflx-upsert',
|
|
300
|
+
text: `
|
|
301
|
+
INSERT INTO ${this._getTableIdentifier()} VALUES ($1, $2, $3)
|
|
302
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
303
|
+
points = CASE
|
|
304
|
+
WHEN (${this._getTableIdentifier()}.expire <= $4 OR 1=${forceExpire ? 1 : 0}) THEN $2
|
|
305
|
+
ELSE ${this._getTableIdentifier()}.points + ($2)
|
|
306
|
+
END,
|
|
307
|
+
expire = ${expireQ}
|
|
308
|
+
RETURNING points, expire;`,
|
|
309
|
+
values: [key, points, newExpire, Date.now()],
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_get(rlKey) {
|
|
314
|
+
if (!this.tableCreated) {
|
|
315
|
+
return Promise.reject(Error('Table is not created yet'));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return new Promise((resolve, reject) => {
|
|
319
|
+
this._query({
|
|
320
|
+
name: 'rlflx-get',
|
|
321
|
+
text: `
|
|
322
|
+
SELECT points, expire FROM ${this._getTableIdentifier()} WHERE key = $1 AND (expire > $2 OR expire IS NULL);`,
|
|
323
|
+
values: [rlKey, Date.now()],
|
|
324
|
+
})
|
|
325
|
+
.then((res) => {
|
|
326
|
+
if (res.rowCount === 0) {
|
|
327
|
+
res = null;
|
|
328
|
+
}
|
|
329
|
+
resolve(res);
|
|
330
|
+
})
|
|
331
|
+
.catch((err) => {
|
|
332
|
+
reject(err);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
_delete(rlKey) {
|
|
338
|
+
if (!this.tableCreated) {
|
|
339
|
+
return Promise.reject(Error('Table is not created yet'));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return this._query({
|
|
343
|
+
name: 'rlflx-delete',
|
|
344
|
+
text: `DELETE FROM ${this._getTableIdentifier()} WHERE key = $1`,
|
|
345
|
+
values: [rlKey],
|
|
346
|
+
})
|
|
347
|
+
.then(res => res.rowCount > 0);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
module.exports = RateLimiterPostgres;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
|
|
2
|
+
const RateLimiterRes = require('./RateLimiterRes');
|
|
3
|
+
|
|
4
|
+
class RateLimiterPrisma extends RateLimiterStoreAbstract {
|
|
5
|
+
/**
|
|
6
|
+
* Constructor for the rate limiter
|
|
7
|
+
* @param {Object} opts - Options for the rate limiter
|
|
8
|
+
*/
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
super(opts);
|
|
11
|
+
|
|
12
|
+
this.modelName = opts.tableName || 'RateLimiterFlexible';
|
|
13
|
+
this.prismaClient = opts.storeClient;
|
|
14
|
+
this.clearExpiredByTimeout = opts.clearExpiredByTimeout || true;
|
|
15
|
+
|
|
16
|
+
if (!this.prismaClient) {
|
|
17
|
+
throw new Error('Prisma client is not provided');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (this.clearExpiredByTimeout) {
|
|
21
|
+
this._clearExpiredHourAgo();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_getRateLimiterRes(rlKey, changedPoints, result) {
|
|
26
|
+
const res = new RateLimiterRes();
|
|
27
|
+
|
|
28
|
+
let doc = result;
|
|
29
|
+
|
|
30
|
+
res.isFirstInDuration = doc.points === changedPoints;
|
|
31
|
+
res.consumedPoints = doc.points;
|
|
32
|
+
|
|
33
|
+
res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
|
|
34
|
+
res.msBeforeNext = doc.expire !== null
|
|
35
|
+
? Math.max(new Date(doc.expire).getTime() - Date.now(), 0)
|
|
36
|
+
: -1;
|
|
37
|
+
|
|
38
|
+
return res;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_upsert(key, points, msDuration, forceExpire = false) {
|
|
42
|
+
if (!this.prismaClient) {
|
|
43
|
+
return Promise.reject(new Error('Prisma client is not established'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const now = new Date();
|
|
47
|
+
const newExpire = msDuration > 0 ? new Date(now.getTime() + msDuration) : null;
|
|
48
|
+
|
|
49
|
+
return this.prismaClient.$transaction(async (prisma) => {
|
|
50
|
+
const existingRecord = await prisma[this.modelName].findFirst({
|
|
51
|
+
where: { key: key },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (existingRecord) {
|
|
55
|
+
// Determine if we should update the expire field
|
|
56
|
+
const shouldUpdateExpire = forceExpire || !existingRecord.expire || existingRecord.expire <= now || newExpire === null;
|
|
57
|
+
|
|
58
|
+
return prisma[this.modelName].update({
|
|
59
|
+
where: { key: key },
|
|
60
|
+
data: {
|
|
61
|
+
points: !shouldUpdateExpire ? existingRecord.points + points : points,
|
|
62
|
+
...(shouldUpdateExpire && { expire: newExpire }),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
} else {
|
|
66
|
+
return prisma[this.modelName].create({
|
|
67
|
+
data: {
|
|
68
|
+
key: key,
|
|
69
|
+
points: points,
|
|
70
|
+
expire: newExpire,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_get(rlKey) {
|
|
78
|
+
if (!this.prismaClient) {
|
|
79
|
+
return Promise.reject(new Error('Prisma client is not established'));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return this.prismaClient[this.modelName].findFirst({
|
|
83
|
+
where: {
|
|
84
|
+
AND: [
|
|
85
|
+
{ key: rlKey },
|
|
86
|
+
{
|
|
87
|
+
OR: [
|
|
88
|
+
{ expire: { gt: new Date() } },
|
|
89
|
+
{ expire: null },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_delete(rlKey) {
|
|
98
|
+
if (!this.prismaClient) {
|
|
99
|
+
return Promise.reject(new Error('Prisma client is not established'));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return this.prismaClient[this.modelName].deleteMany({
|
|
103
|
+
where: {
|
|
104
|
+
key: rlKey,
|
|
105
|
+
},
|
|
106
|
+
}).then(res => res.count > 0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_clearExpiredHourAgo() {
|
|
110
|
+
if (this._clearExpiredTimeoutId) {
|
|
111
|
+
clearTimeout(this._clearExpiredTimeoutId);
|
|
112
|
+
}
|
|
113
|
+
this._clearExpiredTimeoutId = setTimeout(async () => {
|
|
114
|
+
await this.prismaClient[this.modelName].deleteMany({
|
|
115
|
+
where: {
|
|
116
|
+
expire: {
|
|
117
|
+
lt: new Date(Date.now() - 3600000),
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
this._clearExpiredHourAgo();
|
|
122
|
+
}, 300000); // Clear every 5 minutes
|
|
123
|
+
this._clearExpiredTimeoutId.unref();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = RateLimiterPrisma;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const RateLimiterQueueError = require('./component/RateLimiterQueueError')
|
|
2
|
+
const MAX_QUEUE_SIZE = 4294967295;
|
|
3
|
+
const KEY_DEFAULT = 'limiter';
|
|
4
|
+
|
|
5
|
+
module.exports = class RateLimiterQueue {
|
|
6
|
+
constructor(limiterFlexible, opts = {}) {
|
|
7
|
+
const maxQueueSize =
|
|
8
|
+
opts.maxQueueSize !== undefined ? opts.maxQueueSize : MAX_QUEUE_SIZE;
|
|
9
|
+
this._queueLimiters = {
|
|
10
|
+
KEY_DEFAULT: new RateLimiterQueueInternal(limiterFlexible, {
|
|
11
|
+
...opts,
|
|
12
|
+
maxQueueSize,
|
|
13
|
+
key: KEY_DEFAULT
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
this._limiterFlexible = limiterFlexible;
|
|
17
|
+
this._maxQueueSize = maxQueueSize;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getTokensRemaining(key = KEY_DEFAULT) {
|
|
21
|
+
if (this._queueLimiters[key]) {
|
|
22
|
+
return this._queueLimiters[key].getTokensRemaining()
|
|
23
|
+
} else {
|
|
24
|
+
return Promise.resolve(this._limiterFlexible.points)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
removeTokens(tokens, key = KEY_DEFAULT) {
|
|
29
|
+
if (!this._queueLimiters[key]) {
|
|
30
|
+
this._queueLimiters[key] = new RateLimiterQueueInternal(
|
|
31
|
+
this._limiterFlexible, {
|
|
32
|
+
key,
|
|
33
|
+
maxQueueSize: this._maxQueueSize,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return this._queueLimiters[key].removeTokens(tokens)
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
class RateLimiterQueueInternal {
|
|
42
|
+
|
|
43
|
+
constructor(limiterFlexible, opts = {
|
|
44
|
+
maxQueueSize: MAX_QUEUE_SIZE,
|
|
45
|
+
key: KEY_DEFAULT,
|
|
46
|
+
}) {
|
|
47
|
+
this._key = opts.key;
|
|
48
|
+
this._waitTimeout = null;
|
|
49
|
+
this._queue = [];
|
|
50
|
+
this._limiterFlexible = limiterFlexible;
|
|
51
|
+
|
|
52
|
+
this._maxQueueSize = opts.maxQueueSize
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getTokensRemaining() {
|
|
56
|
+
return this._limiterFlexible.get(this._key)
|
|
57
|
+
.then((rlRes) => {
|
|
58
|
+
return rlRes !== null ? rlRes.remainingPoints : this._limiterFlexible.points;
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
removeTokens(tokens) {
|
|
63
|
+
const _this = this;
|
|
64
|
+
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
if (tokens > _this._limiterFlexible.points) {
|
|
67
|
+
reject(new RateLimiterQueueError(`Requested tokens ${tokens} exceeds maximum ${_this._limiterFlexible.points} tokens per interval`));
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (_this._queue.length > 0) {
|
|
72
|
+
_this._queueRequest.call(_this, resolve, reject, tokens);
|
|
73
|
+
} else {
|
|
74
|
+
_this._limiterFlexible.consume(_this._key, tokens)
|
|
75
|
+
.then((res) => {
|
|
76
|
+
resolve(res.remainingPoints);
|
|
77
|
+
})
|
|
78
|
+
.catch((rej) => {
|
|
79
|
+
if (rej instanceof Error) {
|
|
80
|
+
reject(rej);
|
|
81
|
+
} else {
|
|
82
|
+
_this._queueRequest.call(_this, resolve, reject, tokens);
|
|
83
|
+
if (_this._waitTimeout === null) {
|
|
84
|
+
_this._waitTimeout = setTimeout(_this._processFIFO.bind(_this), rej.msBeforeNext);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_queueRequest(resolve, reject, tokens) {
|
|
93
|
+
const _this = this;
|
|
94
|
+
if (_this._queue.length < _this._maxQueueSize) {
|
|
95
|
+
_this._queue.push({resolve, reject, tokens});
|
|
96
|
+
} else {
|
|
97
|
+
reject(new RateLimiterQueueError(`Number of requests reached it's maximum ${_this._maxQueueSize}`))
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_processFIFO() {
|
|
102
|
+
const _this = this;
|
|
103
|
+
|
|
104
|
+
if (_this._waitTimeout !== null) {
|
|
105
|
+
clearTimeout(_this._waitTimeout);
|
|
106
|
+
_this._waitTimeout = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (_this._queue.length === 0) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const item = _this._queue.shift();
|
|
114
|
+
_this._limiterFlexible.consume(_this._key, item.tokens)
|
|
115
|
+
.then((res) => {
|
|
116
|
+
item.resolve(res.remainingPoints);
|
|
117
|
+
_this._processFIFO.call(_this);
|
|
118
|
+
})
|
|
119
|
+
.catch((rej) => {
|
|
120
|
+
if (rej instanceof Error) {
|
|
121
|
+
item.reject(rej);
|
|
122
|
+
_this._processFIFO.call(_this);
|
|
123
|
+
} else {
|
|
124
|
+
_this._queue.unshift(item);
|
|
125
|
+
if (_this._waitTimeout === null) {
|
|
126
|
+
_this._waitTimeout = setTimeout(_this._processFIFO.bind(_this), rej.msBeforeNext);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|