@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 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 --save rate-limiter-redis
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.value will return null
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
- const { REDIS_HOST, REDIS_USER, REDIS_PASSWORD, REDIS_PORT, } = process.env;
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
- const redisURL = `redis://${REDIS_USER}:${REDIS_PASSWORD}@${this.host}:${this.port}`;
39
- this.client = redis_1.default.createClient({ url: redisURL, socket: { connectTimeout: 5000 } });
40
- this.client.on('error', this.onError);
41
- this.client.on('connect', this.onConnect);
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* () {
@@ -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>}
@@ -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
- context === null || context === void 0 ? void 0 : context.addEventListener('abort', this.abortHandler);
88
- const response = yield this.client.RPOP(TokenBucket.BUCKET_NAME);
89
- if (!response) {
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(response);
93
- token.remaining = yield this.getTotalTokens();
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(TokenBucket.BUCKET_NAME);
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(TokenBucket.BUCKET_NAME, tokens);
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(TokenBucket.BUCKET_NAME);
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(TokenBucket.BUCKET_NAME, 1, 0);
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.2",
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",