@fsilva3/rate-limiter-redis 0.0.2 → 0.0.4
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 +10 -15
- package/dist/bucket.d.ts +2 -0
- package/dist/bucket.js +22 -6
- package/dist/token-bucket.d.ts +2 -2
- package/dist/token-bucket.js +29 -16
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
@@ -20,7 +20,7 @@ This library is useful for controlling the rate of requests from your applicatio
|
|
20
20
|
To install the library, use npm:
|
21
21
|
|
22
22
|
```bash
|
23
|
-
npm i
|
23
|
+
npm i -P @fsilva3/rate-limiter-redis
|
24
24
|
```
|
25
25
|
|
26
26
|
## Usage
|
@@ -35,25 +35,19 @@ const second = 1000
|
|
35
35
|
// 60 tokens (requests) per minute
|
36
36
|
const tbSettings: TokenBucketSettings = {
|
37
37
|
capacity: 60,
|
38
|
-
interval: (60*second)
|
38
|
+
interval: (60*second),
|
39
|
+
key: 'my-rate-limiter-bucket' // optional key param to identify the bucket, otherwise it will use the default key
|
39
40
|
}
|
40
41
|
|
41
42
|
// Create a new token bucket instance
|
42
|
-
const bucket = await TokenBucket.create(tbSettings);
|
43
|
+
const bucket = await TokenBucket.create(tbSettings);
|
43
44
|
```
|
44
45
|
|
45
46
|
2. Take Method
|
46
47
|
```javascript
|
47
|
-
// Takes the first token created in the bucket, if exists! Otherwise the token
|
48
|
+
// Takes the first token created in the bucket, if exists! Otherwise the token will be null
|
48
49
|
const token = await bucket.take();
|
49
|
-
|
50
|
-
const {
|
51
|
-
value, // value is a hash string if a token is available, null otherwise
|
52
|
-
timestamp, // timestamp when the token was created
|
53
|
-
remaning // remaining is the number of tokens
|
54
|
-
} = token;
|
55
|
-
|
56
|
-
if (!value) {
|
50
|
+
if (!token) {
|
57
51
|
// re-queue the message, throw exception or return error
|
58
52
|
}
|
59
53
|
|
@@ -71,9 +65,9 @@ const controler = new AbortController()
|
|
71
65
|
const token = await bucket.delay(controler.signal);
|
72
66
|
|
73
67
|
const {
|
74
|
-
value,
|
75
|
-
timestamp,
|
76
|
-
remaning
|
68
|
+
value, // value is a hash string if a token is available, null otherwise
|
69
|
+
timestamp, // timestamp when the token was created
|
70
|
+
remaning // remaining is the number of tokens
|
77
71
|
} = token;
|
78
72
|
|
79
73
|
...
|
@@ -89,6 +83,7 @@ REDIS_HOST=localhost
|
|
89
83
|
REDIS_PORT=6379
|
90
84
|
REDIS_USER=default
|
91
85
|
REDIS_PASSWORD=mysecretpassword
|
86
|
+
REDIS_DATABASE=0
|
92
87
|
```
|
93
88
|
|
94
89
|
<br>
|
package/dist/bucket.d.ts
CHANGED
@@ -2,10 +2,12 @@ import { RedisClientType } from 'redis';
|
|
2
2
|
export default class Bucket {
|
3
3
|
private host;
|
4
4
|
private port;
|
5
|
+
private database;
|
5
6
|
protected client: RedisClientType;
|
6
7
|
private maxConnectionRetries;
|
7
8
|
private retryConnectionCount;
|
8
9
|
constructor();
|
10
|
+
private validateEnvVariables;
|
9
11
|
protected connect(): Promise<void>;
|
10
12
|
protected quit(): Promise<void>;
|
11
13
|
private onConnect;
|
package/dist/bucket.js
CHANGED
@@ -19,9 +19,19 @@ class Bucket {
|
|
19
19
|
constructor() {
|
20
20
|
this.host = '';
|
21
21
|
this.port = 6379;
|
22
|
+
this.database = 0;
|
22
23
|
this.maxConnectionRetries = 5;
|
23
24
|
this.retryConnectionCount = 0;
|
24
|
-
|
25
|
+
this.validateEnvVariables();
|
26
|
+
const { REDIS_HOST, REDIS_USER, REDIS_PASSWORD } = process.env;
|
27
|
+
this.host = REDIS_HOST;
|
28
|
+
const redisURL = `redis://${REDIS_USER}:${REDIS_PASSWORD}@${this.host}:${this.port}/${this.database}`;
|
29
|
+
this.client = redis_1.default.createClient({ url: redisURL, socket: { connectTimeout: 5000 } });
|
30
|
+
this.client.on('error', this.onError);
|
31
|
+
this.client.on('connect', this.onConnect);
|
32
|
+
}
|
33
|
+
validateEnvVariables() {
|
34
|
+
const { REDIS_HOST, REDIS_USER, REDIS_PASSWORD, REDIS_PORT, REDIS_DATABASE } = process.env;
|
25
35
|
if (!REDIS_HOST) {
|
26
36
|
throw new exception_1.RateLimiterException('REDIS_HOST environment is required');
|
27
37
|
}
|
@@ -31,14 +41,20 @@ class Bucket {
|
|
31
41
|
if (!REDIS_PASSWORD) {
|
32
42
|
throw new exception_1.RateLimiterException('REDIS_PASSWORD environment is required');
|
33
43
|
}
|
34
|
-
this.host = REDIS_HOST;
|
35
44
|
if (REDIS_PORT && parseInt(REDIS_PORT) !== 6379) {
|
45
|
+
const port = parseInt(REDIS_PORT);
|
46
|
+
if (Number.isNaN(port)) {
|
47
|
+
throw new exception_1.RateLimiterException('REDIS_PORT wrongly set, please use a correct number');
|
48
|
+
}
|
36
49
|
this.port = parseInt(REDIS_PORT);
|
37
50
|
}
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
51
|
+
if (REDIS_DATABASE && parseInt(REDIS_DATABASE) !== 0) {
|
52
|
+
const database = parseInt(REDIS_DATABASE);
|
53
|
+
if (Number.isNaN(database)) {
|
54
|
+
throw new exception_1.RateLimiterException('REDIS_DATABASE wrongly set, please use a correct number');
|
55
|
+
}
|
56
|
+
this.database = parseInt(REDIS_DATABASE);
|
57
|
+
}
|
42
58
|
}
|
43
59
|
connect() {
|
44
60
|
return __awaiter(this, void 0, void 0, function* () {
|
package/dist/token-bucket.d.ts
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
import { Token, TokenBucketSettings } from './types';
|
2
2
|
import Bucket from './bucket';
|
3
3
|
export default class TokenBucket extends Bucket {
|
4
|
-
private static readonly BUCKET_NAME;
|
5
4
|
private readonly maxDelayRetryCount;
|
5
|
+
private bucketName;
|
6
6
|
private capacity;
|
7
7
|
private interval;
|
8
8
|
private delayRetryCount;
|
@@ -11,7 +11,6 @@ export default class TokenBucket extends Bucket {
|
|
11
11
|
constructor(settings: TokenBucketSettings);
|
12
12
|
private generateToken;
|
13
13
|
private getNextExecutionInMilliseconds;
|
14
|
-
private abortHandler;
|
15
14
|
/**
|
16
15
|
* Static method to create a new TokenBucket instance to ensure the bucket is refilled
|
17
16
|
* @param {TokenBucketSettings} settings - The constructor settings for the Bucket
|
@@ -54,6 +53,7 @@ export default class TokenBucket extends Bucket {
|
|
54
53
|
* @return {Promise<void>}
|
55
54
|
*/
|
56
55
|
close(): Promise<void>;
|
56
|
+
isReady(): boolean;
|
57
57
|
/**
|
58
58
|
* Get the current total of tokens in the bucket
|
59
59
|
* @return {Promise<number>}
|
package/dist/token-bucket.js
CHANGED
@@ -25,6 +25,7 @@ class TokenBucket extends bucket_1.default {
|
|
25
25
|
constructor(settings) {
|
26
26
|
super();
|
27
27
|
this.maxDelayRetryCount = 5;
|
28
|
+
this.bucketName = 'rate-limiter-tokens';
|
28
29
|
this.capacity = 0;
|
29
30
|
this.interval = 0;
|
30
31
|
this.delayRetryCount = 0;
|
@@ -32,6 +33,9 @@ class TokenBucket extends bucket_1.default {
|
|
32
33
|
this.startTime = 0;
|
33
34
|
this.capacity = settings.capacity;
|
34
35
|
this.interval = settings.interval;
|
36
|
+
if (settings.key) {
|
37
|
+
this.bucketName = settings.key;
|
38
|
+
}
|
35
39
|
this.startTime = Date.now();
|
36
40
|
this.timer = setInterval(this.refill.bind(this), this.interval);
|
37
41
|
}
|
@@ -43,11 +47,6 @@ class TokenBucket extends bucket_1.default {
|
|
43
47
|
const nextExecution = this.interval - Math.ceil(elapsedTime % this.interval);
|
44
48
|
return nextExecution;
|
45
49
|
}
|
46
|
-
abortHandler() {
|
47
|
-
if (this.aborted) {
|
48
|
-
throw new Error('Operation aborted');
|
49
|
-
}
|
50
|
-
}
|
51
50
|
/**
|
52
51
|
* Static method to create a new TokenBucket instance to ensure the bucket is refilled
|
53
52
|
* @param {TokenBucketSettings} settings - The constructor settings for the Bucket
|
@@ -84,17 +83,29 @@ class TokenBucket extends bucket_1.default {
|
|
84
83
|
if (context === null || context === void 0 ? void 0 : context.aborted) {
|
85
84
|
throw new Error('Operation aborted');
|
86
85
|
}
|
87
|
-
|
88
|
-
|
89
|
-
|
86
|
+
const timeoutAbortSignal = new Promise((_, reject) => {
|
87
|
+
if (context) {
|
88
|
+
context.addEventListener('abort', () => reject(new Error('Operation aborted')));
|
89
|
+
}
|
90
|
+
});
|
91
|
+
const promises = Promise.all([
|
92
|
+
this.client.RPOP(this.bucketName),
|
93
|
+
this.getTotalTokens()
|
94
|
+
]);
|
95
|
+
const responses = yield Promise.race([promises, timeoutAbortSignal]);
|
96
|
+
const responsesArray = responses;
|
97
|
+
const tokenResponse = responsesArray[0];
|
98
|
+
if (!tokenResponse) {
|
90
99
|
return null;
|
91
100
|
}
|
92
|
-
const token = JSON.parse(
|
93
|
-
token.remaining =
|
94
|
-
context === null || context === void 0 ? void 0 : context.removeEventListener('abort', this.abortHandler);
|
101
|
+
const token = JSON.parse(tokenResponse);
|
102
|
+
token.remaining = responsesArray[1];
|
95
103
|
return token;
|
96
104
|
}
|
97
105
|
catch (error) {
|
106
|
+
if (error instanceof Error && error.message === 'Operation aborted') {
|
107
|
+
throw error;
|
108
|
+
}
|
98
109
|
throw new exception_1.RateLimiterException(`Error taking token from bucket | ${error}`);
|
99
110
|
}
|
100
111
|
});
|
@@ -137,7 +148,7 @@ class TokenBucket extends bucket_1.default {
|
|
137
148
|
if (!this.client.isReady) {
|
138
149
|
throw new exception_1.RateLimiterException('Redis client is not ready');
|
139
150
|
}
|
140
|
-
const countTokens = yield this.client.LLEN(
|
151
|
+
const countTokens = yield this.client.LLEN(this.bucketName);
|
141
152
|
if (countTokens >= this.capacity) {
|
142
153
|
return;
|
143
154
|
}
|
@@ -149,7 +160,7 @@ class TokenBucket extends bucket_1.default {
|
|
149
160
|
timestamp: Date.now()
|
150
161
|
});
|
151
162
|
});
|
152
|
-
yield this.client.LPUSH(
|
163
|
+
yield this.client.LPUSH(this.bucketName, tokens);
|
153
164
|
}
|
154
165
|
catch (error) {
|
155
166
|
throw new exception_1.RateLimiterException(`Error filling token bucket | ${error}`);
|
@@ -170,13 +181,16 @@ class TokenBucket extends bucket_1.default {
|
|
170
181
|
console.log('Closed Token Bucket instance!');
|
171
182
|
});
|
172
183
|
}
|
184
|
+
isReady() {
|
185
|
+
return this.client.isReady;
|
186
|
+
}
|
173
187
|
/**
|
174
188
|
* Get the current total of tokens in the bucket
|
175
189
|
* @return {Promise<number>}
|
176
190
|
*/
|
177
191
|
getTotalTokens() {
|
178
192
|
return __awaiter(this, void 0, void 0, function* () {
|
179
|
-
return this.client.LLEN(
|
193
|
+
return this.client.LLEN(this.bucketName);
|
180
194
|
});
|
181
195
|
}
|
182
196
|
/**
|
@@ -185,9 +199,8 @@ class TokenBucket extends bucket_1.default {
|
|
185
199
|
*/
|
186
200
|
clearTokens() {
|
187
201
|
return __awaiter(this, void 0, void 0, function* () {
|
188
|
-
yield this.client.LTRIM(
|
202
|
+
yield this.client.LTRIM(this.bucketName, 1, 0);
|
189
203
|
});
|
190
204
|
}
|
191
205
|
}
|
192
|
-
TokenBucket.BUCKET_NAME = 'rate-limiter-tokens';
|
193
206
|
exports.default = TokenBucket;
|
package/dist/types.d.ts
CHANGED
@@ -1,6 +1,13 @@
|
|
1
|
+
/**
|
2
|
+
* @typedef {Object} TokenBucketSettings - creates a new type named 'TokenBucketSettings'
|
3
|
+
* @property {number} capacity - the total tokesn to be refilled in the bucket
|
4
|
+
* @property {number} interval - the time interval when it should refill the tokens in milliseconds
|
5
|
+
* @property {string?} key - an optional parameter to define a different bucket name key on redis list
|
6
|
+
*/
|
1
7
|
export type TokenBucketSettings = {
|
2
8
|
capacity: number;
|
3
9
|
interval: number;
|
10
|
+
key?: string;
|
4
11
|
};
|
5
12
|
export type Token = {
|
6
13
|
value: string;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@fsilva3/rate-limiter-redis",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.4",
|
4
4
|
"license": "MIT",
|
5
5
|
"description": "A javascript library to rate limiter requests using different algorithms using Redis as shared state where you can sync the tokens between multiple services instances",
|
6
6
|
"main": "./dist/index.js",
|