redis-traffic-limiter 1.0.0 → 1.1.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/README.md +113 -16
- package/example/TokenBucket.js +22 -0
- package/package.json +1 -1
- package/src/index.js +2 -1
- package/src/limiter/BaseLimiter.js +44 -0
- package/src/limiter/FixedWindow.js +14 -6
- package/src/limiter/SlidingWindow.js +15 -14
- package/src/limiter/TokenBucket.js +80 -0
package/README.md
CHANGED
|
@@ -1,11 +1,40 @@
|
|
|
1
|
-
# Redis
|
|
1
|
+
# Redis Traffic Limiter
|
|
2
2
|
|
|
3
3
|
A lightweight Redis-based rate limiter for Node.js using Redis.
|
|
4
4
|
|
|
5
5
|
Currently supported algorithms:
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
* ✅ Fixed Window
|
|
8
|
+
* ✅ Sliding Window
|
|
9
|
+
* ✅ Token Bucket
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## What's New in v1.1.0
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
* ✅ Token Bucket rate limiting algorithm.
|
|
18
|
+
|
|
19
|
+
### Improved
|
|
20
|
+
|
|
21
|
+
* Introduced a shared `BaseLimiter` for cleaner architecture.
|
|
22
|
+
* Added constructor validation with clear error messages for invalid configuration.
|
|
23
|
+
* Standardized the response object across all algorithms.
|
|
24
|
+
|
|
25
|
+
All limiters now return the same response format:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
{
|
|
29
|
+
allowed: true,
|
|
30
|
+
limit: 100,
|
|
31
|
+
remaining: 99,
|
|
32
|
+
retryAfter: null,
|
|
33
|
+
resetTime: null
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
> **Note:** `retryAfter` and `resetTime` are currently `null` and will be fully implemented in future releases.
|
|
9
38
|
|
|
10
39
|
---
|
|
11
40
|
|
|
@@ -13,8 +42,8 @@ Currently supported algorithms:
|
|
|
13
42
|
|
|
14
43
|
Before using this package, make sure you have:
|
|
15
44
|
|
|
16
|
-
|
|
17
|
-
|
|
45
|
+
* Node.js
|
|
46
|
+
* Redis Server
|
|
18
47
|
|
|
19
48
|
---
|
|
20
49
|
|
|
@@ -51,7 +80,7 @@ docker run -d \
|
|
|
51
80
|
Install the package along with the official Redis client.
|
|
52
81
|
|
|
53
82
|
```bash
|
|
54
|
-
|
|
83
|
+
npm install redis-traffic-limiter redis
|
|
55
84
|
```
|
|
56
85
|
|
|
57
86
|
---
|
|
@@ -76,8 +105,9 @@ Pass the connected Redis client while creating the limiter.
|
|
|
76
105
|
|
|
77
106
|
Example programs are available in the `examples/` directory.
|
|
78
107
|
|
|
79
|
-
|
|
80
|
-
|
|
108
|
+
* `examples/fixed.js`
|
|
109
|
+
* `examples/sliding.js`
|
|
110
|
+
* `examples/tokenBucket.js`
|
|
81
111
|
|
|
82
112
|
Run them using:
|
|
83
113
|
|
|
@@ -91,25 +121,92 @@ or
|
|
|
91
121
|
node examples/sliding.js
|
|
92
122
|
```
|
|
93
123
|
|
|
124
|
+
or
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
node examples/tokenBucket.js
|
|
128
|
+
```
|
|
129
|
+
|
|
94
130
|
---
|
|
95
131
|
|
|
96
132
|
## Supported Algorithms
|
|
97
133
|
|
|
98
|
-
|
|
99
|
-
|
|
134
|
+
### Fixed Window
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
import { FixedWindow } from "redis-traffic-limiter";
|
|
138
|
+
|
|
139
|
+
const limiter = new FixedWindow({
|
|
140
|
+
redis,
|
|
141
|
+
window: 60000,
|
|
142
|
+
max: 100
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### Sliding Window
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
import { SlidingWindow } from "redis-traffic-limiter";
|
|
152
|
+
|
|
153
|
+
const limiter = new SlidingWindow({
|
|
154
|
+
redis,
|
|
155
|
+
window: 60000,
|
|
156
|
+
max: 100
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### Token Bucket
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
import { TokenBucket } from "redis-traffic-limiter";
|
|
166
|
+
|
|
167
|
+
const limiter = new TokenBucket({
|
|
168
|
+
redis,
|
|
169
|
+
capacity: 100,
|
|
170
|
+
refillRate: 10,
|
|
171
|
+
interval: 60
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Where:
|
|
176
|
+
|
|
177
|
+
* `capacity` – Maximum number of tokens the bucket can hold.
|
|
178
|
+
* `refillRate` – Number of tokens added every interval.
|
|
179
|
+
* `interval` – Refill interval in seconds.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Response Format
|
|
184
|
+
|
|
185
|
+
Every limiter returns a standardized response object.
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
{
|
|
189
|
+
allowed: true,
|
|
190
|
+
limit: 100,
|
|
191
|
+
remaining: 99,
|
|
192
|
+
retryAfter: null,
|
|
193
|
+
resetTime: null
|
|
194
|
+
}
|
|
195
|
+
```
|
|
100
196
|
|
|
101
197
|
---
|
|
102
198
|
|
|
103
199
|
## Notes
|
|
104
200
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
201
|
+
* The package expects a **connected node-redis client**.
|
|
202
|
+
* Redis connections are managed by your application.
|
|
203
|
+
* `window` is specified in **milliseconds**.
|
|
204
|
+
* `interval` for the Token Bucket algorithm is specified in **seconds**.
|
|
205
|
+
* Currently supports the official **node-redis** client only.
|
|
206
|
+
* Support for additional Redis clients (e.g. ioredis), Express middleware, Lua scripts for atomic operations, and additional features are planned for future releases.
|
|
110
207
|
|
|
111
208
|
---
|
|
112
209
|
|
|
113
210
|
## License
|
|
114
211
|
|
|
115
|
-
MIT
|
|
212
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createClient } from "redis";
|
|
2
|
+
import { TokenBucket } from "../src";
|
|
3
|
+
|
|
4
|
+
// Create a Redis client.
|
|
5
|
+
const redis = createClient({
|
|
6
|
+
url: "redis://localhost:6379"
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// Connect to Redis.
|
|
10
|
+
await redis.connect();
|
|
11
|
+
|
|
12
|
+
const limiter = new TokenBucket({
|
|
13
|
+
redis,
|
|
14
|
+
capacity: 10,
|
|
15
|
+
refillRate: 5,
|
|
16
|
+
interval: 60
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
for (let i = 1; i <= 12; i++) {
|
|
20
|
+
const result = await limiter.consume("user:1");
|
|
21
|
+
console.log(i, result);
|
|
22
|
+
}
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { default as FixedWindow } from "./limiter/FixedWindow.js";
|
|
2
|
-
export { default as SlidingWindow } from "./limiter/SlidingWindow.js";
|
|
2
|
+
export { default as SlidingWindow } from "./limiter/SlidingWindow.js";
|
|
3
|
+
export { default as TokenBucket } from "./limiter/TokenBucket.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
class BaseLimiter {
|
|
2
|
+
constructor({ redis }) {
|
|
3
|
+
this.validateRedis(redis);
|
|
4
|
+
this.redis = redis;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
validateRedis(redis) {
|
|
8
|
+
if (!redis) {
|
|
9
|
+
throw new Error("A connected Redis client is required.");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (
|
|
13
|
+
typeof redis.get !== "function" ||
|
|
14
|
+
typeof redis.set !== "function"
|
|
15
|
+
) {
|
|
16
|
+
throw new Error("Invalid Redis client provided.");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
validatePositiveInteger(value, name) {
|
|
21
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
22
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
createResponse({
|
|
28
|
+
allowed,
|
|
29
|
+
limit,
|
|
30
|
+
remaining,
|
|
31
|
+
retryAfter = null,
|
|
32
|
+
resetTime = null
|
|
33
|
+
}) {
|
|
34
|
+
return {
|
|
35
|
+
allowed,
|
|
36
|
+
limit,
|
|
37
|
+
remaining,
|
|
38
|
+
retryAfter,
|
|
39
|
+
resetTime
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default BaseLimiter;
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import BaseLimiter from "./BaseLimiter.js";
|
|
2
|
+
|
|
3
|
+
class FixedWindow extends BaseLimiter {
|
|
2
4
|
|
|
3
5
|
constructor({ redis, window, max }) {
|
|
4
|
-
|
|
6
|
+
super({ redis });
|
|
7
|
+
|
|
8
|
+
this.validatePositiveInteger(window, "window");
|
|
9
|
+
this.validatePositiveInteger(max, "max");
|
|
10
|
+
|
|
5
11
|
this.window = window;
|
|
6
12
|
this.max = max;
|
|
7
13
|
}
|
|
@@ -15,16 +21,18 @@ class FixedWindow {
|
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
if (count <= this.max) {
|
|
18
|
-
return {
|
|
24
|
+
return this.createResponse({
|
|
19
25
|
allowed: true,
|
|
26
|
+
limit: this.max,
|
|
20
27
|
remaining: this.max - count
|
|
21
|
-
};
|
|
28
|
+
});
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
return {
|
|
31
|
+
return this.createResponse({
|
|
25
32
|
allowed: false,
|
|
33
|
+
limit: this.max,
|
|
26
34
|
remaining: 0
|
|
27
|
-
};
|
|
35
|
+
});
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import BaseLimiter from "./BaseLimiter.js";
|
|
2
|
+
|
|
3
|
+
class SlidingWindow extends BaseLimiter {
|
|
2
4
|
|
|
3
5
|
constructor({ redis, window, max }) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
super({ redis });
|
|
7
|
+
|
|
8
|
+
this.validatePositiveInteger(window, "window");
|
|
9
|
+
this.validatePositiveInteger(max, "max");
|
|
10
|
+
|
|
11
|
+
this.window = window;
|
|
6
12
|
this.max = max;
|
|
7
13
|
}
|
|
8
14
|
|
|
@@ -10,42 +16,37 @@ class SlidingWindow {
|
|
|
10
16
|
|
|
11
17
|
const now = Date.now();
|
|
12
18
|
|
|
13
|
-
|
|
14
19
|
await this.redis.zRemRangeByScore(
|
|
15
20
|
key,
|
|
16
21
|
0,
|
|
17
22
|
now - this.window
|
|
18
23
|
);
|
|
19
24
|
|
|
20
|
-
|
|
21
25
|
const count = await this.redis.zCard(key);
|
|
22
26
|
|
|
23
|
-
|
|
24
27
|
if (count >= this.max) {
|
|
25
|
-
|
|
26
|
-
return {
|
|
28
|
+
return this.createResponse({
|
|
27
29
|
allowed: false,
|
|
30
|
+
limit: this.max,
|
|
28
31
|
remaining: 0
|
|
29
|
-
};
|
|
30
|
-
|
|
32
|
+
});
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
|
|
34
35
|
await this.redis.zAdd(key, {
|
|
35
36
|
score: now,
|
|
36
37
|
value: `${now}-${Math.random()}`
|
|
37
38
|
});
|
|
38
39
|
|
|
39
|
-
|
|
40
40
|
await this.redis.expire(
|
|
41
41
|
key,
|
|
42
42
|
Math.ceil(this.window / 1000)
|
|
43
43
|
);
|
|
44
44
|
|
|
45
|
-
return {
|
|
45
|
+
return this.createResponse({
|
|
46
46
|
allowed: true,
|
|
47
|
+
limit: this.max,
|
|
47
48
|
remaining: this.max - count - 1
|
|
48
|
-
};
|
|
49
|
+
});
|
|
49
50
|
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import BaseLimiter from "./BaseLimiter.js";
|
|
2
|
+
|
|
3
|
+
class TokenBucket extends BaseLimiter {
|
|
4
|
+
|
|
5
|
+
constructor({ redis, capacity, refillRate, interval }) {
|
|
6
|
+
super({ redis });
|
|
7
|
+
|
|
8
|
+
this.validatePositiveInteger(capacity, "capacity");
|
|
9
|
+
this.validatePositiveInteger(refillRate, "refillRate");
|
|
10
|
+
this.validatePositiveInteger(interval, "interval");
|
|
11
|
+
|
|
12
|
+
this.capacity = capacity;
|
|
13
|
+
this.refillRate = refillRate;
|
|
14
|
+
this.interval = interval;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async consume(key) {
|
|
18
|
+
|
|
19
|
+
const tokenKey = `${key}:tokens`;
|
|
20
|
+
const timeKey = `${key}:lastRefill`;
|
|
21
|
+
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
|
|
24
|
+
const storedTokens = await this.redis.get(tokenKey);
|
|
25
|
+
const storedLastRefill = await this.redis.get(timeKey);
|
|
26
|
+
|
|
27
|
+
let tokens;
|
|
28
|
+
let lastRefill;
|
|
29
|
+
|
|
30
|
+
// First request
|
|
31
|
+
if (storedTokens === null || storedLastRefill === null) {
|
|
32
|
+
tokens = this.capacity;
|
|
33
|
+
lastRefill = now;
|
|
34
|
+
} else {
|
|
35
|
+
tokens = Number(storedTokens);
|
|
36
|
+
lastRefill = Number(storedLastRefill);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Calculate elapsed time
|
|
40
|
+
const elapsed = (now - lastRefill) / 1000;
|
|
41
|
+
|
|
42
|
+
// Number of complete intervals passed
|
|
43
|
+
const intervalsPassed = Math.floor(elapsed / this.interval);
|
|
44
|
+
|
|
45
|
+
// Refill tokens
|
|
46
|
+
if (intervalsPassed > 0) {
|
|
47
|
+
tokens = Math.min(
|
|
48
|
+
this.capacity,
|
|
49
|
+
tokens + intervalsPassed * this.refillRate
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
lastRefill += intervalsPassed * this.interval * 1000;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (tokens <= 0) {
|
|
56
|
+
|
|
57
|
+
await this.redis.set(tokenKey, tokens);
|
|
58
|
+
await this.redis.set(timeKey, lastRefill);
|
|
59
|
+
|
|
60
|
+
return this.createResponse({
|
|
61
|
+
allowed: false,
|
|
62
|
+
limit: this.capacity,
|
|
63
|
+
remaining: 0
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
tokens--;
|
|
68
|
+
|
|
69
|
+
await this.redis.set(tokenKey, tokens);
|
|
70
|
+
await this.redis.set(timeKey, lastRefill);
|
|
71
|
+
|
|
72
|
+
return this.createResponse({
|
|
73
|
+
allowed: true,
|
|
74
|
+
limit: this.capacity,
|
|
75
|
+
remaining: tokens
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default TokenBucket;
|