rate-limiter-flexible 11.1.0 → 11.2.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.
@@ -68,11 +68,19 @@ module.exports = class RateLimiterAbstract {
68
68
  }
69
69
 
70
70
  get execEvenlyMinDelayMs() {
71
- return this._execEvenlyMinDelayMs;
71
+ return this._execEvenlyMinDelayMs === undefined
72
+ ? this._getExecEvenlyMinDelayMsDefault()
73
+ : this._execEvenlyMinDelayMs;
72
74
  }
73
75
 
74
76
  set execEvenlyMinDelayMs(value) {
75
- this._execEvenlyMinDelayMs = typeof value === 'undefined' ? Math.ceil(this.msDuration / this.points) : value;
77
+ this._execEvenlyMinDelayMs = value;
78
+ }
79
+
80
+ _getExecEvenlyMinDelayMsDefault() {
81
+ return this.points > 0
82
+ ? Math.ceil(this.msDuration / this.points)
83
+ : 0;
76
84
  }
77
85
 
78
86
  get keyPrefix() {
@@ -29,7 +29,7 @@ class RateLimiterMemory extends RateLimiterAbstract {
29
29
  res = this._memoryStorage.set(rlKey, res.consumedPoints, this.blockDuration);
30
30
  }
31
31
  reject(res);
32
- } else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) {
32
+ } else if (this.execEvenly && this.points > 0 && res.msBeforeNext > 0 && !res.isFirstInDuration) {
33
33
  // Execute evenly
34
34
  let delay = Math.ceil(res.msBeforeNext / (res.remainingPoints + 2));
35
35
  if (delay < this.execEvenlyMinDelayMs) {
@@ -262,7 +262,13 @@ class RateLimiterPostgres extends RateLimiterStoreAbstract {
262
262
 
263
263
  _query(q) {
264
264
  const prefix = this.tableName.toLowerCase();
265
- const queryObj = { name: `${prefix}:${q.name}`, text: q.text, values: q.values };
265
+ const queryObj = { text: q.text, values: q.values };
266
+ // Only name the prepared statement when a name is provided. The one-off
267
+ // create-table query passes no name, and naming it `${prefix}:undefined`
268
+ // pollutes the prepared-statement cache (see #196).
269
+ if (q.name) {
270
+ queryObj.name = `${prefix}:${q.name}`;
271
+ }
266
272
  return new Promise((resolve, reject) => {
267
273
  this._getConnection()
268
274
  .then((conn) => {
@@ -25,7 +25,7 @@ module.exports = class RateLimiterQueue {
25
25
  }
26
26
  }
27
27
 
28
- removeTokens(tokens, key = KEY_DEFAULT) {
28
+ removeTokens(tokens, key = KEY_DEFAULT, expiresUnixAt = 0) {
29
29
  if (!this._queueLimiters[key]) {
30
30
  this._queueLimiters[key] = new RateLimiterQueueInternal(
31
31
  this._limiterFlexible, {
@@ -34,7 +34,7 @@ module.exports = class RateLimiterQueue {
34
34
  })
35
35
  }
36
36
 
37
- return this._queueLimiters[key].removeTokens(tokens)
37
+ return this._queueLimiters[key].removeTokens(tokens, expiresUnixAt)
38
38
  }
39
39
  };
40
40
 
@@ -48,6 +48,12 @@ class RateLimiterQueueInternal {
48
48
  this._waitTimeout = null;
49
49
  this._queue = [];
50
50
  this._limiterFlexible = limiterFlexible;
51
+ // Set to true once a request carrying an expiration deadline
52
+ // (expiresUnixAt > 0) has been queued. While false, _processFIFO skips the
53
+ // expiry sweep entirely, so projects that never pass expiresUnixAt pay no
54
+ // extra cost. It is only ever set, never cleared by the sweep (see
55
+ // _processFIFO for why clearing it would be unsafe).
56
+ this._hasExpiringRequests = false;
51
57
 
52
58
  this._maxQueueSize = opts.maxQueueSize
53
59
  }
@@ -59,7 +65,7 @@ class RateLimiterQueueInternal {
59
65
  })
60
66
  }
61
67
 
62
- removeTokens(tokens) {
68
+ removeTokens(tokens, expiresUnixAt = 0) {
63
69
  const _this = this;
64
70
 
65
71
  return new Promise((resolve, reject) => {
@@ -69,7 +75,7 @@ class RateLimiterQueueInternal {
69
75
  }
70
76
 
71
77
  if (_this._queue.length > 0) {
72
- _this._queueRequest.call(_this, resolve, reject, tokens);
78
+ _this._queueRequest.call(_this, resolve, reject, tokens, expiresUnixAt);
73
79
  } else {
74
80
  _this._limiterFlexible.consume(_this._key, tokens)
75
81
  .then((res) => {
@@ -79,7 +85,7 @@ class RateLimiterQueueInternal {
79
85
  if (rej instanceof Error) {
80
86
  reject(rej);
81
87
  } else {
82
- _this._queueRequest.call(_this, resolve, reject, tokens);
88
+ _this._queueRequest.call(_this, resolve, reject, tokens, expiresUnixAt);
83
89
  if (_this._waitTimeout === null) {
84
90
  _this._waitTimeout = setTimeout(_this._processFIFO.bind(_this), rej.msBeforeNext);
85
91
  }
@@ -89,10 +95,13 @@ class RateLimiterQueueInternal {
89
95
  })
90
96
  }
91
97
 
92
- _queueRequest(resolve, reject, tokens) {
98
+ _queueRequest(resolve, reject, tokens, expiresUnixAt = 0) {
93
99
  const _this = this;
94
100
  if (_this._queue.length < _this._maxQueueSize) {
95
- _this._queue.push({resolve, reject, tokens});
101
+ _this._queue.push({resolve, reject, tokens, expiresUnixAt});
102
+ if (expiresUnixAt > 0) {
103
+ _this._hasExpiringRequests = true;
104
+ }
96
105
  } else {
97
106
  reject(new RateLimiterQueueError(`Number of requests reached it's maximum ${_this._maxQueueSize}`))
98
107
  }
@@ -106,6 +115,28 @@ class RateLimiterQueueInternal {
106
115
  _this._waitTimeout = null;
107
116
  }
108
117
 
118
+ // Reject any queued requests that have reached their expiration deadline
119
+ // (expiresUnixAt, in Unix seconds) before they could be fulfilled. The
120
+ // sweep is skipped until a request with a deadline has been queued, so
121
+ // projects that never pass expiresUnixAt keep the original O(1) cost here.
122
+ //
123
+ // The flag is deliberately only ever set, never cleared from a snapshot of
124
+ // _queue: while a request is being consumed it is momentarily shift()ed out
125
+ // of _queue and may be unshift()ed back by the rate-limit retry path below
126
+ // (without going through _queueRequest). Clearing the flag from a snapshot
127
+ // that excludes such an in-flight request could strand it with the sweep
128
+ // permanently disabled, so it would never expire.
129
+ if (_this._hasExpiringRequests) {
130
+ const nowSecs = Math.floor(Date.now() / 1000);
131
+ _this._queue = _this._queue.filter((item) => {
132
+ if (item.expiresUnixAt > 0 && nowSecs >= item.expiresUnixAt) {
133
+ item.reject(new RateLimiterQueueError('The request to remove tokens expired before it could be fulfilled'));
134
+ return false;
135
+ }
136
+ return true;
137
+ });
138
+ }
139
+
109
140
  if (_this._queue.length === 0) {
110
141
  return;
111
142
  }
@@ -79,7 +79,7 @@ module.exports = class RateLimiterStoreAbstract extends RateLimiterInsuredAbstra
79
79
  .catch((err) => {
80
80
  reject(err);
81
81
  });
82
- } else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) {
82
+ } else if (this.execEvenly && this.points > 0 && res.msBeforeNext > 0 && !res.isFirstInDuration) {
83
83
  let delay = Math.ceil(res.msBeforeNext / (res.remainingPoints + 2));
84
84
  if (delay < this.execEvenlyMinDelayMs) {
85
85
  delay = res.consumedPoints * this.execEvenlyMinDelayMs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rate-limiter-flexible",
3
- "version": "11.1.0",
3
+ "version": "11.2.0",
4
4
  "description": "Node.js atomic and non-atomic counters, rate limiting tools, protection from DoS and brute-force attacks at scale",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/types.d.ts CHANGED
@@ -514,7 +514,16 @@ export class RateLimiterQueue {
514
514
 
515
515
  getTokensRemaining(key?: string | number): Promise<number>;
516
516
 
517
- removeTokens(tokens: number, key?: string | number): Promise<number>;
517
+ /**
518
+ * Remove tokens from the queue.
519
+ *
520
+ * @param tokens Number of tokens to remove.
521
+ * @param key Optional queue key for separate FIFO queues.
522
+ * @param expiresUnixAt Optional absolute deadline as a Unix timestamp in
523
+ * seconds. If the request is still queued when this time is reached, it is
524
+ * rejected with a `RateLimiterQueueError`. Defaults to `0` (never expires).
525
+ */
526
+ removeTokens(tokens: number, key?: string | number, expiresUnixAt?: number): Promise<number>;
518
527
  }
519
528
 
520
529
  export class BurstyRateLimiter {