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 CHANGED
@@ -1,11 +1,40 @@
1
- # Redis Rate Limiter
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
- - ✅ Fixed Window
8
- - ✅ Sliding Window
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
- - Node.js
17
- - Redis Server
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
- npm install redis-traffic-limiter redis
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
- - `examples/fixed.js`
80
- - `examples/sliding.js`
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
- - Fixed Window
99
- - Sliding Window
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
- - The package expects a **connected node-redis client**.
106
- - Redis connections are managed by your application.
107
- - `window` is specified in **milliseconds**.
108
- - Currently supports the official **node-redis** client only.
109
- - Support for additional Redis clients (e.g. ioredis) is planned for future releases.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redis-traffic-limiter",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A lightweight Redis-based rate limiter for Node.js supporting Fixed Window and Sliding Window algorithms.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
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
- class FixedWindow {
1
+ import BaseLimiter from "./BaseLimiter.js";
2
+
3
+ class FixedWindow extends BaseLimiter {
2
4
 
3
5
  constructor({ redis, window, max }) {
4
- this.redis = redis;
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
- class SlidingWindow {
1
+ import BaseLimiter from "./BaseLimiter.js";
2
+
3
+ class SlidingWindow extends BaseLimiter {
2
4
 
3
5
  constructor({ redis, window, max }) {
4
- this.redis = redis;
5
- this.window = window;
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;