@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.
Files changed (42) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +25 -0
  3. package/changes.json +5 -0
  4. package/index.js +55 -0
  5. package/lib/BurstyRateLimiter.js +78 -0
  6. package/lib/ExpressBruteFlexible.js +359 -0
  7. package/lib/RLWrapperBlackAndWhite.js +195 -0
  8. package/lib/RLWrapperTimeouts.js +82 -0
  9. package/lib/RateLimiterAbstract.js +125 -0
  10. package/lib/RateLimiterCluster.js +367 -0
  11. package/lib/RateLimiterDrizzle.js +174 -0
  12. package/lib/RateLimiterDrizzleNonAtomic.js +175 -0
  13. package/lib/RateLimiterDynamo.js +401 -0
  14. package/lib/RateLimiterEtcd.js +63 -0
  15. package/lib/RateLimiterEtcdNonAtomic.js +80 -0
  16. package/lib/RateLimiterInsuredAbstract.js +112 -0
  17. package/lib/RateLimiterMemcache.js +150 -0
  18. package/lib/RateLimiterMemory.js +106 -0
  19. package/lib/RateLimiterMongo.js +261 -0
  20. package/lib/RateLimiterMySQL.js +400 -0
  21. package/lib/RateLimiterPostgres.js +351 -0
  22. package/lib/RateLimiterPrisma.js +127 -0
  23. package/lib/RateLimiterQueue.js +131 -0
  24. package/lib/RateLimiterRedis.js +209 -0
  25. package/lib/RateLimiterRedisNonAtomic.js +195 -0
  26. package/lib/RateLimiterRes.js +64 -0
  27. package/lib/RateLimiterSQLite.js +338 -0
  28. package/lib/RateLimiterStoreAbstract.js +349 -0
  29. package/lib/RateLimiterUnion.js +51 -0
  30. package/lib/RateLimiterValkey.js +117 -0
  31. package/lib/RateLimiterValkeyGlide.js +273 -0
  32. package/lib/component/BlockedKeys/BlockedKeys.js +75 -0
  33. package/lib/component/BlockedKeys/index.js +3 -0
  34. package/lib/component/MemoryStorage/MemoryStorage.js +83 -0
  35. package/lib/component/MemoryStorage/Record.js +40 -0
  36. package/lib/component/MemoryStorage/index.js +3 -0
  37. package/lib/component/RateLimiterEtcdTransactionFailedError.js +10 -0
  38. package/lib/component/RateLimiterQueueError.js +13 -0
  39. package/lib/component/RateLimiterSetupError.js +10 -0
  40. package/lib/constants.js +21 -0
  41. package/package.json +100 -0
  42. package/types.d.ts +581 -0
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Implements rate limiting in cluster using built-in IPC
3
+ *
4
+ * Two classes are described here: master and worker
5
+ * Master have to be create in the master process without any options.
6
+ * Any number of rate limiters can be created in workers, but each rate limiter must be with unique keyPrefix
7
+ *
8
+ * Workflow:
9
+ * 1. master rate limiter created in master process
10
+ * 2. worker rate limiter sends 'init' message with necessary options during creating
11
+ * 3. master receives options and adds new rate limiter by keyPrefix if it isn't created yet
12
+ * 4. master sends 'init' back to worker's rate limiter
13
+ * 5. worker can process requests immediately,
14
+ * but they will be postponed by 'workerWaitInit' until master sends 'init' to worker
15
+ * 6. every request to worker rate limiter creates a promise
16
+ * 7. if master doesn't response for 'timeout', promise is rejected
17
+ * 8. master sends 'resolve' or 'reject' command to worker
18
+ * 9. worker resolves or rejects promise depending on message from master
19
+ *
20
+ */
21
+
22
+ const cluster = require('cluster');
23
+ const crypto = require('crypto');
24
+ const RateLimiterAbstract = require('./RateLimiterAbstract');
25
+ const RateLimiterMemory = require('./RateLimiterMemory');
26
+ const RateLimiterRes = require('./RateLimiterRes');
27
+
28
+ const channel = 'rate_limiter_flexible';
29
+ let masterInstance = null;
30
+
31
+ const masterSendToWorker = function (worker, msg, type, res) {
32
+ let data;
33
+ if (res === null || res === true || res === false) {
34
+ data = res;
35
+ } else {
36
+ data = {
37
+ remainingPoints: res.remainingPoints,
38
+ msBeforeNext: res.msBeforeNext,
39
+ consumedPoints: res.consumedPoints,
40
+ isFirstInDuration: res.isFirstInDuration,
41
+ };
42
+ }
43
+ worker.send({
44
+ channel,
45
+ keyPrefix: msg.keyPrefix, // which rate limiter exactly
46
+ promiseId: msg.promiseId,
47
+ type,
48
+ data,
49
+ });
50
+ };
51
+
52
+ const workerWaitInit = function (payload) {
53
+ setTimeout(() => {
54
+ if (this._initiated) {
55
+ process.send(payload);
56
+ // Promise will be removed by timeout if too long
57
+ } else if (typeof this._promises[payload.promiseId] !== 'undefined') {
58
+ workerWaitInit.call(this, payload);
59
+ }
60
+ }, 30);
61
+ };
62
+
63
+ const workerSendToMaster = function (func, promiseId, key, arg, opts) {
64
+ const payload = {
65
+ channel,
66
+ keyPrefix: this.keyPrefix,
67
+ func,
68
+ promiseId,
69
+ data: {
70
+ key,
71
+ arg,
72
+ opts,
73
+ },
74
+ };
75
+
76
+ if (!this._initiated) {
77
+ // Wait init before sending messages to master
78
+ workerWaitInit.call(this, payload);
79
+ } else {
80
+ process.send(payload);
81
+ }
82
+ };
83
+
84
+ const masterProcessMsg = function (worker, msg) {
85
+ if (!msg || msg.channel !== channel || typeof this._rateLimiters[msg.keyPrefix] === 'undefined') {
86
+ return false;
87
+ }
88
+
89
+ let promise;
90
+
91
+ switch (msg.func) {
92
+ case 'consume':
93
+ promise = this._rateLimiters[msg.keyPrefix].consume(msg.data.key, msg.data.arg, msg.data.opts);
94
+ break;
95
+ case 'penalty':
96
+ promise = this._rateLimiters[msg.keyPrefix].penalty(msg.data.key, msg.data.arg, msg.data.opts);
97
+ break;
98
+ case 'reward':
99
+ promise = this._rateLimiters[msg.keyPrefix].reward(msg.data.key, msg.data.arg, msg.data.opts);
100
+ break;
101
+ case 'block':
102
+ promise = this._rateLimiters[msg.keyPrefix].block(msg.data.key, msg.data.arg, msg.data.opts);
103
+ break;
104
+ case 'get':
105
+ promise = this._rateLimiters[msg.keyPrefix].get(msg.data.key, msg.data.opts);
106
+ break;
107
+ case 'delete':
108
+ promise = this._rateLimiters[msg.keyPrefix].delete(msg.data.key, msg.data.opts);
109
+ break;
110
+ default:
111
+ return false;
112
+ }
113
+
114
+ if (promise) {
115
+ promise
116
+ .then((res) => {
117
+ masterSendToWorker(worker, msg, 'resolve', res);
118
+ })
119
+ .catch((rejRes) => {
120
+ masterSendToWorker(worker, msg, 'reject', rejRes);
121
+ });
122
+ }
123
+ };
124
+
125
+ const workerProcessMsg = function (msg) {
126
+ if (!msg || msg.channel !== channel || msg.keyPrefix !== this.keyPrefix) {
127
+ return false;
128
+ }
129
+
130
+ if (this._promises[msg.promiseId]) {
131
+ clearTimeout(this._promises[msg.promiseId].timeoutId);
132
+ let res;
133
+ if (msg.data === null || msg.data === true || msg.data === false) {
134
+ res = msg.data;
135
+ } else {
136
+ res = new RateLimiterRes(
137
+ msg.data.remainingPoints,
138
+ msg.data.msBeforeNext,
139
+ msg.data.consumedPoints,
140
+ msg.data.isFirstInDuration // eslint-disable-line comma-dangle
141
+ );
142
+ }
143
+
144
+ switch (msg.type) {
145
+ case 'resolve':
146
+ this._promises[msg.promiseId].resolve(res);
147
+ break;
148
+ case 'reject':
149
+ this._promises[msg.promiseId].reject(res);
150
+ break;
151
+ default:
152
+ throw new Error(`RateLimiterCluster: no such message type '${msg.type}'`);
153
+ }
154
+
155
+ delete this._promises[msg.promiseId];
156
+ }
157
+ };
158
+ /**
159
+ * Prepare options to send to master
160
+ * Master will create rate limiter depending on options
161
+ *
162
+ * @returns {{points: *, duration: *, blockDuration: *, execEvenly: *, execEvenlyMinDelayMs: *, keyPrefix: *}}
163
+ */
164
+ const getOpts = function () {
165
+ return {
166
+ points: this.points,
167
+ duration: this.duration,
168
+ blockDuration: this.blockDuration,
169
+ execEvenly: this.execEvenly,
170
+ execEvenlyMinDelayMs: this.execEvenlyMinDelayMs,
171
+ keyPrefix: this.keyPrefix,
172
+ };
173
+ };
174
+
175
+ const savePromise = function (resolve, reject) {
176
+ const hrtime = process.hrtime();
177
+ let promiseId = hrtime[0].toString() + hrtime[1].toString();
178
+
179
+ if (typeof this._promises[promiseId] !== 'undefined') {
180
+ promiseId += crypto.randomBytes(12).toString('base64');
181
+ }
182
+
183
+ this._promises[promiseId] = {
184
+ resolve,
185
+ reject,
186
+ timeoutId: setTimeout(() => {
187
+ delete this._promises[promiseId];
188
+ reject(new Error('RateLimiterCluster timeout: no answer from master in time'));
189
+ }, this.timeoutMs),
190
+ };
191
+
192
+ return promiseId;
193
+ };
194
+
195
+ class RateLimiterClusterMaster {
196
+ constructor() {
197
+ if (masterInstance) {
198
+ return masterInstance;
199
+ }
200
+
201
+ this._rateLimiters = {};
202
+
203
+ cluster.setMaxListeners(0);
204
+
205
+ cluster.on('message', (worker, msg) => {
206
+ if (msg && msg.channel === channel && msg.type === 'init') {
207
+ // If init request, check or create rate limiter by key prefix and send 'init' back to worker
208
+ if (typeof this._rateLimiters[msg.opts.keyPrefix] === 'undefined') {
209
+ this._rateLimiters[msg.opts.keyPrefix] = new RateLimiterMemory(msg.opts);
210
+ }
211
+
212
+ worker.send({
213
+ channel,
214
+ type: 'init',
215
+ keyPrefix: msg.opts.keyPrefix,
216
+ });
217
+ } else {
218
+ masterProcessMsg.call(this, worker, msg);
219
+ }
220
+ });
221
+
222
+ masterInstance = this;
223
+ }
224
+ }
225
+
226
+ class RateLimiterClusterMasterPM2 {
227
+ constructor(pm2) {
228
+ if (masterInstance) {
229
+ return masterInstance;
230
+ }
231
+
232
+ this._rateLimiters = {};
233
+
234
+ pm2.launchBus((err, pm2Bus) => {
235
+ pm2Bus.on('process:msg', (packet) => {
236
+ const msg = packet.raw;
237
+ if (msg && msg.channel === channel && msg.type === 'init') {
238
+ // If init request, check or create rate limiter by key prefix and send 'init' back to worker
239
+ if (typeof this._rateLimiters[msg.opts.keyPrefix] === 'undefined') {
240
+ this._rateLimiters[msg.opts.keyPrefix] = new RateLimiterMemory(msg.opts);
241
+ }
242
+
243
+ pm2.sendDataToProcessId(packet.process.pm_id, {
244
+ data: {},
245
+ topic: channel,
246
+ channel,
247
+ type: 'init',
248
+ keyPrefix: msg.opts.keyPrefix,
249
+ }, (sendErr, res) => {
250
+ if (sendErr) {
251
+ console.log(sendErr, res);
252
+ }
253
+ });
254
+ } else {
255
+ const worker = {
256
+ send: (msgData) => {
257
+ const pm2Message = msgData;
258
+ pm2Message.topic = channel;
259
+ if (typeof pm2Message.data === 'undefined') {
260
+ pm2Message.data = {};
261
+ }
262
+ pm2.sendDataToProcessId(packet.process.pm_id, pm2Message, (sendErr, res) => {
263
+ if (sendErr) {
264
+ console.log(sendErr, res);
265
+ }
266
+ });
267
+ },
268
+ };
269
+ masterProcessMsg.call(this, worker, msg);
270
+ }
271
+ });
272
+ });
273
+
274
+ masterInstance = this;
275
+ }
276
+ }
277
+
278
+ class RateLimiterClusterWorker extends RateLimiterAbstract {
279
+ get timeoutMs() {
280
+ return this._timeoutMs;
281
+ }
282
+
283
+ set timeoutMs(value) {
284
+ this._timeoutMs = typeof value === 'undefined' ? 5000 : Math.abs(parseInt(value));
285
+ }
286
+
287
+ constructor(opts = {}) {
288
+ super(opts);
289
+
290
+ process.setMaxListeners(0);
291
+
292
+ this.timeoutMs = opts.timeoutMs;
293
+
294
+ this._initiated = false;
295
+
296
+ process.on('message', (msg) => {
297
+ if (msg && msg.channel === channel && msg.type === 'init' && msg.keyPrefix === this.keyPrefix) {
298
+ this._initiated = true;
299
+ } else {
300
+ workerProcessMsg.call(this, msg);
301
+ }
302
+ });
303
+
304
+ // Create limiter on master with specific options
305
+ process.send({
306
+ channel,
307
+ type: 'init',
308
+ opts: getOpts.call(this),
309
+ });
310
+
311
+ this._promises = {};
312
+ }
313
+
314
+ consume(key, pointsToConsume = 1, options = {}) {
315
+ return new Promise((resolve, reject) => {
316
+ const promiseId = savePromise.call(this, resolve, reject);
317
+
318
+ workerSendToMaster.call(this, 'consume', promiseId, key, pointsToConsume, options);
319
+ });
320
+ }
321
+
322
+ penalty(key, points = 1, options = {}) {
323
+ return new Promise((resolve, reject) => {
324
+ const promiseId = savePromise.call(this, resolve, reject);
325
+
326
+ workerSendToMaster.call(this, 'penalty', promiseId, key, points, options);
327
+ });
328
+ }
329
+
330
+ reward(key, points = 1, options = {}) {
331
+ return new Promise((resolve, reject) => {
332
+ const promiseId = savePromise.call(this, resolve, reject);
333
+
334
+ workerSendToMaster.call(this, 'reward', promiseId, key, points, options);
335
+ });
336
+ }
337
+
338
+ block(key, secDuration, options = {}) {
339
+ return new Promise((resolve, reject) => {
340
+ const promiseId = savePromise.call(this, resolve, reject);
341
+
342
+ workerSendToMaster.call(this, 'block', promiseId, key, secDuration, options);
343
+ });
344
+ }
345
+
346
+ get(key, options = {}) {
347
+ return new Promise((resolve, reject) => {
348
+ const promiseId = savePromise.call(this, resolve, reject);
349
+
350
+ workerSendToMaster.call(this, 'get', promiseId, key, options);
351
+ });
352
+ }
353
+
354
+ delete(key, options = {}) {
355
+ return new Promise((resolve, reject) => {
356
+ const promiseId = savePromise.call(this, resolve, reject);
357
+
358
+ workerSendToMaster.call(this, 'delete', promiseId, key, options);
359
+ });
360
+ }
361
+ }
362
+
363
+ module.exports = {
364
+ RateLimiterClusterMaster,
365
+ RateLimiterClusterMasterPM2,
366
+ RateLimiterCluster: RateLimiterClusterWorker,
367
+ };
@@ -0,0 +1,174 @@
1
+ let drizzleOperators = null;
2
+ const CLEANUP_INTERVAL_MS = 300000; // 5 minutes
3
+ const EXPIRED_THRESHOLD_MS = 3600000; // 1 hour
4
+
5
+ class RateLimiterDrizzleError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = 'RateLimiterDrizzleError';
9
+ }
10
+ }
11
+
12
+ async function getDrizzleOperators() {
13
+ if (drizzleOperators) return drizzleOperators;
14
+
15
+ try {
16
+ // Use dynamic import to prevent static analysis tools from detecting the import
17
+ function getPackageName() {
18
+ return ['drizzle', 'orm'].join('-');
19
+ }
20
+ const drizzleOrm = await import(`${getPackageName()}`);
21
+ const { and, or, gt, lt, eq, isNull, sql } = drizzleOrm.default || drizzleOrm;
22
+ drizzleOperators = { and, or, gt, lt, eq, isNull, sql };
23
+ return drizzleOperators;
24
+ } catch (error) {
25
+ throw new RateLimiterDrizzleError(
26
+ 'drizzle-orm is not installed. Please install drizzle-orm to use RateLimiterDrizzle.'
27
+ );
28
+ }
29
+ }
30
+
31
+ const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
32
+ const RateLimiterRes = require('./RateLimiterRes');
33
+
34
+ class RateLimiterDrizzle extends RateLimiterStoreAbstract {
35
+ constructor(opts) {
36
+ super(opts);
37
+
38
+ if (!opts?.schema) {
39
+ throw new RateLimiterDrizzleError('Drizzle schema is required');
40
+ }
41
+
42
+ if (!opts?.storeClient) {
43
+ throw new RateLimiterDrizzleError('Drizzle client is required');
44
+ }
45
+
46
+ this.schema = opts.schema;
47
+ this.drizzleClient = opts.storeClient;
48
+ this.clearExpiredByTimeout = opts.clearExpiredByTimeout ?? true;
49
+
50
+ if (this.clearExpiredByTimeout) {
51
+ this._clearExpiredHourAgo();
52
+ }
53
+ }
54
+
55
+ _getRateLimiterRes(rlKey, changedPoints, result) {
56
+ const res = new RateLimiterRes();
57
+
58
+ let doc = result;
59
+ res.isFirstInDuration = doc.points === changedPoints;
60
+ res.consumedPoints = doc.points;
61
+ res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
62
+ res.msBeforeNext = doc.expire !== null
63
+ ? Math.max(new Date(doc.expire).getTime() - Date.now(), 0)
64
+ : -1;
65
+
66
+ return res;
67
+ }
68
+
69
+ async _upsert(key, points, msDuration, forceExpire = false) {
70
+ if (!this.drizzleClient) {
71
+ return Promise.reject(new RateLimiterDrizzleError('Drizzle client is not established'))
72
+ }
73
+
74
+ const { eq, sql } = await getDrizzleOperators();
75
+ const now = new Date();
76
+ const newExpire = msDuration > 0 ? new Date(now.getTime() + msDuration) : null;
77
+
78
+ const query = await this.drizzleClient.transaction(async (tx) => {
79
+ const [existingRecord] = await tx
80
+ .select()
81
+ .from(this.schema)
82
+ .where(eq(this.schema.key, key))
83
+ .limit(1);
84
+
85
+ const shouldUpdateExpire =
86
+ forceExpire ||
87
+ !existingRecord?.expire ||
88
+ existingRecord?.expire <= now ||
89
+ newExpire === null;
90
+
91
+ const [data] = await tx
92
+ .insert(this.schema)
93
+ .values({
94
+ key,
95
+ points,
96
+ expire: newExpire,
97
+ })
98
+ .onConflictDoUpdate({
99
+ target: this.schema.key,
100
+ set: {
101
+ points: !shouldUpdateExpire
102
+ ? sql`${this.schema.points} + ${points}`
103
+ : points,
104
+ ...(shouldUpdateExpire && { expire: newExpire }),
105
+ },
106
+ })
107
+ .returning();
108
+
109
+ return data;
110
+ })
111
+
112
+ return query
113
+ }
114
+
115
+ async _get(rlKey) {
116
+ if (!this.drizzleClient) {
117
+ return Promise.reject(new RateLimiterDrizzleError('Drizzle client is not established'))
118
+ }
119
+
120
+ const { and, or, gt, eq, isNull } = await getDrizzleOperators();
121
+
122
+ const [response] = await this.drizzleClient
123
+ .select()
124
+ .from(this.schema)
125
+ .where(
126
+ and(
127
+ eq(this.schema.key, rlKey),
128
+ or(gt(this.schema.expire, new Date()), isNull(this.schema.expire))
129
+ )
130
+ )
131
+ .limit(1);
132
+
133
+ return response || null;
134
+
135
+ }
136
+
137
+ async _delete(rlKey) {
138
+ if (!this.drizzleClient) {
139
+ return Promise.reject(new RateLimiterDrizzleError('Drizzle client is not established'))
140
+ }
141
+
142
+ const { eq } = await getDrizzleOperators();
143
+
144
+ const [result] = await this.drizzleClient
145
+ .delete(this.schema)
146
+ .where(eq(this.schema.key, rlKey))
147
+ .returning({ key: this.schema.key });
148
+
149
+ return !!result?.key
150
+ }
151
+
152
+ _clearExpiredHourAgo() {
153
+ if (this._clearExpiredTimeoutId) {
154
+ clearTimeout(this._clearExpiredTimeoutId);
155
+ }
156
+
157
+ this._clearExpiredTimeoutId = setTimeout(async () => {
158
+ try {
159
+ const { lt } = await getDrizzleOperators();
160
+ await this.drizzleClient
161
+ .delete(this.schema)
162
+ .where(lt(this.schema.expire, new Date(Date.now() - EXPIRED_THRESHOLD_MS)));
163
+ } catch (error) {
164
+ console.warn('Failed to clear expired records:', error);
165
+ }
166
+
167
+ this._clearExpiredHourAgo();
168
+ }, CLEANUP_INTERVAL_MS);
169
+
170
+ this._clearExpiredTimeoutId.unref();
171
+ }
172
+ }
173
+
174
+ module.exports = RateLimiterDrizzle;
@@ -0,0 +1,175 @@
1
+ let drizzleOperators = null;
2
+ const CLEANUP_INTERVAL_MS = 300000; // 5 minutes
3
+ const EXPIRED_THRESHOLD_MS = 3600000; // 1 hour
4
+
5
+ class RateLimiterDrizzleError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = 'RateLimiterDrizzleError';
9
+ }
10
+ }
11
+
12
+ async function getDrizzleOperators() {
13
+ if (drizzleOperators) return drizzleOperators;
14
+
15
+ try {
16
+ // Use dynamic import to prevent static analysis tools from detecting the import
17
+ function getPackageName() {
18
+ return ['drizzle', 'orm'].join('-');
19
+ }
20
+ const drizzleOrm = await import(`${getPackageName()}`);
21
+ const { and, or, gt, lt, eq, isNull, sql } = drizzleOrm.default || drizzleOrm;
22
+ drizzleOperators = { and, or, gt, lt, eq, isNull, sql };
23
+ return drizzleOperators;
24
+ } catch (error) {
25
+ throw new RateLimiterDrizzleError(
26
+ 'drizzle-orm is not installed. Please install drizzle-orm to use RateLimiterDrizzleNonAtomic.'
27
+ );
28
+ }
29
+ }
30
+
31
+ const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
32
+ const RateLimiterRes = require('./RateLimiterRes');
33
+
34
+ class RateLimiterDrizzleNonAtomic extends RateLimiterStoreAbstract {
35
+ constructor(opts) {
36
+ super(opts);
37
+
38
+ if (!opts?.schema) {
39
+ throw new RateLimiterDrizzleError('Drizzle schema is required');
40
+ }
41
+
42
+ if (!opts?.storeClient) {
43
+ throw new RateLimiterDrizzleError('Drizzle client is required');
44
+ }
45
+
46
+ this.schema = opts.schema;
47
+ this.drizzleClient = opts.storeClient;
48
+ this.clearExpiredByTimeout = opts.clearExpiredByTimeout ?? true;
49
+
50
+ if (this.clearExpiredByTimeout) {
51
+ this._clearExpiredHourAgo();
52
+ }
53
+ }
54
+
55
+ _getRateLimiterRes(rlKey, changedPoints, result) {
56
+ const res = new RateLimiterRes();
57
+
58
+ let doc = result;
59
+ res.isFirstInDuration = doc.points === changedPoints;
60
+ res.consumedPoints = doc.points;
61
+ res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
62
+ res.msBeforeNext = doc.expire !== null
63
+ ? Math.max(new Date(doc.expire).getTime() - Date.now(), 0)
64
+ : -1;
65
+
66
+ return res;
67
+ }
68
+
69
+ async _upsert(key, points, msDuration, forceExpire = false) {
70
+ if (!this.drizzleClient) {
71
+ return Promise.reject(new RateLimiterDrizzleError('Drizzle client is not established'));
72
+ }
73
+
74
+ const { eq } = await getDrizzleOperators();
75
+ const now = new Date();
76
+ const newExpire = msDuration > 0 ? new Date(now.getTime() + msDuration) : null;
77
+
78
+ const [existingRecord] = await this.drizzleClient
79
+ .select()
80
+ .from(this.schema)
81
+ .where(eq(this.schema.key, key))
82
+ .limit(1);
83
+
84
+ const shouldUpdateExpire =
85
+ forceExpire ||
86
+ !existingRecord ||
87
+ !existingRecord.expire ||
88
+ existingRecord.expire <= now ||
89
+ newExpire === null;
90
+
91
+ let newPoints;
92
+ if (existingRecord && !shouldUpdateExpire) {
93
+ newPoints = existingRecord.points + points;
94
+ } else {
95
+ newPoints = points;
96
+ }
97
+
98
+ const [data] = await this.drizzleClient
99
+ .insert(this.schema)
100
+ .values({
101
+ key,
102
+ points: newPoints,
103
+ expire: newExpire,
104
+ })
105
+ .onConflictDoUpdate({
106
+ target: this.schema.key,
107
+ set: {
108
+ points: newPoints,
109
+ ...(shouldUpdateExpire && { expire: newExpire }),
110
+ },
111
+ })
112
+ .returning();
113
+
114
+ return data;
115
+ }
116
+
117
+ async _get(rlKey) {
118
+ if (!this.drizzleClient) {
119
+ return Promise.reject(new RateLimiterDrizzleError('Drizzle client is not established'));
120
+ }
121
+
122
+ const { and, or, gt, eq, isNull } = await getDrizzleOperators();
123
+
124
+ const [response] = await this.drizzleClient
125
+ .select()
126
+ .from(this.schema)
127
+ .where(
128
+ and(
129
+ eq(this.schema.key, rlKey),
130
+ or(gt(this.schema.expire, new Date()), isNull(this.schema.expire))
131
+ )
132
+ )
133
+ .limit(1);
134
+
135
+ return response || null;
136
+ }
137
+
138
+ async _delete(rlKey) {
139
+ if (!this.drizzleClient) {
140
+ return Promise.reject(new RateLimiterDrizzleError('Drizzle client is not established'));
141
+ }
142
+
143
+ const { eq } = await getDrizzleOperators();
144
+
145
+ const [result] = await this.drizzleClient
146
+ .delete(this.schema)
147
+ .where(eq(this.schema.key, rlKey))
148
+ .returning({ key: this.schema.key });
149
+
150
+ return !!(result && result.key);
151
+ }
152
+
153
+ _clearExpiredHourAgo() {
154
+ if (this._clearExpiredTimeoutId) {
155
+ clearTimeout(this._clearExpiredTimeoutId);
156
+ }
157
+
158
+ this._clearExpiredTimeoutId = setTimeout(async () => {
159
+ try {
160
+ const { lt } = await getDrizzleOperators();
161
+ await this.drizzleClient
162
+ .delete(this.schema)
163
+ .where(lt(this.schema.expire, new Date(Date.now() - EXPIRED_THRESHOLD_MS)));
164
+ } catch (error) {
165
+ console.warn('Failed to clear expired records:', error);
166
+ }
167
+
168
+ this._clearExpiredHourAgo();
169
+ }, CLEANUP_INTERVAL_MS);
170
+
171
+ this._clearExpiredTimeoutId.unref();
172
+ }
173
+ }
174
+
175
+ module.exports = RateLimiterDrizzleNonAtomic;