├── .npmrc ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── package.json ├── LICENSE ├── index.js ├── test.js └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node: 13 | - 10 14 | - 12 15 | - 14 16 | - 16 17 | - 18 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node }} 23 | - run: npm install 24 | - run: npm test 25 | - uses: coverallsapp/github-action@v2 26 | if: matrix.node == 18 27 | with: 28 | github-token: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "valvelet", 3 | "version": "1.1.1", 4 | "description": "Limit the execution rate of a function", 5 | "homepage": "https://github.com/lpinca/valvelet", 6 | "bugs": "https://github.com/lpinca/valvelet/issues", 7 | "repository": "lpinca/valvelet", 8 | "author": "Luigi Pinca", 9 | "license": "MIT", 10 | "main": "index.js", 11 | "keywords": [ 12 | "function", 13 | "throttle", 14 | "promise", 15 | "limit", 16 | "rate" 17 | ], 18 | "scripts": { 19 | "test": "c8 --reporter=lcov --reporter=text tape test.js" 20 | }, 21 | "engines": { 22 | "node": ">=4.0.0" 23 | }, 24 | "files": [ 25 | "index.js" 26 | ], 27 | "devDependencies": { 28 | "c8": "^7.3.0", 29 | "pre-commit": "^1.2.2", 30 | "tape": "^5.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Luigi Pinca 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Limit the execution rate of a function. 5 | * 6 | * @param {Function} fn The function to rate limit calls to 7 | * @param {Number} limit The maximum number of allowed calls per `interval` 8 | * @param {Number} interval The timespan where `limit` is calculated 9 | * @param {Number} size The maximum size of the queue 10 | * @return {Function} 11 | * @public 12 | */ 13 | function valvelet(fn, limit, interval, size) { 14 | const queue = []; 15 | let count = 0; 16 | 17 | size || (size = Math.pow(2, 32) - 1); 18 | 19 | function timeout() { 20 | count--; 21 | if (queue.length) shift(); 22 | } 23 | 24 | function shift() { 25 | count++; 26 | const data = queue.shift(); 27 | data[2](fn.apply(data[0], data[1])); 28 | setTimeout(timeout, interval); 29 | } 30 | 31 | return function limiter() { 32 | const args = arguments; 33 | 34 | return new Promise((resolve, reject) => { 35 | if (queue.length === size) return reject(new Error('Queue is full')); 36 | 37 | queue.push([this, args, resolve]); 38 | if (count < limit) shift(); 39 | }); 40 | }; 41 | } 42 | 43 | module.exports = valvelet; 44 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | 5 | const valvelet = require('.'); 6 | 7 | test('is exported as a function', (t) => { 8 | t.equal(typeof valvelet, 'function'); 9 | t.end(); 10 | }); 11 | 12 | test('returns a function that returns a promise', (t) => { 13 | const limit = valvelet(() => 'foo', 1, 10); 14 | 15 | limit().then((value) => { 16 | t.equal(value, 'foo'); 17 | t.end(); 18 | }); 19 | }); 20 | 21 | test('calls the original function with the same context and arguments', (t) => { 22 | const limit = valvelet(function () { 23 | t.deepEqual(arguments, (function () { return arguments; })(1, 2)); 24 | t.equal(this, 'foo'); 25 | }, 1, 10); 26 | 27 | limit.call('foo', 1, 2).then(t.end); 28 | }); 29 | 30 | test('allows to limit the queue size', (t) => { 31 | const limit = valvelet(() => {}, 1, 10, 2); 32 | 33 | limit(); 34 | limit(); 35 | limit(); 36 | limit().then(() => { 37 | t.fail('Promise should not be fulfilled'); 38 | t.end(); 39 | }, (err) => { 40 | t.equal(err instanceof Error, true); 41 | t.equal(err.message, 'Queue is full'); 42 | t.end(); 43 | }); 44 | }); 45 | 46 | test('limits the execution rate of the original function', (t) => { 47 | const values = [1, 2, 3, 4, 5]; 48 | const start = Date.now(); 49 | const times = []; 50 | const limit = valvelet((arg) => { 51 | times.push(Date.now()); 52 | return Promise.resolve(arg); 53 | }, 2, 100); 54 | 55 | Promise.all(values.map((i) => limit(i))).then((data) => { 56 | t.deepEqual(data, values); 57 | times.forEach((time, i) => { 58 | const delay = Math.floor(i / 2) * 100; 59 | const diff = time - start - delay; 60 | 61 | t.ok(diff >= 0 && diff < 20); 62 | }); 63 | t.end(); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # valvelet 2 | 3 | [![Version npm][npm-valvelet-badge]][npm-valvelet] 4 | [![Build Status][ci-valvelet-badge]][ci-valvelet] 5 | [![Coverage Status][coverage-valvelet-badge]][coverage-valvelet] 6 | 7 | This is a small utility to limit the execution rate of a function. It is useful 8 | for scenarios such as REST APIs consumption where the amount of requests per 9 | unit of time should not exceed a given threshold. 10 | 11 | This module is very similar to 12 | [`node-function-rate-limit`][function-rate-limit]. The difference is that 13 | `valvelet` works seamlessly with promise-returning functions. 14 | 15 | ## Install 16 | 17 | ``` 18 | npm install --save valvelet 19 | ``` 20 | 21 | ## API 22 | 23 | The module exports a single function that takes four arguments. 24 | 25 | ### `valvelet(fn, limit, interval[, size])` 26 | 27 | Returns a function which should be called instead of `fn`. 28 | 29 | #### Arguments 30 | 31 | - `fn` - The function to rate limit calls to. 32 | - `limit` - The maximum number of allowed calls per `interval`. 33 | - `interval` - The timespan where `limit` is calculated. 34 | - `size` - The maximum size of the internal queue. Defaults to 2^32 - 1 which is 35 | the maximum array size in JavaScript. 36 | 37 | #### Return value 38 | 39 | A function that returns a promise which resolves to the value returned by the 40 | original `fn` function. When the internal queue is at capacity the returned 41 | promise is rejected. 42 | 43 | #### Example 44 | 45 | ```js 46 | const valvelet = require('valvelet'); 47 | 48 | const get = valvelet( 49 | function request(i) { 50 | return Promise.resolve(`${i} - ${new Date().toISOString()}`); 51 | }, 52 | 2, 53 | 1000 54 | ); 55 | 56 | function log(data) { 57 | console.log(data); 58 | } 59 | 60 | for (let i = 0; i < 10; i++) { 61 | get(i).then(log); 62 | } 63 | 64 | /* 65 | 0 - 2016-06-02T20:07:33.843Z 66 | 1 - 2016-06-02T20:07:33.844Z 67 | 2 - 2016-06-02T20:07:34.846Z 68 | 3 - 2016-06-02T20:07:34.846Z 69 | 4 - 2016-06-02T20:07:35.846Z 70 | 5 - 2016-06-02T20:07:35.846Z 71 | 6 - 2016-06-02T20:07:36.848Z 72 | 7 - 2016-06-02T20:07:36.848Z 73 | 8 - 2016-06-02T20:07:37.851Z 74 | 9 - 2016-06-02T20:07:37.851Z 75 | */ 76 | ``` 77 | 78 | ## Disclaimers 79 | 80 | This module is not a complete solution if you are trying to throttle your 81 | requests to a remote API, but have multiple Node.js processes on the same or 82 | multiple hosts, since the state is not shared between the services. That case 83 | can be addressed by allowing each process to send up to only a fraction of the 84 | total limit. Ex: If you have 4 processes, let each process send up to $limit/4. 85 | 86 | ## License 87 | 88 | [MIT](LICENSE) 89 | 90 | [npm-valvelet-badge]: https://img.shields.io/npm/v/valvelet.svg 91 | [npm-valvelet]: https://www.npmjs.com/package/valvelet 92 | [ci-valvelet-badge]: 93 | https://img.shields.io/github/actions/workflow/status/lpinca/valvelet/ci.yml?branch=master&label=CI 94 | [ci-valvelet]: 95 | https://github.com/lpinca/valvelet/actions?query=workflow%3ACI+branch%3Amaster 96 | [coverage-valvelet-badge]: 97 | https://img.shields.io/coveralls/lpinca/valvelet/master.svg 98 | [coverage-valvelet]: https://coveralls.io/r/lpinca/valvelet?branch=master 99 | [function-rate-limit]: https://github.com/wankdanker/node-function-rate-limit 100 | --------------------------------------------------------------------------------