The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .editorconfig
├── .eslintrc.json
├── .github
    ├── FUNDING.yml
    └── workflows
    │   └── test.yml
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE.md
├── README.md
├── docker-compose.valkey-cluster.yml
├── docker-compose.yml
├── img
    ├── chart-exec-evenly-10r-end.png
    ├── chart-exec-evenly-10r-start.png
    ├── express-brute-example.png
    ├── heap-cluster-master.png
    ├── heap-cluster-worker.png
    ├── heap-memcache.png
    ├── heap-memory.png
    ├── heap-mongo.png
    ├── heap-mysql.png
    ├── heap-postgres.png
    ├── heap-redis.png
    └── rlflx-logo-small.png
├── index.js
├── lib
    ├── BurstyRateLimiter.js
    ├── ExpressBruteFlexible.js
    ├── RLWrapperBlackAndWhite.js
    ├── RateLimiterAbstract.js
    ├── RateLimiterCluster.js
    ├── RateLimiterDynamo.js
    ├── RateLimiterEtcd.js
    ├── RateLimiterEtcdNonAtomic.js
    ├── RateLimiterMemcache.js
    ├── RateLimiterMemory.js
    ├── RateLimiterMongo.js
    ├── RateLimiterMySQL.js
    ├── RateLimiterPostgres.js
    ├── RateLimiterPrisma.js
    ├── RateLimiterQueue.js
    ├── RateLimiterRedis.js
    ├── RateLimiterRes.js
    ├── RateLimiterSQLite.js
    ├── RateLimiterStoreAbstract.js
    ├── RateLimiterUnion.js
    ├── RateLimiterValkey.js
    ├── RateLimiterValkeyGlide.js
    ├── component
    │   ├── BlockedKeys
    │   │   ├── BlockedKeys.js
    │   │   └── index.js
    │   ├── MemoryStorage
    │   │   ├── MemoryStorage.js
    │   │   ├── Record.js
    │   │   └── index.js
    │   ├── RateLimiterEtcdTransactionFailedError.js
    │   ├── RateLimiterQueueError.js
    │   ├── RateLimiterSetupError.js
    │   └── index.d.ts
    ├── constants.js
    └── index.d.ts
├── package.json
└── test
    ├── BurstyRateLimiter.test.js
    ├── ExpressBruteFlexible.test.js
    ├── RLWrapperBlackAndWhite.test.js
    ├── RateLimiterAbstract.test.js
    ├── RateLimiterCluster.test.js
    ├── RateLimiterDynamo.test.js
    ├── RateLimiterEtcd.test.js
    ├── RateLimiterEtcdNonAtomic.test.js
    ├── RateLimiterMemcache.test.js
    ├── RateLimiterMemory.test.js
    ├── RateLimiterMongo.test.js
    ├── RateLimiterMySQL.test.js
    ├── RateLimiterPostgres.test.js
    ├── RateLimiterPrisma
        └── Postgres
        │   ├── RateLimiterPrismaPostgres.test.js
        │   └── schema.prisma
    ├── RateLimiterQueue.test.js
    ├── RateLimiterRedis.ioredis.test.js
    ├── RateLimiterRedis.redis.test.js
    ├── RateLimiterRes.test.js
    ├── RateLimiterSQLite.test.js
    ├── RateLimiterStoreAbstract.test.js
    ├── RateLimiterUnion.test.js
    ├── RateLimiterValkey.iovalkey.test.js
    ├── RateLimiterValkeyGlide.test.js
    ├── RedisOptions.js
    ├── component
        ├── BlockedKeys
        │   └── BlockedKeys.test.js
        ├── MemoryStorage
        │   ├── MemoryStorage.test.js
        │   └── Record.test.js
        └── RateLimiterQueueError.test.js
    └── scripts
        └── cluster-setup.sh


/.editorconfig:
--------------------------------------------------------------------------------
 1 | root = true
 2 | 
 3 | [*]
 4 | indent_size = 2
 5 | indent_style = space
 6 | end_of_line = lf
 7 | charset = utf-8
 8 | trim_trailing_whitespace = true
 9 | insert_final_newline = true
10 | 
11 | [*.md]
12 | insert_final_newline = false
13 | trim_trailing_whitespace = false
14 | 


--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "plugins": [
 3 |     "node",
 4 |     "security"
 5 |   ],
 6 |   "extends": [
 7 |     "plugin:node/recommended",
 8 |     "plugin:security/recommended",
 9 |     "eslint:recommended",
10 |     "airbnb-base"
11 |   ],
12 |   "env": {
13 |     "node": true
14 |   },
15 |   "rules": {
16 |     "no-underscore-dangle": "off",
17 |     "no-param-reassign": "off",
18 |     "no-plusplus": "off",
19 |     "radix": ["error", "as-needed"],
20 |     "consistent-return": "off",
21 |     "class-methods-use-this": "off",
22 |     "max-len": ["error", { "code": 140 }],
23 |     "node/no-unpublished-require": ["error", {
24 |       "allowModules": ["mocha", "chai", "redis-mock"]
25 |     }],
26 |     "node/no-unsupported-features": "off"
27 |   }
28 | }
29 | 


--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: animir
2 | patreon: animir
3 | 


--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
 3 | 
 4 | name: Tests
 5 | 
 6 | on:
 7 |   push:
 8 |     branches: [master]
 9 |   pull_request:
10 |     branches: [master]
11 |   workflow_dispatch:
12 | 
13 | concurrency:
14 |   group: test-${{ github.ref }}
15 |   cancel-in-progress: true
16 | 
17 | permissions:
18 |   contents: read # to fetch code (actions/checkout)
19 | 
20 | jobs:
21 |   lint:
22 |     runs-on: ubuntu-latest
23 |     steps:
24 |       - uses: actions/checkout@v4
25 |       - uses: actions/setup-node@v4
26 |         with:
27 |           node-version: "lts/*"
28 |           cache: "npm"
29 |           cache-dependency-path: ./package.json
30 |       - name: Install dependencies
31 |         run: |
32 |           npm install
33 |       - name: ESLint
34 |         run: |
35 |           npm run eslint
36 | 
37 |   compatibility:
38 |     runs-on: ubuntu-latest
39 | 
40 |     name: testing node@${{ matrix.node-version }}, valkey@${{ matrix.valkey-version }}
41 | 
42 |     strategy:
43 |       matrix:
44 |         node-version: [20.x, 22.x, 23.x]
45 |         valkey-version: [7.2, 8]
46 | 
47 |     services:
48 |       postgres:
49 |         image: postgres
50 |         env:
51 |           POSTGRES_PASSWORD: secret
52 |           POSTGRES_USER: root
53 |         ports:
54 |           - 5432:5432
55 |         # Set health checks to wait until postgres has started
56 |         options: >-
57 |           --health-cmd pg_isready
58 |           --health-interval 10s
59 |           --health-timeout 5s
60 |           --health-retries 5
61 | 
62 |     steps:
63 |       - name: Checkout repository
64 |         uses: actions/checkout@v4.0.0
65 | 
66 |       - name: Use Node.js ${{ matrix.node-version }}
67 |         uses: actions/setup-node@v3.8.1
68 |         with:
69 |           node-version: ${{ matrix.node-version }}
70 |           cache: npm
71 |           cache-dependency-path: ./package.json
72 | 
73 |       - name: Start Redis
74 |         uses: supercharge/redis-github-action@1.7.0
75 |         with:
76 |           redis-version: 7
77 | 
78 |       - uses: shogo82148/actions-setup-redis@v1
79 |         with:
80 |           distribution: "valkey"
81 |           redis-version: ${{ matrix.valkey-version }}
82 |           redis-port: 8080
83 | 
84 |       - name: Start DynamoDB local
85 |         uses: rrainn/dynamodb-action@v3.0.0
86 | 
87 |       - run: npm install
88 |       - run: npm run test
89 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | coverage
4 | package-lock.json
5 | .nyc_output/
6 | dump.rdb
7 | nodes.conf
8 | data/
9 | 


--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
 1 | .idea
 2 | .gitignore
 3 | .github
 4 | .npmignore
 5 | *.json
 6 | .editorconfig
 7 | test
 8 | coverage
 9 | img
10 | .github
11 | *.yml
12 | *.yaml
13 | .nyc_output
14 | 


--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
 1 | language: node_js
 2 | node_js:
 3 |   - "8"
 4 |   - "10"
 5 |   - "12"
 6 |   - "14"
 7 | script:
 8 |   - npm run eslint
 9 |   - npm run test
10 | after_success: 'npm run coveralls'
11 | 


--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | ## ISC License (ISC)
2 | 
3 | Copyright 2019 Roman Voloboev
4 | 
5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6 | 
7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
8 | 


--------------------------------------------------------------------------------
/docker-compose.valkey-cluster.yml:
--------------------------------------------------------------------------------
 1 | services:
 2 |   valkey-cluster:
 3 |     image: valkey/valkey:latest
 4 |     ipc: host
 5 |     ports:
 6 |       - "7001-7003:7001-7003"
 7 |     volumes:
 8 |       - ./test/scripts/cluster-setup.sh:/usr/local/bin/cluster-setup.sh:ro
 9 |     command: ["bash", "/usr/local/bin/cluster-setup.sh"]
10 |     environment:
11 |       - SKIP_SYSCTL=1
12 |     network_mode: "host"
13 |     healthcheck:
14 |       test: ["CMD", "valkey-cli", "-p", "7001", "cluster", "info"]
15 |       interval: 10s
16 |       timeout: 5s
17 |       retries: 5
18 |     privileged: true
19 |     restart: unless-stopped
20 |     stop_grace_period: 30s
21 | 


--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
 1 | services:
 2 |   redis:
 3 |     image: redis:7-alpine
 4 |     container_name: cache
 5 |     ports:
 6 |       - 6379:6379
 7 | 
 8 |   valkey:
 9 |     image: valkey/valkey:8-alpine
10 |     container_name: valkey-cache
11 |     command: valkey-server --port 8080
12 |     ports:
13 |       - 8080:8080
14 |     networks:
15 |       - valkey-network
16 | 
17 |   valkey-cluster:
18 |     image: valkey/valkey:8-alpine
19 |     container_name: valkey-cluster
20 |     command: >
21 |       sh -c "
22 |         valkey-server --port 8081 --cluster-enabled yes &
23 |         sleep 5;
24 |         valkey-cli -p 8081 cluster addslotsrange 0 16383;
25 |         tail -f /dev/null
26 |       "
27 |     ports:
28 |       - 8081:8081
29 |     networks:
30 |       - valkey-network
31 | 
32 |   dynamodb:
33 |     image: amazon/dynamodb-local
34 |     container_name: dynamo
35 |     ports:
36 |       - 8000:8000
37 | 
38 |   postgres:
39 |     image: postgres:latest
40 |     restart: always
41 |     environment:
42 |       POSTGRES_USER: root
43 |       POSTGRES_PASSWORD: secret
44 |     ports:
45 |       - "5432:5432"
46 | 
47 |   etcd:
48 |     image: bitnami/etcd:3.5
49 |     container_name: etcd
50 |     environment:
51 |       ALLOW_NONE_AUTHENTICATION: yes
52 |     ports:
53 |       - 2379:2379
54 | 
55 | networks:
56 |   valkey-network:
57 |     driver: bridge
58 | 


--------------------------------------------------------------------------------
/img/chart-exec-evenly-10r-end.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/animir/node-rate-limiter-flexible/2906f1a95e9b39d11e9706bdc19e210d11f815b5/img/chart-exec-evenly-10r-end.png


--------------------------------------------------------------------------------
/img/chart-exec-evenly-10r-start.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/animir/node-rate-limiter-flexible/2906f1a95e9b39d11e9706bdc19e210d11f815b5/img/chart-exec-evenly-10r-start.png


--------------------------------------------------------------------------------
/img/express-brute-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/animir/node-rate-limiter-flexible/2906f1a95e9b39d11e9706bdc19e210d11f815b5/img/express-brute-example.png


--------------------------------------------------------------------------------
/img/heap-cluster-master.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/animir/node-rate-limiter-flexible/2906f1a95e9b39d11e9706bdc19e210d11f815b5/img/heap-cluster-master.png


--------------------------------------------------------------------------------
/img/heap-cluster-worker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/animir/node-rate-limiter-flexible/2906f1a95e9b39d11e9706bdc19e210d11f815b5/img/heap-cluster-worker.png


--------------------------------------------------------------------------------
/img/heap-memcache.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/animir/node-rate-limiter-flexible/2906f1a95e9b39d11e9706bdc19e210d11f815b5/img/heap-memcache.png


--------------------------------------------------------------------------------
/img/heap-memory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/animir/node-rate-limiter-flexible/2906f1a95e9b39d11e9706bdc19e210d11f815b5/img/heap-memory.png


--------------------------------------------------------------------------------
/img/heap-mongo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/animir/node-rate-limiter-flexible/2906f1a95e9b39d11e9706bdc19e210d11f815b5/img/heap-mongo.png


--------------------------------------------------------------------------------
/img/heap-mysql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/animir/node-rate-limiter-flexible/2906f1a95e9b39d11e9706bdc19e210d11f815b5/img/heap-mysql.png


--------------------------------------------------------------------------------
/img/heap-postgres.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/animir/node-rate-limiter-flexible/2906f1a95e9b39d11e9706bdc19e210d11f815b5/img/heap-postgres.png


--------------------------------------------------------------------------------
/img/heap-redis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/animir/node-rate-limiter-flexible/2906f1a95e9b39d11e9706bdc19e210d11f815b5/img/heap-redis.png


--------------------------------------------------------------------------------
/img/rlflx-logo-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/animir/node-rate-limiter-flexible/2906f1a95e9b39d11e9706bdc19e210d11f815b5/img/rlflx-logo-small.png


--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
 1 | const RateLimiterRedis = require('./lib/RateLimiterRedis');
 2 | const RateLimiterMongo = require('./lib/RateLimiterMongo');
 3 | const RateLimiterMySQL = require('./lib/RateLimiterMySQL');
 4 | const RateLimiterPostgres = require('./lib/RateLimiterPostgres');
 5 | const { RateLimiterClusterMaster, RateLimiterClusterMasterPM2, RateLimiterCluster } = require('./lib/RateLimiterCluster');
 6 | const RateLimiterMemory = require('./lib/RateLimiterMemory');
 7 | const RateLimiterMemcache = require('./lib/RateLimiterMemcache');
 8 | const RLWrapperBlackAndWhite = require('./lib/RLWrapperBlackAndWhite');
 9 | const RateLimiterUnion = require('./lib/RateLimiterUnion');
10 | const RateLimiterQueue = require('./lib/RateLimiterQueue');
11 | const BurstyRateLimiter = require('./lib/BurstyRateLimiter');
12 | const RateLimiterRes = require('./lib/RateLimiterRes');
13 | const RateLimiterDynamo = require('./lib/RateLimiterDynamo');
14 | const RateLimiterPrisma = require('./lib/RateLimiterPrisma');
15 | const RateLimiterValkey = require('./lib/RateLimiterValkey');
16 | const RateLimiterValkeyGlide = require('./lib/RateLimiterValkeyGlide');
17 | const RateLimiterSQLite = require('./lib/RateLimiterSQLite');
18 | const RateLimiterEtcd = require('./lib/RateLimiterEtcd');
19 | const RateLimiterEtcdNonAtomic = require('./lib/RateLimiterEtcdNonAtomic');
20 | 
21 | module.exports = {
22 |   RateLimiterRedis,
23 |   RateLimiterMongo,
24 |   RateLimiterMySQL,
25 |   RateLimiterPostgres,
26 |   RateLimiterMemory,
27 |   RateLimiterMemcache,
28 |   RateLimiterClusterMaster,
29 |   RateLimiterClusterMasterPM2,
30 |   RateLimiterCluster,
31 |   RLWrapperBlackAndWhite,
32 |   RateLimiterUnion,
33 |   RateLimiterQueue,
34 |   BurstyRateLimiter,
35 |   RateLimiterRes,
36 |   RateLimiterDynamo,
37 |   RateLimiterPrisma,
38 |   RateLimiterValkey,
39 |   RateLimiterValkeyGlide,
40 |   RateLimiterSQLite,
41 |   RateLimiterEtcd,
42 |   RateLimiterEtcdNonAtomic,
43 | };
44 | 


--------------------------------------------------------------------------------
/lib/BurstyRateLimiter.js:
--------------------------------------------------------------------------------
 1 | const RateLimiterRes = require("./RateLimiterRes");
 2 | 
 3 | /**
 4 |  * Bursty rate limiter exposes only msBeforeNext time and doesn't expose points from bursty limiter by default
 5 |  * @type {BurstyRateLimiter}
 6 |  */
 7 | module.exports = class BurstyRateLimiter {
 8 |   constructor(rateLimiter, burstLimiter) {
 9 |     this._rateLimiter = rateLimiter;
10 |     this._burstLimiter = burstLimiter
11 |   }
12 | 
13 |   /**
14 |    * Merge rate limiter response objects. Responses can be null
15 |    *
16 |    * @param {RateLimiterRes} [rlRes] Rate limiter response
17 |    * @param {RateLimiterRes} [blRes] Bursty limiter response
18 |    */
19 |   _combineRes(rlRes, blRes) {
20 |     if (!rlRes) {
21 |       return null
22 |     }
23 | 
24 |     return new RateLimiterRes(
25 |       rlRes.remainingPoints,
26 |       Math.min(rlRes.msBeforeNext, blRes ? blRes.msBeforeNext : 0),
27 |       rlRes.consumedPoints,
28 |       rlRes.isFirstInDuration
29 |     )
30 |   }
31 | 
32 |   /**
33 |    * @param key
34 |    * @param pointsToConsume
35 |    * @param options
36 |    * @returns {Promise<any>}
37 |    */
38 |   consume(key, pointsToConsume = 1, options = {}) {
39 |     return this._rateLimiter.consume(key, pointsToConsume, options)
40 |       .catch((rlRej) => {
41 |         if (rlRej instanceof RateLimiterRes) {
42 |           return this._burstLimiter.consume(key, pointsToConsume, options)
43 |             .then((blRes) => {
44 |               return Promise.resolve(this._combineRes(rlRej, blRes))
45 |             })
46 |             .catch((blRej) => {
47 |                 if (blRej instanceof RateLimiterRes) {
48 |                   return Promise.reject(this._combineRes(rlRej, blRej))
49 |                 } else {
50 |                   return Promise.reject(blRej)
51 |                 }
52 |               }
53 |             )
54 |         } else {
55 |           return Promise.reject(rlRej)
56 |         }
57 |       })
58 |   }
59 | 
60 |   /**
61 |    * It doesn't expose available points from burstLimiter
62 |    *
63 |    * @param key
64 |    * @returns {Promise<RateLimiterRes>}
65 |    */
66 |   get(key) {
67 |     return Promise.all([
68 |       this._rateLimiter.get(key),
69 |       this._burstLimiter.get(key),
70 |     ]).then(([rlRes, blRes]) => {
71 |       return this._combineRes(rlRes, blRes);
72 |     });
73 |   }
74 | 
75 |   get points() {
76 |     return this._rateLimiter.points;
77 |   }
78 | };
79 | 


--------------------------------------------------------------------------------
/lib/ExpressBruteFlexible.js:
--------------------------------------------------------------------------------
  1 | const {
  2 |   LIMITER_TYPES,
  3 |   ERR_UNKNOWN_LIMITER_TYPE_MESSAGE,
  4 | } = require('./constants');
  5 | const crypto = require('crypto');
  6 | const {
  7 |   RateLimiterMemory,
  8 |   RateLimiterCluster,
  9 |   RateLimiterMemcache,
 10 |   RateLimiterMongo,
 11 |   RateLimiterMySQL,
 12 |   RateLimiterPostgres,
 13 |   RateLimiterRedis,
 14 |   RateLimiterValkey,
 15 |   RateLimiterValkeyGlide,
 16 | } = require('../index');
 17 | 
 18 | function getDelayMs(count, delays, maxWait) {
 19 |   let msDelay = maxWait;
 20 |   const delayIndex = count - 1;
 21 |   if (delayIndex >= 0 && delayIndex < delays.length) {
 22 |     msDelay = delays[delayIndex];
 23 |   }
 24 | 
 25 |   return msDelay;
 26 | }
 27 | 
 28 | const ExpressBruteFlexible = function (limiterType, options) {
 29 |   ExpressBruteFlexible.instanceCount++;
 30 |   this.name = `brute${ExpressBruteFlexible.instanceCount}`;
 31 | 
 32 |   this.options = Object.assign({}, ExpressBruteFlexible.defaults, options);
 33 |   if (this.options.minWait < 1) {
 34 |     this.options.minWait = 1;
 35 |   }
 36 | 
 37 |   const validLimiterTypes = Object.keys(ExpressBruteFlexible.LIMITER_TYPES).map(k => ExpressBruteFlexible.LIMITER_TYPES[k]);
 38 |   if (!validLimiterTypes.includes(limiterType)) {
 39 |     throw new Error(ERR_UNKNOWN_LIMITER_TYPE_MESSAGE);
 40 |   }
 41 |   this.limiterType = limiterType;
 42 | 
 43 |   this.delays = [this.options.minWait];
 44 |   while (this.delays[this.delays.length - 1] < this.options.maxWait) {
 45 |     const nextNum = this.delays[this.delays.length - 1] + (this.delays.length > 1 ? this.delays[this.delays.length - 2] : 0);
 46 |     this.delays.push(nextNum);
 47 |   }
 48 |   this.delays[this.delays.length - 1] = this.options.maxWait;
 49 | 
 50 |   // set default lifetime
 51 |   if (typeof this.options.lifetime === 'undefined') {
 52 |     this.options.lifetime = Math.ceil((this.options.maxWait / 1000) * (this.delays.length + this.options.freeRetries));
 53 |   }
 54 | 
 55 |   this.prevent = this.getMiddleware({
 56 |     prefix: this.options.prefix,
 57 |   });
 58 | };
 59 | 
 60 | ExpressBruteFlexible.prototype.getMiddleware = function (options) {
 61 |   const opts = Object.assign({}, options);
 62 |   const commonKeyPrefix = opts.prefix || '';
 63 |   const freeLimiterOptions = {
 64 |     storeClient: this.options.storeClient,
 65 |     storeType: this.options.storeType,
 66 |     keyPrefix: `${commonKeyPrefix}free`,
 67 |     dbName: this.options.dbName,
 68 |     tableName: this.options.tableName,
 69 |     points: this.options.freeRetries > 0 ? this.options.freeRetries - 1 : 0,
 70 |     duration: this.options.lifetime,
 71 |   };
 72 | 
 73 |   const blockLimiterOptions = {
 74 |     storeClient: this.options.storeClient,
 75 |     storeType: this.options.storeType,
 76 |     keyPrefix: `${commonKeyPrefix}block`,
 77 |     dbName: this.options.dbName,
 78 |     tableName: this.options.tableName,
 79 |     points: 1,
 80 |     duration: Math.min(this.options.lifetime, Math.ceil((this.options.maxWait / 1000))),
 81 |   };
 82 | 
 83 |   const counterLimiterOptions = {
 84 |     storeClient: this.options.storeClient,
 85 |     storeType: this.options.storeType,
 86 |     keyPrefix: `${commonKeyPrefix}counter`,
 87 |     dbName: this.options.dbName,
 88 |     tableName: this.options.tableName,
 89 |     points: 1,
 90 |     duration: this.options.lifetime,
 91 |   };
 92 | 
 93 |   switch (this.limiterType) {
 94 |     case 'memory':
 95 |       this.freeLimiter = new RateLimiterMemory(freeLimiterOptions);
 96 |       this.blockLimiter = new RateLimiterMemory(blockLimiterOptions);
 97 |       this.counterLimiter = new RateLimiterMemory(counterLimiterOptions);
 98 |       break;
 99 |     case 'cluster':
100 |       this.freeLimiter = new RateLimiterCluster(freeLimiterOptions);
101 |       this.blockLimiter = new RateLimiterCluster(blockLimiterOptions);
102 |       this.counterLimiter = new RateLimiterCluster(counterLimiterOptions);
103 |       break;
104 |     case 'memcache':
105 |       this.freeLimiter = new RateLimiterMemcache(freeLimiterOptions);
106 |       this.blockLimiter = new RateLimiterMemcache(blockLimiterOptions);
107 |       this.counterLimiter = new RateLimiterMemcache(counterLimiterOptions);
108 |       break;
109 |     case 'mongo':
110 |       this.freeLimiter = new RateLimiterMongo(freeLimiterOptions);
111 |       this.blockLimiter = new RateLimiterMongo(blockLimiterOptions);
112 |       this.counterLimiter = new RateLimiterMongo(counterLimiterOptions);
113 |       break;
114 |     case 'mysql':
115 |       this.freeLimiter = new RateLimiterMySQL(freeLimiterOptions);
116 |       this.blockLimiter = new RateLimiterMySQL(blockLimiterOptions);
117 |       this.counterLimiter = new RateLimiterMySQL(counterLimiterOptions);
118 |       break;
119 |     case 'postgres':
120 |       this.freeLimiter = new RateLimiterPostgres(freeLimiterOptions);
121 |       this.blockLimiter = new RateLimiterPostgres(blockLimiterOptions);
122 |       this.counterLimiter = new RateLimiterPostgres(counterLimiterOptions);
123 |       break;
124 |     case 'valkey-glide':
125 |       this.freeLimiter = new RateLimiterValkeyGlide(freeLimiterOptions);
126 |       this.blockLimiter = new RateLimiterValkeyGlide(blockLimiterOptions);
127 |       this.counterLimiter = new RateLimiterValkeyGlide(counterLimiterOptions);
128 |       break;
129 |     case 'valkey':
130 |       this.freeLimiter = new RateLimiterValkey(freeLimiterOptions);
131 |       this.blockLimiter = new RateLimiterValkey(blockLimiterOptions);
132 |       this.counterLimiter = new RateLimiterValkey(counterLimiterOptions);
133 |       break;
134 |     case 'redis':
135 |       this.freeLimiter = new RateLimiterRedis(freeLimiterOptions);
136 |       this.blockLimiter = new RateLimiterRedis(blockLimiterOptions);
137 |       this.counterLimiter = new RateLimiterRedis(counterLimiterOptions);
138 |       break;
139 |     default:
140 |       throw new Error(ERR_UNKNOWN_LIMITER_TYPE_MESSAGE);
141 |   }
142 | 
143 |   let keyFunc = opts.key;
144 |   if (typeof keyFunc !== 'function') {
145 |     keyFunc = function (req, res, next) {
146 |       next(opts.key);
147 |     };
148 |   }
149 | 
150 |   const getFailCallback = (() => (typeof opts.failCallback === 'undefined' ? this.options.failCallback : opts.failCallback));
151 | 
152 |   return (req, res, next) => {
153 |     const cannotIncrementErrorObjectBase = {
154 |       req,
155 |       res,
156 |       next,
157 |       message: 'Cannot increment request count',
158 |     };
159 | 
160 |     keyFunc(req, res, (key) => {
161 |       if (!opts.ignoreIP) {
162 |         key = ExpressBruteFlexible._getKey([req.ip, this.name, key]);
163 |       } else {
164 |         key = ExpressBruteFlexible._getKey([this.name, key]);
165 |       }
166 | 
167 |       // attach a simpler "reset" function to req.brute.reset
168 |       if (this.options.attachResetToRequest) {
169 |         let reset = ((callback) => {
170 |           Promise.all([
171 |             this.freeLimiter.delete(key),
172 |             this.blockLimiter.delete(key),
173 |             this.counterLimiter.delete(key),
174 |           ]).then(() => {
175 |             if (typeof callback === 'function') {
176 |               process.nextTick(() => {
177 |                 callback();
178 |               });
179 |             }
180 |           }).catch((err) => {
181 |             if (typeof callback === 'function') {
182 |               process.nextTick(() => {
183 |                 callback(err);
184 |               });
185 |             }
186 |           });
187 |         });
188 | 
189 |         if (req.brute && req.brute.reset) {
190 |           // wrap existing reset if one exists
191 |           const oldReset = req.brute.reset;
192 |           const newReset = reset;
193 |           reset = function (callback) {
194 |             oldReset(() => {
195 |               newReset(callback);
196 |             });
197 |           };
198 |         }
199 |         req.brute = {
200 |           reset,
201 |         };
202 |       }
203 | 
204 |       this.freeLimiter.consume(key)
205 |         .then(() => {
206 |           if (typeof next === 'function') {
207 |             next();
208 |           }
209 |         })
210 |         .catch(() => {
211 |           Promise.all([
212 |             this.blockLimiter.get(key),
213 |             this.counterLimiter.get(key),
214 |           ])
215 |             .then((allRes) => {
216 |               const [blockRes, counterRes] = allRes;
217 | 
218 |               if (blockRes === null) {
219 |                 const msDelay = getDelayMs(
220 |                   counterRes ? counterRes.consumedPoints + 1 : 1,
221 |                   this.delays,
222 |                   // eslint-disable-next-line
223 |                   this.options.maxWait
224 |                 );
225 | 
226 |                 this.blockLimiter.penalty(key, 1, { customDuration: Math.ceil(msDelay / 1000) })
227 |                   .then((blockPenaltyRes) => {
228 |                     if (blockPenaltyRes.consumedPoints === 1) {
229 |                       this.counterLimiter.penalty(key)
230 |                         .then(() => {
231 |                           if (typeof next === 'function') {
232 |                             next();
233 |                           }
234 |                         })
235 |                         .catch((err) => {
236 |                           this.options.handleStoreError(Object.assign({}, cannotIncrementErrorObjectBase, { parent: err }));
237 |                         });
238 |                     } else {
239 |                       const nextValidDate = new Date(Date.now() + blockPenaltyRes.msBeforeNext);
240 | 
241 |                       const failCallback = getFailCallback();
242 |                       if (typeof failCallback === 'function') {
243 |                         failCallback(req, res, next, nextValidDate);
244 |                       }
245 |                     }
246 |                   })
247 |                   .catch((err) => {
248 |                     this.options.handleStoreError(Object.assign({}, cannotIncrementErrorObjectBase, { parent: err }));
249 |                   });
250 |               } else {
251 |                 const nextValidDate = new Date(Date.now() + blockRes.msBeforeNext);
252 | 
253 |                 const failCallback = getFailCallback();
254 |                 if (typeof failCallback === 'function') {
255 |                   failCallback(req, res, next, nextValidDate);
256 |                 }
257 |               }
258 |             })
259 |             .catch((err) => {
260 |               this.options.handleStoreError(Object.assign({}, cannotIncrementErrorObjectBase, { parent: err }));
261 |             });
262 |         });
263 |     });
264 |   };
265 | };
266 | 
267 | ExpressBruteFlexible.prototype.reset = function (ip, key, callback) {
268 |   let keyArgs = [];
269 |   if (ip) {
270 |     keyArgs.push(ip)
271 |   }
272 |   keyArgs.push(this.name);
273 |   keyArgs.push(key);
274 |   const ebKey = ExpressBruteFlexible._getKey(keyArgs);
275 | 
276 |   Promise.all([
277 |     this.freeLimiter.delete(ebKey),
278 |     this.blockLimiter.delete(ebKey),
279 |     this.counterLimiter.delete(ebKey),
280 |   ]).then(() => {
281 |     if (typeof callback === 'function') {
282 |       process.nextTick(() => {
283 |         callback();
284 |       });
285 |     }
286 |   }).catch((err) => {
287 |     this.options.handleStoreError({
288 |       message: 'Cannot reset request count',
289 |       parent: err,
290 |       key,
291 |       ip,
292 |     });
293 |   });
294 | };
295 | 
296 | ExpressBruteFlexible._getKey = function (arr) {
297 |   let key = '';
298 | 
299 |   arr.forEach((part) => {
300 |     if (part) {
301 |       key += crypto.createHash('sha256').update(part).digest('base64');
302 |     }
303 |   });
304 | 
305 |   return crypto.createHash('sha256').update(key).digest('base64');
306 | };
307 | 
308 | const setRetryAfter = function (res, nextValidRequestDate) {
309 |   const secondUntilNextRequest = Math.ceil((nextValidRequestDate.getTime() - Date.now()) / 1000);
310 |   res.header('Retry-After', secondUntilNextRequest);
311 | };
312 | ExpressBruteFlexible.FailTooManyRequests = function (req, res, next, nextValidRequestDate) {
313 |   setRetryAfter(res, nextValidRequestDate);
314 |   res.status(429);
315 |   res.send({
316 |     error: {
317 |       text: 'Too many requests in this time frame.',
318 |       nextValidRequestDate,
319 |     },
320 |   });
321 | };
322 | ExpressBruteFlexible.FailForbidden = function (req, res, next, nextValidRequestDate) {
323 |   setRetryAfter(res, nextValidRequestDate);
324 |   res.status(403);
325 |   res.send({
326 |     error: {
327 |       text: 'Too many requests in this time frame.',
328 |       nextValidRequestDate,
329 |     },
330 |   });
331 | };
332 | ExpressBruteFlexible.FailMark = function (req, res, next, nextValidRequestDate) {
333 |   res.status(429);
334 |   setRetryAfter(res, nextValidRequestDate);
335 |   res.nextValidRequestDate = nextValidRequestDate;
336 |   next();
337 | };
338 | 
339 | ExpressBruteFlexible.defaults = {
340 |   freeRetries: 2,
341 |   attachResetToRequest: true,
342 |   minWait: 500,
343 |   maxWait: 1000 * 60 * 15,
344 |   failCallback: ExpressBruteFlexible.FailTooManyRequests,
345 |   handleStoreError(err) {
346 |     // eslint-disable-next-line
347 |     throw {
348 |       message: err.message,
349 |       parent: err.parent,
350 |     };
351 |   },
352 | };
353 | 
354 | ExpressBruteFlexible.LIMITER_TYPES = LIMITER_TYPES;
355 | 
356 | ExpressBruteFlexible.instanceCount = 0;
357 | 
358 | 
359 | module.exports = ExpressBruteFlexible;
360 | 


