├── .checkbuild
├── .github
└── workflows
│ ├── build.yml
│ └── test.yml
├── .gitignore
├── .jscsrc
├── .jshintrc
├── .npmignore
├── LICENSE
├── README.md
├── lib
└── fast_ratelimit.js
├── package-lock.json
├── package.json
└── test
└── fast_ratelimit-test.js
/.checkbuild:
--------------------------------------------------------------------------------
1 | {
2 | "checkbuild": {
3 | "enable": ["jshint", "jscs"],
4 | "continueOnError": true,
5 | "allowFailures": false
6 | },
7 | "jshint": {
8 | "args": ["lib/**/*.js"]
9 | },
10 | "jscs": {
11 | "args": ["lib/**/*.js"]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | tags:
4 | - "v*.*.*"
5 |
6 | permissions:
7 | id-token: write
8 |
9 | name: Build and Release
10 |
11 | jobs:
12 | release:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout code
17 | uses: actions/checkout@v2
18 |
19 | - name: Install NodeJS
20 | uses: actions/setup-node@v1
21 | with:
22 | node-version: 20.x
23 | registry-url: https://registry.npmjs.org
24 |
25 | - name: Verify versions
26 | run: node --version && npm --version && node -p process.versions.v8
27 |
28 | - name: Release package
29 | run: npm publish --ignore-scripts --provenance
30 | env:
31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
32 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | on: [push, pull_request]
2 |
3 | name: Test and Build
4 |
5 | jobs:
6 | test:
7 | strategy:
8 | matrix:
9 | os: [ubuntu-latest]
10 | node-version: [6.x, 8.x, 10.x, 12.x, 14.x, 16.x, 18.x, 20.x]
11 | fail-fast: false
12 |
13 | runs-on: ${{ matrix.os }}
14 |
15 | steps:
16 | - name: Checkout code
17 | uses: actions/checkout@v2
18 |
19 | - name: Install NodeJS
20 | uses: actions/setup-node@v1
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 |
24 | - name: Verify versions
25 | run: node --version && npm --version && node -p process.versions.v8 && gcc -v
26 |
27 | - name: Cache build artifacts
28 | id: cache-node
29 | uses: actions/cache@v4
30 | with:
31 | path: |
32 | ~/.npm
33 | node_modules
34 | key: test-${{ runner.os }}-node-${{ matrix.node-version }}
35 |
36 | - name: Install dependencies
37 | run: npm install
38 |
39 | - name: Run tests
40 | run: npm test
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | npm-debug.log
4 | coverage/
5 |
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | {
2 | "validateIndentation": 2,
3 | "disallowMixedSpacesAndTabs": true,
4 | "maximumLineLength": 80,
5 | "disallowTrailingWhitespace": true,
6 | "requireCurlyBraces": null
7 | }
8 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "camelcase": false,
3 | "node": true,
4 | "esnext" : true,
5 | "predef": [
6 | "require",
7 | "define",
8 | "escape",
9 | "Buffer",
10 | "console",
11 | "module"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | test/
2 | coverage/
3 | .github/
4 | .jshintrc
5 | .jscsrc
6 | .checkbuild
7 | package-lock.json
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Valerian Saliou
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # node-fast-ratelimit
2 |
3 | [](https://github.com/valeriansaliou/node-fast-ratelimit/actions?query=workflow%3A%22Test+and+Build%22) [](https://github.com/valeriansaliou/node-fast-ratelimit/actions?query=workflow%3A%22Build+and+Release%22) [](https://www.npmjs.com/package/fast-ratelimit) [](https://www.npmjs.com/package/fast-ratelimit) [](https://gitter.im/valeriansaliou/node-fast-ratelimit) [](https://www.buymeacoffee.com/valeriansaliou)
4 |
5 | Fast and efficient in-memory rate-limit, used to alleviate most common DOS attacks.
6 |
7 | This rate-limiter was designed to be as generic as possible, usable in any NodeJS project environment, regardless of whether you're using a framework or just vanilla code. It does not require any dependencies, making it lightweight to install and use.
8 |
9 | **🇫🇷 Crafted in Lannion, France.**
10 |
11 | ## Who uses it?
12 |
13 |
14 |
15 |  |
16 |  |
17 |  |
18 |  |
19 |
20 |
21 | Crisp |
22 | Doctrine |
23 | Anchor.Chat |
24 | WeStudents |
25 |
26 |
27 |
28 | _👋 You use fast-ratelimit and you want to be listed there? [Contact me](https://valeriansaliou.name/)._
29 |
30 | ## How to install?
31 |
32 | Include `fast-ratelimit` in your `package.json` dependencies.
33 |
34 | Alternatively, you can run `npm install fast-ratelimit --save`.
35 |
36 | TypeScript users can install type definitions by running `npm install --save-dev @types/fast-ratelimit`. _Note that this is a third-party package, ie. not maintained by myself._
37 |
38 | ## How to use?
39 |
40 | The `fast-ratelimit` API is pretty simple, here are some keywords used in the docs:
41 |
42 | * `ratelimiter`: ratelimiter instance, which plays the role of limits storage
43 | * `namespace`: the master ratelimit storage namespace (eg: set `namespace` to the user client IP, or user username)
44 |
45 | You can create as many `ratelimiter` instances as you need in your application. This is great if you need to rate-limit IPs on specific zones (eg: for a chat application, you don't want the message send rate limit to affect the message composing notification rate limit).
46 |
47 | Here's how to proceed (we take the example of rate-limiting messages sending in a chat app):
48 |
49 | ### 1. Create the rate-limiter
50 |
51 | The rate-limiter can be instanciated as such:
52 |
53 | ```javascript
54 | var FastRateLimit = require("fast-ratelimit").FastRateLimit;
55 |
56 | var messageLimiter = new FastRateLimit({
57 | threshold : 20, // available tokens over timespan
58 | ttl : 60 // time-to-live value of token bucket (in seconds)
59 | });
60 | ```
61 |
62 | This limiter will allow 20 messages to be sent every minute per namespace.
63 | An user can send a maximum number of 20 messages in a 1 minute timespan, with a token counter reset every minute for a given namespace.
64 |
65 | The reset scheduling is done per-namespace; eg: if namespace `user_1` sends 1 message at 11:00:32am, he will have 19 messages remaining from 11:00:32am to 11:01:32am. Hence, his limiter will reset at 11:01:32am, and won't scheduler any more reset until he consumes another token.
66 |
67 | ### 2. Check by consuming a token
68 |
69 | On the message send portion of our application code, we would add a call to the ratelimiter instance.
70 |
71 | #### 2.1. Consume token with asynchronous API (Promise catch/reject)
72 |
73 | ```javascript
74 | // This would be dynamic in your application, based on user session data, or user IP
75 | namespace = "user_1";
76 |
77 | // Check if user is allowed to send message
78 | messageLimiter.consume(namespace)
79 | .then(() => {
80 | // Consumed a token
81 | // Send message
82 | message.send();
83 | })
84 | .catch(() => {
85 | // No more token for namespace in current timespan
86 | // Silently discard message
87 | });
88 | ```
89 |
90 | #### 2.2. Consume token with synchronous API (boolean test)
91 |
92 | ```javascript
93 | // This would be dynamic in your application, based on user session data, or user IP
94 | namespace = "user_1";
95 |
96 | // Check if user is allowed to send message
97 | if (messageLimiter.consumeSync(namespace) === true) {
98 | // Consumed a token
99 | // Send message
100 | message.send();
101 | } else {
102 | // consumeSync returned false since there is no more tokens available
103 | // Silently discard message
104 | }
105 | ```
106 |
107 | ### 3. Check without consuming a token
108 |
109 | In some instances, like password brute forcing prevention, you may want to check without consuming a token and consume only when password validation fails.
110 |
111 | #### 3.1. Check whether there are remaining tokens with asynchronous API (Promise catch/reject)
112 |
113 | ```javascript
114 | limiter.hasToken(request.ip).then(() => {
115 | return authenticate(request.login, request.password)
116 | })
117 | .then(
118 | () => {
119 | // User is authenticated
120 | },
121 |
122 | () => {
123 | // User is not authenticated
124 | // Consume a token and reject promise
125 | return limiter.consume(request.ip)
126 | .then(() => Promise.reject())
127 | }
128 | )
129 | .catch(() => {
130 | // Either invalid authentication or too many invalid login
131 | return response.unauthorized();
132 | })
133 | ```
134 |
135 | #### 3.2. Check whether there are remaining tokens with synchronous API (boolean test)
136 |
137 | ```javascript
138 | if (!limiter.hasTokenSync(request.ip)) {
139 | throw new Error("Too many invalid login");
140 | }
141 |
142 | const is_authenticated = authenticateSync(request.login, request.password);
143 |
144 | if (!is_authenticated) {
145 | limiter.consumeSync(request.ip);
146 |
147 | throw new Error("Invalid login/password");
148 | }
149 | ```
150 |
151 | ## Notes on performance
152 |
153 | This module is used extensively on edge WebSocket servers, handling thousands of connections every second with multiple rate limit lists on the top of each other. Everything works smoothly, I/O doesn't block and RAM didn't move that much with the rate-limiting module enabled.
154 |
155 | On one core of a 2,3 GHz 8-Core Intel Core i9, the parallel asynchronous processing of 100,000 namespaces in the same limiter take an average of 160 ms, which is fine (1.6 microseconds per operation).
156 |
157 | ## Why not using existing similar modules?
158 |
159 | I was looking for an efficient, yet simple, DOS-prevention technique that wouldn't hurt performance and consume tons of memory. All proper modules I found were relying on Redis as the keystore for limits, which is definitely not great if you want to keep away from DOS attacks: using such a module under DOS conditions would subsequently DOS Redis since 1 (or more) Redis queries are made per limit check (1 attacker request = 1 limit check). Attacks should definitely not be allieviated this way, although a Redis-based solution would be perfect to limit abusing users.
160 |
161 | This module keeps all limits in-memory, which is much better for our attack-prevention concern. The only downside: since the limits database isn't shared, limits are per-process. This means that you should only use this module to prevent hard-attacks at any level of your infrastructure. This works pretty well for micro-service infrastructures, which is what we're using it in.
162 |
--------------------------------------------------------------------------------
/lib/fast_ratelimit.js:
--------------------------------------------------------------------------------
1 | /*
2 | * node-fast-ratelimit
3 | *
4 | * Copyright 2016, Valerian Saliou
5 | * Author: Valerian Saliou
6 | */
7 |
8 |
9 | "use strict";
10 |
11 |
12 | /**
13 | * FastRateLimit
14 | * @class
15 | * @classdesc Instanciates a new rate-limiter
16 | * @param {object} options
17 | */
18 | var FastRateLimit = function(options) {
19 | // Sanitize options
20 | if (typeof options !== "object") {
21 | throw new Error("Invalid or missing options");
22 | }
23 | if (typeof options.threshold !== "number" || options.threshold < 0) {
24 | throw new Error("Invalid or missing options.threshold");
25 | }
26 | if (typeof options.ttl !== "number" || options.ttl < 0) {
27 | throw new Error("Invalid or missing options.ttl");
28 | }
29 |
30 | // Environment
31 | var secondInMilliseconds = 1000;
32 |
33 | // Storage space
34 | this.__options = {
35 | threshold : options.threshold,
36 | ttl_millisec : (options.ttl * secondInMilliseconds)
37 | };
38 |
39 | this.__tokens = new Map();
40 | };
41 |
42 |
43 | /**
44 | * tokenCheck
45 | * @private
46 | * @param {boolean} consumeToken Whether to consume token or not
47 | * @returns {function} A configured token checking function
48 | */
49 | var tokenCheck = function(consumeToken) {
50 | return function(namespace) {
51 | // No namespace provided?
52 | if (!namespace) {
53 | // Do not rate-limit (1 token remaining each hop)
54 | return true;
55 | }
56 |
57 | let _tokens_count;
58 |
59 | // Token bucket empty for namespace?
60 | if (this.__tokens.has(namespace) === false) {
61 | _tokens_count = this.__options.threshold;
62 |
63 | this.__scheduleExpireToken(namespace);
64 | } else {
65 | _tokens_count = this.__tokens.get(namespace);
66 | }
67 |
68 | // Check remaining tokens in bucket
69 | if (_tokens_count > 0) {
70 | if (consumeToken) {
71 | this.__tokens.set(
72 | namespace, (_tokens_count - 1)
73 | );
74 | }
75 |
76 | return true;
77 | }
78 |
79 | return false;
80 | };
81 | };
82 |
83 |
84 | /**
85 | * FastRateLimit.prototype.consumeSync
86 | * @public
87 | * @param {string} namespace
88 | * @return {boolean} Whether tokens remain in current timespan or not
89 | */
90 | FastRateLimit.prototype.consumeSync = tokenCheck(true);
91 |
92 |
93 | /**
94 | * FastRateLimit.prototype.hasTokenSync
95 | * @public
96 | * @param {string} namespace
97 | * @return {boolean} Whether tokens remain in current timespan or not
98 | */
99 |
100 | FastRateLimit.prototype.hasTokenSync = tokenCheck(false);
101 |
102 |
103 | /**
104 | * FastRateLimit.prototype.consume
105 | * @public
106 | * @param {string} namespace
107 | * @return {object} Promise object
108 | */
109 | FastRateLimit.prototype.consume = function(namespace) {
110 | if (this.consumeSync(namespace) === true) {
111 | return Promise.resolve();
112 | }
113 |
114 | return Promise.reject();
115 | };
116 |
117 |
118 | /**
119 | * FastRateLimit.prototype.hasToken
120 | * @public
121 | * @param {string} namespace
122 | * @return {object} Promise object
123 | */
124 | FastRateLimit.prototype.hasToken = function(namespace) {
125 | if (this.hasTokenSync(namespace) === true) {
126 | return Promise.resolve();
127 | }
128 |
129 | return Promise.reject();
130 | };
131 |
132 |
133 | /**
134 | * FastRateLimit.prototype.__scheduleExpireToken
135 | * @private
136 | * @param {string} namespace
137 | * @return {undefined}
138 | */
139 | FastRateLimit.prototype.__scheduleExpireToken = function(namespace) {
140 | var self = this;
141 |
142 | setTimeout(function() {
143 | // Expire token storage for namespace
144 | self.__tokens.delete(namespace);
145 | }, this.__options.ttl_millisec);
146 | };
147 |
148 |
149 | exports.FastRateLimit = FastRateLimit;
150 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fast-ratelimit",
3 | "description": "Fast and efficient in-memory rate-limit for Node, used to alleviate severe DOS attacks.",
4 | "version": "3.0.1",
5 | "homepage": "https://github.com/valeriansaliou/node-fast-ratelimit",
6 | "license": "MIT",
7 | "author": {
8 | "name": "Valerian Saliou",
9 | "email": "valerian@valeriansaliou.name",
10 | "url": "https://valeriansaliou.name/"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git://github.com/valeriansaliou/node-fast-ratelimit.git"
15 | },
16 | "bugs": {
17 | "url": "https://github.com/valeriansaliou/node-fast-ratelimit/issues"
18 | },
19 | "licenses": [
20 | {
21 | "type": "MIT",
22 | "url": "https://github.com/valeriansaliou/node-fast-ratelimit/blob/master/LICENSE"
23 | }
24 | ],
25 | "main": "lib/fast_ratelimit",
26 | "engines": {
27 | "node": ">= 6.0.0"
28 | },
29 | "scripts": {
30 | "test": "check-build && istanbul cover _mocha"
31 | },
32 | "devDependencies": {
33 | "check-build": "2.8.2",
34 | "mocha": "3.4.2",
35 | "mocha-lcov-reporter": "1.3.0",
36 | "istanbul": "0.4.5"
37 | },
38 | "keywords": [
39 | "ratelimit",
40 | "rate-limit",
41 | "rate",
42 | "limit",
43 | "attack",
44 | "flood",
45 | "security",
46 | "dos",
47 | "ddos"
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/test/fast_ratelimit-test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * node-fast-ratelimit
3 | *
4 | * Copyright 2016, Valerian Saliou
5 | * Author: Valerian Saliou
6 | */
7 |
8 |
9 | "use strict";
10 |
11 |
12 | var FastRateLimit = require("../").FastRateLimit;
13 | var assert = require("assert");
14 |
15 |
16 | var __Promise = (
17 | (typeof Promise !== "undefined") ?
18 | Promise : require("es6-promise-polyfill").Promise
19 | );
20 |
21 |
22 | describe("node-fast-ratelimit", function() {
23 | describe("constructor", function() {
24 | it("should succeed creating a limiter with valid options", function() {
25 | assert.doesNotThrow(
26 | function() {
27 | new FastRateLimit({
28 | threshold : 5,
29 | ttl : 10
30 | });
31 | },
32 |
33 | "FastRateLimit should not throw on valid options"
34 | );
35 | });
36 |
37 | it("should fail creating a limiter with missing threshold", function() {
38 | assert.throws(
39 | function() {
40 | new FastRateLimit({
41 | ttl : 10
42 | });
43 | },
44 |
45 | "FastRateLimit should throw on missing threshold"
46 | );
47 | });
48 |
49 | it("should fail creating a limiter with invalid threshold", function() {
50 | assert.throws(
51 | function() {
52 | new FastRateLimit({
53 | threshold : -1,
54 | ttl : 10
55 | });
56 | },
57 |
58 | "FastRateLimit should throw on invalid threshold"
59 | );
60 | });
61 |
62 | it("should fail creating a limiter with missing ttl", function() {
63 | assert.throws(
64 | function() {
65 | new FastRateLimit({
66 | threshold : 2
67 | });
68 | },
69 |
70 | "FastRateLimit should throw on missing ttl"
71 | );
72 | });
73 |
74 | it("should fail creating a limiter with invalid ttl", function() {
75 | assert.throws(
76 | function() {
77 | new FastRateLimit({
78 | ttl : "120"
79 | });
80 | },
81 |
82 | "FastRateLimit should throw on invalid ttl"
83 | );
84 | });
85 | });
86 |
87 | describe("consumeSync method", function() {
88 | it("should not rate limit an empty namespace", function() {
89 | var limiter = new FastRateLimit({
90 | threshold : 100,
91 | ttl : 10
92 | });
93 |
94 | assert.ok(
95 | limiter.consumeSync(null),
96 | "Limiter consume should succeed for `null` (null) namespace (resolve)"
97 | );
98 |
99 | assert.ok(
100 | limiter.consumeSync(""),
101 | "Limiter consume should succeed for `` (blank) namespace (resolve)"
102 | );
103 |
104 | assert.ok(
105 | limiter.consumeSync(0),
106 | "Limiter consume should succeed for `0` (number) namespace (resolve)"
107 | );
108 | });
109 |
110 | it("should not rate limit a single namespace", function() {
111 | var options = {
112 | threshold : 100,
113 | ttl : 10
114 | };
115 |
116 | var namespace = "127.0.0.1";
117 | var limiter = new FastRateLimit(options);
118 |
119 | for (var i = 1; i <= options.threshold; i++) {
120 | assert.ok(
121 | limiter.consumeSync(namespace),
122 | "Limiter consume should succeed"
123 | );
124 | }
125 | });
126 |
127 | it("should rate limit a single namespace", function() {
128 | var namespace = "127.0.0.1";
129 |
130 | var limiter = new FastRateLimit({
131 | threshold : 3,
132 | ttl : 10
133 | });
134 |
135 | assert.ok(
136 | limiter.consumeSync(namespace),
137 | "Limiter consume succeed at consume #1 (resolve)"
138 | );
139 |
140 | assert.ok(
141 | limiter.consumeSync(namespace),
142 | "Limiter consume succeed at consume #2 (resolve)"
143 | );
144 |
145 | assert.ok(
146 | limiter.consumeSync(namespace),
147 | "Limiter consume succeed at consume #3 (resolve)"
148 | );
149 |
150 | assert.ok(
151 | !(limiter.consumeSync(namespace)),
152 | "Limiter consume fail at consume #4 (reject)"
153 | );
154 | });
155 |
156 | it("should not rate limit multiple namespaces", function() {
157 | var limiter = new FastRateLimit({
158 | threshold : 2,
159 | ttl : 10
160 | });
161 |
162 | assert.ok(
163 | limiter.consumeSync("user_1"),
164 | "Limiter consume should succeed at consume #1 of user_1 (resolve)"
165 | );
166 |
167 | assert.ok(
168 | limiter.consumeSync("user_2"),
169 | "Limiter consume should succeed at consume #1 of user_2 (resolve)"
170 | );
171 | });
172 |
173 | it("should rate limit multiple namespaces", function() {
174 | var limiter = new FastRateLimit({
175 | threshold : 2,
176 | ttl : 10
177 | });
178 |
179 | assert.ok(
180 | limiter.consumeSync("user_1"),
181 | "Limiter consume should succeed at consume #1 of user_1 (resolve)"
182 | );
183 |
184 | assert.ok(
185 | limiter.consumeSync("user_2"),
186 | "Limiter consume should succeed at consume #1 of user_2 (resolve)"
187 | );
188 |
189 | assert.ok(
190 | limiter.consumeSync("user_1"),
191 | "Limiter consume should succeed at consume #2 of user_1 (resolve)"
192 | );
193 |
194 | assert.ok(
195 | limiter.consumeSync("user_2"),
196 | "Limiter consume should succeed at consume #2 of user_2 (resolve)"
197 | );
198 |
199 | assert.ok(
200 | !(limiter.consumeSync("user_1")),
201 | "Limiter consume should fail at consume #3 of user_1 (reject)"
202 | );
203 |
204 | assert.ok(
205 | !(limiter.consumeSync("user_2")),
206 | "Limiter consume should fail at consume #3 of user_2 (reject)"
207 | );
208 | });
209 |
210 | it("should expire token according to TTL", function(done) {
211 | // Do not consider timeout as slow
212 | this.timeout(2000);
213 | this.slow(5000);
214 |
215 | var options = {
216 | threshold : 2,
217 | ttl : 1
218 | };
219 |
220 | var namespace = "127.0.0.1";
221 | var limiter = new FastRateLimit(options);
222 |
223 | assert.ok(
224 | limiter.consumeSync(namespace),
225 | "Limiter consume should succeed at consume #1 (resolve)"
226 | );
227 |
228 | assert.ok(
229 | limiter.consumeSync(namespace),
230 | "Limiter consume should succeed at consume #2 (resolve)"
231 | );
232 |
233 | assert.ok(
234 | !(limiter.consumeSync(namespace)),
235 | "Limiter consume should fail at consume #3 (reject)"
236 | );
237 |
238 | // Wait for TTL reset.
239 | setTimeout(function() {
240 | assert.ok(
241 | limiter.consumeSync(namespace),
242 | "Limiter consume should succeed at consume #4 (resolve)"
243 | );
244 |
245 | done();
246 | }, ((options.ttl * 1000) + 100));
247 | });
248 |
249 | it("should not block writing random namespaces", function(done) {
250 | // Timeout if longer than 1 second (check for blocking writes)
251 | this.timeout(1000);
252 | this.slow(250);
253 |
254 | var limiter = new FastRateLimit({
255 | threshold : 100,
256 | ttl : 60
257 | });
258 |
259 | var asyncFlowSteps = 10000,
260 | asyncFlowTotal = 10,
261 | asyncFlowCountDone = 0;
262 |
263 | var launchAsyncFlow = function(id) {
264 | setTimeout(function() {
265 | for (var i = 0; i < asyncFlowSteps; i++) {
266 | assert.ok(
267 | limiter.consumeSync("flow-" + id + "-" + i),
268 | "Limiter consume should succeed at flow #" + id + " (resolve)"
269 | );
270 | }
271 |
272 | if (++asyncFlowCountDone === asyncFlowTotal) {
273 | done();
274 | }
275 | });
276 | }
277 |
278 | // Launch asynchronous flows
279 | for (var i = 1; i <= asyncFlowTotal; i++) {
280 | launchAsyncFlow(i);
281 | }
282 | });
283 |
284 |
285 | });
286 |
287 | describe("hasTokenSync method", function() {
288 | it("should not consume token", function() {
289 | var limiter = new FastRateLimit({
290 | threshold : 1,
291 | ttl : 10
292 | });
293 | var namespace = "127.0.0.1";
294 |
295 | assert.ok(limiter.hasTokenSync(namespace), "Limiter hasTokenSync should succeed at hasTokenSync #1");
296 | assert.ok(limiter.hasTokenSync(namespace), "Limiter hasTokenSync should succeed at hasTokenSync #2");
297 | });
298 |
299 | it("should rate limit", function() {
300 | var limiter = new FastRateLimit({
301 | threshold : 1,
302 | ttl : 10
303 | });
304 | var namespace = "127.0.0.1";
305 |
306 | assert.ok(limiter.hasTokenSync(namespace), "Limiter hasTokenSync should succeed at hasTokenSync #1");
307 | assert.ok(limiter.consumeSync(namespace), "Limiter consumeSync should succeed at consumeSync #1");
308 | assert.ok(!limiter.hasTokenSync(namespace), "Limiter hasTokenSync should fail at hasTokenSync #2");
309 | });
310 | });
311 |
312 | describe("hasToken method", function() {
313 | it("should not consume token", function(done) {
314 | var limiter = new FastRateLimit({
315 | threshold : 1,
316 | ttl : 10
317 | });
318 | var namespace = "127.0.0.1";
319 | var promises_all = [];
320 |
321 | promises_all.push(limiter.hasToken(namespace));
322 | promises_all.push(limiter.hasToken(namespace));
323 |
324 | __Promise.all(promises_all)
325 | .then(function() {
326 | done();
327 | })
328 | .catch(function(error) {
329 | if (error) {
330 | done(error);
331 | } else {
332 | done(
333 | new Error("Limiter hasToken should not fail at the end (reject)")
334 | );
335 | }
336 | });
337 | });
338 |
339 | it("should rate limit", function(done) {
340 | var limiter = new FastRateLimit({
341 | threshold : 1,
342 | ttl : 10
343 | });
344 | var namespace = "127.0.0.1";
345 | var promises_all = [];
346 |
347 | promises_all.push(limiter.hasToken(namespace));
348 | promises_all.push(limiter.consume(namespace));
349 | promises_all.push(limiter.hasToken(namespace));
350 |
351 | __Promise.all(promises_all)
352 | .then(function() {
353 | done(new Error("Limiter hasToken should not succeed at the end (reject)"));
354 | })
355 | .catch(function(error) {
356 | if (error) {
357 | done(error);
358 | }
359 |
360 | done();
361 | });
362 | });
363 | });
364 |
365 | describe("consume method", function() {
366 | it("should not rate limit", function(done) {
367 | var options = {
368 | threshold : 100,
369 | ttl : 10
370 | };
371 |
372 | var namespace = "127.0.0.1";
373 | var limiter = new FastRateLimit(options);
374 |
375 | var promises_all = [];
376 |
377 | for (var i = 1; i <= options.threshold; i++) {
378 | promises_all.push(
379 | limiter.consume(namespace)
380 | );
381 | }
382 |
383 | __Promise.all(promises_all)
384 | .then(function() {
385 | done();
386 | })
387 | .catch(function(error) {
388 | if (error) {
389 | done(error);
390 | } else {
391 | done(
392 | new Error("Limiter consume should not fail at the end (reject)")
393 | );
394 | }
395 | });
396 | });
397 |
398 | it("should rate limit", function(done) {
399 | var options = {
400 | threshold : 100,
401 | ttl : 10
402 | };
403 |
404 | var namespace = "127.0.0.1";
405 | var limiter = new FastRateLimit(options);
406 |
407 | var promises_all = [];
408 |
409 | for (var i = 1; i <= (options.threshold + 5); i++) {
410 | promises_all.push(
411 | limiter.consume(namespace)
412 | );
413 | }
414 |
415 | __Promise.all(promises_all)
416 | .then(function(remaining_tokens_list) {
417 | done(
418 | new Error("Limiter consume should not succeed at the end (reject)")
419 | );
420 | })
421 | .catch(function(error) {
422 | if (error) {
423 | done(error);
424 | } else {
425 | done();
426 | }
427 | });
428 | });
429 | });
430 | });
431 |
--------------------------------------------------------------------------------