sliding-window-limiter 1.0.1 → 1.1.1

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.
@@ -1,4 +1,5 @@
1
- const {isNumber} = require('lodash/fp');
1
+ const {isNumber, isObject, isString} = require('lodash/fp');
2
+ const {DateTime} = require('luxon');
2
3
  const {Window} = require('./window');
3
4
  const {NullStore} = require('./null_store');
4
5
 
@@ -42,7 +43,7 @@ class RateLimiter {
42
43
  }
43
44
  }
44
45
 
45
- // should update the buckets according to configuration
46
+ // should update the buckets according to configuration.
46
47
  // returns true if update successful
47
48
  async update(value, timestamp) {
48
49
  if (!isNumber(value) || value <= 0)
@@ -62,6 +63,29 @@ class RateLimiter {
62
63
  }
63
64
  }
64
65
 
66
+ // should update the buckets according to configuration.
67
+ // only updates the time and recalculates window's buckets
68
+ async updateTime(timestamp) {
69
+ let _timestamp = timestamp;
70
+ if (timestamp instanceof Date)
71
+ _timestamp = DateTime.fromJSDate(timestamp);
72
+ if (isString(timestamp))
73
+ _timestamp = DateTime.fromISO(timestamp);
74
+ if (DateTime.isDateTime(_timestamp) && !_timestamp.isValid)
75
+ throw new TypeError('timestamp must be valid DateTime, Date or ISO string');
76
+ if (!_timestamp)
77
+ throw new TypeError('timestamp is required');
78
+
79
+ try {
80
+ this.window = this.window || Window.fromConfig(this.windowConfig);
81
+ this.window.recalculate(_timestamp);
82
+ await this.store.set(this.name, this.window.toObject());
83
+ }
84
+ catch (err) {
85
+ throw new Error(`Failed to update rate-limit window ${this.name}`, {cause: err});
86
+ }
87
+ }
88
+
65
89
  set(state) {
66
90
  if (this.window) return this;
67
91
  this.window = state ? new Window(state) : Window.fromConfig(this.windowConfig);
package/lib/window.js CHANGED
@@ -45,12 +45,13 @@ class Window {
45
45
  _timestamp = DateTime.fromJSDate(timestamp);
46
46
  if (isString(timestamp))
47
47
  _timestamp = DateTime.fromISO(timestamp);
48
- if (!_timestamp.isValid)
48
+ if (DateTime.isDateTime(_timestamp) && !_timestamp.isValid)
49
49
  throw new TypeError('timestamp must be valid DateTime, Date or ISO string');
50
+ if (!_timestamp)
51
+ throw new TypeError('timestamp is required');
50
52
 
51
53
  this.recalculate(_timestamp);
52
54
  this.timeSlices[0] += value;
53
- this.updated = _timestamp;
54
55
  }
55
56
 
56
57
  slicesBetween(now, start) {
@@ -79,6 +80,7 @@ class Window {
79
80
  ...slice(0, -invalidSlices)(this.timeSlices),
80
81
  ];
81
82
  }
83
+ this.updated = timestamp;
82
84
  }
83
85
 
84
86
  valueOf() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliding-window-limiter",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "Implementation of the Sliding Window rate limiting algorithm",
5
5
  "keywords": [
6
6
  "sliding-window",
@@ -147,4 +147,44 @@ describe('RateLimiter', () => {
147
147
  await expect(fn()).rejects.toThrow('Failed to update rate-limit window ip-req-per-minute-max10-u00001');
148
148
  });
149
149
  });
150
+
151
+ describe('#updateTime', () => {
152
+ test('should update window with ISO timestamp string', async () => {
153
+ const lim = new RateLimiter(DefaultOptions());
154
+ await expect(lim.updateTime('2025-01-01T00:00:30Z')).resolves.toBeUndefined();
155
+ expect(MockStore.set).toHaveBeenCalled();
156
+ });
157
+
158
+ test('should update window with Date object', async () => {
159
+ const lim = new RateLimiter(DefaultOptions());
160
+ const date = new Date('2025-01-01T00:00:30Z');
161
+ await expect(lim.updateTime(date)).resolves.toBeUndefined();
162
+ expect(MockStore.set).toHaveBeenCalled();
163
+ });
164
+
165
+ test('should throw error if timestamp is invalid', async () => {
166
+ const lim = new RateLimiter(DefaultOptions());
167
+ const fn = () => lim.updateTime('invalid-timestamp');
168
+ await expect(fn()).rejects.toThrow('timestamp must be valid DateTime, Date or ISO string');
169
+ });
170
+
171
+ test('should throw error if timestamp is null or undefined', async () => {
172
+ const lim = new RateLimiter(DefaultOptions());
173
+ const fn = () => lim.updateTime(null);
174
+ await expect(fn()).rejects.toThrow('timestamp is required');
175
+ });
176
+
177
+ test('should throw an error if failed to update store', async () => {
178
+ const lim = new RateLimiter({...DefaultOptions(), store: ErrorStore});
179
+ const fn = () => lim.updateTime('2025-01-01T00:00:30Z');
180
+ await expect(fn()).rejects.toThrow('Failed to update rate-limit window ip-req-per-minute-max10-u00001');
181
+ });
182
+
183
+ test('should initialize window if not already initialized', async () => {
184
+ const lim = new RateLimiter(DefaultOptions());
185
+ expect(lim.window).toBeFalsy();
186
+ await lim.updateTime('2025-01-01T00:00:30Z');
187
+ expect(lim.window).toBeTruthy();
188
+ });
189
+ });
150
190
  });