--------------------------------------------------------------------------------
/lib/RLWrapperBlackAndWhite.js:
--------------------------------------------------------------------------------
  1 | const RateLimiterRes = require('./RateLimiterRes');
  2 | 
  3 | module.exports = class RLWrapperBlackAndWhite {
  4 |   constructor(opts = {}) {
  5 |     this.limiter = opts.limiter;
  6 |     this.blackList = opts.blackList;
  7 |     this.whiteList = opts.whiteList;
  8 |     this.isBlackListed = opts.isBlackListed;
  9 |     this.isWhiteListed = opts.isWhiteListed;
 10 |     this.runActionAnyway = opts.runActionAnyway;
 11 |   }
 12 | 
 13 |   get limiter() {
 14 |     return this._limiter;
 15 |   }
 16 | 
 17 |   set limiter(value) {
 18 |     if (typeof value === 'undefined') {
 19 |       throw new Error('limiter is not set');
 20 |     }
 21 | 
 22 |     this._limiter = value;
 23 |   }
 24 | 
 25 |   get runActionAnyway() {
 26 |     return this._runActionAnyway;
 27 |   }
 28 | 
 29 |   set runActionAnyway(value) {
 30 |     this._runActionAnyway = typeof value === 'undefined' ? false : value;
 31 |   }
 32 | 
 33 |   get blackList() {
 34 |     return this._blackList;
 35 |   }
 36 | 
 37 |   set blackList(value) {
 38 |     this._blackList = Array.isArray(value) ? value : [];
 39 |   }
 40 | 
 41 |   get isBlackListed() {
 42 |     return this._isBlackListed;
 43 |   }
 44 | 
 45 |   set isBlackListed(func) {
 46 |     if (typeof func === 'undefined') {
 47 |       func = () => false;
 48 |     }
 49 |     if (typeof func !== 'function') {
 50 |       throw new Error('isBlackListed must be function');
 51 |     }
 52 |     this._isBlackListed = func;
 53 |   }
 54 | 
 55 |   get whiteList() {
 56 |     return this._whiteList;
 57 |   }
 58 | 
 59 |   set whiteList(value) {
 60 |     this._whiteList = Array.isArray(value) ? value : [];
 61 |   }
 62 | 
 63 |   get isWhiteListed() {
 64 |     return this._isWhiteListed;
 65 |   }
 66 | 
 67 |   set isWhiteListed(func) {
 68 |     if (typeof func === 'undefined') {
 69 |       func = () => false;
 70 |     }
 71 |     if (typeof func !== 'function') {
 72 |       throw new Error('isWhiteListed must be function');
 73 |     }
 74 |     this._isWhiteListed = func;
 75 |   }
 76 | 
 77 |   isBlackListedSomewhere(key) {
 78 |     return this.blackList.indexOf(key) >= 0 || this.isBlackListed(key);
 79 |   }
 80 | 
 81 |   isWhiteListedSomewhere(key) {
 82 |     return this.whiteList.indexOf(key) >= 0 || this.isWhiteListed(key);
 83 |   }
 84 | 
 85 |   getBlackRes() {
 86 |     return new RateLimiterRes(0, Number.MAX_SAFE_INTEGER, 0, false);
 87 |   }
 88 | 
 89 |   getWhiteRes() {
 90 |     return new RateLimiterRes(Number.MAX_SAFE_INTEGER, 0, 0, false);
 91 |   }
 92 | 
 93 |   rejectBlack() {
 94 |     return Promise.reject(this.getBlackRes());
 95 |   }
 96 | 
 97 |   resolveBlack() {
 98 |     return Promise.resolve(this.getBlackRes());
 99 |   }
100 | 
101 |   resolveWhite() {
102 |     return Promise.resolve(this.getWhiteRes());
103 |   }
104 | 
105 |   consume(key, pointsToConsume = 1) {
106 |     let res;
107 |     if (this.isWhiteListedSomewhere(key)) {
108 |       res = this.resolveWhite();
109 |     } else if (this.isBlackListedSomewhere(key)) {
110 |       res = this.rejectBlack();
111 |     }
112 | 
113 |     if (typeof res === 'undefined') {
114 |       return this.limiter.consume(key, pointsToConsume);
115 |     }
116 | 
117 |     if (this.runActionAnyway) {
118 |       this.limiter.consume(key, pointsToConsume).catch(() => {});
119 |     }
120 |     return res;
121 |   }
122 | 
123 |   block(key, secDuration) {
124 |     let res;
125 |     if (this.isWhiteListedSomewhere(key)) {
126 |       res = this.resolveWhite();
127 |     } else if (this.isBlackListedSomewhere(key)) {
128 |       res = this.resolveBlack();
129 |     }
130 | 
131 |     if (typeof res === 'undefined') {
132 |       return this.limiter.block(key, secDuration);
133 |     }
134 | 
135 |     if (this.runActionAnyway) {
136 |       this.limiter.block(key, secDuration).catch(() => {});
137 |     }
138 |     return res;
139 |   }
140 | 
141 |   penalty(key, points) {
142 |     let res;
143 |     if (this.isWhiteListedSomewhere(key)) {
144 |       res = this.resolveWhite();
145 |     } else if (this.isBlackListedSomewhere(key)) {
146 |       res = this.resolveBlack();
147 |     }
148 | 
149 |     if (typeof res === 'undefined') {
150 |       return this.limiter.penalty(key, points);
151 |     }
152 | 
153 |     if (this.runActionAnyway) {
154 |       this.limiter.penalty(key, points).catch(() => {});
155 |     }
156 |     return res;
157 |   }
158 | 
159 |   reward(key, points) {
160 |     let res;
161 |     if (this.isWhiteListedSomewhere(key)) {
162 |       res = this.resolveWhite();
163 |     } else if (this.isBlackListedSomewhere(key)) {
164 |       res = this.resolveBlack();
165 |     }
166 | 
167 |     if (typeof res === 'undefined') {
168 |       return this.limiter.reward(key, points);
169 |     }
170 | 
171 |     if (this.runActionAnyway) {
172 |       this.limiter.reward(key, points).catch(() => {});
173 |     }
174 |     return res;
175 |   }
176 | 
177 |   get(key) {
178 |     let res;
179 |     if (this.isWhiteListedSomewhere(key)) {
180 |       res = this.resolveWhite();
181 |     } else if (this.isBlackListedSomewhere(key)) {
182 |       res = this.resolveBlack();
183 |     }
184 | 
185 |     if (typeof res === 'undefined' || this.runActionAnyway) {
186 |       return this.limiter.get(key);
187 |     }
188 | 
189 |     return res;
190 |   }
191 | 
192 |   delete(key) {
193 |     return this.limiter.delete(key);
194 |   }
195 | };
196 | 


--------------------------------------------------------------------------------
/lib/RateLimiterAbstract.js:
--------------------------------------------------------------------------------
  1 | module.exports = class RateLimiterAbstract {
  2 |   /**
  3 |    *
  4 |    * @param opts Object Defaults {
  5 |    *   points: 4, // Number of points
  6 |    *   duration: 1, // Per seconds
  7 |    *   blockDuration: 0, // Block if consumed more than points in current duration for blockDuration seconds
  8 |    *   execEvenly: false, // Execute allowed actions evenly over duration
  9 |    *   execEvenlyMinDelayMs: duration * 1000 / points, // ms, works with execEvenly=true option
 10 |    *   keyPrefix: 'rlflx',
 11 |    * }
 12 |    */
 13 |   constructor(opts = {}) {
 14 |     this.points = opts.points;
 15 |     this.duration = opts.duration;
 16 |     this.blockDuration = opts.blockDuration;
 17 |     this.execEvenly = opts.execEvenly;
 18 |     this.execEvenlyMinDelayMs = opts.execEvenlyMinDelayMs;
 19 |     this.keyPrefix = opts.keyPrefix;
 20 |   }
 21 | 
 22 |   get points() {
 23 |     return this._points;
 24 |   }
 25 | 
 26 |   set points(value) {
 27 |     this._points = value >= 0 ? value : 4;
 28 |   }
 29 | 
 30 |   get duration() {
 31 |     return this._duration;
 32 |   }
 33 | 
 34 |   set duration(value) {
 35 |     this._duration = typeof value === 'undefined' ? 1 : value;
 36 |   }
 37 | 
 38 |   get msDuration() {
 39 |     return this.duration * 1000;
 40 |   }
 41 | 
 42 |   get blockDuration() {
 43 |     return this._blockDuration;
 44 |   }
 45 | 
 46 |   set blockDuration(value) {
 47 |     this._blockDuration = typeof value === 'undefined' ? 0 : value;
 48 |   }
 49 | 
 50 |   get msBlockDuration() {
 51 |     return this.blockDuration * 1000;
 52 |   }
 53 | 
 54 |   get execEvenly() {
 55 |     return this._execEvenly;
 56 |   }
 57 | 
 58 |   set execEvenly(value) {
 59 |     this._execEvenly = typeof value === 'undefined' ? false : Boolean(value);
 60 |   }
 61 | 
 62 |   get execEvenlyMinDelayMs() {
 63 |     return this._execEvenlyMinDelayMs;
 64 |   }
 65 | 
 66 |   set execEvenlyMinDelayMs(value) {
 67 |     this._execEvenlyMinDelayMs = typeof value === 'undefined' ? Math.ceil(this.msDuration / this.points) : value;
 68 |   }
 69 | 
 70 |   get keyPrefix() {
 71 |     return this._keyPrefix;
 72 |   }
 73 | 
 74 |   set keyPrefix(value) {
 75 |     if (typeof value === 'undefined') {
 76 |       value = 'rlflx';
 77 |     }
 78 |     if (typeof value !== 'string') {
 79 |       throw new Error('keyPrefix must be string');
 80 |     }
 81 |     this._keyPrefix = value;
 82 |   }
 83 | 
 84 |   _getKeySecDuration(options = {}) {
 85 |     return options && options.customDuration >= 0
 86 |       ? options.customDuration
 87 |       : this.duration;
 88 |   }
 89 | 
 90 |   getKey(key) {
 91 |     return this.keyPrefix.length > 0 ? `${this.keyPrefix}:${key}` : key;
 92 |   }
 93 | 
 94 |   parseKey(rlKey) {
 95 |     return rlKey.substring(this.keyPrefix.length);
 96 |   }
 97 | 
 98 |   consume() {
 99 |     throw new Error("You have to implement the method 'consume'!");
100 |   }
101 | 
102 |   penalty() {
103 |     throw new Error("You have to implement the method 'penalty'!");
104 |   }
105 | 
106 |   reward() {
107 |     throw new Error("You have to implement the method 'reward'!");
108 |   }
109 | 
110 |   get() {
111 |     throw new Error("You have to implement the method 'get'!");
112 |   }
113 | 
114 |   set() {
115 |     throw new Error("You have to implement the method 'set'!");
116 |   }
117 | 
118 |   block() {
119 |     throw new Error("You have to implement the method 'block'!");
120 |   }
121 | 
122 |   delete() {
123 |     throw new Error("You have to implement the method 'delete'!");
124 |   }
125 | };
126 | 


--------------------------------------------------------------------------------
/lib/RateLimiterCluster.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Implements rate limiting in cluster using built-in IPC
  3 |  *
  4 |  * Two classes are described here: master and worker
  5 |  * Master have to be create in the master process without any options.
  6 |  * Any number of rate limiters can be created in workers, but each rate limiter must be with unique keyPrefix
  7 |  *
  8 |  * Workflow:
  9 |  * 1. master rate limiter created in master process
 10 |  * 2. worker rate limiter sends 'init' message with necessary options during creating
 11 |  * 3. master receives options and adds new rate limiter by keyPrefix if it isn't created yet
 12 |  * 4. master sends 'init' back to worker's rate limiter
 13 |  * 5. worker can process requests immediately,
 14 |  *    but they will be postponed by 'workerWaitInit' until master sends 'init' to worker
 15 |  * 6. every request to worker rate limiter creates a promise
 16 |  * 7. if master doesn't response for 'timeout', promise is rejected
 17 |  * 8. master sends 'resolve' or 'reject' command to worker
 18 |  * 9. worker resolves or rejects promise depending on message from master
 19 |  *
 20 |  */
 21 | 
 22 | const cluster = require('cluster');
 23 | const crypto = require('crypto');
 24 | const RateLimiterAbstract = require('./RateLimiterAbstract');
 25 | const RateLimiterMemory = require('./RateLimiterMemory');
 26 | const RateLimiterRes = require('./RateLimiterRes');
 27 | 
 28 | const channel = 'rate_limiter_flexible';
 29 | let masterInstance = null;
 30 | 
 31 | const masterSendToWorker = function (worker, msg, type, res) {
 32 |   let data;
 33 |   if (res === null || res === true || res === false) {
 34 |     data = res;
 35 |   } else {
 36 |     data = {
 37 |       remainingPoints: res.remainingPoints,
 38 |       msBeforeNext: res.msBeforeNext,
 39 |       consumedPoints: res.consumedPoints,
 40 |       isFirstInDuration: res.isFirstInDuration,
 41 |     };
 42 |   }
 43 |   worker.send({
 44 |     channel,
 45 |     keyPrefix: msg.keyPrefix, // which rate limiter exactly
 46 |     promiseId: msg.promiseId,
 47 |     type,
 48 |     data,
 49 |   });
 50 | };
 51 | 
 52 | const workerWaitInit = function (payload) {
 53 |   setTimeout(() => {
 54 |     if (this._initiated) {
 55 |       process.send(payload);
 56 |       // Promise will be removed by timeout if too long
 57 |     } else if (typeof this._promises[payload.promiseId] !== 'undefined') {
 58 |       workerWaitInit.call(this, payload);
 59 |     }
 60 |   }, 30);
 61 | };
 62 | 
 63 | const workerSendToMaster = function (func, promiseId, key, arg, opts) {
 64 |   const payload = {
 65 |     channel,
 66 |     keyPrefix: this.keyPrefix,
 67 |     func,
 68 |     promiseId,
 69 |     data: {
 70 |       key,
 71 |       arg,
 72 |       opts,
 73 |     },
 74 |   };
 75 | 
 76 |   if (!this._initiated) {
 77 |     // Wait init before sending messages to master
 78 |     workerWaitInit.call(this, payload);
 79 |   } else {
 80 |     process.send(payload);
 81 |   }
 82 | };
 83 | 
 84 | const masterProcessMsg = function (worker, msg) {
 85 |   if (!msg || msg.channel !== channel || typeof this._rateLimiters[msg.keyPrefix] === 'undefined') {
 86 |     return false;
 87 |   }
 88 | 
 89 |   let promise;
 90 | 
 91 |   switch (msg.func) {
 92 |     case 'consume':
 93 |       promise = this._rateLimiters[msg.keyPrefix].consume(msg.data.key, msg.data.arg, msg.data.opts);
 94 |       break;
 95 |     case 'penalty':
 96 |       promise = this._rateLimiters[msg.keyPrefix].penalty(msg.data.key, msg.data.arg, msg.data.opts);
 97 |       break;
 98 |     case 'reward':
 99 |       promise = this._rateLimiters[msg.keyPrefix].reward(msg.data.key, msg.data.arg, msg.data.opts);
100 |       break;
101 |     case 'block':
102 |       promise = this._rateLimiters[msg.keyPrefix].block(msg.data.key, msg.data.arg, msg.data.opts);
103 |       break;
104 |     case 'get':
105 |       promise = this._rateLimiters[msg.keyPrefix].get(msg.data.key, msg.data.opts);
106 |       break;
107 |     case 'delete':
108 |       promise = this._rateLimiters[msg.keyPrefix].delete(msg.data.key, msg.data.opts);
109 |       break;
110 |     default:
111 |       return false;
112 |   }
113 | 
114 |   if (promise) {
115 |     promise
116 |       .then((res) => {
117 |         masterSendToWorker(worker, msg, 'resolve', res);
118 |       })
119 |       .catch((rejRes) => {
120 |         masterSendToWorker(worker, msg, 'reject', rejRes);
121 |       });
122 |   }
123 | };
124 | 
125 | const workerProcessMsg = function (msg) {
126 |   if (!msg || msg.channel !== channel || msg.keyPrefix !== this.keyPrefix) {
127 |     return false;
128 |   }
129 | 
130 |   if (this._promises[msg.promiseId]) {
131 |     clearTimeout(this._promises[msg.promiseId].timeoutId);
132 |     let res;
133 |     if (msg.data === null || msg.data === true || msg.data === false) {
134 |       res = msg.data;
135 |     } else {
136 |       res = new RateLimiterRes(
137 |         msg.data.remainingPoints,
138 |         msg.data.msBeforeNext,
139 |         msg.data.consumedPoints,
140 |         msg.data.isFirstInDuration // eslint-disable-line comma-dangle
141 |       );
142 |     }
143 | 
144 |     switch (msg.type) {
145 |       case 'resolve':
146 |         this._promises[msg.promiseId].resolve(res);
147 |         break;
148 |       case 'reject':
149 |         this._promises[msg.promiseId].reject(res);
150 |         break;
151 |       default:
152 |         throw new Error(`RateLimiterCluster: no such message type '${msg.type}'`);
153 |     }
154 | 
155 |     delete this._promises[msg.promiseId];
156 |   }
157 | };
158 | /**
159 |  * Prepare options to send to master
160 |  * Master will create rate limiter depending on options
161 |  *
162 |  * @returns {{points: *, duration: *, blockDuration: *, execEvenly: *, execEvenlyMinDelayMs: *, keyPrefix: *}}
163 |  */
164 | const getOpts = function () {
165 |   return {
166 |     points: this.points,
167 |     duration: this.duration,
168 |     blockDuration: this.blockDuration,
169 |     execEvenly: this.execEvenly,
170 |     execEvenlyMinDelayMs: this.execEvenlyMinDelayMs,
171 |     keyPrefix: this.keyPrefix,
172 |   };
173 | };
174 | 
175 | const savePromise = function (resolve, reject) {
176 |   const hrtime = process.hrtime();
177 |   let promiseId = hrtime[0].toString() + hrtime[1].toString();
178 | 
179 |   if (typeof this._promises[promiseId] !== 'undefined') {
180 |     promiseId += crypto.randomBytes(12).toString('base64');
181 |   }
182 | 
183 |   this._promises[promiseId] = {
184 |     resolve,
185 |     reject,
186 |     timeoutId: setTimeout(() => {
187 |       delete this._promises[promiseId];
188 |       reject(new Error('RateLimiterCluster timeout: no answer from master in time'));
189 |     }, this.timeoutMs),
190 |   };
191 | 
192 |   return promiseId;
193 | };
194 | 
195 | class RateLimiterClusterMaster {
196 |   constructor() {
197 |     if (masterInstance) {
198 |       return masterInstance;
199 |     }
200 | 
201 |     this._rateLimiters = {};
202 | 
203 |     cluster.setMaxListeners(0);
204 | 
205 |     cluster.on('message', (worker, msg) => {
206 |       if (msg && msg.channel === channel && msg.type === 'init') {
207 |         // If init request, check or create rate limiter by key prefix and send 'init' back to worker
208 |         if (typeof this._rateLimiters[msg.opts.keyPrefix] === 'undefined') {
209 |           this._rateLimiters[msg.opts.keyPrefix] = new RateLimiterMemory(msg.opts);
210 |         }
211 | 
212 |         worker.send({
213 |           channel,
214 |           type: 'init',
215 |           keyPrefix: msg.opts.keyPrefix,
216 |         });
217 |       } else {
218 |         masterProcessMsg.call(this, worker, msg);
219 |       }
220 |     });
221 | 
222 |     masterInstance = this;
223 |   }
224 | }
225 | 
226 | class RateLimiterClusterMasterPM2 {
227 |   constructor(pm2) {
228 |     if (masterInstance) {
229 |       return masterInstance;
230 |     }
231 | 
232 |     this._rateLimiters = {};
233 | 
234 |     pm2.launchBus((err, pm2Bus) => {
235 |       pm2Bus.on('process:msg', (packet) => {
236 |         const msg = packet.raw;
237 |         if (msg && msg.channel === channel && msg.type === 'init') {
238 |           // If init request, check or create rate limiter by key prefix and send 'init' back to worker
239 |           if (typeof this._rateLimiters[msg.opts.keyPrefix] === 'undefined') {
240 |             this._rateLimiters[msg.opts.keyPrefix] = new RateLimiterMemory(msg.opts);
241 |           }
242 | 
243 |           pm2.sendDataToProcessId(packet.process.pm_id, {
244 |             data: {},
245 |             topic: channel,
246 |             channel,
247 |             type: 'init',
248 |             keyPrefix: msg.opts.keyPrefix,
249 |           }, (sendErr, res) => {
250 |             if (sendErr) {
251 |               console.log(sendErr, res);
252 |             }
253 |           });
254 |         } else {
255 |           const worker = {
256 |             send: (msgData) => {
257 |               const pm2Message = msgData;
258 |               pm2Message.topic = channel;
259 |               if (typeof pm2Message.data === 'undefined') {
260 |                 pm2Message.data = {};
261 |               }
262 |               pm2.sendDataToProcessId(packet.process.pm_id, pm2Message, (sendErr, res) => {
263 |                 if (sendErr) {
264 |                   console.log(sendErr, res);
265 |                 }
266 |               });
267 |             },
268 |           };
269 |           masterProcessMsg.call(this, worker, msg);
270 |         }
271 |       });
272 |     });
273 | 
274 |     masterInstance = this;
275 |   }
276 | }
277 | 
278 | class RateLimiterClusterWorker extends RateLimiterAbstract {
279 |   get timeoutMs() {
280 |     return this._timeoutMs;
281 |   }
282 | 
283 |   set timeoutMs(value) {
284 |     this._timeoutMs = typeof value === 'undefined' ? 5000 : Math.abs(parseInt(value));
285 |   }
286 | 
287 |   constructor(opts = {}) {
288 |     super(opts);
289 | 
290 |     process.setMaxListeners(0);
291 | 
292 |     this.timeoutMs = opts.timeoutMs;
293 | 
294 |     this._initiated = false;
295 | 
296 |     process.on('message', (msg) => {
297 |       if (msg && msg.channel === channel && msg.type === 'init' && msg.keyPrefix === this.keyPrefix) {
298 |         this._initiated = true;
299 |       } else {
300 |         workerProcessMsg.call(this, msg);
301 |       }
302 |     });
303 | 
304 |     // Create limiter on master with specific options
305 |     process.send({
306 |       channel,
307 |       type: 'init',
308 |       opts: getOpts.call(this),
309 |     });
310 | 
311 |     this._promises = {};
312 |   }
313 | 
314 |   consume(key, pointsToConsume = 1, options = {}) {
315 |     return new Promise((resolve, reject) => {
316 |       const promiseId = savePromise.call(this, resolve, reject);
317 | 
318 |       workerSendToMaster.call(this, 'consume', promiseId, key, pointsToConsume, options);
319 |     });
320 |   }
321 | 
322 |   penalty(key, points = 1, options = {}) {
323 |     return new Promise((resolve, reject) => {
324 |       const promiseId = savePromise.call(this, resolve, reject);
325 | 
326 |       workerSendToMaster.call(this, 'penalty', promiseId, key, points, options);
327 |     });
328 |   }
329 | 
330 |   reward(key, points = 1, options = {}) {
331 |     return new Promise((resolve, reject) => {
332 |       const promiseId = savePromise.call(this, resolve, reject);
333 | 
334 |       workerSendToMaster.call(this, 'reward', promiseId, key, points, options);
335 |     });
336 |   }
337 | 
338 |   block(key, secDuration, options = {}) {
339 |     return new Promise((resolve, reject) => {
340 |       const promiseId = savePromise.call(this, resolve, reject);
341 | 
342 |       workerSendToMaster.call(this, 'block', promiseId, key, secDuration, options);
343 |     });
344 |   }
345 | 
346 |   get(key, options = {}) {
347 |     return new Promise((resolve, reject) => {
348 |       const promiseId = savePromise.call(this, resolve, reject);
349 | 
350 |       workerSendToMaster.call(this, 'get', promiseId, key, options);
351 |     });
352 |   }
353 | 
354 |   delete(key, options = {}) {
355 |     return new Promise((resolve, reject) => {
356 |       const promiseId = savePromise.call(this, resolve, reject);
357 | 
358 |       workerSendToMaster.call(this, 'delete', promiseId, key, options);
359 |     });
360 |   }
361 | }
362 | 
363 | module.exports = {
364 |   RateLimiterClusterMaster,
365 |   RateLimiterClusterMasterPM2,
366 |   RateLimiterCluster: RateLimiterClusterWorker,
367 | };
368 | 


--------------------------------------------------------------------------------
/lib/RateLimiterEtcd.js:
--------------------------------------------------------------------------------
 1 | const RateLimiterEtcdTransactionFailedError = require('./component/RateLimiterEtcdTransactionFailedError');
 2 | const RateLimiterEtcdNonAtomic = require('./RateLimiterEtcdNonAtomic');
 3 | 
 4 | const MAX_TRANSACTION_TRIES = 5;
 5 | 
 6 | class RateLimiterEtcd extends RateLimiterEtcdNonAtomic {
 7 |   /**
 8 |    * Resolve with object used for {@link _getRateLimiterRes} to generate {@link RateLimiterRes}.
 9 |    */
10 |   async _upsert(rlKey, points, msDuration, forceExpire = false) {
11 |     const expire = msDuration > 0 ? Date.now() + msDuration : null;
12 | 
13 |     let newValue = { points, expire };
14 |     let oldValue;
15 | 
16 |     // If we need to force the expiration, just set the key.
17 |     if (forceExpire) {
18 |       await this.client
19 |         .put(rlKey)
20 |         .value(JSON.stringify(newValue));
21 |     } else {
22 |       // First try to add a new key
23 |       const added = await this.client
24 |         .if(rlKey, 'Version', '===', '0')
25 |         .then(this.client
26 |           .put(rlKey)
27 |           .value(JSON.stringify(newValue)))
28 |         .commit()
29 |         .then(result => !!result.succeeded);
30 | 
31 |       // If the key already existed, try to update it in a transaction
32 |       if (!added) {
33 |         let success = false;
34 | 
35 |         for (let i = 0; i < MAX_TRANSACTION_TRIES; i++) {
36 |           // eslint-disable-next-line no-await-in-loop
37 |           oldValue = await this._get(rlKey);
38 |           newValue = { points: oldValue.points + points, expire };
39 | 
40 |           // eslint-disable-next-line no-await-in-loop
41 |           success = await this.client
42 |             .if(rlKey, 'Value', '===', JSON.stringify(oldValue))
43 |             .then(this.client
44 |               .put(rlKey)
45 |               .value(JSON.stringify(newValue)))
46 |             .commit()
47 |             .then(result => !!result.succeeded);
48 |           if (success) {
49 |             break;
50 |           }
51 |         }
52 | 
53 |         if (!success) {
54 |           throw new RateLimiterEtcdTransactionFailedError('Could not set new value in a transaction.');
55 |         }
56 |       }
57 |     }
58 | 
59 |     return newValue;
60 |   }
61 | }
62 | 
63 | module.exports = RateLimiterEtcd;
64 | 


--------------------------------------------------------------------------------
/lib/RateLimiterEtcdNonAtomic.js:
--------------------------------------------------------------------------------
 1 | const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
 2 | const RateLimiterRes = require('./RateLimiterRes');
 3 | const RateLimiterSetupError = require('./component/RateLimiterSetupError');
 4 | 
 5 | class RateLimiterEtcdNonAtomic extends RateLimiterStoreAbstract {
 6 |   /**
 7 |    * @param {Object} opts
 8 |    */
 9 |   constructor(opts) {
10 |     super(opts);
11 | 
12 |     if (!opts.storeClient) {
13 |       throw new RateLimiterSetupError('You need to set the option "storeClient" to an instance of class "Etcd3".');
14 |     }
15 | 
16 |     this.client = opts.storeClient;
17 |   }
18 | 
19 |   /**
20 |    * Get RateLimiterRes object filled depending on storeResult, which specific for exact store.
21 |    */
22 |   _getRateLimiterRes(rlKey, changedPoints, result) {
23 |     const res = new RateLimiterRes();
24 | 
25 |     res.isFirstInDuration = changedPoints === result.points;
26 |     res.consumedPoints = res.isFirstInDuration ? changedPoints : result.points;
27 |     res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
28 |     res.msBeforeNext = result.expire ? Math.max(result.expire - Date.now(), 0) : -1;
29 | 
30 |     return res;
31 |   }
32 | 
33 |   /**
34 |    * Resolve with object used for {@link _getRateLimiterRes} to generate {@link RateLimiterRes}.
35 |    */
36 |   async _upsert(rlKey, points, msDuration, forceExpire = false) {
37 |     const expire = msDuration > 0 ? Date.now() + msDuration : null;
38 | 
39 |     let newValue = { points, expire };
40 | 
41 |     // If we need to force the expiration, just set the key.
42 |     if (forceExpire) {
43 |       await this.client
44 |         .put(rlKey)
45 |         .value(JSON.stringify(newValue));
46 |     } else {
47 |       const oldValue = await this._get(rlKey);
48 |       newValue = { points: (oldValue !== null ? oldValue.points : 0) + points, expire };
49 |       await this.client
50 |         .put(rlKey)
51 |         .value(JSON.stringify(newValue));
52 |     }
53 | 
54 |     return newValue;
55 |   }
56 | 
57 |   /**
58 |    * Resolve with raw result from Store OR null if rlKey is not set
59 |    * or Reject with error
60 |    */
61 |   async _get(rlKey) {
62 |     return this.client
63 |       .get(rlKey)
64 |       .string()
65 |       .then(result => (result !== null ? JSON.parse(result) : null));
66 |   }
67 | 
68 |   /**
69 |    * Resolve with true OR false if rlKey doesn't exist.
70 |    * or Reject with error.
71 |    */
72 |   async _delete(rlKey) {
73 |     return this.client
74 |       .delete()
75 |       .key(rlKey)
76 |       .then(result => result.deleted === '1');
77 |   }
78 | }
79 | 
80 | module.exports = RateLimiterEtcdNonAtomic;
81 | 


