├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── example.js ├── index.d.ts ├── index.js ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 20 14 | - 18 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const throttle = require('./index.js'); 2 | 3 | const onProgress = throttle(number => { 4 | console.log(`Progress: ${number}`); 5 | }, 500); 6 | 7 | let number = 0; 8 | const intervalId = setInterval(() => { 9 | if (number > 100) { 10 | clearInterval(intervalId); 11 | return; 12 | } 13 | 14 | onProgress(number++); 15 | }, 50); 16 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Throttle a function to limit its execution rate. 3 | 4 | Creates a throttled function that limits calls to the original function to at most once every `wait` milliseconds. It guarantees execution after the final invocation and maintains the last context (`this`) and arguments. 5 | 6 | @param function_ - The function to be throttled. 7 | @param wait - The number of milliseconds to throttle invocations to. 8 | @returns A new throttled version of the provided function. 9 | 10 | @example 11 | ``` 12 | import throttle from 'throttleit'; 13 | 14 | // Throttling a function that processes data. 15 | function processData(data) { 16 | console.log('Processing:', data); 17 | 18 | // Add data processing logic here. 19 | } 20 | 21 | // Throttle the `processData` function to be called at most once every 3 seconds. 22 | const throttledProcessData = throttle(processData, 3000); 23 | 24 | // Simulate calling the function multiple times with different data. 25 | throttledProcessData('Data 1'); 26 | throttledProcessData('Data 2'); 27 | throttledProcessData('Data 3'); 28 | ``` 29 | */ 30 | declare function throttle unknown>(function_: T, wait: number): T; 31 | 32 | export = throttle; 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | function throttle(function_, wait) { 2 | if (typeof function_ !== 'function') { 3 | throw new TypeError(`Expected the first argument to be a \`function\`, got \`${typeof function_}\`.`); 4 | } 5 | 6 | // TODO: Add `wait` validation too in the next major version. 7 | 8 | let timeoutId; 9 | let lastCallTime = 0; 10 | 11 | return function throttled(...arguments_) { // eslint-disable-line func-names 12 | clearTimeout(timeoutId); 13 | 14 | const now = Date.now(); 15 | const timeSinceLastCall = now - lastCallTime; 16 | const delayForNextCall = wait - timeSinceLastCall; 17 | 18 | if (delayForNextCall <= 0) { 19 | lastCallTime = now; 20 | function_.apply(this, arguments_); 21 | } else { 22 | timeoutId = setTimeout(() => { 23 | lastCallTime = Date.now(); 24 | function_.apply(this, arguments_); 25 | }, delayForNextCall); 26 | } 27 | }; 28 | } 29 | 30 | module.exports = throttle; 31 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) TJ Holowaychuk 4 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "throttleit", 3 | "version": "2.1.0", 4 | "description": "Throttle a function to limit its execution rate", 5 | "license": "MIT", 6 | "repository": "sindresorhus/throttleit", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "exports": { 9 | "types": "./index.d.ts", 10 | "default": "./index.js" 11 | }, 12 | "main": "./index.js", 13 | "types": "./index.d.ts", 14 | "sideEffects": false, 15 | "engines": { 16 | "node": ">=18" 17 | }, 18 | "scripts": { 19 | "test": "xo && ava" 20 | }, 21 | "files": [ 22 | "index.js", 23 | "index.d.ts" 24 | ], 25 | "keywords": [ 26 | "throttle", 27 | "rate", 28 | "limit", 29 | "limited", 30 | "rate-limit", 31 | "ratelimit", 32 | "throttling", 33 | "optimization", 34 | "performance", 35 | "function", 36 | "execution", 37 | "interval", 38 | "batch" 39 | ], 40 | "devDependencies": { 41 | "ava": "^5.3.1", 42 | "xo": "^0.56.0" 43 | }, 44 | "xo": { 45 | "rules": { 46 | "unicorn/prefer-module": "off" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # throttleit 2 | 3 | > Throttle a function to limit its execution rate 4 | 5 | ## Install 6 | 7 | ```sh 8 | npm install throttleit 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | import throttle from 'throttleit'; 15 | 16 | // Throttling a function that processes data. 17 | function processData(data) { 18 | console.log('Processing:', data); 19 | 20 | // Add data processing logic here. 21 | } 22 | 23 | // Throttle the `processData` function to be called at most once every 3 seconds. 24 | const throttledProcessData = throttle(processData, 3000); 25 | 26 | // Simulate calling the function multiple times with different data. 27 | throttledProcessData('Data 1'); 28 | throttledProcessData('Data 2'); 29 | throttledProcessData('Data 3'); 30 | ``` 31 | 32 | ## API 33 | 34 | ### throttle(function, wait) 35 | 36 | Creates a throttled function that limits calls to the original function to at most once every `wait` milliseconds. It guarantees execution after the final invocation and maintains the last context (`this`) and arguments. 37 | 38 | #### function 39 | 40 | Type: `function` 41 | 42 | The function to be throttled. 43 | 44 | #### wait 45 | 46 | Type: `number` 47 | 48 | The number of milliseconds to throttle invocations to. 49 | 50 | ## Related 51 | 52 | - [p-throttle](https://github.com/sindresorhus/p-throttle) - Throttle async functions 53 | - [debounce](https://github.com/sindresorhus/debounce) - Delay function calls until a set time elapses after the last invocation 54 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const throttle = require('./index.js'); 3 | 4 | const delay = async duration => new Promise(resolve => { 5 | setTimeout(resolve, duration); 6 | }); 7 | 8 | function counter() { 9 | function count() { 10 | count.callCount++; 11 | } 12 | 13 | count.callCount = 0; 14 | return count; 15 | } 16 | 17 | test('throttled function is called at most once per interval', async t => { 18 | const count = counter(); 19 | const wait = 100; 20 | const total = 300; 21 | const throttled = throttle(count, wait); 22 | const interval = setInterval(throttled, 20); 23 | 24 | await delay(total); 25 | clearInterval(interval); 26 | 27 | // Using floor since the first call happens immediately 28 | const expectedCalls = 1 + Math.floor((total - wait) / wait); 29 | t.is(count.callCount, expectedCalls, 'Should call function based on total time and wait interval'); 30 | }); 31 | 32 | test('throttled function executes final call after wait time', async t => { 33 | const count = counter(); 34 | const wait = 100; 35 | const throttled = throttle(count, wait); 36 | throttled(); 37 | throttled(); 38 | 39 | t.is(count.callCount, 1, 'Should call once immediately'); 40 | 41 | await delay(wait + 10); 42 | t.is(count.callCount, 2, 'Should call again after wait interval'); 43 | }); 44 | 45 | test('throttled function preserves last context', async t => { 46 | let context; 47 | const wait = 100; 48 | const throttled = throttle(function () { 49 | context = this; // eslint-disable-line unicorn/no-this-assignment 50 | }, wait); 51 | 52 | const foo = {}; 53 | const bar = {}; 54 | throttled.call(foo); 55 | throttled.call(bar); 56 | 57 | t.is(context, foo, 'Context should be first call context initially'); 58 | 59 | await delay(wait + 5); 60 | t.is(context, bar, 'Context should be last call context after wait'); 61 | }); 62 | 63 | test('throttled function preserves last arguments', async t => { 64 | let arguments_; 65 | const wait = 100; 66 | const throttled = throttle((...localArguments) => { 67 | arguments_ = localArguments; 68 | }, wait); 69 | 70 | throttled(1); 71 | throttled(2); 72 | 73 | t.is(arguments_[0], 1, 'Arguments should be from first call initially'); 74 | 75 | await delay(wait + 5); 76 | t.is(arguments_[0], 2, 'Arguments should be from last call after wait'); 77 | }); 78 | 79 | test('throttled function handles rapid succession calls', async t => { 80 | const count = counter(); 81 | const wait = 50; 82 | const throttled = throttle(count, wait); 83 | 84 | throttled(); 85 | throttled(); 86 | throttled(); 87 | 88 | t.is(count.callCount, 1, 'Should call once immediately despite multiple rapid calls'); 89 | 90 | await delay(wait + 10); 91 | t.is(count.callCount, 2, 'Should call again after wait interval'); 92 | }); 93 | 94 | test('throttled function responds to different arguments', async t => { 95 | let lastArg; 96 | const wait = 50; 97 | const throttled = throttle(arg => { 98 | lastArg = arg; 99 | }, wait); 100 | 101 | throttled(1); 102 | throttled(2); 103 | throttled(3); 104 | 105 | t.is(lastArg, 1, 'Should capture first argument initially'); 106 | 107 | await delay(wait + 10); 108 | t.is(lastArg, 3, 'Should capture last argument after wait interval'); 109 | }); 110 | 111 | test('throttled function handles repeated calls post-wait', async t => { 112 | const count = counter(); 113 | const wait = 50; 114 | const throttled = throttle(count, wait); 115 | 116 | throttled(); 117 | await delay(wait + 10); 118 | throttled(); 119 | 120 | t.is(count.callCount, 2, 'Should allow a call after wait period has elapsed'); 121 | }); 122 | 123 | test('throttled function does not call function within wait time', async t => { 124 | const count = counter(); 125 | const wait = 100; 126 | const throttled = throttle(count, wait); 127 | 128 | throttled(); 129 | await delay(wait / 2); 130 | throttled(); 131 | 132 | t.is(count.callCount, 1, 'Should not call function again within wait time'); 133 | }); 134 | 135 | test('throttled function with zero wait time calls function immediately each time', t => { 136 | const count = counter(); 137 | const wait = 0; 138 | const throttled = throttle(count, wait); 139 | 140 | throttled(); 141 | throttled(); 142 | throttled(); 143 | 144 | t.is(count.callCount, 3, 'Should call function immediately on each invocation with zero wait time'); 145 | }); 146 | 147 | test('throttled function with large wait time delays subsequent calls appropriately', async t => { 148 | const count = counter(); 149 | const wait = 1000; // 1 second 150 | const throttled = throttle(count, wait); 151 | 152 | throttled(); 153 | t.is(count.callCount, 1, 'Should call function immediately for the first time'); 154 | 155 | // Attempt a call before the wait time elapses 156 | await delay(500); 157 | throttled(); 158 | t.is(count.callCount, 1, 'Should not call function again before wait time elapses'); 159 | 160 | // Check after the wait time 161 | await delay(600); // Total 1100ms 162 | t.is(count.callCount, 2, 'Should call function again after wait time elapses'); 163 | }); 164 | 165 | test('throttled function handles calls from different contexts', async t => { 166 | const wait = 100; 167 | 168 | const throttled = throttle(function () { 169 | this.callCount = (this.callCount ?? 0) + 1; 170 | }, wait); 171 | 172 | const objectA = {}; 173 | const objectB = {}; 174 | 175 | throttled.call(objectA); 176 | throttled.call(objectB); 177 | 178 | t.is(objectA.callCount, 1, 'Should call function with first context immediately'); 179 | t.is(objectB.callCount, undefined, 'Should not call function with second context immediately'); 180 | 181 | await delay(wait + 10); 182 | t.is(objectB.callCount, 1, 'Should call function with second context after wait time'); 183 | }); 184 | 185 | test('throttled function allows immediate invocation after wait time from last call', async t => { 186 | const count = counter(); 187 | const wait = 100; 188 | const throttled = throttle(count, wait); 189 | 190 | throttled(); 191 | await delay(wait + 10); 192 | throttled(); 193 | 194 | t.is(count.callCount, 2, 'Should allow immediate invocation after wait time from last call'); 195 | }); 196 | 197 | test('throttled function handles rapid calls with short delays', async t => { 198 | const count = counter(); 199 | const wait = 100; 200 | const throttled = throttle(count, wait); 201 | 202 | throttled(); 203 | await delay(30); 204 | throttled(); 205 | await delay(30); 206 | throttled(); 207 | 208 | t.is(count.callCount, 1, 'Should only call once despite rapid calls with short delays'); 209 | 210 | await delay(wait); 211 | t.is(count.callCount, 2, 'Should call again after wait time'); 212 | }); 213 | 214 | test('throttled function with extremely short wait time behaves correctly', async t => { 215 | const count = counter(); 216 | const wait = 1; // 1 millisecond 217 | const throttled = throttle(count, wait); 218 | 219 | throttled(); 220 | throttled(); 221 | throttled(); 222 | 223 | await delay(5); // Slightly longer than the wait time 224 | t.true(count.callCount >= 1, 'Should call at least once with extremely short wait time'); 225 | }); 226 | 227 | test('simultaneous throttled functions with different wait times operate independently', async t => { 228 | const count1 = counter(); 229 | const count2 = counter(); 230 | const wait1 = 50; 231 | const wait2 = 150; 232 | const throttled1 = throttle(count1, wait1); 233 | const throttled2 = throttle(count2, wait2); 234 | 235 | throttled1(); 236 | throttled2(); 237 | await delay(60); // Just over wait1, but under wait2 238 | throttled1(); 239 | throttled2(); 240 | 241 | t.is(count1.callCount, 2, 'First throttled function should be called twice'); 242 | t.is(count2.callCount, 1, 'Second throttled function should be called once'); 243 | }); 244 | 245 | test('throttled functions with side effects only apply effects once per interval', async t => { 246 | let sideEffectCounter = 0; 247 | const incrementSideEffect = () => { 248 | sideEffectCounter++; 249 | }; 250 | 251 | const wait = 100; 252 | const throttledIncrement = throttle(incrementSideEffect, wait); 253 | 254 | throttledIncrement(); 255 | throttledIncrement(); 256 | throttledIncrement(); 257 | 258 | t.is(sideEffectCounter, 1, 'Side effect should only have occurred once'); 259 | 260 | await delay(wait + 10); 261 | t.is(sideEffectCounter, 2, 'Side effect should occur again after wait time'); 262 | }); 263 | 264 | test('throttled function handles system time changes', async t => { 265 | const count = counter(); 266 | const wait = 100; 267 | const throttled = throttle(count, wait); 268 | 269 | const originalNow = Date.now; 270 | Date.now = () => originalNow() + 1000; // Simulate a time jump forward 271 | 272 | throttled(); 273 | throttled(); 274 | 275 | Date.now = originalNow; // Reset Date.now to original 276 | 277 | t.is(count.callCount, 1, 'Should respect throttling despite time change'); 278 | 279 | await delay(wait); 280 | t.is(count.callCount, 2, 'Should allow a call after wait time'); 281 | }); 282 | 283 | test('parameter validation', t => { 284 | t.throws(() => { 285 | throttle(undefined, 0); 286 | }, {instanceOf: TypeError}); 287 | 288 | /// t.throws(() => { 289 | // throttle(() => {}); 290 | // }, {instanceOf: TypeError}); 291 | }); 292 | --------------------------------------------------------------------------------