@fsilva3/rate-limiter-redis 0.0.2
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/LICENSE +21 -0
- package/README.md +115 -0
- package/dist/bucket.d.ts +13 -0
- package/dist/bucket.js +74 -0
- package/dist/exception.d.ts +3 -0
- package/dist/exception.js +10 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +12 -0
- package/dist/token-bucket.d.ts +67 -0
- package/dist/token-bucket.js +193 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +32 -0
- package/package.json +43 -0
package/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Filipe Silva
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# Rate Limiter Redis
|
2
|
+
|
3
|
+
Rate Limiter Redis is a JavaScript library that provides different algorithms to rate limit your requests using Redis as shared state between the distributed services.
|
4
|
+
|
5
|
+
This library is useful for controlling the rate of requests from your application to a third-party service or API.
|
6
|
+
|
7
|
+
## Reference
|
8
|
+
|
9
|
+
- [Redis Blog](https://redis.io/glossary/rate-limiting/)
|
10
|
+
- [Token Bucket](https://en.wikipedia.org/wiki/Token_bucket)
|
11
|
+
|
12
|
+
## Benefits
|
13
|
+
|
14
|
+
- **Cost-Effective**: Instead calling HTTP APIs and handling execptions might throw from the Third-party API, you can block the request before it reaches the API, or retry the request later.
|
15
|
+
|
16
|
+
- **Distributed**: Rate Limiter Redis is designed to work in a distributed environment, where multiple instances of your application can share the same rate limit.
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
To install the library, use npm:
|
21
|
+
|
22
|
+
```bash
|
23
|
+
npm i --save rate-limiter-redis
|
24
|
+
```
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
Here is a few examples of how to use Rate Limiter Redis:
|
29
|
+
|
30
|
+
1. Token Bucket Instance
|
31
|
+
```javascript
|
32
|
+
import { TokenBucket, TokenBucketSettings } from ('@fsilva3/rate-limiter-redis')
|
33
|
+
const second = 1000
|
34
|
+
|
35
|
+
// 60 tokens (requests) per minute
|
36
|
+
const tbSettings: TokenBucketSettings = {
|
37
|
+
capacity: 60,
|
38
|
+
interval: (60*second)
|
39
|
+
}
|
40
|
+
|
41
|
+
// Create a new token bucket instance
|
42
|
+
const bucket = await TokenBucket.create(tbSettings);
|
43
|
+
```
|
44
|
+
|
45
|
+
2. Take Method
|
46
|
+
```javascript
|
47
|
+
// Takes the first token created in the bucket, if exists! Otherwise the token.value will return null
|
48
|
+
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) {
|
57
|
+
// re-queue the message, throw exception or return error
|
58
|
+
}
|
59
|
+
|
60
|
+
...
|
61
|
+
// Call the Third-party API
|
62
|
+
```
|
63
|
+
|
64
|
+
3. Delay Method
|
65
|
+
```javascript
|
66
|
+
// Usually from HTTP frameworks to cancel requests
|
67
|
+
const controler = new AbortController()
|
68
|
+
|
69
|
+
// In case the bucket is empty, it will block the operation until receive a new token!
|
70
|
+
// This method accepts abort signal, which means you can cancel the operation at any time
|
71
|
+
const token = await bucket.delay(controler.signal);
|
72
|
+
|
73
|
+
const {
|
74
|
+
value,
|
75
|
+
timestamp,
|
76
|
+
remaning
|
77
|
+
} = token;
|
78
|
+
|
79
|
+
...
|
80
|
+
// Call the Third-party API
|
81
|
+
```
|
82
|
+
|
83
|
+
## Configuration
|
84
|
+
|
85
|
+
The `Redis` connection is made by using environment variables; make sure to have these keys available in your `.env` or any other environment setup:
|
86
|
+
```bash
|
87
|
+
# .env
|
88
|
+
REDIS_HOST=localhost
|
89
|
+
REDIS_PORT=6379
|
90
|
+
REDIS_USER=default
|
91
|
+
REDIS_PASSWORD=mysecretpassword
|
92
|
+
```
|
93
|
+
|
94
|
+
<br>
|
95
|
+
|
96
|
+
The `TokenBucket` constructor accepts the following options:
|
97
|
+
|
98
|
+
- `capacity`: The maximum number of tokens in the bucket.
|
99
|
+
- `interval`: The milliseconds interval at which tokens are added to the bucket
|
100
|
+
|
101
|
+
## License
|
102
|
+
|
103
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
104
|
+
|
105
|
+
## Contributing
|
106
|
+
|
107
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
108
|
+
|
109
|
+
## Acknowledgements
|
110
|
+
|
111
|
+
This library is inspired by the token bucket algorithm and uses Redis for storage.
|
112
|
+
|
113
|
+
## Contact
|
114
|
+
|
115
|
+
For any questions or issues, please open an issue on GitHub.
|
package/dist/bucket.d.ts
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
import { RedisClientType } from 'redis';
|
2
|
+
export default class Bucket {
|
3
|
+
private host;
|
4
|
+
private port;
|
5
|
+
protected client: RedisClientType;
|
6
|
+
private maxConnectionRetries;
|
7
|
+
private retryConnectionCount;
|
8
|
+
constructor();
|
9
|
+
protected connect(): Promise<void>;
|
10
|
+
protected quit(): Promise<void>;
|
11
|
+
private onConnect;
|
12
|
+
private onError;
|
13
|
+
}
|
package/dist/bucket.js
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
9
|
+
});
|
10
|
+
};
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
13
|
+
};
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
15
|
+
const redis_1 = __importDefault(require("redis"));
|
16
|
+
const exception_1 = require("./exception");
|
17
|
+
const utils_1 = require("./utils");
|
18
|
+
class Bucket {
|
19
|
+
constructor() {
|
20
|
+
this.host = '';
|
21
|
+
this.port = 6379;
|
22
|
+
this.maxConnectionRetries = 5;
|
23
|
+
this.retryConnectionCount = 0;
|
24
|
+
const { REDIS_HOST, REDIS_USER, REDIS_PASSWORD, REDIS_PORT, } = process.env;
|
25
|
+
if (!REDIS_HOST) {
|
26
|
+
throw new exception_1.RateLimiterException('REDIS_HOST environment is required');
|
27
|
+
}
|
28
|
+
if (!REDIS_USER) {
|
29
|
+
throw new exception_1.RateLimiterException('REDIS_USER environment is required');
|
30
|
+
}
|
31
|
+
if (!REDIS_PASSWORD) {
|
32
|
+
throw new exception_1.RateLimiterException('REDIS_PASSWORD environment is required');
|
33
|
+
}
|
34
|
+
this.host = REDIS_HOST;
|
35
|
+
if (REDIS_PORT && parseInt(REDIS_PORT) !== 6379) {
|
36
|
+
this.port = parseInt(REDIS_PORT);
|
37
|
+
}
|
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);
|
42
|
+
}
|
43
|
+
connect() {
|
44
|
+
return __awaiter(this, void 0, void 0, function* () {
|
45
|
+
if (this.retryConnectionCount >= this.maxConnectionRetries) {
|
46
|
+
throw new Error('The max Redis connection retry reached out');
|
47
|
+
}
|
48
|
+
if (this.client.isOpen) {
|
49
|
+
yield (0, utils_1.sleep)(200);
|
50
|
+
this.retryConnectionCount += 1;
|
51
|
+
return this.connect();
|
52
|
+
}
|
53
|
+
if (this.client.isReady) {
|
54
|
+
return;
|
55
|
+
}
|
56
|
+
yield this.client.connect();
|
57
|
+
});
|
58
|
+
}
|
59
|
+
quit() {
|
60
|
+
return __awaiter(this, void 0, void 0, function* () {
|
61
|
+
if (!this.client.isOpen) {
|
62
|
+
return;
|
63
|
+
}
|
64
|
+
yield this.client.disconnect();
|
65
|
+
});
|
66
|
+
}
|
67
|
+
onConnect() {
|
68
|
+
console.log('Connected to Redis');
|
69
|
+
}
|
70
|
+
onError(err) {
|
71
|
+
throw new exception_1.RateLimiterException(err.message);
|
72
|
+
}
|
73
|
+
}
|
74
|
+
exports.default = Bucket;
|
@@ -0,0 +1,10 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.RateLimiterException = void 0;
|
4
|
+
class RateLimiterException extends Error {
|
5
|
+
constructor(msg) {
|
6
|
+
super(msg);
|
7
|
+
Object.setPrototypeOf(this, RateLimiterException.prototype);
|
8
|
+
}
|
9
|
+
}
|
10
|
+
exports.RateLimiterException = RateLimiterException;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.TokenBucket = exports.sleep = exports.RateLimiterException = void 0;
|
7
|
+
var exception_1 = require("./exception");
|
8
|
+
Object.defineProperty(exports, "RateLimiterException", { enumerable: true, get: function () { return exception_1.RateLimiterException; } });
|
9
|
+
var utils_1 = require("./utils");
|
10
|
+
Object.defineProperty(exports, "sleep", { enumerable: true, get: function () { return utils_1.sleep; } });
|
11
|
+
const token_bucket_1 = __importDefault(require("./token-bucket"));
|
12
|
+
exports.TokenBucket = token_bucket_1.default;
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import { Token, TokenBucketSettings } from './types';
|
2
|
+
import Bucket from './bucket';
|
3
|
+
export default class TokenBucket extends Bucket {
|
4
|
+
private static readonly BUCKET_NAME;
|
5
|
+
private readonly maxDelayRetryCount;
|
6
|
+
private capacity;
|
7
|
+
private interval;
|
8
|
+
private delayRetryCount;
|
9
|
+
private timer;
|
10
|
+
private startTime;
|
11
|
+
constructor(settings: TokenBucketSettings);
|
12
|
+
private generateToken;
|
13
|
+
private getNextExecutionInMilliseconds;
|
14
|
+
private abortHandler;
|
15
|
+
/**
|
16
|
+
* Static method to create a new TokenBucket instance to ensure the bucket is refilled
|
17
|
+
* @param {TokenBucketSettings} settings - The constructor settings for the Bucket
|
18
|
+
* @return {Promise<TokenBucket>} TokenBucket instance
|
19
|
+
* @throws {RateLimiterException}
|
20
|
+
*/
|
21
|
+
static create(settings: TokenBucketSettings): Promise<TokenBucket>;
|
22
|
+
/**
|
23
|
+
* If there is tokens available, it takes the token right away, null otherwise
|
24
|
+
* @return {Promise<Token | null>} returns a token or null token
|
25
|
+
*
|
26
|
+
* @example
|
27
|
+
* const controller = new AbortController()
|
28
|
+
* const token = await bucket.delay(controller.signal)
|
29
|
+
* if (!token) {
|
30
|
+
* // re-queue the message
|
31
|
+
* }
|
32
|
+
*/
|
33
|
+
take(context?: AbortSignal): Promise<Token | null>;
|
34
|
+
/**
|
35
|
+
* Block the operation until receive a new token based on the delay time
|
36
|
+
* @param {AbortSignal?} context - The context in case wants to abort the request
|
37
|
+
* @return {Promise<Token>} The token object
|
38
|
+
*
|
39
|
+
* @example
|
40
|
+
* const controller = new AbortController()
|
41
|
+
* const myFreshToken = await bucket.delay(controller.signal)
|
42
|
+
* ...
|
43
|
+
* // call the API
|
44
|
+
*/
|
45
|
+
delay(context?: AbortSignal): Promise<Token>;
|
46
|
+
/**
|
47
|
+
* Refill the token-bucket with new tokens, in case the bucket is not at the full capacity
|
48
|
+
* @return {Promise<void>}
|
49
|
+
* @throws {RateLimiterException}
|
50
|
+
*/
|
51
|
+
refill(): Promise<void>;
|
52
|
+
/**
|
53
|
+
* Close the TokenBucket instance, clear the time and disconnect from Redis
|
54
|
+
* @return {Promise<void>}
|
55
|
+
*/
|
56
|
+
close(): Promise<void>;
|
57
|
+
/**
|
58
|
+
* Get the current total of tokens in the bucket
|
59
|
+
* @return {Promise<number>}
|
60
|
+
*/
|
61
|
+
getTotalTokens(): Promise<number>;
|
62
|
+
/**
|
63
|
+
* Remove all tokens from the bucket
|
64
|
+
* @return {Promise<void>}
|
65
|
+
*/
|
66
|
+
clearTokens(): Promise<void>;
|
67
|
+
}
|
@@ -0,0 +1,193 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
9
|
+
});
|
10
|
+
};
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
13
|
+
};
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
15
|
+
const exception_1 = require("./exception");
|
16
|
+
const bucket_1 = __importDefault(require("./bucket"));
|
17
|
+
const utils_1 = require("./utils");
|
18
|
+
/*
|
19
|
+
* TokenBucket algorithm implemented using Redis as storage
|
20
|
+
* @class TokenBucket
|
21
|
+
* @extends Bucket
|
22
|
+
* @since 0.0.1
|
23
|
+
*/
|
24
|
+
class TokenBucket extends bucket_1.default {
|
25
|
+
constructor(settings) {
|
26
|
+
super();
|
27
|
+
this.maxDelayRetryCount = 5;
|
28
|
+
this.capacity = 0;
|
29
|
+
this.interval = 0;
|
30
|
+
this.delayRetryCount = 0;
|
31
|
+
this.timer = null;
|
32
|
+
this.startTime = 0;
|
33
|
+
this.capacity = settings.capacity;
|
34
|
+
this.interval = settings.interval;
|
35
|
+
this.startTime = Date.now();
|
36
|
+
this.timer = setInterval(this.refill.bind(this), this.interval);
|
37
|
+
}
|
38
|
+
generateToken() {
|
39
|
+
return Math.random().toString(36).substring(2);
|
40
|
+
}
|
41
|
+
getNextExecutionInMilliseconds() {
|
42
|
+
const elapsedTime = Date.now() - this.startTime;
|
43
|
+
const nextExecution = this.interval - Math.ceil(elapsedTime % this.interval);
|
44
|
+
return nextExecution;
|
45
|
+
}
|
46
|
+
abortHandler() {
|
47
|
+
if (this.aborted) {
|
48
|
+
throw new Error('Operation aborted');
|
49
|
+
}
|
50
|
+
}
|
51
|
+
/**
|
52
|
+
* Static method to create a new TokenBucket instance to ensure the bucket is refilled
|
53
|
+
* @param {TokenBucketSettings} settings - The constructor settings for the Bucket
|
54
|
+
* @return {Promise<TokenBucket>} TokenBucket instance
|
55
|
+
* @throws {RateLimiterException}
|
56
|
+
*/
|
57
|
+
static create(settings) {
|
58
|
+
return __awaiter(this, void 0, void 0, function* () {
|
59
|
+
try {
|
60
|
+
const bucket = new TokenBucket(settings);
|
61
|
+
yield bucket.connect();
|
62
|
+
yield bucket.refill();
|
63
|
+
return bucket;
|
64
|
+
}
|
65
|
+
catch (error) {
|
66
|
+
throw new exception_1.RateLimiterException(`Error creating TokenBucket instance | ${error}`);
|
67
|
+
}
|
68
|
+
});
|
69
|
+
}
|
70
|
+
/**
|
71
|
+
* If there is tokens available, it takes the token right away, null otherwise
|
72
|
+
* @return {Promise<Token | null>} returns a token or null token
|
73
|
+
*
|
74
|
+
* @example
|
75
|
+
* const controller = new AbortController()
|
76
|
+
* const token = await bucket.delay(controller.signal)
|
77
|
+
* if (!token) {
|
78
|
+
* // re-queue the message
|
79
|
+
* }
|
80
|
+
*/
|
81
|
+
take(context) {
|
82
|
+
return __awaiter(this, void 0, void 0, function* () {
|
83
|
+
try {
|
84
|
+
if (context === null || context === void 0 ? void 0 : context.aborted) {
|
85
|
+
throw new Error('Operation aborted');
|
86
|
+
}
|
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) {
|
90
|
+
return null;
|
91
|
+
}
|
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);
|
95
|
+
return token;
|
96
|
+
}
|
97
|
+
catch (error) {
|
98
|
+
throw new exception_1.RateLimiterException(`Error taking token from bucket | ${error}`);
|
99
|
+
}
|
100
|
+
});
|
101
|
+
}
|
102
|
+
/**
|
103
|
+
* Block the operation until receive a new token based on the delay time
|
104
|
+
* @param {AbortSignal?} context - The context in case wants to abort the request
|
105
|
+
* @return {Promise<Token>} The token object
|
106
|
+
*
|
107
|
+
* @example
|
108
|
+
* const controller = new AbortController()
|
109
|
+
* const myFreshToken = await bucket.delay(controller.signal)
|
110
|
+
* ...
|
111
|
+
* // call the API
|
112
|
+
*/
|
113
|
+
delay(context) {
|
114
|
+
return __awaiter(this, void 0, void 0, function* () {
|
115
|
+
if (this.delayRetryCount >= this.maxDelayRetryCount) {
|
116
|
+
throw new exception_1.RateLimiterException('Max delay retries reached out');
|
117
|
+
}
|
118
|
+
const token = yield this.take(context);
|
119
|
+
if (token === null || token === void 0 ? void 0 : token.value) {
|
120
|
+
this.delayRetryCount = 0;
|
121
|
+
return token;
|
122
|
+
}
|
123
|
+
const delayInMilliseconds = this.getNextExecutionInMilliseconds();
|
124
|
+
yield (0, utils_1.sleep)(delayInMilliseconds + 10, context);
|
125
|
+
this.delayRetryCount += 1;
|
126
|
+
return this.delay(context);
|
127
|
+
});
|
128
|
+
}
|
129
|
+
/**
|
130
|
+
* Refill the token-bucket with new tokens, in case the bucket is not at the full capacity
|
131
|
+
* @return {Promise<void>}
|
132
|
+
* @throws {RateLimiterException}
|
133
|
+
*/
|
134
|
+
refill() {
|
135
|
+
return __awaiter(this, void 0, void 0, function* () {
|
136
|
+
try {
|
137
|
+
if (!this.client.isReady) {
|
138
|
+
throw new exception_1.RateLimiterException('Redis client is not ready');
|
139
|
+
}
|
140
|
+
const countTokens = yield this.client.LLEN(TokenBucket.BUCKET_NAME);
|
141
|
+
if (countTokens >= this.capacity) {
|
142
|
+
return;
|
143
|
+
}
|
144
|
+
const tokens = Array.from({
|
145
|
+
length: this.capacity - countTokens
|
146
|
+
}, () => {
|
147
|
+
return JSON.stringify({
|
148
|
+
value: this.generateToken(),
|
149
|
+
timestamp: Date.now()
|
150
|
+
});
|
151
|
+
});
|
152
|
+
yield this.client.LPUSH(TokenBucket.BUCKET_NAME, tokens);
|
153
|
+
}
|
154
|
+
catch (error) {
|
155
|
+
throw new exception_1.RateLimiterException(`Error filling token bucket | ${error}`);
|
156
|
+
}
|
157
|
+
});
|
158
|
+
}
|
159
|
+
/**
|
160
|
+
* Close the TokenBucket instance, clear the time and disconnect from Redis
|
161
|
+
* @return {Promise<void>}
|
162
|
+
*/
|
163
|
+
close() {
|
164
|
+
return __awaiter(this, void 0, void 0, function* () {
|
165
|
+
if (this.timer) {
|
166
|
+
clearInterval(this.timer);
|
167
|
+
this.timer = null;
|
168
|
+
}
|
169
|
+
yield this.quit();
|
170
|
+
console.log('Closed Token Bucket instance!');
|
171
|
+
});
|
172
|
+
}
|
173
|
+
/**
|
174
|
+
* Get the current total of tokens in the bucket
|
175
|
+
* @return {Promise<number>}
|
176
|
+
*/
|
177
|
+
getTotalTokens() {
|
178
|
+
return __awaiter(this, void 0, void 0, function* () {
|
179
|
+
return this.client.LLEN(TokenBucket.BUCKET_NAME);
|
180
|
+
});
|
181
|
+
}
|
182
|
+
/**
|
183
|
+
* Remove all tokens from the bucket
|
184
|
+
* @return {Promise<void>}
|
185
|
+
*/
|
186
|
+
clearTokens() {
|
187
|
+
return __awaiter(this, void 0, void 0, function* () {
|
188
|
+
yield this.client.LTRIM(TokenBucket.BUCKET_NAME, 1, 0);
|
189
|
+
});
|
190
|
+
}
|
191
|
+
}
|
192
|
+
TokenBucket.BUCKET_NAME = 'rate-limiter-tokens';
|
193
|
+
exports.default = TokenBucket;
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
9
|
+
});
|
10
|
+
};
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
12
|
+
exports.sleep = sleep;
|
13
|
+
const abortHandler = (reject, timeoutId) => {
|
14
|
+
clearTimeout(timeoutId);
|
15
|
+
reject(new Error('Operation aborted'));
|
16
|
+
};
|
17
|
+
function sleep(milliseconds, context) {
|
18
|
+
return __awaiter(this, void 0, void 0, function* () {
|
19
|
+
return new Promise((resolve, reject) => {
|
20
|
+
if (context === null || context === void 0 ? void 0 : context.aborted) {
|
21
|
+
return reject(new Error('Operation aborted'));
|
22
|
+
}
|
23
|
+
const timeoutId = setTimeout(() => {
|
24
|
+
resolve();
|
25
|
+
}, milliseconds);
|
26
|
+
const fn = () => abortHandler(reject, timeoutId);
|
27
|
+
context === null || context === void 0 ? void 0 : context.addEventListener('abort', fn);
|
28
|
+
}).finally(() => {
|
29
|
+
context === null || context === void 0 ? void 0 : context.removeEventListener('abort', () => abortHandler);
|
30
|
+
});
|
31
|
+
});
|
32
|
+
}
|
package/package.json
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
{
|
2
|
+
"name": "@fsilva3/rate-limiter-redis",
|
3
|
+
"version": "0.0.2",
|
4
|
+
"license": "MIT",
|
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
|
+
"main": "./dist/index.js",
|
7
|
+
"types": "./dist/index.d.ts",
|
8
|
+
"files": [
|
9
|
+
"dist/"
|
10
|
+
],
|
11
|
+
"scripts": {
|
12
|
+
"build": "tsc",
|
13
|
+
"test": "vitest",
|
14
|
+
"coverage": "vitest run --coverage",
|
15
|
+
"prepare": "npm run build"
|
16
|
+
},
|
17
|
+
"dependencies": {
|
18
|
+
"redis": "^4.7.0"
|
19
|
+
},
|
20
|
+
"devDependencies": {
|
21
|
+
"@eslint/js": "^9.20.0",
|
22
|
+
"@types/node": "^22.13.4",
|
23
|
+
"@vitest/coverage-v8": "^3.0.6",
|
24
|
+
"eslint": "^9.20.1",
|
25
|
+
"typescript": "^5.7.3",
|
26
|
+
"typescript-eslint": "^8.24.0",
|
27
|
+
"vitest": "^3.0.6"
|
28
|
+
},
|
29
|
+
"repository": {
|
30
|
+
"type": "git",
|
31
|
+
"url": "git+https://github.com/fsilva3/rate-limiter-redis.git"
|
32
|
+
},
|
33
|
+
"keywords": [
|
34
|
+
"token-bucket",
|
35
|
+
"rate limiter",
|
36
|
+
"redis"
|
37
|
+
],
|
38
|
+
"bugs": {
|
39
|
+
"url": "https://github.com/fsilva3/rate-limiter-redis/issues"
|
40
|
+
},
|
41
|
+
"homepage": "https://github.com/fsilva3/rate-limiter-redis#readme",
|
42
|
+
"author": "Filipe Silva"
|
43
|
+
}
|