--------------------------------------------------------------------------------
/lib/RateLimiterMemcache.js:
--------------------------------------------------------------------------------
  1 | const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
  2 | const RateLimiterRes = require('./RateLimiterRes');
  3 | 
  4 | class RateLimiterMemcache extends RateLimiterStoreAbstract {
  5 |   /**
  6 |    *
  7 |    * @param {Object} opts
  8 |    * Defaults {
  9 |    *   ... see other in RateLimiterStoreAbstract
 10 |    *
 11 |    *   storeClient: memcacheClient
 12 |    * }
 13 |    */
 14 |   constructor(opts) {
 15 |     super(opts);
 16 | 
 17 |     this.client = opts.storeClient;
 18 |   }
 19 | 
 20 |   _getRateLimiterRes(rlKey, changedPoints, result) {
 21 |     const res = new RateLimiterRes();
 22 |     res.consumedPoints = parseInt(result.consumedPoints);
 23 |     res.isFirstInDuration = result.consumedPoints === changedPoints;
 24 |     res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
 25 |     res.msBeforeNext = result.msBeforeNext;
 26 | 
 27 |     return res;
 28 |   }
 29 | 
 30 |   _upsert(rlKey, points, msDuration, forceExpire = false, options = {}) {
 31 |     return new Promise((resolve, reject) => {
 32 |       const nowMs = Date.now();
 33 |       const secDuration = Math.floor(msDuration / 1000);
 34 | 
 35 |       if (forceExpire) {
 36 |         this.client.set(rlKey, points, secDuration, (err) => {
 37 |           if (!err) {
 38 |             this.client.set(
 39 |               `${rlKey}_expire`,
 40 |               secDuration > 0 ? nowMs + (secDuration * 1000) : -1,
 41 |               secDuration,
 42 |               () => {
 43 |                 const res = {
 44 |                   consumedPoints: points,
 45 |                   msBeforeNext: secDuration > 0 ? secDuration * 1000 : -1,
 46 |                 };
 47 |                 resolve(res);
 48 |               }
 49 |             );
 50 |           } else {
 51 |             reject(err);
 52 |           }
 53 |         });
 54 |       } else {
 55 |         this.client.incr(rlKey, points, (err, consumedPoints) => {
 56 |           if (err || consumedPoints === false) {
 57 |             this.client.add(rlKey, points, secDuration, (errAddKey, createdNew) => {
 58 |               if (errAddKey || !createdNew) {
 59 |                 // Try to upsert again in case of race condition
 60 |                 if (typeof options.attemptNumber === 'undefined' || options.attemptNumber < 3) {
 61 |                   const nextOptions = Object.assign({}, options);
 62 |                   nextOptions.attemptNumber = nextOptions.attemptNumber ? (nextOptions.attemptNumber + 1) : 1;
 63 | 
 64 |                   this._upsert(rlKey, points, msDuration, forceExpire, nextOptions)
 65 |                     .then(resUpsert => resolve(resUpsert))
 66 |                     .catch(errUpsert => reject(errUpsert));
 67 |                 } else {
 68 |                   reject(new Error('Can not add key'));
 69 |                 }
 70 |               } else {
 71 |                 this.client.add(
 72 |                   `${rlKey}_expire`,
 73 |                   secDuration > 0 ? nowMs + (secDuration * 1000) : -1,
 74 |                   secDuration,
 75 |                   () => {
 76 |                     const res = {
 77 |                       consumedPoints: points,
 78 |                       msBeforeNext: secDuration > 0 ? secDuration * 1000 : -1,
 79 |                     };
 80 |                     resolve(res);
 81 |                   }
 82 |                 );
 83 |               }
 84 |             });
 85 |           } else {
 86 |             this.client.get(`${rlKey}_expire`, (errGetExpire, resGetExpireMs) => {
 87 |               if (errGetExpire) {
 88 |                 reject(errGetExpire);
 89 |               } else {
 90 |                 const expireMs = resGetExpireMs === false ? 0 : resGetExpireMs;
 91 |                 const res = {
 92 |                   consumedPoints,
 93 |                   msBeforeNext: expireMs >= 0 ? Math.max(expireMs - nowMs, 0) : -1,
 94 |                 };
 95 |                 resolve(res);
 96 |               }
 97 |             });
 98 |           }
 99 |         });
100 |       }
101 |     });
102 |   }
103 | 
104 |   _get(rlKey) {
105 |     return new Promise((resolve, reject) => {
106 |       const nowMs = Date.now();
107 | 
108 |       this.client.get(rlKey, (err, consumedPoints) => {
109 |         if (!consumedPoints) {
110 |           resolve(null);
111 |         } else {
112 |           this.client.get(`${rlKey}_expire`, (errGetExpire, resGetExpireMs) => {
113 |             if (errGetExpire) {
114 |               reject(errGetExpire);
115 |             } else {
116 |               const expireMs = resGetExpireMs === false ? 0 : resGetExpireMs;
117 |               const res = {
118 |                 consumedPoints,
119 |                 msBeforeNext: expireMs >= 0 ? Math.max(expireMs - nowMs, 0) : -1,
120 |               };
121 |               resolve(res);
122 |             }
123 |           });
124 |         }
125 |       });
126 |     });
127 |   }
128 | 
129 |   _delete(rlKey) {
130 |     return new Promise((resolve, reject) => {
131 |       this.client.del(rlKey, (err, res) => {
132 |         if (err) {
133 |           reject(err);
134 |         } else if (res === false) {
135 |           resolve(res);
136 |         } else {
137 |           this.client.del(`${rlKey}_expire`, (errDelExpire) => {
138 |             if (errDelExpire) {
139 |               reject(errDelExpire);
140 |             } else {
141 |               resolve(res);
142 |             }
143 |           });
144 |         }
145 |       });
146 |     });
147 |   }
148 | }
149 | 
150 | module.exports = RateLimiterMemcache;
151 | 


--------------------------------------------------------------------------------
/lib/RateLimiterMemory.js:
--------------------------------------------------------------------------------
  1 | const RateLimiterAbstract = require('./RateLimiterAbstract');
  2 | const MemoryStorage = require('./component/MemoryStorage/MemoryStorage');
  3 | const RateLimiterRes = require('./RateLimiterRes');
  4 | 
  5 | class RateLimiterMemory extends RateLimiterAbstract {
  6 |   constructor(opts = {}) {
  7 |     super(opts);
  8 | 
  9 |     this._memoryStorage = new MemoryStorage();
 10 |   }
 11 |   /**
 12 |    *
 13 |    * @param key
 14 |    * @param pointsToConsume
 15 |    * @param {Object} options
 16 |    * @returns {Promise<RateLimiterRes>}
 17 |    */
 18 |   consume(key, pointsToConsume = 1, options = {}) {
 19 |     return new Promise((resolve, reject) => {
 20 |       const rlKey = this.getKey(key);
 21 |       const secDuration = this._getKeySecDuration(options);
 22 |       let res = this._memoryStorage.incrby(rlKey, pointsToConsume, secDuration);
 23 |       res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
 24 | 
 25 |       if (res.consumedPoints > this.points) {
 26 |         // Block only first time when consumed more than points
 27 |         if (this.blockDuration > 0 && res.consumedPoints <= (this.points + pointsToConsume)) {
 28 |           // Block key
 29 |           res = this._memoryStorage.set(rlKey, res.consumedPoints, this.blockDuration);
 30 |         }
 31 |         reject(res);
 32 |       } else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) {
 33 |         // Execute evenly
 34 |         let delay = Math.ceil(res.msBeforeNext / (res.remainingPoints + 2));
 35 |         if (delay < this.execEvenlyMinDelayMs) {
 36 |           delay = res.consumedPoints * this.execEvenlyMinDelayMs;
 37 |         }
 38 | 
 39 |         setTimeout(resolve, delay, res);
 40 |       } else {
 41 |         resolve(res);
 42 |       }
 43 |     });
 44 |   }
 45 | 
 46 |   penalty(key, points = 1, options = {}) {
 47 |     const rlKey = this.getKey(key);
 48 |     return new Promise((resolve) => {
 49 |       const secDuration = this._getKeySecDuration(options);
 50 |       const res = this._memoryStorage.incrby(rlKey, points, secDuration);
 51 |       res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
 52 |       resolve(res);
 53 |     });
 54 |   }
 55 | 
 56 |   reward(key, points = 1, options = {}) {
 57 |     const rlKey = this.getKey(key);
 58 |     return new Promise((resolve) => {
 59 |       const secDuration = this._getKeySecDuration(options);
 60 |       const res = this._memoryStorage.incrby(rlKey, -points, secDuration);
 61 |       res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
 62 |       resolve(res);
 63 |     });
 64 |   }
 65 | 
 66 |   /**
 67 |    * Block any key for secDuration seconds
 68 |    *
 69 |    * @param key
 70 |    * @param secDuration
 71 |    */
 72 |   block(key, secDuration) {
 73 |     const msDuration = secDuration * 1000;
 74 |     const initPoints = this.points + 1;
 75 | 
 76 |     this._memoryStorage.set(this.getKey(key), initPoints, secDuration);
 77 |     return Promise.resolve(
 78 |       new RateLimiterRes(0, msDuration === 0 ? -1 : msDuration, initPoints)
 79 |     );
 80 |   }
 81 | 
 82 |   set(key, points, secDuration) {
 83 |     const msDuration = (secDuration >= 0 ? secDuration : this.duration) * 1000;
 84 | 
 85 |     this._memoryStorage.set(this.getKey(key), points, secDuration);
 86 |     return Promise.resolve(
 87 |       new RateLimiterRes(0, msDuration === 0 ? -1 : msDuration, points)
 88 |     );
 89 |   }
 90 | 
 91 |   get(key) {
 92 |     const res = this._memoryStorage.get(this.getKey(key));
 93 |     if (res !== null) {
 94 |       res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
 95 |     }
 96 | 
 97 |     return Promise.resolve(res);
 98 |   }
 99 | 
100 |   delete(key) {
101 |     return Promise.resolve(this._memoryStorage.delete(this.getKey(key)));
102 |   }
103 | }
104 | 
105 | module.exports = RateLimiterMemory;
106 | 
107 | 


--------------------------------------------------------------------------------
/lib/RateLimiterMongo.js:
--------------------------------------------------------------------------------
  1 | const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
  2 | const RateLimiterRes = require('./RateLimiterRes');
  3 | 
  4 | /**
  5 |  * Get MongoDB driver version as upsert options differ
  6 |  * @params {Object} Client instance
  7 |  * @returns {Object} Version Object containing major, feature & minor versions.
  8 |  */
  9 | function getDriverVersion(client) {
 10 |   try {
 11 |     const _client = client.client ? client.client : client;
 12 | 
 13 |     let _v = [0, 0, 0];
 14 |     if (typeof _client.topology === 'undefined') {
 15 |       const { version } = _client.options.metadata.driver;
 16 |       _v = version.split('|', 1)[0].split('.').map(v => parseInt(v));
 17 |     } else {
 18 |       const { version } = _client.topology.s.options.metadata.driver;
 19 |       _v = version.split('.').map(v => parseInt(v));
 20 |     }
 21 | 
 22 |     return {
 23 |       major: _v[0],
 24 |       feature: _v[1],
 25 |       patch: _v[2],
 26 |     };
 27 |   } catch (err) {
 28 |     return { major: 0, feature: 0, patch: 0 };
 29 |   }
 30 | }
 31 | 
 32 | class RateLimiterMongo extends RateLimiterStoreAbstract {
 33 |   /**
 34 |    *
 35 |    * @param {Object} opts
 36 |    * Defaults {
 37 |    *   indexKeyPrefix: {attr1: 1, attr2: 1}
 38 |    *   ... see other in RateLimiterStoreAbstract
 39 |    *
 40 |    *   mongo: MongoClient
 41 |    * }
 42 |    */
 43 |   constructor(opts) {
 44 |     super(opts);
 45 | 
 46 |     this.dbName = opts.dbName;
 47 |     this.tableName = opts.tableName;
 48 |     this.indexKeyPrefix = opts.indexKeyPrefix;
 49 | 
 50 |     if (opts.mongo) {
 51 |       this.client = opts.mongo;
 52 |     } else {
 53 |       this.client = opts.storeClient;
 54 |     }
 55 |     if (typeof this.client.then === 'function') {
 56 |       // If Promise
 57 |       this.client
 58 |         .then((conn) => {
 59 |           this.client = conn;
 60 |           this._initCollection();
 61 |           this._driverVersion = getDriverVersion(this.client);
 62 |         });
 63 |     } else {
 64 |       this._initCollection();
 65 |       this._driverVersion = getDriverVersion(this.client);
 66 |     }
 67 |   }
 68 | 
 69 |   get dbName() {
 70 |     return this._dbName;
 71 |   }
 72 | 
 73 |   set dbName(value) {
 74 |     this._dbName = typeof value === 'undefined' ? RateLimiterMongo.getDbName() : value;
 75 |   }
 76 | 
 77 |   static getDbName() {
 78 |     return 'node-rate-limiter-flexible';
 79 |   }
 80 | 
 81 |   get tableName() {
 82 |     return this._tableName;
 83 |   }
 84 | 
 85 |   set tableName(value) {
 86 |     this._tableName = typeof value === 'undefined' ? this.keyPrefix : value;
 87 |   }
 88 | 
 89 |   get client() {
 90 |     return this._client;
 91 |   }
 92 | 
 93 |   set client(value) {
 94 |     if (typeof value === 'undefined') {
 95 |       throw new Error('mongo is not set');
 96 |     }
 97 |     this._client = value;
 98 |   }
 99 | 
100 |   get indexKeyPrefix() {
101 |     return this._indexKeyPrefix;
102 |   }
103 | 
104 |   set indexKeyPrefix(obj) {
105 |     this._indexKeyPrefix = obj || {};
106 |   }
107 | 
108 |   _initCollection() {
109 |     const db = typeof this.client.db === 'function'
110 |       ? this.client.db(this.dbName)
111 |       : this.client;
112 | 
113 |     const collection = db.collection(this.tableName);
114 |     collection.createIndex({ expire: -1 }, { expireAfterSeconds: 0 });
115 |     collection.createIndex(Object.assign({}, this.indexKeyPrefix, { key: 1 }), { unique: true });
116 | 
117 |     this._collection = collection;
118 |   }
119 | 
120 |   _getRateLimiterRes(rlKey, changedPoints, result) {
121 |     const res = new RateLimiterRes();
122 | 
123 |     let doc;
124 |     if (typeof result.value === 'undefined') {
125 |       doc = result;
126 |     } else {
127 |       doc = result.value;
128 |     }
129 | 
130 |     res.isFirstInDuration = doc.points === changedPoints;
131 |     res.consumedPoints = doc.points;
132 | 
133 |     res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
134 |     res.msBeforeNext = doc.expire !== null
135 |       ? Math.max(new Date(doc.expire).getTime() - Date.now(), 0)
136 |       : -1;
137 | 
138 |     return res;
139 |   }
140 | 
141 |   _upsert(key, points, msDuration, forceExpire = false, options = {}) {
142 |     if (!this._collection) {
143 |       return Promise.reject(Error('Mongo connection is not established'));
144 |     }
145 | 
146 |     const docAttrs = options.attrs || {};
147 | 
148 |     let where;
149 |     let upsertData;
150 |     if (forceExpire) {
151 |       where = { key };
152 |       where = Object.assign(where, docAttrs);
153 |       upsertData = {
154 |         $set: {
155 |           key,
156 |           points,
157 |           expire: msDuration > 0 ? new Date(Date.now() + msDuration) : null,
158 |         },
159 |       };
160 |       upsertData.$set = Object.assign(upsertData.$set, docAttrs);
161 |     } else {
162 |       where = {
163 |         $or: [
164 |           { expire: { $gt: new Date() } },
165 |           { expire: { $eq: null } },
166 |         ],
167 |         key,
168 |       };
169 |       where = Object.assign(where, docAttrs);
170 |       upsertData = {
171 |         $setOnInsert: {
172 |           key,
173 |           expire: msDuration > 0 ? new Date(Date.now() + msDuration) : null,
174 |         },
175 |         $inc: { points },
176 |       };
177 |       upsertData.$setOnInsert = Object.assign(upsertData.$setOnInsert, docAttrs);
178 |     }
179 | 
180 |     // Options for collection updates differ between driver versions
181 |     const upsertOptions = {
182 |       upsert: true,
183 |     };
184 |     if ((this._driverVersion.major >= 4) ||
185 |         (this._driverVersion.major === 3 &&
186 |           (this._driverVersion.feature >=7) || 
187 |           (this._driverVersion.feature >= 6 && 
188 |               this._driverVersion.patch >= 7 ))) 
189 |     {
190 |       upsertOptions.returnDocument = 'after';
191 |     } else {
192 |       upsertOptions.returnOriginal = false;
193 |     }
194 | 
195 |     /*
196 |      * 1. Find actual limit and increment points
197 |      * 2. If limit expired, but Mongo doesn't clean doc by TTL yet, try to replace limit doc completely
198 |      * 3. If 2 or more Mongo threads try to insert the new limit doc, only the first succeed
199 |      * 4. Try to upsert from step 1. Actual limit is created now, points are incremented without problems
200 |      */
201 |     return new Promise((resolve, reject) => {
202 |       this._collection.findOneAndUpdate(
203 |         where,
204 |         upsertData,
205 |         upsertOptions
206 |       ).then((res) => {
207 |         resolve(res);
208 |       }).catch((errUpsert) => {
209 |         if (errUpsert && errUpsert.code === 11000) { // E11000 duplicate key error collection
210 |           const replaceWhere = Object.assign({ // try to replace OLD limit doc
211 |             $or: [
212 |               { expire: { $lte: new Date() } },
213 |               { expire: { $eq: null } },
214 |             ],
215 |             key,
216 |           }, docAttrs);
217 | 
218 |           const replaceTo = {
219 |             $set: Object.assign({
220 |               key,
221 |               points,
222 |               expire: msDuration > 0 ? new Date(Date.now() + msDuration) : null,
223 |             }, docAttrs)
224 |           };
225 | 
226 |           this._collection.findOneAndUpdate(
227 |             replaceWhere,
228 |             replaceTo,
229 |             upsertOptions
230 |           ).then((res) => {
231 |             resolve(res);
232 |           }).catch((errReplace) => {
233 |             if (errReplace && errReplace.code === 11000) { // E11000 duplicate key error collection
234 |               this._upsert(key, points, msDuration, forceExpire)
235 |                 .then(res => resolve(res))
236 |                 .catch(err => reject(err));
237 |             } else {
238 |               reject(errReplace);
239 |             }
240 |           });
241 |         } else {
242 |           reject(errUpsert);
243 |         }
244 |       });
245 |     });
246 |   }
247 | 
248 |   _get(rlKey, options = {}) {
249 |     if (!this._collection) {
250 |       return Promise.reject(Error('Mongo connection is not established'));
251 |     }
252 | 
253 |     const docAttrs = options.attrs || {};
254 | 
255 |     const where = Object.assign({
256 |       key: rlKey,
257 |       $or: [
258 |         { expire: { $gt: new Date() } },
259 |         { expire: { $eq: null } },
260 |       ],
261 |     }, docAttrs);
262 | 
263 |     return this._collection.findOne(where);
264 |   }
265 | 
266 |   _delete(rlKey, options = {}) {
267 |     if (!this._collection) {
268 |       return Promise.reject(Error('Mongo connection is not established'));
269 |     }
270 | 
271 |     const docAttrs = options.attrs || {};
272 |     const where = Object.assign({ key: rlKey }, docAttrs);
273 | 
274 |     return this._collection.deleteOne(where)
275 |       .then(res => res.deletedCount > 0);
276 |   }
277 | }
278 | 
279 | module.exports = RateLimiterMongo;
280 | 


--------------------------------------------------------------------------------
/lib/RateLimiterMySQL.js:
--------------------------------------------------------------------------------
  1 | const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
  2 | const RateLimiterRes = require('./RateLimiterRes');
  3 | 
  4 | class RateLimiterMySQL extends RateLimiterStoreAbstract {
  5 |   /**
  6 |    * @callback callback
  7 |    * @param {Object} err
  8 |    *
  9 |    * @param {Object} opts
 10 |    * @param {callback} cb
 11 |    * Defaults {
 12 |    *   ... see other in RateLimiterStoreAbstract
 13 |    *
 14 |    *   storeClient: anySqlClient,
 15 |    *   storeType: 'knex', // required only for Knex instance
 16 |    *   dbName: 'string',
 17 |    *   tableName: 'string',
 18 |    * }
 19 |    */
 20 |   constructor(opts, cb = null) {
 21 |     super(opts);
 22 | 
 23 |     this.client = opts.storeClient;
 24 |     this.clientType = opts.storeType;
 25 | 
 26 |     this.dbName = opts.dbName;
 27 |     this.tableName = opts.tableName;
 28 | 
 29 |     this.clearExpiredByTimeout = opts.clearExpiredByTimeout;
 30 | 
 31 |     this.tableCreated = opts.tableCreated;
 32 |     if (!this.tableCreated) {
 33 |       this._createDbAndTable()
 34 |         .then(() => {
 35 |           this.tableCreated = true;
 36 |           if (this.clearExpiredByTimeout) {
 37 |             this._clearExpiredHourAgo();
 38 |           }
 39 |           if (typeof cb === 'function') {
 40 |             cb();
 41 |           }
 42 |         })
 43 |         .catch((err) => {
 44 |           if (typeof cb === 'function') {
 45 |             cb(err);
 46 |           } else {
 47 |             throw err;
 48 |           }
 49 |         });
 50 |     } else {
 51 |       if (this.clearExpiredByTimeout) {
 52 |         this._clearExpiredHourAgo();
 53 |       }
 54 |       if (typeof cb === 'function') {
 55 |         cb();
 56 |       }
 57 |     }
 58 |   }
 59 | 
 60 |   clearExpired(expire) {
 61 |     return new Promise((resolve) => {
 62 |       this._getConnection()
 63 |         .then((conn) => {
 64 |           conn.query(`DELETE FROM ??.?? WHERE expire < ?`, [this.dbName, this.tableName, expire], () => {
 65 |             this._releaseConnection(conn);
 66 |             resolve();
 67 |           });
 68 |         })
 69 |         .catch(() => {
 70 |           resolve();
 71 |         });
 72 |     });
 73 |   }
 74 | 
 75 |   _clearExpiredHourAgo() {
 76 |     if (this._clearExpiredTimeoutId) {
 77 |       clearTimeout(this._clearExpiredTimeoutId);
 78 |     }
 79 |     this._clearExpiredTimeoutId = setTimeout(() => {
 80 |       this.clearExpired(Date.now() - 3600000) // Never rejected
 81 |         .then(() => {
 82 |           this._clearExpiredHourAgo();
 83 |         });
 84 |     }, 300000);
 85 |     this._clearExpiredTimeoutId.unref();
 86 |   }
 87 | 
 88 |   /**
 89 |    *
 90 |    * @return Promise<any>
 91 |    * @private
 92 |    */
 93 |   _getConnection() {
 94 |     switch (this.clientType) {
 95 |       case 'pool':
 96 |         return new Promise((resolve, reject) => {
 97 |           this.client.getConnection((errConn, conn) => {
 98 |             if (errConn) {
 99 |               return reject(errConn);
100 |             }
101 | 
102 |             resolve(conn);
103 |           });
104 |         });
105 |       case 'sequelize':
106 |         return this.client.connectionManager.getConnection();
107 |       case 'knex':
108 |         return this.client.client.acquireConnection();
109 |       default:
110 |         return Promise.resolve(this.client);
111 |     }
112 |   }
113 | 
114 |   _releaseConnection(conn) {
115 |     switch (this.clientType) {
116 |       case 'pool':
117 |         return conn.release();
118 |       case 'sequelize':
119 |         return this.client.connectionManager.releaseConnection(conn);
120 |       case 'knex':
121 |         return this.client.client.releaseConnection(conn);
122 |       default:
123 |         return true;
124 |     }
125 |   }
126 | 
127 |   /**
128 |    *
129 |    * @returns {Promise<any>}
130 |    * @private
131 |    */
132 |   _createDbAndTable() {
133 |     return new Promise((resolve, reject) => {
134 |       this._getConnection()
135 |         .then((conn) => {
136 |           conn.query(`CREATE DATABASE IF NOT EXISTS \`${this.dbName}\`;`, (errDb) => {
137 |             if (errDb) {
138 |               this._releaseConnection(conn);
139 |               return reject(errDb);
140 |             }
141 |             conn.query(this._getCreateTableStmt(), (err) => {
142 |               if (err) {
143 |                 this._releaseConnection(conn);
144 |                 return reject(err);
145 |               }
146 |               this._releaseConnection(conn);
147 |               resolve();
148 |             });
149 |           });
150 |         })
151 |         .catch((err) => {
152 |           reject(err);
153 |         });
154 |     });
155 |   }
156 | 
157 |   _getCreateTableStmt() {
158 |     return `CREATE TABLE IF NOT EXISTS \`${this.dbName}\`.\`${this.tableName}\` (` +
159 |       '`key` VARCHAR(255) CHARACTER SET utf8 NOT NULL,' +
160 |       '`points` INT(9) NOT NULL default 0,' +
161 |       '`expire` BIGINT UNSIGNED,' +
162 |       'PRIMARY KEY (`key`)' +
163 |       ') ENGINE = INNODB;';
164 |   }
165 | 
166 |   get clientType() {
167 |     return this._clientType;
168 |   }
169 | 
170 |   set clientType(value) {
171 |     if (typeof value === 'undefined') {
172 |       if (this.client.constructor.name === 'Connection') {
173 |         value = 'connection';
174 |       } else if (this.client.constructor.name === 'Pool') {
175 |         value = 'pool';
176 |       } else if (this.client.constructor.name === 'Sequelize') {
177 |         value = 'sequelize';
178 |       } else {
179 |         throw new Error('storeType is not defined');
180 |       }
181 |     }
182 |     this._clientType = value.toLowerCase();
183 |   }
184 | 
185 |   get dbName() {
186 |     return this._dbName;
187 |   }
188 | 
189 |   set dbName(value) {
190 |     this._dbName = typeof value === 'undefined' ? 'rtlmtrflx' : value;
191 |   }
192 | 
193 |   get tableName() {
194 |     return this._tableName;
195 |   }
196 | 
197 |   set tableName(value) {
198 |     this._tableName = typeof value === 'undefined' ? this.keyPrefix : value;
199 |   }
200 | 
201 |   get tableCreated() {
202 |     return this._tableCreated
203 |   }
204 | 
205 |   set tableCreated(value) {
206 |     this._tableCreated = typeof value === 'undefined' ? false : !!value;
207 |   }
208 | 
209 |   get clearExpiredByTimeout() {
210 |     return this._clearExpiredByTimeout;
211 |   }
212 | 
213 |   set clearExpiredByTimeout(value) {
214 |     this._clearExpiredByTimeout = typeof value === 'undefined' ? true : Boolean(value);
215 |   }
216 | 
217 |   _getRateLimiterRes(rlKey, changedPoints, result) {
218 |     const res = new RateLimiterRes();
219 |     const [row] = result;
220 | 
221 |     res.isFirstInDuration = changedPoints === row.points;
222 |     res.consumedPoints = res.isFirstInDuration ? changedPoints : row.points;
223 | 
224 |     res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
225 |     res.msBeforeNext = row.expire
226 |       ? Math.max(row.expire - Date.now(), 0)
227 |       : -1;
228 | 
229 |     return res;
230 |   }
231 | 
232 |   _upsertTransaction(conn, key, points, msDuration, forceExpire) {
233 |     return new Promise((resolve, reject) => {
234 |       conn.query('BEGIN', (errBegin) => {
235 |         if (errBegin) {
236 |           conn.rollback();
237 | 
238 |           return reject(errBegin);
239 |         }
240 | 
241 |         const dateNow = Date.now();
242 |         const newExpire = msDuration > 0 ? dateNow + msDuration : null;
243 | 
244 |         let q;
245 |         let values;
246 |         if (forceExpire) {
247 |           q = `INSERT INTO ??.?? VALUES (?, ?, ?)
248 |           ON DUPLICATE KEY UPDATE 
249 |             points = ?, 
250 |             expire = ?;`;
251 |           values = [
252 |             this.dbName, this.tableName, key, points, newExpire,
253 |             points,
254 |             newExpire,
255 |           ];
256 |         } else {
257 |           q = `INSERT INTO ??.?? VALUES (?, ?, ?)
258 |           ON DUPLICATE KEY UPDATE 
259 |             points = IF(expire <= ?, ?, points + (?)), 
260 |             expire = IF(expire <= ?, ?, expire);`;
261 |           values = [
262 |             this.dbName, this.tableName, key, points, newExpire,
263 |             dateNow, points, points,
264 |             dateNow, newExpire,
265 |           ];
266 |         }
267 | 
268 |         conn.query(q, values, (errUpsert) => {
269 |           if (errUpsert) {
270 |             conn.rollback();
271 | 
272 |             return reject(errUpsert);
273 |           }
274 |           conn.query('SELECT points, expire FROM ??.?? WHERE `key` = ?;', [this.dbName, this.tableName, key], (errSelect, res) => {
275 |             if (errSelect) {
276 |               conn.rollback();
277 | 
278 |               return reject(errSelect);
279 |             }
280 | 
281 |             conn.query('COMMIT', (err) => {
282 |               if (err) {
283 |                 conn.rollback();
284 | 
285 |                 return reject(err);
286 |               }
287 | 
288 |               resolve(res);
289 |             });
290 |           });
291 |         });
292 |       });
293 |     });
294 |   }
295 | 
296 |   _upsert(key, points, msDuration, forceExpire = false) {
297 |     if (!this.tableCreated) {
298 |       return Promise.reject(Error('Table is not created yet'));
299 |     }
300 | 
301 |     return new Promise((resolve, reject) => {
302 |       this._getConnection()
303 |         .then((conn) => {
304 |           this._upsertTransaction(conn, key, points, msDuration, forceExpire)
305 |             .then((res) => {
306 |               resolve(res);
307 |               this._releaseConnection(conn);
308 |             })
309 |             .catch((err) => {
310 |               reject(err);
311 |               this._releaseConnection(conn);
312 |             });
313 |         })
314 |         .catch((err) => {
315 |           reject(err);
316 |         });
317 |     });
318 |   }
319 | 
320 |   _get(rlKey) {
321 |     if (!this.tableCreated) {
322 |       return Promise.reject(Error('Table is not created yet'));
323 |     }
324 | 
325 |     return new Promise((resolve, reject) => {
326 |       this._getConnection()
327 |         .then((conn) => {
328 |           conn.query(
329 |             'SELECT points, expire FROM ??.?? WHERE `key` = ? AND (`expire` > ? OR `expire` IS NULL)',
330 |             [this.dbName, this.tableName, rlKey, Date.now()],
331 |             (err, res) => {
332 |               if (err) {
333 |                 reject(err);
334 |               } else if (res.length === 0) {
335 |                 resolve(null);
336 |               } else {
337 |                 resolve(res);
338 |               }
339 | 
340 |               this._releaseConnection(conn);
341 |             } // eslint-disable-line
342 |           );
343 |         })
344 |         .catch((err) => {
345 |           reject(err);
346 |         });
347 |     });
348 |   }
349 | 
350 |   _delete(rlKey) {
351 |     if (!this.tableCreated) {
352 |       return Promise.reject(Error('Table is not created yet'));
353 |     }
354 | 
355 |     return new Promise((resolve, reject) => {
356 |       this._getConnection()
357 |         .then((conn) => {
358 |           conn.query(
359 |             'DELETE FROM ??.?? WHERE `key` = ?',
360 |             [this.dbName, this.tableName, rlKey],
361 |             (err, res) => {
362 |               if (err) {
363 |                 reject(err);
364 |               } else {
365 |                 resolve(res.affectedRows > 0);
366 |               }
367 | 
368 |               this._releaseConnection(conn);
369 |             } // eslint-disable-line
370 |           );
371 |         })
372 |         .catch((err) => {
373 |           reject(err);
374 |         });
375 |     });
376 |   }
377 | }
378 | 
379 | module.exports = RateLimiterMySQL;
380 | 


--------------------------------------------------------------------------------
/lib/RateLimiterPostgres.js:
--------------------------------------------------------------------------------
  1 | const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
  2 | const RateLimiterRes = require('./RateLimiterRes');
  3 | 
  4 | class RateLimiterPostgres extends RateLimiterStoreAbstract {
  5 |   /**
  6 |    * @callback callback
  7 |    * @param {Object} err
  8 |    *
  9 |    * @param {Object} opts
 10 |    * @param {callback} cb
 11 |    * Defaults {
 12 |    *   ... see other in RateLimiterStoreAbstract
 13 |    *
 14 |    *   storeClient: postgresClient,
 15 |    *   storeType: 'knex', // required only for Knex instance
 16 |    *   tableName: 'string',
 17 |    *   schemaName: 'string', // optional
 18 |    * }
 19 |    */
 20 |   constructor(opts, cb = null) {
 21 |     super(opts);
 22 | 
 23 |     this.client = opts.storeClient;
 24 |     this.clientType = opts.storeType;
 25 | 
 26 |     this.tableName = opts.tableName;
 27 |     this.schemaName = opts.schemaName;
 28 | 
 29 |     this.clearExpiredByTimeout = opts.clearExpiredByTimeout;
 30 | 
 31 |     this.tableCreated = opts.tableCreated;
 32 |     if (!this.tableCreated) {
 33 |       this._createTable()
 34 |         .then(() => {
 35 |           this.tableCreated = true;
 36 |           if (this.clearExpiredByTimeout) {
 37 |             this._clearExpiredHourAgo();
 38 |           }
 39 |           if (typeof cb === 'function') {
 40 |             cb();
 41 |           }
 42 |         })
 43 |         .catch((err) => {
 44 |           if (typeof cb === 'function') {
 45 |             cb(err);
 46 |           } else {
 47 |             throw err;
 48 |           }
 49 |         });
 50 |     } else {
 51 |       if (this.clearExpiredByTimeout) {
 52 |         this._clearExpiredHourAgo();
 53 |       }
 54 |       if (typeof cb === 'function') {
 55 |         cb();
 56 |       }
 57 |     }
 58 |   }
 59 | 
 60 |   _getTableIdentifier() {
 61 |     return this.schemaName ? `"${this.schemaName}"."${this.tableName}"` : `"${this.tableName}"`;
 62 |   }
 63 | 
 64 |   clearExpired(expire) {
 65 |     return new Promise((resolve) => {
 66 |       const q = {
 67 |         name: 'rlflx-clear-expired',
 68 |         text: `DELETE FROM ${this._getTableIdentifier()} WHERE expire < $1`,
 69 |         values: [expire],
 70 |       };
 71 |       this._query(q)
 72 |         .then(() => {
 73 |           resolve();
 74 |         })
 75 |         .catch(() => {
 76 |           // Deleting expired query is not critical
 77 |           resolve();
 78 |         });
 79 |     });
 80 |   }
 81 | 
 82 |   /**
 83 |    * Delete all rows expired 1 hour ago once per 5 minutes
 84 |    *
 85 |    * @private
 86 |    */
 87 |   _clearExpiredHourAgo() {
 88 |     if (this._clearExpiredTimeoutId) {
 89 |       clearTimeout(this._clearExpiredTimeoutId);
 90 |     }
 91 |     this._clearExpiredTimeoutId = setTimeout(() => {
 92 |       this.clearExpired(Date.now() - 3600000) // Never rejected
 93 |         .then(() => {
 94 |           this._clearExpiredHourAgo();
 95 |         });
 96 |     }, 300000);
 97 |     this._clearExpiredTimeoutId.unref();
 98 |   }
 99 | 
100 |   /**
101 |    *
102 |    * @return Promise<any>
103 |    * @private
104 |    */
105 |   _getConnection() {
106 |     switch (this.clientType) {
107 |       case 'pool':
108 |         return Promise.resolve(this.client);
109 |       case 'sequelize':
110 |         return this.client.connectionManager.getConnection();
111 |       case 'knex':
112 |         return this.client.client.acquireConnection();
113 |       case 'typeorm':
114 |         return Promise.resolve(this.client.driver.master);
115 |       default:
116 |         return Promise.resolve(this.client);
117 |     }
118 |   }
119 | 
120 |   _releaseConnection(conn) {
121 |     switch (this.clientType) {
122 |       case 'pool':
123 |         return true;
124 |       case 'sequelize':
125 |         return this.client.connectionManager.releaseConnection(conn);
126 |       case 'knex':
127 |         return this.client.client.releaseConnection(conn);
128 |       case 'typeorm':
129 |         return true;
130 |       default:
131 |         return true;
132 |     }
133 |   }
134 | 
135 |   /**
136 |    *
137 |    * @returns {Promise<any>}
138 |    * @private
139 |    */
140 |   _createTable() {
141 |     return new Promise((resolve, reject) => {
142 |       this._query({
143 |         text: this._getCreateTableStmt(),
144 |       })
145 |         .then(() => {
146 |           resolve();
147 |         })
148 |         .catch((err) => {
149 |           if (err.code === '23505') {
150 |             // Error: duplicate key value violates unique constraint "pg_type_typname_nsp_index"
151 |             // Postgres doesn't handle concurrent table creation
152 |             // It is supposed, that table is created by another worker
153 |             resolve();
154 |           } else {
155 |             reject(err);
156 |           }
157 |         });
158 |     });
159 |   }
160 | 
161 |   _getCreateTableStmt() {
162 |     return `CREATE TABLE IF NOT EXISTS ${this._getTableIdentifier()} (
163 |       key varchar(255) PRIMARY KEY,
164 |       points integer NOT NULL DEFAULT 0,
165 |       expire bigint
166 |     );`;
167 |   }
168 | 
169 |   get clientType() {
170 |     return this._clientType;
171 |   }
172 | 
173 |   set clientType(value) {
174 |     const constructorName = this.client.constructor.name;
175 | 
176 |     if (typeof value === 'undefined') {
177 |       if (constructorName === 'Client') {
178 |         value = 'client';
179 |       } else if (
180 |         constructorName === 'Pool' ||
181 |         constructorName === 'BoundPool'
182 |       ) {
183 |         value = 'pool';
184 |       } else if (constructorName === 'Sequelize') {
185 |         value = 'sequelize';
186 |       } else {
187 |         throw new Error('storeType is not defined');
188 |       }
189 |     }
190 | 
191 |     this._clientType = value.toLowerCase();
192 |   }
193 | 
194 |   get tableName() {
195 |     return this._tableName;
196 |   }
197 | 
198 |   set tableName(value) {
199 |     this._tableName = typeof value === 'undefined' ? this.keyPrefix : value;
200 |   }
201 | 
202 |   get schemaName() {
203 |     return this._schemaName;
204 |   }
205 | 
206 |   set schemaName(value) {
207 |     this._schemaName = value;
208 |   }
209 | 
210 |   get tableCreated() {
211 |     return this._tableCreated;
212 |   }
213 | 
214 |   set tableCreated(value) {
215 |     this._tableCreated = typeof value === 'undefined' ? false : !!value;
216 |   }
217 | 
218 |   get clearExpiredByTimeout() {
219 |     return this._clearExpiredByTimeout;
220 |   }
221 | 
222 |   set clearExpiredByTimeout(value) {
223 |     this._clearExpiredByTimeout = typeof value === 'undefined' ? true : Boolean(value);
224 |   }
225 | 
226 |   _getRateLimiterRes(rlKey, changedPoints, result) {
227 |     const res = new RateLimiterRes();
228 |     const row = result.rows[0];
229 | 
230 |     res.isFirstInDuration = changedPoints === row.points;
231 |     res.consumedPoints = res.isFirstInDuration ? changedPoints : row.points;
232 | 
233 |     res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
234 |     res.msBeforeNext = row.expire
235 |       ? Math.max(row.expire - Date.now(), 0)
236 |       : -1;
237 | 
238 |     return res;
239 |   }
240 | 
241 |   _query(q) {
242 |     const prefix = this.tableName.toLowerCase();
243 |     const queryObj = { name: `${prefix}:${q.name}`, text: q.text, values: q.values };
244 |     return new Promise((resolve, reject) => {
245 |       this._getConnection()
246 |         .then((conn) => {
247 |           conn.query(queryObj)
248 |             .then((res) => {
249 |               resolve(res);
250 |               this._releaseConnection(conn);
251 |             })
252 |             .catch((err) => {
253 |               reject(err);
254 |               this._releaseConnection(conn);
255 |             });
256 |         })
257 |         .catch((err) => {
258 |           reject(err);
259 |         });
260 |     });
261 |   }
262 | 
263 |   _upsert(key, points, msDuration, forceExpire = false) {
264 |     if (!this.tableCreated) {
265 |       return Promise.reject(Error('Table is not created yet'));
266 |     }
267 | 
268 |     const newExpire = msDuration > 0 ? Date.now() + msDuration : null;
269 |     const expireQ = forceExpire
270 |       ? ' $3 '
271 |       : ` CASE
272 |              WHEN ${this._getTableIdentifier()}.expire <= $4 THEN $3
273 |              ELSE ${this._getTableIdentifier()}.expire
274 |             END `;
275 | 
276 |     return this._query({
277 |       name: forceExpire ? 'rlflx-upsert-force' : 'rlflx-upsert',
278 |       text: `
279 |             INSERT INTO ${this._getTableIdentifier()} VALUES ($1, $2, $3)
280 |               ON CONFLICT(key) DO UPDATE SET
281 |                 points = CASE
282 |                           WHEN (${this._getTableIdentifier()}.expire <= $4 OR 1=${forceExpire ? 1 : 0}) THEN $2
283 |                           ELSE ${this._getTableIdentifier()}.points + ($2)
284 |                          END,
285 |                 expire = ${expireQ}
286 |             RETURNING points, expire;`,
287 |       values: [key, points, newExpire, Date.now()],
288 |     });
289 |   }
290 | 
291 |   _get(rlKey) {
292 |     if (!this.tableCreated) {
293 |       return Promise.reject(Error('Table is not created yet'));
294 |     }
295 | 
296 |     return new Promise((resolve, reject) => {
297 |       this._query({
298 |         name: 'rlflx-get',
299 |         text: `
300 |             SELECT points, expire FROM ${this._getTableIdentifier()} WHERE key = $1 AND (expire > $2 OR expire IS NULL);`,
301 |         values: [rlKey, Date.now()],
302 |       })
303 |         .then((res) => {
304 |           if (res.rowCount === 0) {
305 |             res = null;
306 |           }
307 |           resolve(res);
308 |         })
309 |         .catch((err) => {
310 |           reject(err);
311 |         });
312 |     });
313 |   }
314 | 
315 |   _delete(rlKey) {
316 |     if (!this.tableCreated) {
317 |       return Promise.reject(Error('Table is not created yet'));
318 |     }
319 | 
320 |     return this._query({
321 |       name: 'rlflx-delete',
322 |       text: `DELETE FROM ${this._getTableIdentifier()} WHERE key = $1`,
323 |       values: [rlKey],
324 |     })
325 |       .then(res => res.rowCount > 0);
326 |   }
327 | }
328 | 
329 | module.exports = RateLimiterPostgres;
330 | 


--------------------------------------------------------------------------------
/lib/RateLimiterPrisma.js:
--------------------------------------------------------------------------------
  1 | const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
  2 | const RateLimiterRes = require('./RateLimiterRes');
  3 | 
  4 | class RateLimiterPrisma extends RateLimiterStoreAbstract {
  5 |   /**
  6 |    * Constructor for the rate limiter
  7 |    * @param {Object} opts - Options for the rate limiter
  8 |    */
  9 |   constructor(opts) {
 10 |     super(opts);
 11 | 
 12 |     this.modelName = opts.tableName || 'RateLimiterFlexible';
 13 |     this.prismaClient = opts.storeClient;
 14 |     this.clearExpiredByTimeout = opts.clearExpiredByTimeout || true;
 15 | 
 16 |     if (!this.prismaClient) {
 17 |       throw new Error('Prisma client is not provided');
 18 |     }
 19 | 
 20 |     if (this.clearExpiredByTimeout) {
 21 |       this._clearExpiredHourAgo();
 22 |     }
 23 |   }
 24 | 
 25 |   _getRateLimiterRes(rlKey, changedPoints, result) {
 26 |     const res = new RateLimiterRes();
 27 | 
 28 |     let doc = result;
 29 | 
 30 |     res.isFirstInDuration = doc.points === changedPoints;
 31 |     res.consumedPoints = doc.points;
 32 | 
 33 |     res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
 34 |     res.msBeforeNext = doc.expire !== null
 35 |       ? Math.max(new Date(doc.expire).getTime() - Date.now(), 0)
 36 |       : -1;
 37 | 
 38 |     return res;
 39 |   }
 40 | 
 41 |   _upsert(key, points, msDuration, forceExpire = false) {
 42 |     if (!this.prismaClient) {
 43 |       return Promise.reject(new Error('Prisma client is not established'));
 44 |     }
 45 | 
 46 |     const now = new Date();
 47 |     const newExpire = msDuration > 0 ? new Date(now.getTime() + msDuration) : null;
 48 | 
 49 |     return this.prismaClient.$transaction(async (prisma) => {
 50 |       const existingRecord = await prisma[this.modelName].findFirst({
 51 |         where: { key: key },
 52 |       });
 53 | 
 54 |       if (existingRecord) {
 55 |         // Determine if we should update the expire field
 56 |         const shouldUpdateExpire = forceExpire || !existingRecord.expire || existingRecord.expire <= now || newExpire === null;
 57 | 
 58 |         return prisma[this.modelName].update({
 59 |           where: { key: key },
 60 |           data: {
 61 |             points: !shouldUpdateExpire ? existingRecord.points + points : points,
 62 |             ...(shouldUpdateExpire && { expire: newExpire }),
 63 |           },
 64 |         });
 65 |       } else {
 66 |         return prisma[this.modelName].create({
 67 |           data: {
 68 |             key: key,
 69 |             points: points,
 70 |             expire: newExpire,
 71 |           },
 72 |         });
 73 |       }
 74 |     });
 75 |   }
 76 | 
 77 |   _get(rlKey) {
 78 |     if (!this.prismaClient) {
 79 |       return Promise.reject(new Error('Prisma client is not established'));
 80 |     }
 81 | 
 82 |     return this.prismaClient[this.modelName].findFirst({
 83 |       where: {
 84 |         AND: [
 85 |           { key: rlKey },
 86 |           {
 87 |             OR: [
 88 |               { expire: { gt: new Date() } },
 89 |               { expire: null },
 90 |             ],
 91 |           },
 92 |         ],
 93 |       },
 94 |     });
 95 |   }
 96 | 
 97 |   _delete(rlKey) {
 98 |     if (!this.prismaClient) {
 99 |       return Promise.reject(new Error('Prisma client is not established'));
100 |     }
101 | 
102 |     return this.prismaClient[this.modelName].deleteMany({
103 |       where: {
104 |         key: rlKey,
105 |       },
106 |     }).then(res => res.count > 0);
107 |   }
108 | 
109 |   _clearExpiredHourAgo() {
110 |     if (this._clearExpiredTimeoutId) {
111 |       clearTimeout(this._clearExpiredTimeoutId);
112 |     }
113 |     this._clearExpiredTimeoutId = setTimeout(async () => {
114 |       await this.prismaClient[this.modelName].deleteMany({
115 |         where: {
116 |           expire: {
117 |             lt: new Date(Date.now() - 3600000),
118 |           },
119 |         },
120 |       });
121 |       this._clearExpiredHourAgo();
122 |     }, 300000); // Clear every 5 minutes
123 |     this._clearExpiredTimeoutId.unref();
124 |   }
125 | }
126 | 
127 | module.exports = RateLimiterPrisma;
128 | 


--------------------------------------------------------------------------------
/lib/RateLimiterQueue.js:
--------------------------------------------------------------------------------
  1 | const RateLimiterQueueError = require('./component/RateLimiterQueueError')
  2 | const MAX_QUEUE_SIZE = 4294967295;
  3 | const KEY_DEFAULT = 'limiter';
  4 | 
  5 | module.exports = class RateLimiterQueue {
  6 |   constructor(limiterFlexible, opts = {
  7 |     maxQueueSize: MAX_QUEUE_SIZE,
  8 |   }) {
  9 |     this._queueLimiters = {
 10 |       KEY_DEFAULT: new RateLimiterQueueInternal(limiterFlexible, opts)
 11 |     };
 12 |     this._limiterFlexible = limiterFlexible;
 13 |     this._maxQueueSize = opts.maxQueueSize
 14 |   }
 15 | 
 16 |   getTokensRemaining(key = KEY_DEFAULT) {
 17 |     if (this._queueLimiters[key]) {
 18 |       return this._queueLimiters[key].getTokensRemaining()
 19 |     } else {
 20 |       return Promise.resolve(this._limiterFlexible.points)
 21 |     }
 22 |   }
 23 | 
 24 |   removeTokens(tokens, key = KEY_DEFAULT) {
 25 |     if (!this._queueLimiters[key]) {
 26 |       this._queueLimiters[key] = new RateLimiterQueueInternal(
 27 |         this._limiterFlexible, {
 28 |           key,
 29 |           maxQueueSize: this._maxQueueSize,
 30 |         })
 31 |     }
 32 | 
 33 |     return this._queueLimiters[key].removeTokens(tokens)
 34 |   }
 35 | };
 36 | 
 37 | class RateLimiterQueueInternal {
 38 | 
 39 |   constructor(limiterFlexible, opts = {
 40 |     maxQueueSize: MAX_QUEUE_SIZE,
 41 |     key: KEY_DEFAULT,
 42 |   }) {
 43 |     this._key = opts.key;
 44 |     this._waitTimeout = null;
 45 |     this._queue = [];
 46 |     this._limiterFlexible = limiterFlexible;
 47 | 
 48 |     this._maxQueueSize = opts.maxQueueSize
 49 |   }
 50 | 
 51 |   getTokensRemaining() {
 52 |     return this._limiterFlexible.get(this._key)
 53 |       .then((rlRes) => {
 54 |         return rlRes !== null ? rlRes.remainingPoints : this._limiterFlexible.points;
 55 |       })
 56 |   }
 57 | 
 58 |   removeTokens(tokens) {
 59 |     const _this = this;
 60 | 
 61 |     return new Promise((resolve, reject) => {
 62 |       if (tokens > _this._limiterFlexible.points) {
 63 |         reject(new RateLimiterQueueError(`Requested tokens ${tokens} exceeds maximum ${_this._limiterFlexible.points} tokens per interval`));
 64 |         return
 65 |       }
 66 | 
 67 |       if (_this._queue.length > 0) {
 68 |         _this._queueRequest.call(_this, resolve, reject, tokens);
 69 |       } else {
 70 |         _this._limiterFlexible.consume(_this._key, tokens)
 71 |           .then((res) => {
 72 |             resolve(res.remainingPoints);
 73 |           })
 74 |           .catch((rej) => {
 75 |             if (rej instanceof Error) {
 76 |               reject(rej);
 77 |             } else {
 78 |               _this._queueRequest.call(_this, resolve, reject, tokens);
 79 |               if (_this._waitTimeout === null) {
 80 |                 _this._waitTimeout = setTimeout(_this._processFIFO.bind(_this), rej.msBeforeNext);
 81 |               }
 82 |             }
 83 |           });
 84 |       }
 85 |     })
 86 |   }
 87 | 
 88 |   _queueRequest(resolve, reject, tokens) {
 89 |     const _this = this;
 90 |     if (_this._queue.length < _this._maxQueueSize) {
 91 |       _this._queue.push({resolve, reject, tokens});
 92 |     } else {
 93 |       reject(new RateLimiterQueueError(`Number of requests reached it's maximum ${_this._maxQueueSize}`))
 94 |     }
 95 |   }
 96 | 
 97 |   _processFIFO() {
 98 |     const _this = this;
 99 | 
100 |     if (_this._waitTimeout !== null) {
101 |       clearTimeout(_this._waitTimeout);
102 |       _this._waitTimeout = null;
103 |     }
104 | 
105 |     if (_this._queue.length === 0) {
106 |       return;
107 |     }
108 | 
109 |     const item = _this._queue.shift();
110 |     _this._limiterFlexible.consume(_this._key, item.tokens)
111 |       .then((res) => {
112 |         item.resolve(res.remainingPoints);
113 |         _this._processFIFO.call(_this);
114 |       })
115 |       .catch((rej) => {
116 |         if (rej instanceof Error) {
117 |           item.reject(rej);
118 |           _this._processFIFO.call(_this);
119 |         } else {
120 |           _this._queue.unshift(item);
121 |           if (_this._waitTimeout === null) {
122 |             _this._waitTimeout = setTimeout(_this._processFIFO.bind(_this), rej.msBeforeNext);
123 |           }
124 |         }
125 |       });
126 |   }
127 | }
128 | 


--------------------------------------------------------------------------------
/lib/RateLimiterRedis.js:
--------------------------------------------------------------------------------
  1 | const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
  2 | const RateLimiterRes = require('./RateLimiterRes');
  3 | 
  4 | const incrTtlLuaScript = `redis.call('set', KEYS[1], 0, 'EX', ARGV[2], 'NX') \
  5 | local consumed = redis.call('incrby', KEYS[1], ARGV[1]) \
  6 | local ttl = redis.call('pttl', KEYS[1]) \
  7 | if ttl == -1 then \
  8 |   redis.call('expire', KEYS[1], ARGV[2]) \
  9 |   ttl = 1000 * ARGV[2] \
 10 | end \
 11 | return {consumed, ttl} \
 12 | `;
 13 | 
 14 | class RateLimiterRedis extends RateLimiterStoreAbstract {
 15 |   /**
 16 |    *
 17 |    * @param {Object} opts
 18 |    * Defaults {
 19 |    *   ... see other in RateLimiterStoreAbstract
 20 |    *
 21 |    *   redis: RedisClient
 22 |    *   rejectIfRedisNotReady: boolean = false - reject / invoke insuranceLimiter immediately when redis connection is not "ready"
 23 |    * }
 24 |    */
 25 |   constructor(opts) {
 26 |     super(opts);
 27 |     this.client = opts.storeClient;
 28 | 
 29 |     this._rejectIfRedisNotReady = !!opts.rejectIfRedisNotReady;
 30 |     this._incrTtlLuaScript = opts.customIncrTtlLuaScript || incrTtlLuaScript;
 31 | 
 32 |     this.useRedisPackage = opts.useRedisPackage || this.client.constructor.name === 'Commander' || false;
 33 |     this.useRedis3AndLowerPackage = opts.useRedis3AndLowerPackage;
 34 |     if (typeof this.client.defineCommand === 'function') {
 35 |       this.client.defineCommand("rlflxIncr", {
 36 |         numberOfKeys: 1,
 37 |         lua: this._incrTtlLuaScript,
 38 |       });
 39 |     }
 40 |   }
 41 | 
 42 |   /**
 43 |    * Prevent actual redis call if redis connection is not ready
 44 |    * Because of different connection state checks for ioredis and node-redis, only this clients would be actually checked.
 45 |    * For any other clients all the requests would be passed directly to redis client
 46 |    * @return {boolean}
 47 |    * @private
 48 |    */
 49 |   _isRedisReady() {
 50 |     if (!this._rejectIfRedisNotReady) {
 51 |       return true;
 52 |     }
 53 |     // ioredis client
 54 |     if (this.client.status && this.client.status !== 'ready') {
 55 |       return false;
 56 |     }
 57 |     // node-redis client
 58 |     if (typeof this.client.isReady === 'function' && !this.client.isReady()) {
 59 |       return false;
 60 |     }
 61 |     return true;
 62 |   }
 63 | 
 64 |   _getRateLimiterRes(rlKey, changedPoints, result) {
 65 |     let [consumed, resTtlMs] = result;
 66 |     // Support ioredis results format
 67 |     if (Array.isArray(consumed)) {
 68 |       [, consumed] = consumed;
 69 |       [, resTtlMs] = resTtlMs;
 70 |     }
 71 | 
 72 |     const res = new RateLimiterRes();
 73 |     res.consumedPoints = parseInt(consumed);
 74 |     res.isFirstInDuration = res.consumedPoints === changedPoints;
 75 |     res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
 76 |     res.msBeforeNext = resTtlMs;
 77 | 
 78 |     return res;
 79 |   }
 80 | 
 81 |   async _upsert(rlKey, points, msDuration, forceExpire = false) {
 82 |     if(
 83 |       typeof points == 'string'
 84 |     ){
 85 |       if(!RegExp("^[1-9][0-9]*
quot;).test(points)){
 86 |         throw new Error("Consuming string different than integer values is not supported by this package");
 87 |       }
 88 |     } else if (!Number.isInteger(points)){
 89 |       throw new Error("Consuming decimal number of points is not supported by this package");
 90 |     }
 91 | 
 92 |     if (!this._isRedisReady()) {
 93 |       throw new Error('Redis connection is not ready');
 94 |     }
 95 | 
 96 |     const secDuration = Math.floor(msDuration / 1000);
 97 |     const multi = this.client.multi();
 98 | 
 99 |     if (forceExpire) {
100 |       if (secDuration > 0) {
101 |         if(!this.useRedisPackage && !this.useRedis3AndLowerPackage){
102 |           multi.set(rlKey, points, "EX", secDuration);
103 |         }else{
104 |           multi.set(rlKey, points, { EX: secDuration });
105 |         }
106 |       } else {
107 |         multi.set(rlKey, points);
108 |       }
109 | 
110 |       if(!this.useRedisPackage && !this.useRedis3AndLowerPackage){
111 |         return multi.pttl(rlKey).exec(true);
112 |       }
113 |       return multi.pTTL(rlKey).exec(true);
114 |     }
115 | 
116 |     if (secDuration > 0) {
117 |       if(!this.useRedisPackage && !this.useRedis3AndLowerPackage){
118 |         return this.client.rlflxIncr(
119 |           [rlKey].concat([String(points), String(secDuration), String(this.points), String(this.duration)]));
120 |       }
121 |       if (this.useRedis3AndLowerPackage) {
122 |         return new Promise((resolve, reject) => {
123 |           const incrCallback = function (err, result) {
124 |             if (err) {
125 |               return reject(err);
126 |             }
127 | 
128 |             return resolve(result);
129 |           };
130 | 
131 |           if (typeof this.client.rlflxIncr === 'function') {
132 |             this.client.rlflxIncr(rlKey, points, secDuration, this.points, this.duration, incrCallback);
133 |           } else {
134 |             this.client.eval(this._incrTtlLuaScript, 1, rlKey, points, secDuration, this.points, this.duration, incrCallback);
135 |           }
136 |         });
137 |       } else {
138 |         return this.client.eval(this._incrTtlLuaScript, {
139 |           keys: [rlKey],
140 |           arguments: [String(points), String(secDuration), String(this.points), String(this.duration)],
141 |         });
142 |       }
143 |     } else {
144 |       if(!this.useRedisPackage && !this.useRedis3AndLowerPackage){
145 |         return multi.incrby(rlKey, points).pttl(rlKey).exec(true);
146 |       }
147 | 
148 |       return multi.incrBy(rlKey, points).pTTL(rlKey).exec(true);
149 |     }
150 |   }
151 | 
152 |   async _get(rlKey) {
153 |     if (!this._isRedisReady()) {
154 |       throw new Error('Redis connection is not ready');
155 |     }
156 |     if(!this.useRedisPackage && !this.useRedis3AndLowerPackage){
157 |       return this.client
158 |         .multi()
159 |         .get(rlKey)
160 |         .pttl(rlKey)
161 |         .exec()
162 |         .then((result) => {
163 |           const [[,points]] = result;
164 |           if (points === null) return null;
165 |           return result;
166 |         });
167 |     }
168 | 
169 |     return this.client
170 |       .multi()
171 |       .get(rlKey)
172 |       .pTTL(rlKey)
173 |       .exec(true)
174 |       .then((result) => {
175 |         const [points] = result;
176 |         if (points === null) return null;
177 |         return result;
178 |       });
179 |   }
180 | 
181 |   _delete(rlKey) {
182 |     return this.client
183 |       .del(rlKey)
184 |       .then(result => result > 0);
185 |   }
186 | }
187 | 
188 | module.exports = RateLimiterRedis;
189 | 


--------------------------------------------------------------------------------
/lib/RateLimiterRes.js:
--------------------------------------------------------------------------------
 1 | module.exports = class RateLimiterRes {
 2 |   constructor(remainingPoints, msBeforeNext, consumedPoints, isFirstInDuration) {
 3 |     this.remainingPoints = typeof remainingPoints === 'undefined' ? 0 : remainingPoints; // Remaining points in current duration
 4 |     this.msBeforeNext = typeof msBeforeNext === 'undefined' ? 0 : msBeforeNext; // Milliseconds before next action
 5 |     this.consumedPoints = typeof consumedPoints === 'undefined' ? 0 : consumedPoints; // Consumed points in current duration
 6 |     this.isFirstInDuration = typeof isFirstInDuration === 'undefined' ? false : isFirstInDuration;
 7 |   }
 8 | 
 9 |   get msBeforeNext() {
10 |     return this._msBeforeNext;
11 |   }
12 | 
13 |   set msBeforeNext(ms) {
14 |     this._msBeforeNext = ms;
15 |     return this;
16 |   }
17 | 
18 |   get remainingPoints() {
19 |     return this._remainingPoints;
20 |   }
21 | 
22 |   set remainingPoints(p) {
23 |     this._remainingPoints = p;
24 |     return this;
25 |   }
26 | 
27 |   get consumedPoints() {
28 |     return this._consumedPoints;
29 |   }
30 | 
31 |   set consumedPoints(p) {
32 |     this._consumedPoints = p;
33 |     return this;
34 |   }
35 | 
36 |   get isFirstInDuration() {
37 |     return this._isFirstInDuration;
38 |   }
39 | 
40 |   set isFirstInDuration(value) {
41 |     this._isFirstInDuration = Boolean(value);
42 |   }
43 | 
44 |   _getDecoratedProperties() {
45 |     return {
46 |       remainingPoints: this.remainingPoints,
47 |       msBeforeNext: this.msBeforeNext,
48 |       consumedPoints: this.consumedPoints,
49 |       isFirstInDuration: this.isFirstInDuration,
50 |     };
51 |   }
52 | 
53 |   [Symbol.for("nodejs.util.inspect.custom")]() {
54 |     return this._getDecoratedProperties();
55 |   }
56 | 
57 |   toString() {
58 |     return JSON.stringify(this._getDecoratedProperties());
59 |   }
60 | 
61 |   toJSON() {
62 |     return this._getDecoratedProperties();
63 |   }
64 | };
65 | 


--------------------------------------------------------------------------------
/lib/RateLimiterSQLite.js:
--------------------------------------------------------------------------------
  1 | const RateLimiterStoreAbstract = require("./RateLimiterStoreAbstract");
  2 | const RateLimiterRes = require("./RateLimiterRes");
  3 | 
  4 | class RateLimiterSQLite extends RateLimiterStoreAbstract {
  5 |   /**
  6 |    * Internal store type used to determine the SQLite client in use.
  7 |    * It can be one of the following:
  8 |    * - `"sqlite3".
  9 |    * - `"better-sqlite3".
 10 |    *
 11 |    * @type {("sqlite3" | "better-sqlite3" | null)}
 12 |    * @private
 13 |    */
 14 |   _internalStoreType = null;
 15 | 
 16 |   /**
 17 |    * @callback callback
 18 |    * @param {Object} err
 19 |    *
 20 |    * @param {Object} opts
 21 |    * @param {callback} cb
 22 |    * Defaults {
 23 |    *   ... see other in RateLimiterStoreAbstract
 24 |    *   storeClient: sqliteClient, // SQLite database instance (sqlite3, better-sqlite3, or knex instance)
 25 |    *   storeType: 'sqlite3' | 'better-sqlite3' | 'knex', // Optional, defaults to 'sqlite3'
 26 |    *   tableName: 'string',
 27 |    *   tableCreated: boolean,
 28 |    *   clearExpiredByTimeout: boolean,
 29 |    * }
 30 |    */
 31 |   constructor(opts, cb = null) {
 32 |     super(opts);
 33 | 
 34 |     this.client = opts.storeClient;
 35 |     this.storeType = opts.storeType || "sqlite3";
 36 |     this.tableName = opts.tableName;
 37 |     this.tableCreated = opts.tableCreated || false;
 38 |     this.clearExpiredByTimeout = opts.clearExpiredByTimeout;
 39 | 
 40 |     this._validateStoreTypes(cb);
 41 |     this._validateStoreClient(cb);
 42 |     this._setInternalStoreType(cb);
 43 |     this._validateTableName(cb);
 44 | 
 45 |     if (!this.tableCreated) {
 46 |       this._createDbAndTable()
 47 |         .then(() => {
 48 |           this.tableCreated = true;
 49 |           if (this.clearExpiredByTimeout) this._clearExpiredHourAgo();
 50 |           if (typeof cb === "function") cb();
 51 |         })
 52 |         .catch((err) => {
 53 |           if (typeof cb === "function") cb(err);
 54 |           else throw err;
 55 |         });
 56 |     } else {
 57 |       if (this.clearExpiredByTimeout) this._clearExpiredHourAgo();
 58 |       if (typeof cb === "function") cb();
 59 |     }
 60 |   }
 61 |   _validateStoreTypes(cb) {
 62 |     const validStoreTypes = ["sqlite3", "better-sqlite3", "knex"];
 63 |     if (!validStoreTypes.includes(this.storeType)) {
 64 |       const err = new Error(
 65 |         `storeType must be one of: ${validStoreTypes.join(", ")}`
 66 |       );
 67 |       if (typeof cb === "function") return cb(err);
 68 |       throw err;
 69 |     }
 70 |   }
 71 |   _validateStoreClient(cb) {
 72 |     if (this.storeType === "sqlite3") {
 73 |       if (typeof this.client.run !== "function") {
 74 |         const err = new Error(
 75 |           "storeClient must be an instance of sqlite3.Database when storeType is 'sqlite3' or no storeType was provided"
 76 |         );
 77 |         if (typeof cb === "function") return cb(err);
 78 |         throw err;
 79 |       }
 80 |     } else if (this.storeType === "better-sqlite3") {
 81 |       if (
 82 |         typeof this.client.prepare !== "function" ||
 83 |         typeof this.client.run !== "undefined"
 84 |       ) {
 85 |         const err = new Error(
 86 |           "storeClient must be an instance of better-sqlite3.Database when storeType is 'better-sqlite3'"
 87 |         );
 88 |         if (typeof cb === "function") return cb(err);
 89 |         throw err;
 90 |       }
 91 |     } else if (this.storeType === "knex") {
 92 |       if (typeof this.client.raw !== "function") {
 93 |         const err = new Error(
 94 |           "storeClient must be an instance of Knex when storeType is 'knex'"
 95 |         );
 96 |         if (typeof cb === "function") return cb(err);
 97 |         throw err;
 98 |       }
 99 |     }
100 |   }
101 |   _setInternalStoreType(cb) {
102 |     if (this.storeType === "knex") {
103 |       const knexClientType = this.client.client.config.client;
104 |       if (knexClientType === "sqlite3") {
105 |         this._internalStoreType = "sqlite3";
106 |       } else if (knexClientType === "better-sqlite3") {
107 |         this._internalStoreType = "better-sqlite3";
108 |       } else {
109 |         const err = new Error(
110 |           "Knex must be configured with 'sqlite3' or 'better-sqlite3' for RateLimiterSQLite"
111 |         );
112 |         if (typeof cb === "function") return cb(err);
113 |         throw err;
114 |       }
115 |     } else {
116 |       this._internalStoreType = this.storeType;
117 |     }
118 |   }
119 |   _validateTableName(cb) {
120 |     if (!/^[A-Za-z0-9_]*$/.test(this.tableName)) {
121 |       const err = new Error("Table name must contain only letters and numbers");
122 |       if (typeof cb === "function") return cb(err);
123 |       throw err;
124 |     }
125 |   }
126 | 
127 |   /**
128 |    * Acquires the database connection based on the storeType.
129 |    * @returns {Promise<Object>} The database client or connection
130 |    */
131 |   async _getConnection() {
132 |     if (this.storeType === "knex") {
133 |       return this.client.client.acquireConnection(); // Acquire raw connection from knex pool
134 |     }
135 |     return this.client; // For sqlite3 and better-sqlite3, return the client directly
136 |   }
137 | 
138 |   /**
139 |    * Releases the database connection if necessary.
140 |    * @param {Object} conn The database client or connection
141 |    */
142 |   _releaseConnection(conn) {
143 |     if (this.storeType === "knex") {
144 |       this.client.client.releaseConnection(conn);
145 |     }
146 |     // No release needed for direct sqlite3 or better-sqlite3 clients
147 |   }
148 | 
149 |   async _createDbAndTable() {
150 |     const conn = await this._getConnection();
151 |     try {
152 |       switch (this._internalStoreType) {
153 |         case "sqlite3":
154 |           await new Promise((resolve, reject) => {
155 |             conn.run(this._getCreateTableSQL(), (err) =>
156 |               err ? reject(err) : resolve()
157 |             );
158 |           });
159 |           break;
160 |         case "better-sqlite3":
161 |           conn.prepare(this._getCreateTableSQL()).run();
162 |           break;
163 |         default:
164 |           throw new Error("Unsupported internalStoreType");
165 |       }
166 |     } finally {
167 |       this._releaseConnection(conn);
168 |     }
169 |   }
170 | 
171 |   _getCreateTableSQL() {
172 |     return `CREATE TABLE IF NOT EXISTS ${this.tableName} (
173 |       key TEXT PRIMARY KEY,
174 |       points INTEGER NOT NULL DEFAULT 0,
175 |       expire INTEGER
176 |     )`;
177 |   }
178 | 
179 |   _clearExpiredHourAgo() {
180 |     if (this._clearExpiredTimeoutId) clearTimeout(this._clearExpiredTimeoutId);
181 |     this._clearExpiredTimeoutId = setTimeout(() => {
182 |       this.clearExpired(Date.now() - 3600000) // 1 hour ago
183 |         .then(() => this._clearExpiredHourAgo());
184 |     }, 300000); // Every 5 minutes
185 |     this._clearExpiredTimeoutId.unref();
186 |   }
187 | 
188 |   async clearExpired(nowMs) {
189 |     const sql = `DELETE FROM ${this.tableName} WHERE expire < ?`;
190 |     const conn = await this._getConnection();
191 |     try {
192 |       switch (this._internalStoreType) {
193 |         case "sqlite3":
194 |           await new Promise((resolve, reject) => {
195 |             conn.run(sql, [nowMs], (err) => (err ? reject(err) : resolve()));
196 |           });
197 |           break;
198 |         case "better-sqlite3":
199 |           conn.prepare(sql).run(nowMs);
200 |           break;
201 |         default:
202 |           throw new Error("Unsupported internalStoreType");
203 |       }
204 |     } finally {
205 |       this._releaseConnection(conn);
206 |     }
207 |   }
208 | 
209 |   _getRateLimiterRes(rlKey, changedPoints, result) {
210 |     const res = new RateLimiterRes();
211 |     res.isFirstInDuration = changedPoints === result.points;
212 |     res.consumedPoints = res.isFirstInDuration ? changedPoints : result.points;
213 |     res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
214 |     res.msBeforeNext = result.expire
215 |       ? Math.max(result.expire - Date.now(), 0)
216 |       : -1;
217 |     return res;
218 |   }
219 | 
220 |   async _upsertTransactionSQLite3(conn, upsertQuery, upsertParams) {
221 |     return await new Promise((resolve, reject) => {
222 |       conn.serialize(() => {
223 |         conn.run("SAVEPOINT rate_limiter_trx;", (err) => {
224 |           if (err) return reject(err);
225 |           conn.get(upsertQuery, upsertParams, (err, row) => {
226 |             if (err) {
227 |               conn.run("ROLLBACK TO SAVEPOINT rate_limiter_trx;", () =>
228 |                 reject(err)
229 |               );
230 |               return;
231 |             }
232 |             conn.run("RELEASE SAVEPOINT rate_limiter_trx;", () => resolve(row));
233 |           });
234 |         });
235 |       });
236 |     });
237 |   }
238 | 
239 |   async _upsertTransactionBetterSQLite3(conn, upsertQuery, upsertParams) {
240 |     return conn.transaction(() =>
241 |       conn.prepare(upsertQuery).get(...upsertParams)
242 |     )();
243 |   }
244 |   async _upsertTransaction(rlKey, points, msDuration, forceExpire) {
245 |     const dateNow = Date.now();
246 |     const newExpire = msDuration > 0 ? dateNow + msDuration : null;
247 |     const upsertQuery = forceExpire
248 |       ? `INSERT OR REPLACE INTO ${this.tableName} (key, points, expire) VALUES (?, ?, ?) RETURNING points, expire`
249 |       : `INSERT INTO ${this.tableName} (key, points, expire)
250 |          VALUES (?, ?, ?)
251 |          ON CONFLICT(key) DO UPDATE SET
252 |            points = CASE WHEN expire IS NULL OR expire > ? THEN points + excluded.points ELSE excluded.points END,
253 |            expire = CASE WHEN expire IS NULL OR expire > ? THEN expire ELSE excluded.expire END
254 |          RETURNING points, expire`;
255 |     const upsertParams = forceExpire
256 |       ? [rlKey, points, newExpire]
257 |       : [rlKey, points, newExpire, dateNow, dateNow];
258 | 
259 |     const conn = await this._getConnection();
260 |     try {
261 |       switch (this._internalStoreType) {
262 |         case "sqlite3":
263 |           return this._upsertTransactionSQLite3(
264 |             conn,
265 |             upsertQuery,
266 |             upsertParams
267 |           );
268 |         case "better-sqlite3":
269 |           return this._upsertTransactionBetterSQLite3(
270 |             conn,
271 |             upsertQuery,
272 |             upsertParams
273 |           );
274 |         default:
275 |           throw new Error("Unsupported internalStoreType");
276 |       }
277 |     } finally {
278 |       this._releaseConnection(conn);
279 |     }
280 |   }
281 | 
282 |   _upsert(rlKey, points, msDuration, forceExpire = false) {
283 |     if (!this.tableCreated) {
284 |       return Promise.reject(new Error("Table is not created yet"));
285 |     }
286 |     return this._upsertTransaction(rlKey, points, msDuration, forceExpire);
287 |   }
288 | 
289 |   async _get(rlKey) {
290 |     const sql = `SELECT points, expire FROM ${this.tableName} WHERE key = ? AND (expire > ? OR expire IS NULL)`;
291 |     const now = Date.now();
292 |     const conn = await this._getConnection();
293 |     try {
294 |       switch (this._internalStoreType) {
295 |         case "sqlite3":
296 |           return await new Promise((resolve, reject) => {
297 |             conn.get(sql, [rlKey, now], (err, row) =>
298 |               err ? reject(err) : resolve(row || null)
299 |             );
300 |           });
301 |         case "better-sqlite3":
302 |           return conn.prepare(sql).get(rlKey, now) || null;
303 |         default:
304 |           throw new Error("Unsupported internalStoreType");
305 |       }
306 |     } finally {
307 |       this._releaseConnection(conn);
308 |     }
309 |   }
310 | 
311 |   async _delete(rlKey) {
312 |     if (!this.tableCreated) {
313 |       return Promise.reject(new Error("Table is not created yet"));
314 |     }
315 |     const sql = `DELETE FROM ${this.tableName} WHERE key = ?`;
316 |     const conn = await this._getConnection();
317 |     try {
318 |       switch (this._internalStoreType) {
319 |         case "sqlite3":
320 |           return await new Promise((resolve, reject) => {
321 |             conn.run(sql, [rlKey], function (err) {
322 |               if (err) reject(err);
323 |               else resolve(this.changes > 0);
324 |             });
325 |           });
326 |         case "better-sqlite3":
327 |           const result = conn.prepare(sql).run(rlKey);
328 |           return result.changes > 0;
329 |         default:
330 |           throw new Error("Unsupported internalStoreType");
331 |       }
332 |     } finally {
333 |       this._releaseConnection(conn);
334 |     }
335 |   }
336 | }
337 | 
338 | module.exports = RateLimiterSQLite;
339 | 


--------------------------------------------------------------------------------
/lib/RateLimiterStoreAbstract.js:
--------------------------------------------------------------------------------
  1 | const RateLimiterAbstract = require('./RateLimiterAbstract');
  2 | const BlockedKeys = require('./component/BlockedKeys');
  3 | const RateLimiterRes = require('./RateLimiterRes');
  4 | 
  5 | module.exports = class RateLimiterStoreAbstract extends RateLimiterAbstract {
  6 |   /**
  7 |    *
  8 |    * @param opts Object Defaults {
  9 |    *   ... see other in RateLimiterAbstract
 10 |    *
 11 |    *   inMemoryBlockOnConsumed: 40, // Number of points when key is blocked
 12 |    *   inMemoryBlockDuration: 10, // Block duration in seconds
 13 |    *   insuranceLimiter: RateLimiterAbstract
 14 |    * }
 15 |    */
 16 |   constructor(opts = {}) {
 17 |     super(opts);
 18 | 
 19 |     this.inMemoryBlockOnConsumed = opts.inMemoryBlockOnConsumed;
 20 |     this.inMemoryBlockDuration = opts.inMemoryBlockDuration;
 21 |     this.insuranceLimiter = opts.insuranceLimiter;
 22 |     this._inMemoryBlockedKeys = new BlockedKeys();
 23 |   }
 24 | 
 25 |   get client() {
 26 |     return this._client;
 27 |   }
 28 | 
 29 |   set client(value) {
 30 |     if (typeof value === 'undefined') {
 31 |       throw new Error('storeClient is not set');
 32 |     }
 33 |     this._client = value;
 34 |   }
 35 | 
 36 |   /**
 37 |    * Have to be launched after consume
 38 |    * It blocks key and execute evenly depending on result from store
 39 |    *
 40 |    * It uses _getRateLimiterRes function to prepare RateLimiterRes from store result
 41 |    *
 42 |    * @param resolve
 43 |    * @param reject
 44 |    * @param rlKey
 45 |    * @param changedPoints
 46 |    * @param storeResult
 47 |    * @param {Object} options
 48 |    * @private
 49 |    */
 50 |   _afterConsume(resolve, reject, rlKey, changedPoints, storeResult, options = {}) {
 51 |     const res = this._getRateLimiterRes(rlKey, changedPoints, storeResult);
 52 | 
 53 |     if (this.inMemoryBlockOnConsumed > 0 && !(this.inMemoryBlockDuration > 0)
 54 |       && res.consumedPoints >= this.inMemoryBlockOnConsumed
 55 |     ) {
 56 |       this._inMemoryBlockedKeys.addMs(rlKey, res.msBeforeNext);
 57 |       if (res.consumedPoints > this.points) {
 58 |         return reject(res);
 59 |       } else {
 60 |         return resolve(res)
 61 |       }
 62 |     } else if (res.consumedPoints > this.points) {
 63 |       let blockPromise = Promise.resolve();
 64 |       // Block only first time when consumed more than points
 65 |       if (this.blockDuration > 0 && res.consumedPoints <= (this.points + changedPoints)) {
 66 |         res.msBeforeNext = this.msBlockDuration;
 67 |         blockPromise = this._block(rlKey, res.consumedPoints, this.msBlockDuration, options);
 68 |       }
 69 | 
 70 |       if (this.inMemoryBlockOnConsumed > 0 && res.consumedPoints >= this.inMemoryBlockOnConsumed) {
 71 |         // Block key for this.inMemoryBlockDuration seconds
 72 |         this._inMemoryBlockedKeys.add(rlKey, this.inMemoryBlockDuration);
 73 |         res.msBeforeNext = this.msInMemoryBlockDuration;
 74 |       }
 75 | 
 76 |       blockPromise
 77 |         .then(() => {
 78 |           reject(res);
 79 |         })
 80 |         .catch((err) => {
 81 |           reject(err);
 82 |         });
 83 |     } else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) {
 84 |       let delay = Math.ceil(res.msBeforeNext / (res.remainingPoints + 2));
 85 |       if (delay < this.execEvenlyMinDelayMs) {
 86 |         delay = res.consumedPoints * this.execEvenlyMinDelayMs;
 87 |       }
 88 | 
 89 |       setTimeout(resolve, delay, res);
 90 |     } else {
 91 |       resolve(res);
 92 |     }
 93 |   }
 94 | 
 95 |   _handleError(err, funcName, resolve, reject, key, data = false, options = {}) {
 96 |     if (!(this.insuranceLimiter instanceof RateLimiterAbstract)) {
 97 |       reject(err);
 98 |     } else {
 99 |       this.insuranceLimiter[funcName](key, data, options)
100 |         .then((res) => {
101 |           resolve(res);
102 |         })
103 |         .catch((res) => {
104 |           reject(res);
105 |         });
106 |     }
107 |   }
108 | 
109 |   getInMemoryBlockMsBeforeExpire(rlKey) {
110 |     if (this.inMemoryBlockOnConsumed > 0) {
111 |       return this._inMemoryBlockedKeys.msBeforeExpire(rlKey);
112 |     }
113 | 
114 |     return 0;
115 |   }
116 | 
117 |   get inMemoryBlockOnConsumed() {
118 |     return this._inMemoryBlockOnConsumed;
119 |   }
120 | 
121 |   set inMemoryBlockOnConsumed(value) {
122 |     this._inMemoryBlockOnConsumed = value ? parseInt(value) : 0;
123 |     if (this.inMemoryBlockOnConsumed > 0 && this.points > this.inMemoryBlockOnConsumed) {
124 |       throw new Error('inMemoryBlockOnConsumed option must be greater or equal "points" option');
125 |     }
126 |   }
127 | 
128 |   get inMemoryBlockDuration() {
129 |     return this._inMemoryBlockDuration;
130 |   }
131 | 
132 |   set inMemoryBlockDuration(value) {
133 |     this._inMemoryBlockDuration = value ? parseInt(value) : 0;
134 |     if (this.inMemoryBlockDuration > 0 && this.inMemoryBlockOnConsumed === 0) {
135 |       throw new Error('inMemoryBlockOnConsumed option must be set up');
136 |     }
137 |   }
138 | 
139 |   get msInMemoryBlockDuration() {
140 |     return this._inMemoryBlockDuration * 1000;
141 |   }
142 | 
143 |   get insuranceLimiter() {
144 |     return this._insuranceLimiter;
145 |   }
146 | 
147 |   set insuranceLimiter(value) {
148 |     if (typeof value !== 'undefined' && !(value instanceof RateLimiterAbstract)) {
149 |       throw new Error('insuranceLimiter must be instance of RateLimiterAbstract');
150 |     }
151 |     this._insuranceLimiter = value;
152 |     if (this._insuranceLimiter) {
153 |       this._insuranceLimiter.blockDuration = this.blockDuration;
154 |       this._insuranceLimiter.execEvenly = this.execEvenly;
155 |     }
156 |   }
157 | 
158 |   /**
159 |    * Block any key for secDuration seconds
160 |    *
161 |    * @param key
162 |    * @param secDuration
163 |    * @param {Object} options
164 |    *
165 |    * @return Promise<RateLimiterRes>
166 |    */
167 |   block(key, secDuration, options = {}) {
168 |     const msDuration = secDuration * 1000;
169 |     return this._block(this.getKey(key), this.points + 1, msDuration, options);
170 |   }
171 | 
172 |   /**
173 |    * Set points by key for any duration
174 |    *
175 |    * @param key
176 |    * @param points
177 |    * @param secDuration
178 |    * @param {Object} options
179 |    *
180 |    * @return Promise<RateLimiterRes>
181 |    */
182 |   set(key, points, secDuration, options = {}) {
183 |     const msDuration = (secDuration >= 0 ? secDuration : this.duration) * 1000;
184 |     return this._block(this.getKey(key), points, msDuration, options);
185 |   }
186 | 
187 |   /**
188 |    *
189 |    * @param key
190 |    * @param pointsToConsume
191 |    * @param {Object} options
192 |    * @returns Promise<RateLimiterRes>
193 |    */
194 |   consume(key, pointsToConsume = 1, options = {}) {
195 |     return new Promise((resolve, reject) => {
196 |       const rlKey = this.getKey(key);
197 | 
198 |       const inMemoryBlockMsBeforeExpire = this.getInMemoryBlockMsBeforeExpire(rlKey);
199 |       if (inMemoryBlockMsBeforeExpire > 0) {
200 |         return reject(new RateLimiterRes(0, inMemoryBlockMsBeforeExpire));
201 |       }
202 | 
203 |       this._upsert(rlKey, pointsToConsume, this._getKeySecDuration(options) * 1000, false, options)
204 |         .then((res) => {
205 |           this._afterConsume(resolve, reject, rlKey, pointsToConsume, res);
206 |         })
207 |         .catch((err) => {
208 |           this._handleError(err, 'consume', resolve, reject, key, pointsToConsume, options);
209 |         });
210 |     });
211 |   }
212 | 
213 |   /**
214 |    *
215 |    * @param key
216 |    * @param points
217 |    * @param {Object} options
218 |    * @returns Promise<RateLimiterRes>
219 |    */
220 |   penalty(key, points = 1, options = {}) {
221 |     const rlKey = this.getKey(key);
222 |     return new Promise((resolve, reject) => {
223 |       this._upsert(rlKey, points, this._getKeySecDuration(options) * 1000, false, options)
224 |         .then((res) => {
225 |           resolve(this._getRateLimiterRes(rlKey, points, res));
226 |         })
227 |         .catch((err) => {
228 |           this._handleError(err, 'penalty', resolve, reject, key, points, options);
229 |         });
230 |     });
231 |   }
232 | 
233 |   /**
234 |    *
235 |    * @param key
236 |    * @param points
237 |    * @param {Object} options
238 |    * @returns Promise<RateLimiterRes>
239 |    */
240 |   reward(key, points = 1, options = {}) {
241 |     const rlKey = this.getKey(key);
242 |     return new Promise((resolve, reject) => {
243 |       this._upsert(rlKey, -points, this._getKeySecDuration(options) * 1000, false, options)
244 |         .then((res) => {
245 |           resolve(this._getRateLimiterRes(rlKey, -points, res));
246 |         })
247 |         .catch((err) => {
248 |           this._handleError(err, 'reward', resolve, reject, key, points, options);
249 |         });
250 |     });
251 |   }
252 | 
253 |   /**
254 |    *
255 |    * @param key
256 |    * @param {Object} options
257 |    * @returns Promise<RateLimiterRes>|null
258 |    */
259 |   get(key, options = {}) {
260 |     const rlKey = this.getKey(key);
261 |     return new Promise((resolve, reject) => {
262 |       this._get(rlKey, options)
263 |         .then((res) => {
264 |           if (res === null || typeof res === 'undefined') {
265 |             resolve(null);
266 |           } else {
267 |             resolve(this._getRateLimiterRes(rlKey, 0, res));
268 |           }
269 |         })
270 |         .catch((err) => {
271 |           this._handleError(err, 'get', resolve, reject, key, options);
272 |         });
273 |     });
274 |   }
275 | 
276 |   /**
277 |    *
278 |    * @param key
279 |    * @param {Object} options
280 |    * @returns Promise<boolean>
281 |    */
282 |   delete(key, options = {}) {
283 |     const rlKey = this.getKey(key);
284 |     return new Promise((resolve, reject) => {
285 |       this._delete(rlKey, options)
286 |         .then((res) => {
287 |           this._inMemoryBlockedKeys.delete(rlKey);
288 |           resolve(res);
289 |         })
290 |         .catch((err) => {
291 |           this._handleError(err, 'delete', resolve, reject, key, options);
292 |         });
293 |     });
294 |   }
295 | 
296 |   /**
297 |    * Cleanup keys no-matter expired or not.
298 |    */
299 |   deleteInMemoryBlockedAll() {
300 |     this._inMemoryBlockedKeys.delete();
301 |   }
302 | 
303 |   /**
304 |    * Get RateLimiterRes object filled depending on storeResult, which specific for exact store
305 |    *
306 |    * @param rlKey
307 |    * @param changedPoints
308 |    * @param storeResult
309 |    * @private
310 |    */
311 |   _getRateLimiterRes(rlKey, changedPoints, storeResult) { // eslint-disable-line no-unused-vars
312 |     throw new Error("You have to implement the method '_getRateLimiterRes'!");
313 |   }
314 | 
315 |   /**
316 |    * Block key for this.msBlockDuration milliseconds
317 |    * Usually, it just prolongs lifetime of key
318 |    *
319 |    * @param rlKey
320 |    * @param initPoints
321 |    * @param msDuration
322 |    * @param {Object} options
323 |    *
324 |    * @return Promise<any>
325 |    */
326 |   _block(rlKey, initPoints, msDuration, options = {}) {
327 |     return new Promise((resolve, reject) => {
328 |       this._upsert(rlKey, initPoints, msDuration, true, options)
329 |         .then(() => {
330 |           resolve(new RateLimiterRes(0, msDuration > 0 ? msDuration : -1, initPoints));
331 |         })
332 |         .catch((err) => {
333 |           this._handleError(err, 'block', resolve, reject, this.parseKey(rlKey), msDuration / 1000, options);
334 |         });
335 |     });
336 |   }
337 | 
338 |   /**
339 |    * Have to be implemented in every limiter
340 |    * Resolve with raw result from Store OR null if rlKey is not set
341 |    * or Reject with error
342 |    *
343 |    * @param rlKey
344 |    * @param {Object} options
345 |    * @private
346 |    *
347 |    * @return Promise<any>
348 |    */
349 |   _get(rlKey, options = {}) { // eslint-disable-line no-unused-vars
350 |     throw new Error("You have to implement the method '_get'!");
351 |   }
352 | 
353 |   /**
354 |    * Have to be implemented
355 |    * Resolve with true OR false if rlKey doesn't exist
356 |    * or Reject with error
357 |    *
358 |    * @param rlKey
359 |    * @param {Object} options
360 |    * @private
361 |    *
362 |    * @return Promise<any>
363 |    */
364 |   _delete(rlKey, options = {}) { // eslint-disable-line no-unused-vars
365 |     throw new Error("You have to implement the method '_delete'!");
366 |   }
367 | 
368 |   /**
369 |    * Have to be implemented
370 |    * Resolve with object used for {@link _getRateLimiterRes} to generate {@link RateLimiterRes}
371 |    *
372 |    * @param {string} rlKey
373 |    * @param {number} points
374 |    * @param {number} msDuration
375 |    * @param {boolean} forceExpire
376 |    * @param {Object} options
377 |    * @abstract
378 |    *
379 |    * @return Promise<Object>
380 |    */
381 |   _upsert(rlKey, points, msDuration, forceExpire = false, options = {}) {
382 |     throw new Error("You have to implement the method '_upsert'!");
383 |   }
384 | };
385 | 


--------------------------------------------------------------------------------
/lib/RateLimiterUnion.js:
--------------------------------------------------------------------------------
 1 | const RateLimiterAbstract = require('./RateLimiterAbstract');
 2 | 
 3 | module.exports = class RateLimiterUnion {
 4 |   constructor(...limiters) {
 5 |     if (limiters.length < 1) {
 6 |       throw new Error('RateLimiterUnion: at least one limiter have to be passed');
 7 |     }
 8 |     limiters.forEach((limiter) => {
 9 |       if (!(limiter instanceof RateLimiterAbstract)) {
10 |         throw new Error('RateLimiterUnion: all limiters have to be instance of RateLimiterAbstract');
11 |       }
12 |     });
13 | 
14 |     this._limiters = limiters;
15 |   }
16 | 
17 |   consume(key, points = 1) {
18 |     return new Promise((resolve, reject) => {
19 |       const promises = [];
20 |       this._limiters.forEach((limiter) => {
21 |         promises.push(limiter.consume(key, points).catch(rej => ({ rejected: true, rej })));
22 |       });
23 | 
24 |       Promise.all(promises)
25 |         .then((res) => {
26 |           const resObj = {};
27 |           let rejected = false;
28 | 
29 |           res.forEach((item) => {
30 |             if (item.rejected === true) {
31 |               rejected = true;
32 |             }
33 |           });
34 | 
35 |           for (let i = 0; i < res.length; i++) {
36 |             if (rejected && res[i].rejected === true) {
37 |               resObj[this._limiters[i].keyPrefix] = res[i].rej;
38 |             } else if (!rejected) {
39 |               resObj[this._limiters[i].keyPrefix] = res[i];
40 |             }
41 |           }
42 | 
43 |           if (rejected) {
44 |             reject(resObj);
45 |           } else {
46 |             resolve(resObj);
47 |           }
48 |         });
49 |     });
50 |   }
51 | };
52 | 


--------------------------------------------------------------------------------
/lib/RateLimiterValkey.js:
--------------------------------------------------------------------------------
  1 | const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
  2 | const RateLimiterRes = require('./RateLimiterRes');
  3 | 
  4 | const incrTtlLuaScript = `
  5 | server.call('set', KEYS[1], 0, 'EX', ARGV[2], 'NX')
  6 | local consumed = server.call('incrby', KEYS[1], ARGV[1])
  7 | local ttl = server.call('pttl', KEYS[1])
  8 | return {consumed, ttl}
  9 | `;
 10 | 
 11 | class RateLimiterValkey extends RateLimiterStoreAbstract {
 12 |   /**
 13 |    *
 14 |    * @param {Object} opts
 15 |    * Defaults {
 16 |    *   ... see other in RateLimiterStoreAbstract
 17 |    *
 18 |    *   storeClient: ValkeyClient
 19 |    *   rejectIfValkeyNotReady: boolean = false - reject / invoke insuranceLimiter immediately when valkey connection is not "ready"
 20 |    * }
 21 |    */
 22 |   constructor(opts) {
 23 |     super(opts);
 24 |     this.client = opts.storeClient;
 25 | 
 26 |     this._rejectIfValkeyNotReady = !!opts.rejectIfValkeyNotReady;
 27 |     this._incrTtlLuaScript = opts.customIncrTtlLuaScript || incrTtlLuaScript;
 28 | 
 29 |     this.client.defineCommand('rlflxIncr', {
 30 |       numberOfKeys: 1,
 31 |       lua: this._incrTtlLuaScript,
 32 |     });
 33 |   }
 34 | 
 35 |   /**
 36 |    * Prevent actual valkey call if valkey connection is not ready
 37 |    * @return {boolean}
 38 |    * @private
 39 |    */
 40 |   _isValkeyReady() {
 41 |     if (!this._rejectIfValkeyNotReady) {
 42 |       return true;
 43 |     }
 44 | 
 45 |     return this.client.status === 'ready';
 46 |   }
 47 | 
 48 |   _getRateLimiterRes(rlKey, changedPoints, result) {
 49 |     let consumed;
 50 |     let resTtlMs;
 51 | 
 52 |     if (Array.isArray(result[0])) {
 53 |       [[, consumed], [, resTtlMs]] = result;
 54 |     } else {
 55 |       [consumed, resTtlMs] = result;
 56 |     }
 57 | 
 58 |     const res = new RateLimiterRes();
 59 |     res.consumedPoints = +consumed;
 60 |     res.isFirstInDuration = res.consumedPoints === changedPoints;
 61 |     res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
 62 |     res.msBeforeNext = resTtlMs;
 63 | 
 64 |     return res;
 65 |   }
 66 | 
 67 |   _upsert(rlKey, points, msDuration, forceExpire = false) {
 68 |     if (!this._isValkeyReady()) {
 69 |       throw new Error('Valkey connection is not ready');
 70 |     }
 71 | 
 72 |     const secDuration = Math.floor(msDuration / 1000);
 73 | 
 74 |     if (forceExpire) {
 75 |       const multi = this.client.multi();
 76 | 
 77 |       if (secDuration > 0) {
 78 |         multi.set(rlKey, points, 'EX', secDuration);
 79 |       } else {
 80 |         multi.set(rlKey, points);
 81 |       }
 82 | 
 83 |       return multi.pttl(rlKey).exec();
 84 |     }
 85 | 
 86 |     if (secDuration > 0) {
 87 |       return this.client.rlflxIncr([rlKey, String(points), String(secDuration), String(this.points), String(this.duration)]);
 88 |     }
 89 | 
 90 |     return this.client.multi().incrby(rlKey, points).pttl(rlKey).exec();
 91 |   }
 92 | 
 93 |   _get(rlKey) {
 94 |     if (!this._isValkeyReady()) {
 95 |       throw new Error('Valkey connection is not ready');
 96 |     }
 97 | 
 98 |     return this.client
 99 |       .multi()
100 |       .get(rlKey)
101 |       .pttl(rlKey)
102 |       .exec()
103 |       .then((result) => {
104 |         const [[, points]] = result;
105 |         if (points === null) return null;
106 |         return result;
107 |       });
108 |   }
109 | 
110 |   _delete(rlKey) {
111 |     return this.client
112 |       .del(rlKey)
113 |       .then(result => result > 0);
114 |   }
115 | }
116 | 
117 | module.exports = RateLimiterValkey;
118 | 


--------------------------------------------------------------------------------
/lib/RateLimiterValkeyGlide.js:
--------------------------------------------------------------------------------
  1 | /* eslint-disable no-unused-vars */
  2 | const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
  3 | const RateLimiterRes = require('./RateLimiterRes');
  4 | 
  5 | /**
  6 |  * @typedef {import('@valkey/valkey-glide').GlideClient} GlideClient
  7 |  * @typedef {import('@valkey/valkey-glide').GlideClusterClient} GlideClusterClient
  8 |  */
  9 | 
 10 | const DEFAULT_LIBRARY_NAME = 'ratelimiterflexible';
 11 | 
 12 | const DEFAULT_VALKEY_SCRIPT = `local key = KEYS[1]
 13 | local pointsToConsume = tonumber(ARGV[1])
 14 | if tonumber(ARGV[2]) > 0 then
 15 |   server.call('set', key, "0", 'EX', ARGV[2], 'NX')
 16 |   local consumed = server.call('incrby', key, pointsToConsume)
 17 |   local pttl = server.call('pttl', key)
 18 |   return {consumed, pttl}
 19 | end
 20 | local consumed = server.call('incrby', key, pointsToConsume)
 21 | local pttl = server.call('pttl', key)
 22 | return {consumed, pttl}`;
 23 | 
 24 | const GET_VALKEY_SCRIPT = `local key = KEYS[1]
 25 | local value = server.call('get', key)
 26 | if value == nil then
 27 |   return value
 28 | end
 29 | local pttl = server.call('pttl', key)
 30 | return {tonumber(value), pttl}`;
 31 | 
 32 | class RateLimiterValkeyGlide extends RateLimiterStoreAbstract {
 33 |   /**
 34 |    * Constructor for RateLimiterValkeyGlide
 35 |    *
 36 |    * @param {Object} opts - Configuration options
 37 |    * @param {GlideClient|GlideClusterClient} opts.storeClient - Valkey Glide client instance (required)
 38 |    * @param {number} [opts.points=4] - Maximum number of points that can be consumed over duration
 39 |    * @param {number} [opts.duration=1] - Duration in seconds before points are reset
 40 |    * @param {number} [opts.blockDuration=0] - Duration in seconds that a key will be blocked for if consumed more than points
 41 |    * @param {boolean} [opts.rejectIfValkeyNotReady=false] - Whether to reject requests if Valkey is not ready
 42 |    * @param {boolean} [opts.execEvenly=false] - Delay actions to distribute them evenly over duration
 43 |    * @param {number} [opts.execEvenlyMinDelayMs] - Minimum delay between actions when execEvenly is true
 44 |    * @param {string} [opts.customFunction] - Custom Lua script for rate limiting logic
 45 |    * @param {number} [opts.inMemoryBlockOnConsumed] - Points threshold for in-memory blocking
 46 |    * @param {number} [opts.inMemoryBlockDuration] - Duration in seconds for in-memory blocking
 47 |    * @param {string} [opts.customFunctionLibName] - Custom name for the function library, defaults to 'ratelimiter'.
 48 |    * The name is used to identify the library of the lua function. An custom name should be used only if you
 49 |    * you want to use different libraries for different rate limiters, otherwise it is not needed.
 50 |    * @param {RateLimiterAbstract} [opts.insuranceLimiter] - Backup limiter to use when the primary client fails
 51 |    *
 52 |    * @example
 53 |    * const rateLimiter = new RateLimiterValkeyGlide({
 54 |    *   storeClient: glideClient,
 55 |    *   points: 5,
 56 |    *   duration: 1
 57 |    * });
 58 |    *
 59 |    * @example <caption>With custom Lua function</caption>
 60 |    * const customScript = `local key = KEYS[1]
 61 |    * local pointsToConsume = tonumber(ARGV[1]) or 0
 62 |    * local secDuration = tonumber(ARGV[2]) or 0
 63 |    *
 64 |    * -- Custom implementation
 65 |    * -- ...
 66 |    *
 67 |    * -- Must return exactly two values: [consumed_points, ttl_in_ms]
 68 |    * return {consumed, ttl}`
 69 |    *
 70 |    * const rateLimiter = new RateLimiterValkeyGlide({
 71 |    *   storeClient: glideClient,
 72 |    *   points: 5,
 73 |    *   customFunction: customScript
 74 |    * });
 75 |    *
 76 |    * @example <caption>With insurance limiter</caption>
 77 |    * const rateLimiter = new RateLimiterValkeyGlide({
 78 |    *   storeClient: primaryGlideClient,
 79 |    *   points: 5,
 80 |    *   duration: 2,
 81 |    *   insuranceLimiter: new RateLimiterMemory({
 82 |    *     points: 5,
 83 |    *     duration: 2
 84 |    *   })
 85 |    * });
 86 |    *
 87 |    * @description
 88 |    * When providing a custom Lua script via `opts.customFunction`, it must:
 89 |    *
 90 |    * 1. Accept parameters:
 91 |    *    - KEYS[1]: The key being rate limited
 92 |    *    - ARGV[1]: Points to consume (as string, use tonumber() to convert)
 93 |    *    - ARGV[2]: Duration in seconds (as string, use tonumber() to convert)
 94 |    *
 95 |    * 2. Return an array with exactly two elements:
 96 |    *    - [0]: Consumed points (number)
 97 |    *    - [1]: TTL in milliseconds (number)
 98 |    *
 99 |    * 3. Handle scenarios:
100 |    *    - New key creation: Initialize with expiry for fixed windows
101 |    *    - Key updates: Increment existing counters
102 |    */
103 |   constructor(opts) {
104 |     super(opts);
105 |     this.client = opts.storeClient;
106 |     this._scriptLoaded = false;
107 |     this._getScriptLoaded = false;
108 |     this._rejectIfValkeyNotReady = !!opts.rejectIfValkeyNotReady;
109 |     this._luaScript = opts.customFunction || DEFAULT_VALKEY_SCRIPT;
110 |     this._libraryName = opts.customFunctionLibName || DEFAULT_LIBRARY_NAME;
111 |   }
112 | 
113 |   /**
114 |    * Ensure scripts are loaded in the Valkey server
115 |    * @returns {Promise<boolean>} True if scripts are loaded
116 |    * @private
117 |    */
118 |   async _loadScripts() {
119 |     if (this._scriptLoaded && this._getScriptLoaded) {
120 |       return true;
121 |     }
122 |     if (!this.client) {
123 |       throw new Error('Valkey client is not set');
124 |     }
125 |     const promises = [];
126 |     if (!this._scriptLoaded) {
127 |       const script = Buffer.from(`#!lua name=${this._libraryName}
128 |         local function consume(KEYS, ARGV)
129 |           ${this._luaScript.trim()}
130 |         end
131 |         server.register_function('consume', consume)`);
132 |       promises.push(this.client.functionLoad(script, { replace: true }));
133 |     } else promises.push(Promise.resolve(this._libraryName));
134 | 
135 |     if (!this._getScriptLoaded) {
136 |       const script = Buffer.from(`#!lua name=ratelimiter_get
137 |         local function getValue(KEYS, ARGV)
138 |           ${GET_VALKEY_SCRIPT.trim()}
139 |         end
140 |         server.register_function('getValue', getValue)`);
141 |       promises.push(this.client.functionLoad(script, { replace: true }));
142 |     } else promises.push(Promise.resolve('ratelimiter_get'));
143 | 
144 |     const results = await Promise.all(promises);
145 |     this._scriptLoaded = results[0] === this._libraryName;
146 |     this._getScriptLoaded = results[1] === 'ratelimiter_get';
147 | 
148 |     if ((!this._scriptLoaded || !this._getScriptLoaded)) {
149 |       throw new Error('Valkey connection is not ready, scripts not loaded');
150 |     }
151 |     return true;
152 |   }
153 | 
154 |   /**
155 |    * Update or insert the rate limiter record
156 |    *
157 |    * @param {string} rlKey - The rate limiter key
158 |    * @param {number} pointsToConsume - Points to be consumed
159 |    * @param {number} msDuration - Duration in milliseconds
160 |    * @param {boolean} [forceExpire=false] - Whether to force expiration
161 |    * @param {Object} [options={}] - Additional options
162 |    * @returns {Promise<Array>} Array containing consumed points and TTL
163 |    * @private
164 |    */
165 |   async _upsert(rlKey, pointsToConsume, msDuration, forceExpire = false, options = {}) {
166 |     await this._loadScripts();
167 |     const secDuration = Math.floor(msDuration / 1000);
168 |     if (forceExpire) {
169 |       if (secDuration > 0) {
170 |         await this.client.set(
171 |           rlKey,
172 |           String(pointsToConsume),
173 |           { expiry: { type: 'EX', count: secDuration } },
174 |         );
175 |         return [pointsToConsume, secDuration * 1000];
176 |       }
177 |       await this.client.set(rlKey, String(pointsToConsume));
178 |       return [pointsToConsume, -1];
179 |     }
180 |     const result = await this.client.fcall(
181 |       'consume',
182 |       [rlKey],
183 |       [String(pointsToConsume), String(secDuration)],
184 |     );
185 |     return result;
186 |   }
187 | 
188 |   /**
189 |    * Get the rate limiter record
190 |    *
191 |    * @param {string} rlKey - The rate limiter key
192 |    * @param {Object} [options={}] - Additional options
193 |    * @returns {Promise<Array|null>} Array containing consumed points and TTL, or null if not found
194 |    * @private
195 |    */
196 |   async _get(rlKey, options = {}) {
197 |     await this._loadScripts();
198 |     const res = await this.client.fcall('getValue', [rlKey], []);
199 |     return res.length > 0 ? res : null;
200 |   }
201 | 
202 |   /**
203 |    * Delete the rate limiter record
204 |    *
205 |    * @param {string} rlKey - The rate limiter key
206 |    * @param {Object} [options={}] - Additional options
207 |    * @returns {Promise<boolean>} True if successful, false otherwise
208 |    * @private
209 |    */
210 |   async _delete(rlKey, options = {}) {
211 |     const result = await this.client.del([rlKey]);
212 |     return result > 0;
213 |   }
214 | 
215 |   /**
216 |    * Convert raw result to RateLimiterRes object
217 |    *
218 |    * @param {string} rlKey - The rate limiter key
219 |    * @param {number} changedPoints - Points changed in this operation
220 |    * @param {Array|null} result - Result from Valkey operation
221 |    * @returns {RateLimiterRes|null} RateLimiterRes object or null if result is null
222 |    * @private
223 |    */
224 |   _getRateLimiterRes(rlKey, changedPoints, result) {
225 |     if (result === null) {
226 |       return null;
227 |     }
228 |     const res = new RateLimiterRes();
229 |     const [consumedPointsStr, pttl] = result;
230 |     const consumedPoints = Number(consumedPointsStr);
231 | 
232 |     // Handle consumed points
233 |     res.isFirstInDuration = consumedPoints === changedPoints;
234 |     res.consumedPoints = consumedPoints;
235 |     res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
236 |     res.msBeforeNext = pttl;
237 |     return res;
238 |   }
239 | 
240 |   /**
241 |    * Close the rate limiter and release resources
242 |    * Note: The method won't going to close the Valkey client, as it may be shared with other instances.
243 |    * @returns {Promise<void>} Promise that resolves when the rate limiter is closed
244 |    */
245 |   async close() {
246 |     if (this._scriptLoaded) {
247 |       await this.client.functionDelete(this._libraryName);
248 |       this._scriptLoaded = false;
249 |     }
250 |     if (this._getScriptLoaded) {
251 |       await this.client.functionDelete('ratelimiter_get');
252 |       this._getScriptLoaded = false;
253 |     }
254 |     if (this.insuranceLimiter) {
255 |       try {
256 |         await this.insuranceLimiter.close();
257 |       } catch (e) {
258 |         // We can't assume that insuranceLimiter is a Valkey client or any
259 |         // other insuranceLimiter type which implement close method.
260 |       }
261 |     }
262 |     // Clear instance properties to let garbage collector free memory
263 |     this.client = null;
264 |     this._scriptLoaded = false;
265 |     this._getScriptLoaded = false;
266 |     this._rejectIfValkeyNotReady = false;
267 |     this._luaScript = null;
268 |     this._libraryName = null;
269 |     this.insuranceLimiter = null;
270 |   }
271 | }
272 | 
273 | module.exports = RateLimiterValkeyGlide;
274 | 


--------------------------------------------------------------------------------
/lib/component/BlockedKeys/BlockedKeys.js:
--------------------------------------------------------------------------------
 1 | module.exports = class BlockedKeys {
 2 |   constructor() {
 3 |     this._keys = {}; // {'key': 1526279430331}
 4 |     this._addedKeysAmount = 0;
 5 |   }
 6 | 
 7 |   collectExpired() {
 8 |     const now = Date.now();
 9 | 
10 |     Object.keys(this._keys).forEach((key) => {
11 |       if (this._keys[key] <= now) {
12 |         delete this._keys[key];
13 |       }
14 |     });
15 | 
16 |     this._addedKeysAmount = Object.keys(this._keys).length;
17 |   }
18 | 
19 |   /**
20 |    * Add new blocked key
21 |    *
22 |    * @param key String
23 |    * @param sec Number
24 |    */
25 |   add(key, sec) {
26 |     this.addMs(key, sec * 1000);
27 |   }
28 | 
29 |   /**
30 |    * Add new blocked key for ms
31 |    *
32 |    * @param key String
33 |    * @param ms Number
34 |    */
35 |   addMs(key, ms) {
36 |     this._keys[key] = Date.now() + ms;
37 |     this._addedKeysAmount++;
38 |     if (this._addedKeysAmount > 999) {
39 |       this.collectExpired();
40 |     }
41 |   }
42 | 
43 |   /**
44 |    * 0 means not blocked
45 |    *
46 |    * @param key
47 |    * @returns {number}
48 |    */
49 |   msBeforeExpire(key) {
50 |     const expire = this._keys[key];
51 | 
52 |     if (expire && expire >= Date.now()) {
53 |       this.collectExpired();
54 |       const now = Date.now();
55 |       return expire >= now ? expire - now : 0;
56 |     }
57 | 
58 |     return 0;
59 |   }
60 | 
61 |   /**
62 |    * If key is not given, delete all data in memory
63 |    * 
64 |    * @param {string|undefined} key
65 |    */
66 |   delete(key) {
67 |     if (key) {
68 |       delete this._keys[key];
69 |     } else {
70 |       Object.keys(this._keys).forEach((key) => {
71 |         delete this._keys[key];
72 |       });
73 |     }
74 |   }
75 | };
76 | 


--------------------------------------------------------------------------------
/lib/component/BlockedKeys/index.js:
--------------------------------------------------------------------------------
1 | const BlockedKeys = require('./BlockedKeys');
2 | 
3 | module.exports = BlockedKeys;
4 | 


--------------------------------------------------------------------------------
/lib/component/MemoryStorage/MemoryStorage.js:
--------------------------------------------------------------------------------
 1 | const Record = require('./Record');
 2 | const RateLimiterRes = require('../../RateLimiterRes');
 3 | 
 4 | module.exports = class MemoryStorage {
 5 |   constructor() {
 6 |     /**
 7 |      * @type {Object.<string, Record>}
 8 |      * @private
 9 |      */
10 |     this._storage = {};
11 |   }
12 | 
13 |   incrby(key, value, durationSec) {
14 |     if (this._storage[key]) {
15 |       const msBeforeExpires = this._storage[key].expiresAt
16 |         ? this._storage[key].expiresAt.getTime() - new Date().getTime()
17 |         : -1;
18 |       if (!this._storage[key].expiresAt || msBeforeExpires > 0) {
19 |         // Change value
20 |         this._storage[key].value = this._storage[key].value + value;
21 | 
22 |         return new RateLimiterRes(0, msBeforeExpires, this._storage[key].value, false);
23 |       }
24 | 
25 |       return this.set(key, value, durationSec);
26 |     }
27 |     return this.set(key, value, durationSec);
28 |   }
29 | 
30 |   set(key, value, durationSec) {
31 |     const durationMs = durationSec * 1000;
32 | 
33 |     if (this._storage[key] && this._storage[key].timeoutId) {
34 |       clearTimeout(this._storage[key].timeoutId);
35 |     }
36 | 
37 |     this._storage[key] = new Record(
38 |       value,
39 |       durationMs > 0 ? new Date(Date.now() + durationMs) : null
40 |     );
41 |     if (durationMs > 0) {
42 |       this._storage[key].timeoutId = setTimeout(() => {
43 |         delete this._storage[key];
44 |       }, durationMs);
45 |       if (this._storage[key].timeoutId.unref) {
46 |         this._storage[key].timeoutId.unref();
47 |       }
48 |     }
49 | 
50 |     return new RateLimiterRes(0, durationMs === 0 ? -1 : durationMs, this._storage[key].value, true);
51 |   }
52 | 
53 |   /**
54 |    *
55 |    * @param key
56 |    * @returns {*}
57 |    */
58 |   get(key) {
59 |     if (this._storage[key]) {
60 |       const msBeforeExpires = this._storage[key].expiresAt
61 |         ? this._storage[key].expiresAt.getTime() - new Date().getTime()
62 |         : -1;
63 |       return new RateLimiterRes(0, msBeforeExpires, this._storage[key].value, false);
64 |     }
65 |     return null;
66 |   }
67 | 
68 |   /**
69 |    *
70 |    * @param key
71 |    * @returns {boolean}
72 |    */
73 |   delete(key) {
74 |     if (this._storage[key]) {
75 |       if (this._storage[key].timeoutId) {
76 |         clearTimeout(this._storage[key].timeoutId);
77 |       }
78 |       delete this._storage[key];
79 |       return true;
80 |     }
81 |     return false;
82 |   }
83 | };
84 | 


--------------------------------------------------------------------------------
/lib/component/MemoryStorage/Record.js:
--------------------------------------------------------------------------------
 1 | module.exports = class Record {
 2 |   /**
 3 |    *
 4 |    * @param value int
 5 |    * @param expiresAt Date|int
 6 |    * @param timeoutId
 7 |    */
 8 |   constructor(value, expiresAt, timeoutId = null) {
 9 |     this.value = value;
10 |     this.expiresAt = expiresAt;
11 |     this.timeoutId = timeoutId;
12 |   }
13 | 
14 |   get value() {
15 |     return this._value;
16 |   }
17 | 
18 |   set value(value) {
19 |     this._value = parseInt(value);
20 |   }
21 | 
22 |   get expiresAt() {
23 |     return this._expiresAt;
24 |   }
25 | 
26 |   set expiresAt(value) {
27 |     if (!(value instanceof Date) && Number.isInteger(value)) {
28 |       value = new Date(value);
29 |     }
30 |     this._expiresAt = value;
31 |   }
32 | 
33 |   get timeoutId() {
34 |     return this._timeoutId;
35 |   }
36 | 
37 |   set timeoutId(value) {
38 |     this._timeoutId = value;
39 |   }
40 | };
41 | 


--------------------------------------------------------------------------------
/lib/component/MemoryStorage/index.js:
--------------------------------------------------------------------------------
1 | const MemoryStorage = require('./MemoryStorage');
2 | 
3 | module.exports = MemoryStorage;
4 | 


--------------------------------------------------------------------------------
/lib/component/RateLimiterEtcdTransactionFailedError.js:
--------------------------------------------------------------------------------
 1 | module.exports = class RateLimiterEtcdTransactionFailedError extends Error {
 2 |   constructor(message) {
 3 |     super();
 4 |     if (Error.captureStackTrace) {
 5 |       Error.captureStackTrace(this, this.constructor);
 6 |     }
 7 |     this.name = 'RateLimiterEtcdTransactionFailedError';
 8 |     this.message = message;
 9 |   }
10 | };
11 | 


--------------------------------------------------------------------------------
/lib/component/RateLimiterQueueError.js:
--------------------------------------------------------------------------------
 1 | module.exports = class RateLimiterQueueError extends Error {
 2 |   constructor(message, extra) {
 3 |     super();
 4 |     if (Error.captureStackTrace) {
 5 |       Error.captureStackTrace(this, this.constructor);
 6 |     }
 7 |     this.name = 'CustomError';
 8 |     this.message = message;
 9 |     if (extra) {
10 |       this.extra = extra;
11 |     }
12 |   }
13 | };
14 | 


--------------------------------------------------------------------------------
/lib/component/RateLimiterSetupError.js:
--------------------------------------------------------------------------------
 1 | module.exports = class RateLimiterSetupError extends Error {
 2 |   constructor(message) {
 3 |     super();
 4 |     if (Error.captureStackTrace) {
 5 |       Error.captureStackTrace(this, this.constructor);
 6 |     }
 7 |     this.name = 'RateLimiterSetupError';
 8 |     this.message = message;
 9 |   }
10 | };
11 | 


--------------------------------------------------------------------------------
/lib/component/index.d.ts:
--------------------------------------------------------------------------------
 1 | export class RateLimiterQueueError extends Error {
 2 | 
 3 |   constructor(message?: string, extra?: string);
 4 | 
 5 |   readonly name: string;
 6 |   readonly message: string;
 7 |   readonly extra: string;
 8 | 
 9 | }
10 | 


--------------------------------------------------------------------------------
/lib/constants.js:
--------------------------------------------------------------------------------
 1 | const LIMITER_TYPES = {
 2 |   MEMORY: 'memory',
 3 |   CLUSTER: 'cluster',
 4 |   MEMCACHE: 'memcache',
 5 |   MONGO: 'mongo',
 6 |   REDIS: 'redis',
 7 |   MYSQL: 'mysql',
 8 |   POSTGRES: 'postgres',
 9 |   DYNAMO: 'dynamo',
10 |   PRISMA: 'prisma',
11 |   SQLITE: 'sqlite',
12 |   VALKEY: 'valkey',
13 |   VALKEY_GLIDE: 'valkey-glide',
14 | };
15 | 
16 | const ERR_UNKNOWN_LIMITER_TYPE_MESSAGE = 'Unknown limiter type. Use one of LIMITER_TYPES constants.';
17 | 
18 | module.exports = {
19 |   LIMITER_TYPES,
20 |   ERR_UNKNOWN_LIMITER_TYPE_MESSAGE,
21 | };
22 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "rate-limiter-flexible",
 3 |   "version": "7.1.1",
 4 |   "description": "Node.js rate limiter by key and protection from DDoS and Brute-Force attacks in process Memory, Redis, MongoDb, Memcached, MySQL, PostgreSQL, Cluster or PM",
 5 |   "main": "index.js",
 6 |   "scripts": {
 7 |     "dc:up": "docker-compose -f docker-compose.yml up -d",
 8 |     "dc:down": "docker-compose -f docker-compose.yml down",
 9 |     "valkey-cluster:up": "docker-compose -f docker-compose.valkey-cluster.yml up -d",
10 |     "valkey-cluster:down": "docker-compose -f docker-compose.valkey-cluster.yml down -v",
11 |     "test:valkey-cluster": "VALKEY_CLUSTER_PORT=7001 mocha test/RateLimiterValkeyGlide.test.js --  -g 'RateLimiterValkeyGlide with cluster client'",
12 |     "prisma:postgres": "prisma generate --schema=./test/RateLimiterPrisma/Postgres/schema.prisma && prisma db push --schema=./test/RateLimiterPrisma/Postgres/schema.prisma",
13 |     "test": "npm run prisma:postgres && nyc  --reporter=html --reporter=text mocha",
14 |     "debug-test": "mocha --inspect-brk lib/**/**.test.js",
15 |     "coveralls": "cat ./coverage/lcov.info | coveralls",
16 |     "eslint": "eslint --quiet lib/**/**.js test/**/**.js",
17 |     "eslint-fix": "eslint --fix lib/**/**.js test/**/**.js"
18 |   },
19 |   "repository": {
20 |     "type": "git",
21 |     "url": "git+https://github.com/animir/node-rate-limiter-flexible.git"
22 |   },
23 |   "keywords": [
24 |     "ratelimter",
25 |     "authorization",
26 |     "security",
27 |     "rate",
28 |     "limit",
29 |     "bruteforce",
30 |     "throttle",
31 |     "redis",
32 |     "mongodb",
33 |     "dynamodb",
34 |     "mysql",
35 |     "postgres",
36 |     "prisma",
37 |     "koa",
38 |     "express",
39 |     "hapi",
40 |     "valkey",
41 |     "valkey-glide",
42 |     "GLIDE",
43 |     "cluster",
44 |     "memcached"
45 |   ],
46 |   "author": "animir <animirr@gmail.com>",
47 |   "license": "ISC",
48 |   "bugs": {
49 |     "url": "https://github.com/animir/node-rate-limiter-flexible/issues"
50 |   },
51 |   "homepage": "https://github.com/animir/node-rate-limiter-flexible#readme",
52 |   "types": "./lib/index.d.ts",
53 |   "devDependencies": {
54 |     "@aws-sdk/client-dynamodb": "^3.431.0",
55 |     "@prisma/client": "^5.8.0",
56 |     "better-sqlite3": "^11.9.0",
57 |     "chai": "^4.1.2",
58 |     "coveralls": "^3.0.1",
59 |     "etcd3": "^1.1.2",
60 |     "eslint": "^4.19.1",
61 |     "eslint-config-airbnb-base": "^12.1.0",
62 |     "eslint-plugin-import": "^2.7.0",
63 |     "eslint-plugin-node": "^6.0.1",
64 |     "eslint-plugin-security": "^1.4.0",
65 |     "ioredis": "^5.3.2",
66 |     "iovalkey": "^0.3.1",
67 |     "istanbul": "^1.1.0-alpha.1",
68 |     "knex": "^3.1.0",
69 |     "memcached-mock": "^0.1.0",
70 |     "mocha": "^10.2.0",
71 |     "nyc": "^15.1.0",
72 |     "prisma": "^5.8.0",
73 |     "redis": "^4.6.8",
74 |     "redis-mock": "^0.48.0",
75 |     "sinon": "^17.0.1",
76 |     "sqlite3": "^5.1.7",
77 |     "@valkey/valkey-glide": "^1.3.1"
78 |   },
79 |   "browser": {
80 |     "cluster": false,
81 |     "crypto": false
82 |   }
83 | }
84 | 


--------------------------------------------------------------------------------
/test/BurstyRateLimiter.test.js:
--------------------------------------------------------------------------------
  1 | /* eslint-disable no-unused-expressions */
  2 | const { describe, it } = require('mocha');
  3 | const { expect } = require('chai');
  4 | const RateLimiterMemory = require('../lib/RateLimiterMemory');
  5 | const BurstyRateLimiter = require('../lib/BurstyRateLimiter');
  6 | const RateLimiterRedis = require('../lib/RateLimiterRedis');
  7 | const Redis = require("ioredis");
  8 | 
  9 | describe('BurstyRateLimiter', () => {
 10 |   it('consume 1 point from limiter', (done) => {
 11 |     const testKey = 'consume1';
 12 |     const rlMemory = new RateLimiterMemory({ points: 1, duration: 1 });
 13 |     const blMemory = new RateLimiterMemory({ points: 1, duration: 10 });
 14 |     const bursty = new BurstyRateLimiter(rlMemory, blMemory);
 15 |     bursty.consume(testKey)
 16 |       .then((res) => {
 17 |         expect(res.consumedPoints).to.equal(1);
 18 |         expect(res.remainingPoints).to.equal(0);
 19 |         expect(res.msBeforeNext <= 1000).to.equal(true);
 20 |         expect(res.isFirstInDuration).to.equal(true);
 21 |         done();
 22 |       })
 23 |       .catch((err) => {
 24 |         done(err);
 25 |       });
 26 |   });
 27 | 
 28 |   it('consume 1 point from bursty limiter, if all consumed on limiter', (done) => {
 29 |     const testKey = 'consume1frombursty';
 30 |     const rlMemory = new RateLimiterMemory({ points: 1, duration: 1 });
 31 |     const blMemory = new RateLimiterMemory({ points: 1, duration: 10 });
 32 |     const bursty = new BurstyRateLimiter(rlMemory, blMemory);
 33 |     bursty.consume(testKey)
 34 |       .then(() => {
 35 |         bursty.consume(testKey)
 36 |           .then((res) => {
 37 |             expect(res.consumedPoints).to.equal(2);
 38 |             expect(res.remainingPoints).to.equal(0);
 39 |             expect(res.msBeforeNext <= 1000).to.equal(true);
 40 |             expect(res.isFirstInDuration).to.equal(false);
 41 |             done();
 42 |           })
 43 |           .catch((err) => {
 44 |             done(err);
 45 |           });
 46 |       })
 47 |       .catch((err) => {
 48 |         done(err);
 49 |       });
 50 |   });
 51 | 
 52 |   it('consume 1 point from limiter and 1 from bursty, and then 1 point reject with data from limiter', (done) => {
 53 |     const testKey = 'consume1frombursty';
 54 |     const rlMemory = new RateLimiterMemory({ points: 1, duration: 1 });
 55 |     const blMemory = new RateLimiterMemory({ points: 1, duration: 10 });
 56 |     const bursty = new BurstyRateLimiter(rlMemory, blMemory);
 57 |     bursty.consume(testKey)
 58 |       .then(() => {
 59 |         bursty.consume(testKey)
 60 |           .then(() => {
 61 |             bursty.consume(testKey)
 62 |               .then(() => {
 63 |                 done(new Error('must not'));
 64 |               })
 65 |               .catch((rej) => {
 66 |                 expect(rej.consumedPoints).to.equal(3);
 67 |                 expect(rej.remainingPoints).to.equal(0);
 68 |                 expect(rej.msBeforeNext <= 1000).to.equal(true);
 69 |                 expect(rej.isFirstInDuration).to.equal(false);
 70 |                 done();
 71 |               });
 72 |           })
 73 |           .catch((err) => {
 74 |             done(err);
 75 |           });
 76 |       })
 77 |       .catch((err) => {
 78 |         done(err);
 79 |       });
 80 |   });
 81 | 
 82 |   it('do not consume from burst limiter, if rate limiter consume rejected with error', async() => {
 83 |     const testKey = 'consume-rejected-with-error';
 84 |     const redisMockClient = new Redis();
 85 |     const redisClientClosed = new Redis();
 86 |     await redisClientClosed.disconnect();
 87 |     const rlRedisClosed = new RateLimiterRedis({
 88 |       storeClient: redisClientClosed,
 89 |     });
 90 |     const blRedis = new RateLimiterRedis({
 91 |       storeClient: redisMockClient,
 92 |       keyPrefix: 'bursty',
 93 |       points: 1,
 94 |       duration: 1,
 95 |     });
 96 |     const bursty = new BurstyRateLimiter(rlRedisClosed, blRedis);
 97 | 
 98 |     let testFailed = false
 99 |     try {
100 |       await bursty.consume(testKey)
101 |       testFailed = true;
102 |     } catch(err) {
103 |       expect(err instanceof Error).to.equal(true);
104 |       try {
105 |         const rlRes = await rlRedis.get(testKey)
106 |         expect(rlRes).to.equal(null);
107 |       } catch (err2) {
108 |         testFailed = true;
109 |       }
110 |     }
111 |     await redisMockClient.disconnect();
112 |     if (testFailed) {
113 |       return new Error('must not');
114 |     }
115 |   });
116 | 
117 |   it('reject with burst limiter error if it happens', async() => {
118 |     const testKey = 'consume-rejected-with-error';
119 |     const redisMockClient = new Redis();
120 |     const redisClientClosed = new Redis();
121 |     await redisClientClosed.disconnect();
122 |     const rlRedis = new RateLimiterRedis({
123 |       storeClient: redisMockClient,
124 |       points: 1,
125 |       duration: 1,
126 |     });
127 |     const blRedisClosed = new RateLimiterRedis({
128 |       storeClient: redisClientClosed,
129 |       keyPrefix: 'bursty',
130 |     });
131 |     const bursty = new BurstyRateLimiter(rlRedis, blRedisClosed);
132 |     await bursty.consume(testKey);
133 |     let testFailed = false
134 |     try {
135 |       await bursty.consume(testKey)
136 |       testFailed = true;
137 |     } catch(err) {
138 |       expect(err instanceof Error).to.equal(true);
139 |       const rlRes = await rlRedis.get(testKey)
140 |       expect(rlRes.consumedPoints).to.equal(2);
141 |       expect(rlRes.remainingPoints).to.equal(0);
142 |       expect(rlRes.msBeforeNext <= 1000).to.equal(true);
143 |     }
144 |     await redisMockClient.disconnect();
145 |     if (testFailed) {
146 |       throw new Error('must not');
147 |     }
148 |   });
149 | 
150 |   it('consume and get return the combined RateLimiterRes of both limiters with correct msBeforeNext', (done) => {
151 |     const rlMemory = new RateLimiterMemory({ points: 1, duration: 10 });
152 |     const rlBurstMemory = new RateLimiterMemory({ points: 20, duration: 1 });
153 | 
154 |     const bl = new BurstyRateLimiter(rlMemory, rlBurstMemory);
155 | 
156 |     bl.consume('keyGet', 1)
157 |       .then((firstConsumeRes) => {
158 |         expect(firstConsumeRes.isFirstInDuration).to.equal(true);
159 |         bl.consume('keyGet', 1)
160 |           .then((res) => {
161 |             expect(res.consumedPoints).to.equal(2);
162 |             expect(res.remainingPoints).to.equal(0);
163 |             expect(res.msBeforeNext <= 1000).to.equal(true);
164 |             expect(res.isFirstInDuration).to.equal(false);
165 | 
166 |             bl.get('keyGet')
167 |               .then((rlRes) => {
168 |                 expect(rlRes.consumedPoints).to.equal(2);
169 |                 expect(rlRes.remainingPoints).to.equal(0);
170 |                 expect(rlRes.msBeforeNext <= 1000).to.equal(true);
171 |                 done();
172 |               })
173 |               .catch(err => done(err));
174 |           })
175 |           .catch((err) => {
176 |             done(err);
177 |           });
178 |       });
179 |   });
180 | 
181 |   it('returns points from limiter', (done) => {
182 |     const rlMemory = new RateLimiterMemory({ points: 1, duration: 10 });
183 |     const rlBurstMemory = new RateLimiterMemory({ points: 20, duration: 1 });
184 | 
185 |     const brl = new BurstyRateLimiter(rlMemory, rlBurstMemory);
186 |     expect(brl.points).to.equal(1);
187 |     done();
188 |   });
189 | 
190 |   it('returns null if key does not exist', (done) => {
191 |     const rlMemory = new RateLimiterMemory({ points: 1, duration: 10 });
192 |     const rlBurstMemory = new RateLimiterMemory({ points: 20, duration: 1 });
193 | 
194 |     const brl = new BurstyRateLimiter(rlMemory, rlBurstMemory);
195 |     brl.get('test-null')
196 |       .then((res) => {
197 |         expect(res).to.equal(null);
198 |         done();
199 |       });
200 |   });
201 | 
202 |   it('returns msBeforeNext=0 if key is not set on bursty limiter', (done) => {
203 |     const rlMemory = new RateLimiterMemory({ points: 1, duration: 10 });
204 |     const rlBurstMemory = new RateLimiterMemory({ points: 20, duration: 1 });
205 | 
206 |     const testKey = 'test-burst-null'
207 |     const brl = new BurstyRateLimiter(rlMemory, rlBurstMemory);
208 |     rlMemory.consume(testKey)
209 |       .then(async () => {
210 |         brl.get(testKey)
211 |           .then((res) => {
212 |             expect(res.msBeforeNext).to.equal(0);
213 |             done();
214 |           })
215 |           .catch((err) => {
216 |             done(err);
217 |           })
218 |       })
219 |       .catch((err) => {
220 |         done(err);
221 |       });
222 |   });
223 | });
224 | 


--------------------------------------------------------------------------------
/test/RateLimiterAbstract.test.js:
--------------------------------------------------------------------------------
 1 | const { describe, it } = require('mocha');
 2 | const { expect } = require('chai');
 3 | const RateLimiterAbstract = require('../lib/RateLimiterAbstract');
 4 | 
 5 | describe('RateLimiterAbstract', function () {
 6 |   this.timeout(5000);
 7 | 
 8 |   it('do not prefix key, if keyPrefix is empty string', () => {
 9 |     const testKey = 'test1';
10 |     const rateLimiter = new RateLimiterAbstract({ keyPrefix: '' });
11 |     expect(rateLimiter.getKey(testKey)).to.equal(testKey);
12 |   });
13 | });
14 | 


--------------------------------------------------------------------------------
/test/RateLimiterCluster.test.js:
--------------------------------------------------------------------------------
  1 | /* eslint-env mocha */
  2 | /* eslint-disable no-unused-expressions */
  3 | /* eslint-disable security/detect-object-injection */
  4 | const cluster = require('cluster');
  5 | const sinon = require('sinon');
  6 | const { describe, it, after } = require('mocha');
  7 | const { expect } = require('chai');
  8 | const { RateLimiterClusterMaster, RateLimiterCluster } = require('../lib/RateLimiterCluster');
  9 | 
 10 | const masterEvents = [];
 11 | const workerEvents = [];
 12 | 
 13 | const worker = {
 14 |   send: (data) => {
 15 |     workerEvents.forEach((cb) => {
 16 |       cb(data);
 17 |     });
 18 |   },
 19 | };
 20 | 
 21 | global.process.on = (eventName, cb) => {
 22 |   if (eventName === 'message') {
 23 |     workerEvents.push(cb);
 24 |   }
 25 | };
 26 | global.process.send = (data) => {
 27 |   masterEvents.forEach((cb) => {
 28 |     cb(worker, data);
 29 |   });
 30 | };
 31 | 
 32 | describe('RateLimiterCluster', function RateLimiterClusterTest() {
 33 |   let rateLimiterClusterMaster;
 34 |   let clusterStubOn;
 35 |   this.timeout(5000);
 36 | 
 37 |   before(() => {
 38 |     clusterStubOn = sinon.stub(cluster, 'on').callsFake((eventName, cb) => {
 39 |       masterEvents.push(cb);
 40 |     });
 41 |     rateLimiterClusterMaster = new RateLimiterClusterMaster();
 42 |   });
 43 | 
 44 |   after(() => {
 45 |     clusterStubOn.restore();
 46 |   });
 47 | 
 48 |   it('master must be singleton', () => {
 49 |     const rateLimiterClusterMaster2 = new RateLimiterClusterMaster();
 50 |     expect(rateLimiterClusterMaster2 === rateLimiterClusterMaster).to.equal(true);
 51 |   });
 52 | 
 53 |   it('consume 1 point', (done) => {
 54 |     const key = 'consume1';
 55 |     const rateLimiterCluster = new RateLimiterCluster({ points: 2, duration: 5, keyPrefix: key });
 56 |     rateLimiterCluster.consume(key)
 57 |       .then((res) => {
 58 |         expect(res.remainingPoints).to.equal(1);
 59 |         done();
 60 |       })
 61 |       .catch((rej) => {
 62 |         done(rej);
 63 |       });
 64 |   });
 65 | 
 66 |   it('reject on consuming more than maximum points', (done) => {
 67 |     const key = 'reject';
 68 |     const rateLimiterCluster = new RateLimiterCluster({ points: 2, duration: 5, keyPrefix: key });
 69 |     rateLimiterCluster.consume(key, 3)
 70 |       .then(() => {
 71 | 
 72 |       })
 73 |       .catch((rejRes) => {
 74 |         expect(rejRes.remainingPoints).to.equal(0);
 75 |         done();
 76 |       });
 77 |   });
 78 |   //
 79 |   it('execute evenly over duration', (done) => {
 80 |     const key = 'evenly';
 81 |     const rateLimiterCluster = new RateLimiterCluster({
 82 |       points: 2, duration: 5, execEvenly: true, keyPrefix: key,
 83 |     });
 84 |     rateLimiterCluster.consume(key)
 85 |       .then(() => {
 86 |         const timeFirstConsume = Date.now();
 87 |         rateLimiterCluster.consume(key)
 88 |           .then(() => {
 89 |             /* Second consume should be delayed more than 2 seconds
 90 |                Explanation:
 91 |                1) consume at 0ms, remaining duration = 4444ms
 92 |                2) delayed consume for (4444 / (0 + 2)) ~= 2222ms, where 2 is a fixed value
 93 |                 , because it mustn't delay in the beginning and in the end of duration
 94 |                3) consume after 2222ms by timeout
 95 |             */
 96 |             expect((Date.now() - timeFirstConsume) > 2000).to.equal(true);
 97 |             done();
 98 |           })
 99 |           .catch((err) => {
100 |             done(err);
101 |           });
102 |       })
103 |       .catch((err) => {
104 |         done(err);
105 |       });
106 |   });
107 | 
108 |   it('use keyPrefix from options', (done) => {
109 |     const key = 'use keyPrefix from options';
110 | 
111 |     const keyPrefix = 'test';
112 |     const rateLimiterCluster = new RateLimiterCluster({ points: 2, duration: 5, keyPrefix });
113 |     rateLimiterCluster.consume(key)
114 |       .then(() => {
115 |         expect(typeof rateLimiterClusterMaster._rateLimiters[keyPrefix]._memoryStorage._storage[`${keyPrefix}:${key}`]
116 |           !== 'undefined').to.equal(true);
117 |         done();
118 |       })
119 |       .catch((rejRes) => {
120 |         done(rejRes);
121 |       });
122 |   });
123 | 
124 |   it('create 2 rate limiters depending on keyPrefix', (done) => {
125 |     const keyPrefixes = ['create1', 'create2'];
126 |     const rateLimiterClusterprocess1 = new RateLimiterCluster({ keyPrefix: keyPrefixes[0] });
127 |     const rateLimiterClusterprocess2 = new RateLimiterCluster({ keyPrefix: keyPrefixes[1] });
128 |     rateLimiterClusterprocess1.consume('key1')
129 |       .then(() => {
130 |         rateLimiterClusterprocess2.consume('key2')
131 |           .then(() => {
132 |             const createdKeyLimiters = Object.keys(rateLimiterClusterMaster._rateLimiters);
133 |             expect(createdKeyLimiters.indexOf(keyPrefixes[0]) !== -1 && createdKeyLimiters.indexOf(keyPrefixes[0]) !== -1).to.equal(true);
134 |             done();
135 |           });
136 |       });
137 |   });
138 | 
139 |   it('penalty', (done) => {
140 |     const key = 'penalty';
141 |     const rateLimiterCluster = new RateLimiterCluster({ points: 2, duration: 5, keyPrefix: key });
142 |     rateLimiterCluster.penalty(key)
143 |       .then((res) => {
144 |         expect(res.remainingPoints).to.equal(1);
145 |         done();
146 |       });
147 |   });
148 | 
149 |   it('reward', (done) => {
150 |     const key = 'reward';
151 |     const rateLimiterCluster = new RateLimiterCluster({ points: 2, duration: 5, keyPrefix: key });
152 |     rateLimiterCluster.consume(key)
153 |       .then(() => {
154 |         rateLimiterCluster.reward(key)
155 |           .then((res) => {
156 |             expect(res.remainingPoints).to.equal(2);
157 |             done();
158 |           });
159 |       });
160 |   });
161 | 
162 |   it('block', (done) => {
163 |     const key = 'block';
164 |     const rateLimiterCluster = new RateLimiterCluster({ points: 1, duration: 1, keyPrefix: key });
165 |     rateLimiterCluster.block(key, 2)
166 |       .then((res) => {
167 |         expect(res.msBeforeNext > 1000 && res.msBeforeNext <= 2000).to.equal(true);
168 |         done();
169 |       });
170 |   });
171 | 
172 |   it('get', (done) => {
173 |     const key = 'get';
174 |     const rateLimiterCluster = new RateLimiterCluster({ points: 1, duration: 1, keyPrefix: key });
175 |     rateLimiterCluster.consume(key)
176 |       .then(() => {
177 |         rateLimiterCluster.get(key)
178 |           .then((res) => {
179 |             expect(res.consumedPoints).to.equal(1);
180 |             done();
181 |           });
182 |       });
183 |   });
184 | 
185 |   it('get null', (done) => {
186 |     const key = 'getnull';
187 |     const rateLimiterCluster = new RateLimiterCluster({ points: 1, duration: 1, keyPrefix: key });
188 |     rateLimiterCluster.get(key)
189 |       .then((res) => {
190 |         expect(res).to.equal(null);
191 |         done();
192 |       });
193 |   });
194 | 
195 |   it('delete', (done) => {
196 |     const key = 'deletetrue';
197 |     const rateLimiterCluster = new RateLimiterCluster({ points: 1, duration: 10, keyPrefix: key });
198 |     rateLimiterCluster.consume(key)
199 |       .then(() => {
200 |         rateLimiterCluster.delete(key)
201 |           .then((res) => {
202 |             expect(res).to.equal(true);
203 |             done();
204 |           });
205 |       });
206 |   });
207 | 
208 |   it('consume applies options.customDuration to set expire', (done) => {
209 |     const key = 'consume.customDuration';
210 |     const rateLimiterCluster = new RateLimiterCluster({ points: 2, duration: 5, keyPrefix: key });
211 |     rateLimiterCluster.consume(key, 1, { customDuration: 1 })
212 |       .then((res) => {
213 |         expect(res.msBeforeNext <= 1000).to.be.true;
214 |         done();
215 |       })
216 |       .catch((rej) => {
217 |         done(rej);
218 |       });
219 |   });
220 | });
221 | 
222 | 


--------------------------------------------------------------------------------
/test/RateLimiterDynamo.test.js:
--------------------------------------------------------------------------------
  1 | const {DynamoDB} = require('@aws-sdk/client-dynamodb')
  2 | const { expect } = require('chai');
  3 | const { describe, it } = require('mocha');
  4 | const RateLimiterDynamo = require('../lib/RateLimiterDynamo');
  5 | const sinon = require('sinon');
  6 | 
  7 | describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() {
  8 |     this.timeout(5000);
  9 | 
 10 |     const dynamoClient = new DynamoDB({
 11 |         region: 'eu-central-1',
 12 |         credentials: {
 13 |             accessKeyId: 'fake', 
 14 |             secretAccessKey: 'fake'
 15 |         },
 16 |         endpoint: 'http://127.0.0.1:8000'
 17 |     });
 18 |     
 19 |     it('DynamoDb client connection', (done) => {
 20 |         expect(dynamoClient).to.not.equal(null);
 21 |         dynamoClient.listTables()
 22 |         .then((data) => {
 23 |             done();
 24 |         })
 25 |         .catch((err) => {
 26 |             done(err);
 27 |         });
 28 |     });
 29 | 
 30 |     it('get item from DynamoDB', (done) => {
 31 |         
 32 |         const testKey = 'test';
 33 |         const rateLimiter = new RateLimiterDynamo({
 34 |             storeClient: dynamoClient
 35 |         },
 36 |         () => {
 37 |             rateLimiter.set(testKey, 999, 10000)
 38 |             .then((data) => {
 39 |                 rateLimiter.get(testKey)
 40 |                 .then((response) => {
 41 |                     expect(response).to.not.equal(null);
 42 |                     done();
 43 |                 })
 44 |                 .catch((err) => {
 45 |                     done(err);
 46 |                 });
 47 |             })
 48 |             .catch((err) => {
 49 |                 done(err);
 50 |             })
 51 |         }
 52 |         );
 53 |     });
 54 | 
 55 |     it('get NOT existing item from DynamoDB', (done) => {
 56 |         
 57 |         const testKey = 'not_existing';
 58 |         const rateLimiter = new RateLimiterDynamo({
 59 |             storeClient: dynamoClient
 60 |         },
 61 |         () => {
 62 |             rateLimiter.get(testKey)
 63 |             .then((response) => {
 64 |                 expect(response).to.equal(null);
 65 |                 done();
 66 |             })
 67 |             .catch((err) => {
 68 |                 done(err);
 69 |             });
 70 |         }
 71 |         );
 72 |     });
 73 | 
 74 |     it('delete item from DynamoDB', (done) => {
 75 |         
 76 |         const testKey = 'delete_test';
 77 |         const rateLimiter = new RateLimiterDynamo({
 78 |             storeClient: dynamoClient
 79 |         },
 80 |         () => {
 81 |             rateLimiter.set(testKey, 999, 10000)
 82 |             .then((data) => {
 83 |                 rateLimiter.delete(testKey)
 84 |                 .then((response) => {
 85 |                     expect(response).to.equal(true);
 86 |                     done();
 87 |                 })
 88 |                 .catch((err) => {
 89 |                     done(err);
 90 |                 });
 91 |             })
 92 |             .catch((err) => {
 93 |                 done(err);
 94 |             })
 95 |         }
 96 |         );
 97 |     });
 98 | 
 99 |     it('delete NOT existing item from DynamoDB return false', (done) => {
100 |         
101 |         const testKey = 'delete_test_2';
102 |         const rateLimiter = new RateLimiterDynamo({
103 |             storeClient: dynamoClient
104 |         },
105 |         () => {
106 |             rateLimiter.delete(testKey)
107 |             .then((response) => {
108 |                 expect(response).to.equal(false);
109 |                 done();
110 |             })
111 |             .catch((err) => {
112 |                 done(err);
113 |             });
114 |         }
115 |         );
116 |     });
117 |     
118 |     it('consume 1 point', (done) => {
119 |         const testKey = 'consume1';
120 | 
121 |         const rateLimiter = new RateLimiterDynamo({
122 |             storeClient: dynamoClient,
123 |             points: 2,
124 |             duration: 10
125 |         },
126 |         () => {
127 |             rateLimiter.consume(testKey)
128 |                 .then((result) => {
129 |                     expect(result.consumedPoints).to.equal(1);
130 |                     rateLimiter.delete(testKey);
131 |                     done();
132 |                 })
133 |                 .catch((err) => {
134 |                     done(err);
135 |                 });
136 | 
137 |         });
138 |         
139 |     });
140 | 
141 |     it('rejected when consume more than maximum points', (done) => {
142 |         const testKey = 'consumerej';
143 |     
144 |         const rateLimiter = new RateLimiterDynamo({
145 |             storeClient: dynamoClient,
146 |             points: 1,
147 |             duration: 5
148 |         },
149 |         () => {
150 |             rateLimiter.consume(testKey, 2)
151 |                 .then((result) => {
152 |                     expect(result.consumedPoints).to.equal(2);
153 |                     done(Error('must not resolve'));
154 |                 })
155 |                 .catch((err) => {
156 |                     expect(err.consumedPoints).to.equal(2);
157 |                     done();
158 |                 });
159 | 
160 |         });
161 |     });
162 | 
163 |     it('blocks key for block duration when consumed more than points', (done) => {
164 |         const testKey = 'block';
165 |         
166 |         const rateLimiter = new RateLimiterDynamo({
167 |             storeClient: dynamoClient,
168 |             points: 1,
169 |             duration: 1,
170 |             blockDuration: 2
171 |         },
172 |         () => {
173 |             rateLimiter.consume(testKey, 2)
174 |                 .then((result) => {
175 |                     expect(result.consumedPoints).to.equal(2);
176 |                     done(Error('must not resolve'));
177 |                 })
178 |                 .catch((err) => {
179 |                     expect(err.msBeforeNext > 1000).to.equal(true);
180 |                     done();
181 |                 });
182 | 
183 |         });
184 |         
185 |     });
186 | 
187 |     it('return correct data with _getRateLimiterRes', () => {
188 |         const testKey = 'test';
189 |         
190 |         const rateLimiter = new RateLimiterDynamo({
191 |             storeClient: dynamoClient,
192 |             points: 5,
193 |         },
194 |         () => {
195 |             
196 |             const res = rateLimiter._getRateLimiterRes(
197 |                 'test',
198 |                 1,
199 |                 { key: 'test', points: 3, expire: (Date.now() + 1000) / 1000}
200 |                 );
201 | 
202 |             expect(res.msBeforeNext <= 1000 && 
203 |                         res.consumedPoints === 3 && 
204 |                         res.isFirstInDuration === false && 
205 |                         res.remainingPoints === 2
206 |                     ).to.equal(true);
207 | 
208 |         });
209 |     });
210 | 
211 |     it('get points', (done) => {
212 |         const testKey = 'get';
213 |     
214 |         const rateLimiter = new RateLimiterDynamo({
215 |             storeClient: dynamoClient,
216 |             points: 5,
217 |         },
218 |         () => {
219 |             
220 |             rateLimiter.set(testKey, 999, 10000)
221 |                 .then((data) => {
222 |                     rateLimiter.get(testKey)
223 |                         .then((response) => {
224 |                             expect(response.consumedPoints).to.equal(999);
225 |                             rateLimiter.delete(testKey);
226 |                             done();
227 |                         })
228 |                         .catch((err) => {
229 |                             done(err);
230 |                         });
231 |                 })
232 |                 .catch((err) => {
233 |                     done(err);
234 |                 });
235 | 
236 |         });
237 | 
238 |     });
239 | 
240 |     it('get points return NULL if key is not set', (done) => {
241 |         const testKey = 'getnull';
242 |         
243 |         const rateLimiter = new RateLimiterDynamo({
244 |             storeClient: dynamoClient,
245 |             points: 5,
246 |         },
247 |         () => {
248 |             
249 |             rateLimiter.get(testKey)
250 |                 .then((response) => {
251 |                     expect(response).to.equal(null);
252 |                     done();
253 |                 })
254 |                 .catch((err) => {
255 |                     done(err);
256 |                 });
257 |         });
258 |         
259 |     });
260 | 
261 |     it('delete returns false, if there is no key', (done) => {
262 |         const testKey = 'getnull3';
263 |         
264 |         const rateLimiter = new RateLimiterDynamo({
265 |             storeClient: dynamoClient,
266 |             points: 5,
267 |         },
268 |         () => {
269 |             
270 |             rateLimiter.delete(testKey)
271 |                 .then((response) => {
272 |                     expect(response).to.equal(false);
273 |                     done();
274 |                 })
275 |                 .catch((err) => {
276 |                     done(err);
277 |                 });
278 |         });
279 |         
280 |     });
281 | 
282 |     it('delete rejects on error', (done) => {
283 |         const testKey = 'deleteerr';
284 |         
285 |         const rateLimiter = new RateLimiterDynamo({
286 |             storeClient: dynamoClient,
287 |             points: 5,
288 |         },
289 |         () => {
290 |            
291 |             sinon.stub(dynamoClient, 'deleteItem').callsFake(() => {
292 |                 throw new Error('stub error');
293 |             });
294 | 
295 |             rateLimiter.delete(testKey)
296 |             .catch(() => {
297 |                 done();
298 |             });
299 | 
300 |             dynamoClient.deleteItem.restore();
301 |         });
302 |         
303 |     });
304 |     
305 | 
306 |     it('does not expire key if duration set to 0', (done) => {
307 |         const testKey = 'neverexpire';
308 |         
309 |         const rateLimiter = new RateLimiterDynamo({
310 |             storeClient: dynamoClient,
311 |             points: 2,
312 |             duration: 0
313 |         },
314 |         () => {
315 |             
316 |             rateLimiter.set(testKey, 2, 0)
317 |             .then(() => {
318 |                 rateLimiter.consume(testKey, 1)
319 |                     .then(() => {
320 |                         rateLimiter.get(testKey)
321 |                             .then((res) => {
322 |                                 expect(res.consumedPoints).to.equal(1);
323 |                                 expect(res.msBeforeNext).to.equal(-1);
324 |                                 done();
325 |                             })
326 |                             .catch((err) => {
327 |                                 done(err);
328 |                             })
329 |                     })
330 |                     .catch((err) => {
331 |                         done(err);
332 |                     })
333 |             })
334 |             .catch((err) => {
335 |                 done(err);
336 |             });
337 |             
338 |         });
339 |         
340 |     });
341 |     
342 | });
343 | 


--------------------------------------------------------------------------------
/test/RateLimiterPrisma/Postgres/schema.prisma:
--------------------------------------------------------------------------------
 1 | generator client {
 2 |   provider = "prisma-client-js"
 3 | }
 4 | 
 5 | datasource db {
 6 |   provider = "postgresql"
 7 |   url      = "postgres://root:secret@127.0.0.1:5432"
 8 | }
 9 | 
10 | model RateLimiterFlexible {
11 |   key     String   @id
12 |   points  Int
13 |   expire  DateTime?
14 | }
15 | 


--------------------------------------------------------------------------------
/test/RateLimiterQueue.test.js:
--------------------------------------------------------------------------------
  1 | const { describe, it } = require('mocha');
  2 | const { expect } = require('chai');
  3 | const RateLimiterMemory = require('../lib/RateLimiterMemory');
  4 | const BurstyLimiter = require('../lib/BurstyRateLimiter');
  5 | const RateLimiterQueue = require('../lib/RateLimiterQueue');
  6 | const RateLimiterQueueError = require('../lib/component/RateLimiterQueueError');
  7 | 
  8 | describe('RateLimiterQueue with FIFO queue', function RateLimiterQueueTest() {
  9 |   this.timeout(5000);
 10 | 
 11 |   it('remove 1 token works and 1 remaining', (done) => {
 12 |     const rlMemory = new RateLimiterMemory({ points: 2, duration: 1 });
 13 |     const rlQueue = new RateLimiterQueue(rlMemory);
 14 |     rlQueue.removeTokens(1)
 15 |       .then((remainingTokens) => {
 16 |         expect(remainingTokens).to.equal(1);
 17 |         done();
 18 |       });
 19 |   });
 20 | 
 21 |   it('remove 2 tokens from bursty limiter and returns correct remainingTokens 0', (done) => {
 22 |     const rlMemory = new RateLimiterMemory({ points: 1, duration: 1 });
 23 |     const blMemory = new RateLimiterMemory({ points: 1, duration: 3 });
 24 |     const burstyLimiter = new BurstyLimiter(rlMemory, blMemory);
 25 |     const rlQueue = new RateLimiterQueue(burstyLimiter);
 26 |     const startTime = Date.now();
 27 |     rlQueue.removeTokens(1)
 28 |       .then((remainingTokens1) => {
 29 |         expect(remainingTokens1).to.equal(0);
 30 |         rlQueue.removeTokens(1)
 31 |           .then((remainingTokens2) => {
 32 |             expect(remainingTokens2).to.equal(0);
 33 |             expect(Date.now() - startTime < 1000).to.equal(true);
 34 |             done();
 35 |           });
 36 |       });
 37 |   });
 38 | 
 39 |   it('remove 2 tokens from bursty limiter and wait 1 more', (done) => {
 40 |     const rlMemory = new RateLimiterMemory({ points: 1, duration: 1 });
 41 |     const blMemory = new RateLimiterMemory({ points: 1, duration: 3 });
 42 |     const burstyLimiter = new BurstyLimiter(rlMemory, blMemory);
 43 |     const rlQueue = new RateLimiterQueue(burstyLimiter);
 44 |     const startTime = Date.now();
 45 |     rlQueue.removeTokens(1)
 46 |       .then(() => {
 47 |         rlQueue.removeTokens(1)
 48 |           .then(() => {
 49 |             rlQueue.removeTokens(1)
 50 |               .then((remainingTokens) => {
 51 |                 expect(remainingTokens).to.equal(0);
 52 |                 expect(Date.now() - startTime >= 999).to.equal(true);
 53 |                 done();
 54 |               });
 55 |           });
 56 |       });
 57 |   });
 58 | 
 59 |   it('remove all tokens works and 0 remaining', (done) => {
 60 |     const rlMemory = new RateLimiterMemory({ points: 2, duration: 1 });
 61 |     const rlQueue = new RateLimiterQueue(rlMemory);
 62 |     rlQueue.removeTokens(2)
 63 |       .then((remainingTokens) => {
 64 |         expect(remainingTokens).to.equal(0);
 65 |         done();
 66 |       });
 67 |   });
 68 | 
 69 |   it('return error if try to remove more tokens than allowed', (done) => {
 70 |     const rlMemory = new RateLimiterMemory({ points: 2, duration: 1 });
 71 |     const rlQueue = new RateLimiterQueue(rlMemory);
 72 |     rlQueue.removeTokens(3)
 73 |       .then(() => {
 74 |       })
 75 |       .catch((err) => {
 76 |         expect(err instanceof RateLimiterQueueError).to.equal(true);
 77 |         done();
 78 |       });
 79 |   });
 80 | 
 81 |   it('queues 1 request and fire it after 1 second', (done) => {
 82 |     const rlMemory = new RateLimiterMemory({ points: 1, duration: 1 });
 83 |     const rlQueue = new RateLimiterQueue(rlMemory);
 84 |     const time = Date.now();
 85 |     rlQueue.removeTokens(1).then(() => {
 86 |       rlQueue.removeTokens(1).then((remainingTokens) => {
 87 |         expect(remainingTokens).to.equal(0);
 88 |         expect(Date.now() - time >= 999).to.equal(true);
 89 |         done();
 90 |       });
 91 |     });
 92 |   });
 93 | 
 94 |   it('respects order of queued callbacks', (done) => {
 95 |     const rlMemory = new RateLimiterMemory({ points: 1, duration: 1 });
 96 |     const rlQueue = new RateLimiterQueue(rlMemory);
 97 |     let index;
 98 |     rlQueue.removeTokens(1).then(() => {
 99 |       index = 0;
100 |     });
101 |     rlQueue.removeTokens(1).then(() => {
102 |       expect(index).to.equal(0);
103 |       index = 1;
104 |     });
105 |     rlQueue.removeTokens(1).then(() => {
106 |       expect(index).to.equal(1);
107 |       index = 2;
108 |     });
109 |     rlQueue.removeTokens(1).then(() => {
110 |       expect(index).to.equal(2);
111 |       done();
112 |     });
113 |   });
114 | 
115 |   it('return error if queue length reaches maximum', (done) => {
116 |     const rlMemory = new RateLimiterMemory({ points: 1, duration: 1 });
117 |     const rlQueue = new RateLimiterQueue(rlMemory, { maxQueueSize: 1 });
118 |     rlQueue.removeTokens(1).then(() => {
119 |     });
120 |     rlQueue.removeTokens(1).then(() => {
121 |       done();
122 |     });
123 |     rlQueue.removeTokens(1)
124 |       .then(() => {
125 |         done(new Error('must not allow to queue'));
126 |       })
127 |       .catch((err) => {
128 |         expect(err instanceof RateLimiterQueueError).to.equal(true);
129 |       });
130 |   });
131 | 
132 |   it('getTokensRemaining works', (done) => {
133 |     const rlMemory = new RateLimiterMemory({ points: 2, duration: 1 });
134 |     const rlQueue = new RateLimiterQueue(rlMemory);
135 |     rlQueue.removeTokens(1)
136 |       .then(() => {
137 |         rlQueue.getTokensRemaining()
138 |           .then((tokensRemaining) => {
139 |             expect(tokensRemaining).to.equal(1);
140 |             done();
141 |           });
142 |       });
143 |   });
144 | 
145 |   it('getTokensRemaining returns maximum if internal limiter by key does not exist', (done) => {
146 |     const rlMemory = new RateLimiterMemory({ points: 23, duration: 1 });
147 |     const rlQueue = new RateLimiterQueue(rlMemory);
148 |     rlQueue.getTokensRemaining('test')
149 |       .then((tokensRemaining) => {
150 |         expect(tokensRemaining).to.equal(23);
151 |         done();
152 |       });
153 |   });
154 | 
155 |   it('creates internal instance by key and removes tokens from it', (done) => {
156 |     const rlMemory = new RateLimiterMemory({ points: 2, duration: 1 });
157 |     const rlQueue = new RateLimiterQueue(rlMemory);
158 |     rlQueue.removeTokens(1, 'customkey')
159 |       .then((remainingTokens) => {
160 |         expect(remainingTokens).to.equal(1);
161 |         rlQueue.getTokensRemaining()
162 |           .then((defaultTokensRemaining) => {
163 |             expect(defaultTokensRemaining).to.equal(2);
164 |             done();
165 |           });
166 |       });
167 |   });
168 | 
169 |   it('getTokensRemaining returns maximum if internal limiter does not have data', (done) => {
170 |     const rlMemory = new RateLimiterMemory({ points: 23, duration: 1 });
171 |     const rlQueue = new RateLimiterQueue(rlMemory);
172 |     rlQueue.removeTokens(1, 'nodata')
173 |       .then(() => {
174 |         setTimeout(() => {
175 |           rlQueue.getTokensRemaining('nodata')
176 |             .then((tokensRemaining) => {
177 |               expect(tokensRemaining).to.equal(23);
178 |               done();
179 |             });
180 |         }, 1001)
181 |       })
182 |   });
183 | });
184 | 


--------------------------------------------------------------------------------
/test/RateLimiterRes.test.js:
--------------------------------------------------------------------------------
 1 | const { describe, it, beforeEach } = require('mocha');
 2 | const { expect } = require('chai');
 3 | const RateLimiterRes = require('../lib/RateLimiterRes');
 4 | 
 5 | describe('RateLimiterRes response object', () => {
 6 |   let rateLimiterRes;
 7 |   beforeEach(() => {
 8 |     rateLimiterRes = new RateLimiterRes();
 9 |   });
10 | 
11 |   it('setup defaults on construct', () => {
12 |     expect(rateLimiterRes.msBeforeNext === 0 && rateLimiterRes.remainingPoints === 0)
13 |       .to.be.equal(true);
14 |   });
15 | 
16 |   it('msBeforeNext set and get', () => {
17 |     rateLimiterRes.msBeforeNext = 123;
18 |     expect(rateLimiterRes.msBeforeNext).to.equal(123);
19 |   });
20 | 
21 |   it('points set and get', () => {
22 |     rateLimiterRes.remainingPoints = 4;
23 |     expect(rateLimiterRes.remainingPoints).to.equal(4);
24 |   });
25 | 
26 |   it('consumed points set and get', () => {
27 |     rateLimiterRes.consumedPoints = 5;
28 |     expect(rateLimiterRes.consumedPoints).to.equal(5);
29 |   });
30 | 
31 |   it('isFirstInDuration set and get with cast', () => {
32 |     rateLimiterRes.isFirstInDuration = 1;
33 |     expect(rateLimiterRes.isFirstInDuration).to.equal(true);
34 |   });
35 | 
36 |   it('returns object on toJSON call', () => {
37 |     rateLimiterRes.msBeforeNext = 12;
38 |     rateLimiterRes.remainingPoints = 3;
39 |     rateLimiterRes.consumedPoints = 2;
40 |     rateLimiterRes.isFirstInDuration = true;
41 | 
42 |     expect(rateLimiterRes.toJSON()).to.deep.equal({
43 |       remainingPoints: 3,
44 |       msBeforeNext: 12,
45 |       consumedPoints: 2,
46 |       isFirstInDuration: true,
47 |     });
48 |   });
49 | 
50 |   it('returns JSON string on toString call', () => {
51 |     rateLimiterRes.msBeforeNext = 2;
52 |     rateLimiterRes.remainingPoints = 0;
53 |     rateLimiterRes.consumedPoints = 5;
54 |     rateLimiterRes.isFirstInDuration = false;
55 | 
56 |     expect(rateLimiterRes.toString()).to.equal('{"remainingPoints":0,"msBeforeNext":2,"consumedPoints":5,"isFirstInDuration":false}');
57 |   });
58 | });
59 | 


--------------------------------------------------------------------------------
/test/RateLimiterSQLite.test.js:
--------------------------------------------------------------------------------
  1 | const { describe, it, beforeEach, afterEach } = require("mocha");
  2 | const { expect } = require("chai");
  3 | const sqlite3 = require("sqlite3");
  4 | const betterSQLite3 = require("better-sqlite3");
  5 | const { RateLimiterSQLite } = require("../index");
  6 | const knex = require("knex");
  7 | 
  8 | function testRateLimiterSQLite(library, createDb, clientName = null) {
  9 |   describe(`RateLimiterSQLite with ${clientName || library}`, () => {
 10 |     let db;
 11 |     let rateLimiter;
 12 | 
 13 |     beforeEach((done) => {
 14 |       db = createDb();
 15 |       rateLimiter = new RateLimiterSQLite({
 16 |         storeClient: db,
 17 |         storeType: library,
 18 |         tableName: "rate_limiter_test",
 19 |         points: 5,
 20 |         duration: 5,
 21 |       });
 22 |       // Wait for table creation
 23 |       setTimeout(done, 100);
 24 |     });
 25 | 
 26 |     afterEach((done) => {
 27 |       if (library === "sqlite3") {
 28 |         db.close(() => done());
 29 |         return;
 30 |       }
 31 | 
 32 |       if (library === "knex") {
 33 |         db.destroy().then(() => done());
 34 |         return;
 35 |       }
 36 |       db.close();
 37 |       done();
 38 |     });
 39 | 
 40 |     describe("basic functionality", () => {
 41 |       it("should consume points", async () => {
 42 |         const res = await rateLimiter.consume("test");
 43 |         expect(res.consumedPoints).to.equal(1);
 44 |         expect(res.remainingPoints).to.equal(4);
 45 |       });
 46 | 
 47 |       it("should reject when too many points consumed", async () => {
 48 |         try {
 49 |           await rateLimiter.consume("test", 6);
 50 |           expect.fail("should have thrown");
 51 |         } catch (err) {
 52 |           expect(err.remainingPoints).to.equal(0);
 53 |           expect(err.consumedPoints).to.equal(6);
 54 |         }
 55 |       });
 56 | 
 57 |       it("should respect points and duration", async () => {
 58 |         const res1 = await rateLimiter.consume("test");
 59 |         const res2 = await rateLimiter.consume("test");
 60 |         expect(res1.consumedPoints).to.equal(1);
 61 |         expect(res2.consumedPoints).to.equal(2);
 62 |         expect(res2.remainingPoints).to.equal(3);
 63 |       });
 64 |     });
 65 | 
 66 |     describe("block functionality", () => {
 67 |       it("should block key for specified duration", async () => {
 68 |         await rateLimiter.block("test", 1);
 69 |         try {
 70 |           await rateLimiter.consume("test");
 71 |           expect.fail("should have thrown");
 72 |         } catch (err) {
 73 |           expect(err.msBeforeNext).to.be.at.least(900);
 74 |         }
 75 |       });
 76 |     });
 77 | 
 78 |     describe("get and delete operations", () => {
 79 |       it("should get points consumed", async () => {
 80 |         await rateLimiter.consume("test", 2);
 81 |         const res = await rateLimiter.get("test");
 82 |         expect(res.consumedPoints).to.equal(2);
 83 |       });
 84 | 
 85 |       it("should return null when getting non-existent key", async () => {
 86 |         const res = await rateLimiter.get("nonexistent");
 87 |         expect(res).to.be.null;
 88 |       });
 89 | 
 90 |       it("should delete key", async () => {
 91 |         await rateLimiter.consume("test");
 92 |         const deleted = await rateLimiter.delete("test");
 93 |         expect(deleted).to.be.true;
 94 |         const res = await rateLimiter.get("test");
 95 |         expect(res).to.be.null;
 96 |       });
 97 | 
 98 |       it("should return false when deleting non-existent key", async () => {
 99 |         const deleted = await rateLimiter.delete("nonexistent");
100 |         expect(deleted).to.be.false;
101 |       });
102 |     });
103 | 
104 |     describe("expiration", () => {
105 |       it("should expire points after duration", (done) => {
106 |         const shortLimiter = new RateLimiterSQLite({
107 |           storeClient: db,
108 |           storeType: library,
109 |           tableName: "rate_limiter_test_short",
110 |           points: 5,
111 |           duration: 1, // 1 second duration
112 |         });
113 | 
114 |         setTimeout(() => {
115 |           shortLimiter
116 |             .consume("test")
117 |             .then(async () => {
118 |               await new Promise((resolve) => setTimeout(resolve, 1100));
119 |               const res = await shortLimiter.get("test");
120 |               expect(res).to.be.null;
121 |               done();
122 |             })
123 |             .catch(done);
124 |         }, 100); // Wait for table creation
125 |       });
126 |     });
127 | 
128 |     describe("error handling", () => {
129 |       it("should handle database errors gracefully", async () => {
130 |         // Close the database to simulate errors
131 |         if (library === "sqlite3") {
132 |           await new Promise((resolve) => db.close(resolve));
133 |         } else if (library === "knex") {
134 |           await db.destroy();
135 |         } else {
136 |           db.close();
137 |         }
138 | 
139 |         try {
140 |           await rateLimiter.consume("test");
141 |           expect.fail("should have thrown");
142 |         } catch (err) {
143 |           expect(err).to.be.an("error");
144 |         }
145 |       });
146 | 
147 |       it("should reject table when not valid", async () => {
148 |         try {
149 |           const invalidLimiter = new RateLimiterSQLite({
150 |             storeClient: db,
151 |             storeType: library,
152 |             tableName: "invalid table name with spaces",
153 |             points: 5,
154 |             duration: 5,
155 |           });
156 |           expect.fail("should have thrown");
157 |         } catch (err) {
158 |           expect(err.message).to.equal(
159 |             "Table name must contain only letters and numbers"
160 |           );
161 |         }
162 |       });
163 | 
164 |       it("should reject storeType when it's not supported", async () => {
165 |         const validStoreTypes = ["sqlite3", "better-sqlite3", "knex"];
166 | 
167 |         try {
168 |           const unsupportedStoreType = new RateLimiterSQLite({
169 |             storeClient: db,
170 |             storeType: "not_supported",
171 |             tableName: "invalid table name with spaces",
172 |             points: 5,
173 |             duration: 5,
174 |           });
175 |           expect.fail("should have thrown");
176 |         } catch (err) {
177 |           expect(err.message).to.equal(
178 |             `storeType must be one of: ${validStoreTypes.join(", ")}`
179 |           );
180 |         }
181 |       });
182 |     });
183 | 
184 |     describe("concurrent operations", () => {
185 |       it("should handle multiple concurrent requests", async () => {
186 |         const promises = [];
187 |         for (let i = 0; i < 3; i++) {
188 |           promises.push(rateLimiter.consume("test"));
189 |         }
190 | 
191 |         const results = await Promise.all(promises);
192 | 
193 |         expect(results).to.have.lengthOf(3);
194 |         expect(results[2].consumedPoints).to.equal(3);
195 |         expect(results[2].remainingPoints).to.equal(2);
196 |       });
197 |     });
198 | 
199 |     describe("cleanup", () => {
200 |       it("should clear expired records", async () => {
201 |         const cleanupLimiter = new RateLimiterSQLite({
202 |           storeClient: db,
203 |           storeType: library,
204 |           tableName: "rate_limiter_cleanup",
205 |           points: 5,
206 |           duration: 1,
207 |           clearExpiredByTimeout: true,
208 |         });
209 | 
210 |         // wait for table creation
211 |         await new Promise((resolve) => setTimeout(resolve, 500));
212 | 
213 |         await cleanupLimiter.consume("test");
214 |         await new Promise((resolve) => setTimeout(resolve, 1100));
215 |         await cleanupLimiter.clearExpired(Date.now());
216 | 
217 |         const res = await cleanupLimiter.get("test");
218 |         expect(res).to.be.null;
219 |       });
220 |     });
221 |   });
222 | }
223 | 
224 | // Run tests with sqlite3 in-memory database
225 | testRateLimiterSQLite("sqlite3", () => new sqlite3.Database(":memory:"));
226 | 
227 | // Run tests with better-sqlite3 in-memory-database
228 | testRateLimiterSQLite("better-sqlite3", () => new betterSQLite3(":memory:"));
229 | 
230 | // Run test with knex using better-sqlite3 in-memory database
231 | testRateLimiterSQLite(
232 |   "knex",
233 |   () =>
234 |     knex({
235 |       client: "better-sqlite3",
236 |       connection: { filename: ":memory:" },
237 |       useNullAsDefault: true,
238 |     }),
239 |   "knex(better-sqlite3)"
240 | );
241 | 
242 | // Run test with knex using sqlite3 in-memory database
243 | testRateLimiterSQLite(
244 |   "knex",
245 |   () =>
246 |     knex({
247 |       client: "sqlite3",
248 |       connection: { filename: ":memory:" },
249 |       useNullAsDefault: true,
250 |     }),
251 |   "knex(sqlite3)"
252 | );
253 | 


--------------------------------------------------------------------------------
/test/RateLimiterStoreAbstract.test.js:
--------------------------------------------------------------------------------
  1 | /* eslint-disable security/detect-object-injection */
  2 | const { describe, it } = require('mocha');
  3 | const { expect } = require('chai');
  4 | const RateLimiterStoreAbstract = require('../lib/RateLimiterStoreAbstract');
  5 | const RateLimiterRes = require('../lib/RateLimiterRes');
  6 | 
  7 | class RateLimiterStoreMemory extends RateLimiterStoreAbstract {
  8 |   constructor(opts) {
  9 |     super(opts);
 10 |     this._inMemoryDataAsStorage = {};
 11 |   }
 12 | 
 13 |   _getRateLimiterRes(rlKey, changedPoints, storeResult) {
 14 |     const res = new RateLimiterRes();
 15 |     res.consumedPoints = storeResult.points;
 16 |     res.isFirstInDuration = res.consumedPoints === changedPoints;
 17 |     res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
 18 |     res.msBeforeNext = storeResult.msBeforeNext;
 19 |     return res;
 20 |   }
 21 | 
 22 |   _get(rlKey) {
 23 |     const result = this._inMemoryDataAsStorage[rlKey];
 24 | 
 25 |     return Promise.resolve(typeof result === 'undefined' ? null : result);
 26 |   }
 27 | 
 28 |   _delete(rlKey) {
 29 |     const value = this._inMemoryDataAsStorage[rlKey];
 30 | 
 31 |     if (typeof value === 'undefined') {
 32 |       return Promise.resolve(false);
 33 |     }
 34 | 
 35 |     delete this._inMemoryDataAsStorage[rlKey];
 36 |     return Promise.resolve(true);
 37 |   }
 38 | 
 39 |   _upsert(rlKey, points, msDuration) {
 40 |     const now = Date.now();
 41 |     const result = {
 42 |       points,
 43 |       msBeforeNext: msDuration,
 44 |     };
 45 | 
 46 |     if (typeof this._inMemoryDataAsStorage[rlKey] === 'undefined') {
 47 |       this._inMemoryDataAsStorage[rlKey] = {
 48 |         points,
 49 |         expired: now + msDuration,
 50 |       };
 51 |     } else {
 52 |       const value = this._inMemoryDataAsStorage[rlKey];
 53 |       if (value.expired > now) {
 54 |         value.points += points;
 55 |         result.points = value.points;
 56 |         result.msBeforeNext = value.expired - now;
 57 |       } else {
 58 |         value.points = points;
 59 |         value.expired = now + msDuration;
 60 |       }
 61 |     }
 62 | 
 63 |     return Promise.resolve(result);
 64 |   }
 65 | }
 66 | 
 67 | describe('RateLimiterStoreAbstract with fixed window', () => {
 68 |   it('delete all in memory blocked keys', (done) => {
 69 |     const rateLimiter = new RateLimiterStoreMemory({
 70 |       points: 1,
 71 |       duration: 1,
 72 |       // avoid fire block method
 73 |       blockDuration: 0,
 74 |       inMemoryBlockOnConsumed: 1,
 75 |       inMemoryBlockDuration: 1,
 76 |       keyPrefix: '',
 77 |     });
 78 | 
 79 |     // should start blocking
 80 |     Promise.allSettled([
 81 |       rateLimiter.consume('key1', 2),
 82 |       rateLimiter.consume('key2', 2),
 83 |     ])
 84 |       .then(() => {
 85 |         expect(rateLimiter._inMemoryBlockedKeys._keys.key1).not.eq(undefined);
 86 |         expect(rateLimiter._inMemoryBlockedKeys._keys.key2).not.eq(undefined);
 87 | 
 88 |         rateLimiter.deleteInMemoryBlockedAll();
 89 |         expect(rateLimiter._inMemoryBlockedKeys._keys.key1).eq(undefined);
 90 |         expect(rateLimiter._inMemoryBlockedKeys._keys.key2).eq(undefined);
 91 | 
 92 |         done();
 93 |       })
 94 |       .catch((err) => {
 95 |         done(err);
 96 |       });
 97 |   });
 98 | 
 99 |   it('delete specific key should also deleting in-memory data', (done) => {
100 |     const rateLimiter = new RateLimiterStoreMemory({
101 |       points: 1,
102 |       duration: 1,
103 |       // avoid fire block method
104 |       blockDuration: 0,
105 |       inMemoryBlockOnConsumed: 1,
106 |       inMemoryBlockDuration: 1,
107 |       keyPrefix: '',
108 |     });
109 | 
110 |     // should start blocking
111 |     rateLimiter.consume('key', 2).catch(() => {
112 |       expect(rateLimiter._inMemoryBlockedKeys._keys.key).not.eq(undefined);
113 | 
114 |       rateLimiter.delete('key').then((isExist) => {
115 |         expect(rateLimiter._inMemoryBlockedKeys._keys.key).eq(undefined);
116 |         expect(isExist).eq(true);
117 | 
118 |         done();
119 |       });
120 |     });
121 |   });
122 | });
123 | 


--------------------------------------------------------------------------------
/test/RateLimiterUnion.test.js:
--------------------------------------------------------------------------------
 1 | // eslint-disable no-unused-expressions
 2 | const { describe, it, beforeEach } = require('mocha');
 3 | const { expect } = require('chai');
 4 | const RateLimiterUnion = require('../lib/RateLimiterUnion');
 5 | const RateLimiterMemory = require('../lib/RateLimiterMemory');
 6 | 
 7 | describe('RateLimiterUnion with fixed window', () => {
 8 |   const keyPrefix1 = 'limit1';
 9 |   const keyPrefix2 = 'limit2';
10 |   let rateLimiter;
11 | 
12 |   beforeEach(() => {
13 |     const limiter1 = new RateLimiterMemory({
14 |       keyPrefix: keyPrefix1,
15 |       points: 1,
16 |       duration: 1,
17 |     });
18 |     const limiter2 = new RateLimiterMemory({
19 |       keyPrefix: keyPrefix2,
20 |       points: 2,
21 |       duration: 5,
22 |     });
23 |     rateLimiter = new RateLimiterUnion(limiter1, limiter2);
24 |   });
25 | 
26 |   it('does not allow to create union with limiters number less than 2', () => {
27 |     try {
28 |       new RateLimiterUnion(new RateLimiterMemory({ // eslint-disable-line no-new
29 |         keyPrefix: keyPrefix1,
30 |         points: 1,
31 |         duration: 1,
32 |       }));
33 |     } catch (err) {
34 |       expect(err instanceof Error).to.equal(true);
35 |     }
36 |   });
37 | 
38 |   it('all limiters have to be instance of RateLimiterAbstract', () => {
39 |     try {
40 |       new RateLimiterUnion(new RateLimiterMemory({ // eslint-disable-line no-new
41 |         keyPrefix: keyPrefix1,
42 |         points: 1,
43 |         duration: 1,
44 |       }), {});
45 |     } catch (err) {
46 |       expect(err instanceof Error).to.equal(true);
47 |     }
48 |   });
49 | 
50 |   it('consume from all limiters', (done) => {
51 |     rateLimiter.consume('test')
52 |       .then((res) => {
53 |         expect(res[keyPrefix1].remainingPoints === 0 && res[keyPrefix2].remainingPoints === 1).to.equal(true);
54 |         done();
55 |       })
56 |       .catch(() => {
57 |         done(Error('must not reject'));
58 |       });
59 |   });
60 | 
61 |   it('reject consume one "limit1", which does not have enough points', (done) => {
62 |     rateLimiter.consume('test', 2)
63 |       .then(() => {
64 |         done(Error('must not resolve'));
65 |       })
66 |       .catch((rej) => {
67 |         expect(rej[keyPrefix1].remainingPoints === 0).to.equal(true);
68 |         done();
69 |       });
70 |   });
71 | 
72 |   it('reject both do not have enough points', (done) => {
73 |     rateLimiter.consume('test', 3)
74 |       .then(() => {
75 |         done(Error('must not resolve'));
76 |       })
77 |       .catch((rej) => {
78 |         expect(rej[keyPrefix1].remainingPoints === 0 && rej[keyPrefix2].remainingPoints === 0).to.equal(true);
79 |         done();
80 |       });
81 |   });
82 | });
83 | 
84 | 


--------------------------------------------------------------------------------
/test/RedisOptions.js:
--------------------------------------------------------------------------------
 1 | // This object is used for setting the options for the redis client,
 2 | // so we can connect to the redis server in Docker, using ipv4 and not
 3 | // ipv6, which the client defaults to useing
 4 | module.exports = {
 5 |   socket: {
 6 |     host: '127.0.0.1',
 7 |     port: 6379,
 8 |   },
 9 | };
10 | 


--------------------------------------------------------------------------------
/test/component/BlockedKeys/BlockedKeys.test.js:
--------------------------------------------------------------------------------
 1 | const { describe, it, beforeEach } = require('mocha');
 2 | const { expect } = require('chai');
 3 | const BlockedKeys = require('../../../lib/component/BlockedKeys/BlockedKeys');
 4 | 
 5 | describe('BlockedKeys', () => {
 6 |   let blockedKeys;
 7 |   beforeEach(() => {
 8 |     blockedKeys = new BlockedKeys();
 9 |   });
10 | 
11 |   it('add blocked key', () => {
12 |     blockedKeys.add('key', 5);
13 |     blockedKeys.collectExpired();
14 |     expect(blockedKeys.msBeforeExpire('key') > 0).to.equal(true);
15 |   });
16 | 
17 |   it('expire blocked key', (done) => {
18 |     blockedKeys.add('key', 1);
19 |     setTimeout(() => {
20 |       expect(blockedKeys.msBeforeExpire('key')).to.equal(0);
21 |       done();
22 |     }, 1001);
23 |   });
24 | 
25 |   it('check not blocked key', () => {
26 |     blockedKeys.add('key', 1);
27 |     expect(blockedKeys.msBeforeExpire('key1')).to.equal(0);
28 |   });
29 | 
30 |   it('do not collect expired on add', (done) => {
31 |     blockedKeys.add('key', 1);
32 |     blockedKeys.add('key1', 1);
33 |     setTimeout(() => {
34 |       blockedKeys.add('key2', 1);
35 |       expect(Object.keys(blockedKeys._keys).length).to.equal(3);
36 |       done();
37 |     }, 1001);
38 |   });
39 | 
40 |   it('collect expired on add if there more than 999 blocked keys', (done) => {
41 |     for (let i = 0; i < 1000; i++) {
42 |       blockedKeys.add(`key${i}`, 1);
43 |     }
44 | 
45 |     setTimeout(() => {
46 |       blockedKeys.add('key1', 1);
47 |       expect(Object.keys(blockedKeys._keys).length === 1 && blockedKeys._addedKeysAmount === 1)
48 |         .to.equal(true);
49 |       done();
50 |     }, 1001);
51 |   });
52 | 
53 |   it('do not collect expired when key is not blocked', (done) => {
54 |     blockedKeys.add('key', 1);
55 |     setTimeout(() => {
56 |       blockedKeys.msBeforeExpire('key');
57 |       expect(Object.keys(blockedKeys._keys).length === 1 && blockedKeys._addedKeysAmount === 1)
58 |         .to.equal(true);
59 |       done();
60 |     }, 1001);
61 |   });
62 | 
63 |   it('collect expired when key is blocked', (done) => {
64 |     blockedKeys.add('key', 1);
65 |     blockedKeys.add('blocked', 2);
66 |     setTimeout(() => {
67 |       blockedKeys.msBeforeExpire('blocked');
68 |       expect(Object.keys(blockedKeys._keys).length).to.equal(1);
69 |       done();
70 |     }, 1001);
71 |   });
72 | 
73 |   it('duplicated keys do not brake collectExpired and msBeforeExpire', (done) => {
74 |     blockedKeys.add('key', 1);
75 |     blockedKeys.add('key', 2);
76 |     setTimeout(() => {
77 |       blockedKeys.add('key', 3);
78 |       expect(blockedKeys.msBeforeExpire('key') > 2000).to.equal(true);
79 |       done();
80 |     }, 1001);
81 |   });
82 | });
83 | 


--------------------------------------------------------------------------------
/test/component/MemoryStorage/MemoryStorage.test.js:
--------------------------------------------------------------------------------
 1 | const { describe, it, beforeEach } = require('mocha');
 2 | const { expect } = require('chai');
 3 | const MemoryStorage = require('../../../lib/component/MemoryStorage/MemoryStorage');
 4 | 
 5 | describe('MemoryStorage', function MemoryStorageTest() {
 6 |   const testKey = 'test';
 7 |   const val = 34;
 8 |   let storage;
 9 | 
10 |   this.timeout(5000);
11 | 
12 |   beforeEach(() => {
13 |     storage = new MemoryStorage();
14 |   });
15 | 
16 |   it('should set and get', (done) => {
17 |     storage.set(testKey, val, 5);
18 |     expect(storage.get(testKey).consumedPoints).to.equal(val);
19 |     done();
20 |   });
21 | 
22 |   it('should delete record on expire', (done) => {
23 |     storage.set(testKey, val, 1);
24 |     setTimeout(() => {
25 |       expect(storage.get(testKey)).to.equal(null);
26 |       done();
27 |     }, 2000);
28 |   });
29 | 
30 |   it('should incrby', (done) => {
31 |     storage.set(testKey, val, 5);
32 |     storage.incrby(testKey, 2);
33 |     expect(storage.get(testKey).consumedPoints).to.equal(val + 2);
34 |     done();
35 |   });
36 | 
37 |   it('incrby should create record if it is not set', (done) => {
38 |     storage.incrby(testKey, val, 5);
39 |     expect(storage.get(testKey).consumedPoints).to.equal(val);
40 |     done();
41 |   });
42 | 
43 |   it('incrby should create record if expiresAt is not set', (done) => {
44 |     storage.set(testKey, val)
45 |     expect(storage.get(testKey).expiresAt).to.equal(undefined);
46 |     storage.incrby(testKey, val, 5);
47 |     expect(storage.get(testKey).expiresAt !== null).to.equal(true);
48 |     done();
49 |   });
50 | 
51 |   it('should delete record and return true, if it was there', () => {
52 |     storage.set(testKey, val, 10);
53 |     expect(storage.delete(testKey)).to.equal(true);
54 |     expect(storage.get(testKey)).to.equal(null);
55 |   });
56 | 
57 |   it('return false, if there is no record to delete', () => {
58 |     expect(storage.delete(testKey)).to.equal(false);
59 |   });
60 | 
61 |   it('should not fail in the absence of Timeout::unref', (done) => {
62 |     // Node (where we most likely be running tests) provides `Timeout.prototype.unref`, however
63 |     // MemoryStorage should run in environments where `Timeout.prototype.unref` is not provided
64 |     // (e.g. browsers). For this test we remove `unref` from `Timeout.prototype` only for the
65 |     // duration of this test, to verify that MemoryStorage.prototype.set won't throw.
66 |     const handle = setTimeout(() => {}, 0);
67 |     const isHandleObject = typeof handle === 'object' && !!handle.constructor;
68 |     let timeoutUnref;
69 |     if (isHandleObject) {
70 |       timeoutUnref = handle.constructor.prototype.unref;
71 |       delete handle.constructor.prototype.unref;
72 |     }
73 |     expect(() => new MemoryStorage().set('key', 0, 0.001)).to.not.throw();
74 |     setTimeout(done, 250);
75 |     if (isHandleObject) {
76 |       handle.constructor.prototype.unref = timeoutUnref;
77 |     }
78 |   });
79 | });
80 | 


--------------------------------------------------------------------------------
/test/component/MemoryStorage/Record.test.js:
--------------------------------------------------------------------------------
 1 | const { describe, it, beforeEach } = require('mocha');
 2 | const { expect } = require('chai');
 3 | const Record = require('../../../lib/component/MemoryStorage/Record');
 4 | 
 5 | describe('MemoryStorage Record', () => {
 6 |   let record;
 7 |   beforeEach(() => {
 8 |     record = new Record();
 9 |   });
10 | 
11 |   it('value set with cast to int and get', () => {
12 |     record.value = '123';
13 |     expect(record.value).to.equal(123);
14 |   });
15 | 
16 |   it('expiresAt set unix time and get Date', () => {
17 |     const now = Date.now();
18 |     record.expiresAt = now;
19 |     expect(record.expiresAt.getTime()).to.equal(now);
20 |   });
21 | });
22 | 


--------------------------------------------------------------------------------
/test/component/RateLimiterQueueError.test.js:
--------------------------------------------------------------------------------
 1 | const { describe, it } = require('mocha');
 2 | const { expect } = require('chai');
 3 | const RateLimiterQueueError = require('../../lib/component/RateLimiterQueueError');
 4 | 
 5 | describe('RateLimiterQueueError', () => {
 6 |   it('supports extra argument in constructor', (done) => {
 7 |     const err = new RateLimiterQueueError('test', 'extra');
 8 |     expect(err.extra).to.equal('extra');
 9 |     done();
10 |   });
11 | });
12 | 


--------------------------------------------------------------------------------
/test/scripts/cluster-setup.sh:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env bash
 2 | set -e
 3 | 
 4 | echo "Setting up cluster environment..."
 5 | 
 6 | if [ -z "$SKIP_SYSCTL" ]; then
 7 |     echo "Attempting system settings..."
 8 |     sysctl vm.overcommit_memory=1 2>/dev/null || true
 9 | else
10 |     echo "Skipping system settings (handled by Docker)"
11 | fi
12 | 
13 | echo "Cleaning up data directories..."
14 | rm -rf /data/*
15 | 
16 | # Initialize nodes
17 | init_nodes() {
18 |     for port in $(seq 7001 7003); do
19 |         mkdir -p /data/${port}
20 |         cat > /data/${port}/valkey.conf <<EOF
21 | port ${port}
22 | cluster-enabled yes
23 | cluster-config-file /data/${port}/nodes.conf
24 | cluster-node-timeout 30000
25 | appendonly yes
26 | dir /data/${port}
27 | bind 0.0.0.0
28 | protected-mode no
29 | cluster-announce-ip 127.0.0.1
30 | daemonize yes
31 | EOF
32 |         valkey-server /data/${port}/valkey.conf
33 |         
34 |         until valkey-cli -p ${port} ping 2>/dev/null; do
35 |             echo "Waiting for node ${port} to start..."
36 |             sleep 1
37 |         done
38 |     done
39 | }
40 | 
41 | echo "Initializing nodes..."
42 | init_nodes
43 | 
44 | echo "Waiting for nodes to stabilize..."
45 | sleep 5
46 | 
47 | echo "Initializing cluster..."
48 | yes "yes" | valkey-cli --cluster create \
49 |     127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 \
50 |     --cluster-replicas 0
51 | 
52 | # Wait for cluster to stabilize
53 | echo "Waiting for cluster to stabilize..."
54 | for i in {1..30}; do
55 |     if valkey-cli -p 7001 cluster info | grep -q "cluster_state:ok"; then
56 |         echo "Cluster is stable"
57 |         break
58 |     fi
59 |     echo "Waiting for cluster to stabilize (attempt $i)..."
60 |     sleep 2
61 | done
62 | 
63 | echo "Cluster initialization complete"
64 | 
65 | # Keep container running and monitor cluster
66 | while true; do
67 |     echo "Cluster Status at $(date):"
68 |     valkey-cli -p 7001 cluster info || echo "Failed to get cluster info"
69 |     sleep 60
70 | done
71 | 


--------------------------------------------------------------------------------