├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .mocharc.json ├── .nycrc ├── LICENSE ├── README.md ├── assets ├── diagrams.xml ├── dynamic.png └── fixed.png ├── dynamic └── index.ts ├── fixed └── index.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── browser │ └── set-interval-async.cts ├── clear-interval-async.cts ├── dynamic │ ├── set-interval-async.cts │ └── set-interval-async.mts ├── fixed │ ├── set-interval-async.cts │ └── set-interval-async.mts ├── set-interval-async-handler.cts ├── set-interval-async-strategy.cts └── set-interval-async-timer.cts ├── test ├── clear-interval-async.spec.ts ├── dynamic │ └── set-interval-async.spec.ts ├── exports.spec.ts └── fixed │ └── set-interval-async.spec.ts ├── tsconfig.build.json ├── tsconfig.json └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [14.x, 16.x, 18.x] 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | cache: 'npm' 23 | - run: npm ci 24 | - run: npm run lint 25 | - run: npm run build 26 | - run: npm run test 27 | - run: npm run coverage 28 | 29 | - name: Coveralls 30 | uses: coverallsapp/github-action@master 31 | with: 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Build outputs 64 | dist 65 | 66 | .DS_Store 67 | 68 | .vscode 69 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": ["ts-node/register", "source-map-support/register"], 3 | "spec": "test/**/*.spec.ts" 4 | } 5 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2022 Emilio Almansi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `setInterval``Async`
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) [![npm version](https://img.shields.io/npm/v/set-interval-async.svg?style=flat-square)](https://badge.fury.io/js/set-interval-async) [![Build Status](https://github.com/ealmansi/set-interval-async/actions/workflows/node.js.yml/badge.svg)](https://github.com/ealmansi/set-interval-async/actions) [![Coverage Status](https://img.shields.io/coveralls/github/ealmansi/set-interval-async.svg?style=flat-square)](https://coveralls.io/github/ealmansi/set-interval-async?branch=master) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/ealmansi/set-interval-async/pulls) 2 | 3 | Modern version of [setInterval](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval) for promises and async functions available in Node.js and browsers. 4 | 5 | `setIntervalAsync` works both on Node.js and in the browser, providing the same
6 | familiar interface as `setInterval` for asynchronous functions, while preventing
7 | multiple executions from overlapping in time. 8 | 9 | # Getting Started 10 | 11 | ## Node.js 12 | 13 | First, install `setIntervalAsync` using npm or yarn: 14 | 15 | ```bash 16 | # Using npm: 17 | npm install set-interval-async 18 | 19 | # Or using yarn: 20 | yarn add set-interval-async 21 | ``` 22 | 23 | Now, you can require `setIntervalAsync` in CommonJS: 24 | 25 | ```javascript 26 | const { setIntervalAsync, clearIntervalAsync } = require('set-interval-async'); 27 | ``` 28 | 29 | Or else, you can use ES6 modules syntax: 30 | 31 | ```javascript 32 | import { setIntervalAsync, clearIntervalAsync } from 'set-interval-async'; 33 | ``` 34 | 35 | ## Browser 36 | 37 | In the browser, you can add a script tag in your HTML: 38 | 39 | ```html 40 | 41 | 42 | 43 | 44 | 45 | ``` 46 | 47 | After the script is loaded, a variable called `SetIntervalAsync` will be defined in the global context. From there you can retrieve the `setIntervalAsync` and `clearIntervalAsync` functions. 48 | 49 | ```javascript 50 | var setIntervalAsync = SetIntervalAsync.setIntervalAsync; 51 | var clearIntervalAsync = SetIntervalAsync.clearIntervalAsync; 52 | ``` 53 | 54 | # Usage 55 | 56 | In the most basic scenario, you can use `setIntervalAsync` the same way you would use vanilla `setInterval`. For example, the following code will print 'Hello' to the console once every second. 57 | 58 | ```javascript 59 | const { setIntervalAsync, clearIntervalAsync } = require('set-interval-async'); 60 | 61 | setIntervalAsync(() => { 62 | console.log('Hello') 63 | }, 1000); 64 | ``` 65 | 66 | However, you can also provide an async function (or a function returning a promise), which has the added nicety that now you can wait until the cycle is fully stopped before moving on by using `clearIntervalAsync`. 67 | 68 | ```javascript 69 | const { setIntervalAsync, clearIntervalAsync } = require('set-interval-async'); 70 | 71 | const timer = setIntervalAsync(async () => { 72 | console.log('Hello') 73 | await doSomeWork() 74 | console.log('Bye') 75 | }, 1000); 76 | 77 | // Or equivalently: 78 | 79 | const timer = setIntervalAsync(() => { 80 | console.log('Hello') 81 | return doSomeWork().then( 82 | () => console.log('Bye') 83 | ) 84 | }, 1000); 85 | 86 | 87 | // Later: 88 | 89 | await clearIntervalAsync(timer); 90 | 91 | // At this point, all timers have been cleared, and the last 92 | // execution is guaranteed to have finished as well. 93 | ``` 94 | 95 | This is particularly useful when - for example at the end of a unit test - you want to make sure that no asynchronous code continues running by the time your test manager moves on to the next one. 96 | 97 | ```javascript 98 | it('should test something', async () => { 99 | const timer = setIntervalAsync(/* ... */); 100 | 101 | // Do some assertions. 102 | 103 | await clearIntervalAsync(timer); 104 | // Interval is now fully stopped. 105 | }); 106 | ``` 107 | 108 | ## When Should I Use `setIntervalAsync`? 109 | 110 | Where `setIntervalAsync` really shines is in those situations where the given asynchronous function might take longer to compute than the configured interval and, at the same time, is not safe to execute more than once at a time. Using vanilla `setInterval` will break your code in this scenario, whereas `setIntervalAsync` guarantees that the function will never execute more than once at the same time. 111 | 112 | For example, consider the following code: 113 | 114 | ```javascript 115 | async function processQueue (queue) { 116 | if (queue.length === 0) { 117 | return; 118 | } 119 | let head = queue[0]; 120 | await doSomeWork(head); 121 | queue.shift(); // Removes the first element. 122 | } 123 | ``` 124 | 125 | The function above should never get called again before the previous execution is completed. Otherwise, the queue's first element will get processed twice, and the second element will be skipped. 126 | 127 | However, with `setIntervalAsync`, the following code is perfectly safe: 128 | 129 | ```javascript 130 | setIntervalAsync(processQueue, 1000, queue) 131 | ``` 132 | 133 | since `setIntervalAsync` will guarantee that the function is never executed more than once at any given moment. 134 | 135 | You can choose whether you wish to use the `Dynamic` or `Fixed` strategies, which will either launch every execution as soon as possible or set a fixed delay between the end of one execution and the start of the next one. See [Dynamic and Fixed `setIntervalAsync`](#dynamic-and-fixed-setintervalasync) for more details. 136 | 137 | # Dynamic and Fixed `setIntervalAsync` 138 | 139 | `setIntervalAsync` provides two strategies which can be used to prevent a recurring function from executing more than once at any given moment: 140 | 141 | - **Dynamic**: If possible, the given function is called once every `interval` milliseconds. If any execution takes longer than the desired interval, the next execution is delayed until the previous one has finished, and called immediately after this condition is reached.

![Dynamic setIntervalAsync diagram.](https://github.com/ealmansi/set-interval-async/raw/master/assets/dynamic.png) 142 | 143 | - **Fixed**: The given function is called repeatedly, guaranteeing a fixed delay of `interval` milliseconds between the end of one execution and the start of the following one.

![Fixed setIntervalAsync diagram.](https://github.com/ealmansi/set-interval-async/raw/master/assets/fixed.png) 144 | 145 | You can choose whichever strategy works best for your application. When in doubt, the `Dynamic` strategy will likely suffice for most use cases, keeping the interval as close as possible to the desired one. The default strategy is `Dynamic`. 146 | 147 | ## In Node.js 148 | 149 | You can require a specific strategy for `setIntervalAsync` using CommonJS with the following snippets: 150 | 151 | ```javascript 152 | // Dynamic strategy. 153 | const { setIntervalAsync, clearIntervalAsync } = require('set-interval-async/dynamic'); 154 | 155 | // Fixed strategy. 156 | const { setIntervalAsync, clearIntervalAsync } = require('set-interval-async/fixed'); 157 | ``` 158 | 159 | Or else, you can use ES6 modules syntax: 160 | 161 | ```javascript 162 | // Dynamic strategy. 163 | import { setIntervalAsync, clearIntervalAsync } from 'set-interval-async/dynamic'; 164 | 165 | // Fixed strategy. 166 | import { setIntervalAsync, clearIntervalAsync } from 'set-interval-async/fixed'; 167 | ``` 168 | 169 | ## In the Browser 170 | 171 | After the library has been loaded to the page, a variable called `SetIntervalAsync` will be defined in the global context. From there you can retrieve the `setIntervalAsync` from your desired strategy and `clearIntervalAsync` functions. 172 | 173 | ```javascript 174 | // Dynamic strategy. 175 | var setIntervalAsync = SetIntervalAsync.dynamic.setIntervalAsync; 176 | var clearIntervalAsync = SetIntervalAsync.clearIntervalAsync; 177 | 178 | // Fixed strategy. 179 | var setIntervalAsync = SetIntervalAsync.fixed.setIntervalAsync; 180 | var clearIntervalAsync = SetIntervalAsync.clearIntervalAsync; 181 | ``` 182 | 183 | # API 184 | 185 | ## Function `setIntervalAsync` 186 | 187 | Executes the given `handler` every `intervalMs` milliseconds, while preventing multiple concurrent executions. The handler will never be executed concurrently more than once in any given moment. 188 | 189 | See [Dynamic and Fixed `setIntervalAsync`](#dynamic-and-fixed-setintervalasync) for more details on which strategies can be used to determine the effective interval between executions when the handler takes longer than the target interval to complete. The default strategy is `Dynamic`. 190 | 191 | ```typescript 192 | function setIntervalAsync( 193 | handler: SetIntervalAsyncHandler, 194 | intervalMs: number, 195 | ...handlerArgs: HandlerArgs 196 | ): SetIntervalAsyncTimer; 197 | ``` 198 | 199 | Note: when `intervalMs` is less than 1, it will be set to 1. When `intervalMs` is greater than 2147483647, it will be set to 2147483647. Non-integer values are truncated to an integer. 200 | 201 | ## Function `clearIntervalAsync` 202 | 203 | Stops an execution cycle started by `setIntervalAsync`. Any ongoing function executions will run until completion, but all future ones will be cancelled. 204 | 205 | ```typescript 206 | async function clearIntervalAsync( 207 | timer: SetIntervalAsyncTimer 208 | ): Promise; 209 | ``` 210 | 211 | The promise returned will resolve once the timer has been stopped and the ongoing execution (if available) has been completed. If the last execution ends in a promise rejection, the promise returned by `clearIntervalAsync` will reject with the same value. 212 | 213 | ## Type `SetIntervalAsyncHandler` 214 | 215 | Synchronous or asynchronous function that can be passed in as a handler to `setIntervalAsync`. 216 | 217 | ```typescript 218 | type SetIntervalAsyncHandler = ( 219 | ...handlerArgs: HandlerArgs 220 | ) => void | Promise; 221 | ``` 222 | 223 | ## Type `SetIntervalAsyncTimer` 224 | 225 | Return type of `setIntervalAsync`. Does not have any observable properties, but must be passed in as an argument to `clearIntervalAsync` to stop execution. 226 | 227 | ```typescript 228 | type SetIntervalAsyncTimer; 229 | ``` 230 | 231 | # Avoiding Deadlock When Clearing an Interval 232 | 233 | While calling `clearIntervalAsync` to stop an interval is perfectly safe in any circumstance, please note that awaiting its result *within the async handler itself* will lead to undesirable results. 234 | 235 | For example, the code below leads to a cyclical promise chain that will never be resolved (the `console.log` statement is unreachable). 236 | 237 | ```javascript 238 | const timer = setIntervalAsync(async () => { 239 | // ... 240 | if (shouldStop) { 241 | await clearIntervalAsync(timer); 242 | console.log('Stopped!'); 243 | } 244 | }, interval); 245 | ``` 246 | 247 | This is the case because: 248 | 249 | - `await clearIntervalAsync(timer)` will not resolve until the last execution has finished, *and* 250 | - the last execution will not finish until `await clearIntervalAsync(timer)` has been resolved. 251 | 252 | To prevent this cycle, always allow the async handler to complete without awaiting for the interval to be cleared. For example, by removing the `await` keyword entirely or by using an immediately-invoked function expression: 253 | 254 | ```javascript 255 | if (shouldStop) { 256 | (async () => { 257 | await clearIntervalAsync(timer); 258 | console.log('Stopped!'); 259 | })(); 260 | } 261 | ``` 262 | 263 | # Motivation 264 | 265 | If you've ever had to deal with weird, subtle bugs as a consequence of using `setInterval`[1] on asynchronous functions, or had to manually reimplement `setInterval` using `setTimeout`[2] to prevent multiple executions of the same asynchronous function from overlapping, then this library is a drop-in replacement that will solve your issues. 266 | 267 | `setInterval` runs a given function repeateadly, once every fixed number of milliseconds. This may cause problems whenever the function takes longer to execute than the given interval, since it will be called again before the first execution has finished. This is often a problem for non-reentrant functions; ie. functions that are not designed to allow multiple executions at the same time. 268 | 269 | `setIntervalAsync` is a drop-in replacement of `setInterval` which shares the same API but is safe to use with non-reentrant, asynchronous functions. 270 | 271 | [1] https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout
272 | [2] https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval 273 | 274 | # Contributing 275 | 276 | In order to contribute to this project, you will need to first clone the repository: 277 | 278 | ```bash 279 | git clone https://github.com/ealmansi/set-interval-async.git 280 | ``` 281 | 282 | Make sure that [Yarn](https://yarnpkg.com/en/) is installed globally on your system, 283 | install all project dependencies, and build the project: 284 | 285 | ```bash 286 | yarn 287 | yarn build 288 | ``` 289 | 290 | Now, you can run the tests and make sure that everything is up and running correctly: 291 | 292 | ```bash 293 | yarn test 294 | ``` 295 | 296 | If the previous step succeeds, you're ready to start developing on this project.
Pull requests are welcome! 297 | 298 | You can verify that your code follows the project's style conventions the with the following command: 299 | 300 | ```bash 301 | yarn lint 302 | ``` 303 | 304 | # License 305 | 306 | [MIT](https://raw.githubusercontent.com/ealmansi/set-interval-async/master/LICENSE) 307 | -------------------------------------------------------------------------------- /assets/diagrams.xml: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /assets/dynamic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ealmansi/set-interval-async/a4d82a156085877f9277bef21d35b7a8f4d67225/assets/dynamic.png -------------------------------------------------------------------------------- /assets/fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ealmansi/set-interval-async/a4d82a156085877f9277bef21d35b7a8f4d67225/assets/fixed.png -------------------------------------------------------------------------------- /dynamic/index.ts: -------------------------------------------------------------------------------- 1 | export * from "../dist/dynamic/set-interval-async.cjs"; 2 | -------------------------------------------------------------------------------- /fixed/index.ts: -------------------------------------------------------------------------------- 1 | export * from "../dist/fixed/set-interval-async.cjs"; 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | 3 | process.original = process; 4 | 5 | module.exports = { 6 | preset: 'ts-jest', 7 | testEnvironment: 'node', 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "set-interval-async", 3 | "version": "3.0.3", 4 | "description": "Modern version of setInterval for promises and async functions available in Node.js and browsers.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ealmansi/set-interval-async.git" 8 | }, 9 | "homepage": "https://github.com/ealmansi/set-interval-async", 10 | "keywords": [ 11 | "setInterval", 12 | "setTimeout", 13 | "clearInterval", 14 | "clearTimeout", 15 | "interval", 16 | "timeout", 17 | "milliseconds", 18 | "seconds", 19 | "promise", 20 | "promises", 21 | "async", 22 | "asynchronous", 23 | "await", 24 | "recurrent", 25 | "concurrency", 26 | "reentrancy" 27 | ], 28 | "main": "./dist/dynamic/set-interval-async.cjs", 29 | "types": "./dist/dynamic/set-interval-async.d.cts", 30 | "exports": { 31 | ".": { 32 | "types": "./dist/dynamic/set-interval-async.d.cts", 33 | "require": "./dist/dynamic/set-interval-async.cjs", 34 | "import": "./dist/dynamic/set-interval-async.mjs" 35 | }, 36 | "./dynamic": { 37 | "types": "./dist/dynamic/set-interval-async.d.cts", 38 | "require": "./dist/dynamic/set-interval-async.cjs", 39 | "import": "./dist/dynamic/set-interval-async.mjs" 40 | }, 41 | "./fixed": { 42 | "types": "./dist/fixed/set-interval-async.d.cts", 43 | "require": "./dist/fixed/set-interval-async.cjs", 44 | "import": "./dist/fixed/set-interval-async.mjs" 45 | } 46 | }, 47 | "library": "SetIntervalAsync", 48 | "unpkg": "dist/browser/set-interval-async.iife.js", 49 | "jsdelivr": "dist/browser/set-interval-async.iife.js", 50 | "files": [ 51 | "dist", 52 | "dynamic", 53 | "fixed" 54 | ], 55 | "author": "Emilio Almansi", 56 | "license": "MIT", 57 | "engines": { 58 | "node": ">= 14.0.0" 59 | }, 60 | "scripts": { 61 | "clean": "rimraf dist", 62 | "build": "npm run build:tsc && npm run build:webpack", 63 | "build:tsc": "tsc --project tsconfig.build.json", 64 | "build:webpack": "webpack", 65 | "lint": "eslint src test && prettier --check src test", 66 | "prepack": "npm run clean && npm run build && npm run lint && npm run test", 67 | "format": "prettier --write src test", 68 | "test": "nyc mocha", 69 | "coverage": "nyc report --reporter=lcov" 70 | }, 71 | "devDependencies": { 72 | "@babel/core": "^7.18.10", 73 | "@babel/plugin-transform-modules-commonjs": "^7.18.6", 74 | "@babel/plugin-transform-runtime": "^7.18.10", 75 | "@babel/preset-env": "^7.18.10", 76 | "@istanbuljs/nyc-config-typescript": "^1.0.2", 77 | "@sinonjs/fake-timers": "^9.1.2", 78 | "@types/mocha": "^9.1.1", 79 | "@types/node": "^18.6.4", 80 | "@types/sinon": "^10.0.13", 81 | "@types/sinonjs__fake-timers": "^8.1.2", 82 | "@typescript-eslint/eslint-plugin": "^5.32.0", 83 | "@typescript-eslint/parser": "^5.32.0", 84 | "babel-loader": "^8.2.5", 85 | "eslint": "^8.21.0", 86 | "mocha": "^10.0.0", 87 | "nyc": "^15.1.0", 88 | "prettier": "^2.7.1", 89 | "rimraf": "^3.0.2", 90 | "sinon": "^14.0.0", 91 | "source-map-support": "^0.5.21", 92 | "ts-node": "^10.9.1", 93 | "typescript": "^4.7.4", 94 | "webpack": "^5.74.0", 95 | "webpack-cli": "^4.10.0" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/browser/set-interval-async.cts: -------------------------------------------------------------------------------- 1 | import { clearIntervalAsync } from "../clear-interval-async.cjs"; 2 | import { setIntervalAsync as setIntervalAsyncDynamic } from "../dynamic/set-interval-async.cjs"; 3 | import { setIntervalAsync as setIntervalAsyncFixed } from "../fixed/set-interval-async.cjs"; 4 | 5 | const setIntervalAsync = setIntervalAsyncDynamic; 6 | 7 | const dynamic = { 8 | setIntervalAsync: setIntervalAsyncDynamic, 9 | }; 10 | 11 | const fixed = { 12 | setIntervalAsync: setIntervalAsyncFixed, 13 | }; 14 | 15 | export { setIntervalAsync, clearIntervalAsync, dynamic, fixed }; 16 | -------------------------------------------------------------------------------- /src/clear-interval-async.cts: -------------------------------------------------------------------------------- 1 | import { SetIntervalAsyncTimer } from "./set-interval-async-timer.cjs"; 2 | 3 | /** 4 | * Stops an execution cycle started by setIntervalAsync. 5 | * Any ongoing function executions will run until completion, 6 | * but all future ones will be cancelled. 7 | */ 8 | export async function clearIntervalAsync( 9 | timer: SetIntervalAsyncTimer 10 | ): Promise { 11 | if (!(timer instanceof SetIntervalAsyncTimer)) { 12 | throw new TypeError( 13 | "First argument is not an instance of SetIntervalAsyncTimer" 14 | ); 15 | } 16 | await SetIntervalAsyncTimer.stopTimer(timer); 17 | } 18 | -------------------------------------------------------------------------------- /src/dynamic/set-interval-async.cts: -------------------------------------------------------------------------------- 1 | import { clearIntervalAsync } from "../clear-interval-async.cjs"; 2 | import { SetIntervalAsyncHandler } from "../set-interval-async-handler.cjs"; 3 | import { SetIntervalAsyncTimer } from "../set-interval-async-timer.cjs"; 4 | 5 | export { clearIntervalAsync }; 6 | export type { SetIntervalAsyncHandler }; 7 | export type { SetIntervalAsyncTimer }; 8 | 9 | /** 10 | * Executes the given handler at fixed intervals, while preventing 11 | * multiple concurrent executions. The handler will never be executed 12 | * concurrently more than once in any given moment, providing a fixed 13 | * time interval between the end of a given execution and the start of 14 | * the following one. 15 | */ 16 | export function setIntervalAsync( 17 | handler: SetIntervalAsyncHandler, 18 | intervalMs: number, 19 | ...handlerArgs: HandlerArgs 20 | ): SetIntervalAsyncTimer { 21 | if (!(typeof handler === "function")) { 22 | throw new TypeError("First argument is not a function"); 23 | } 24 | if (!(typeof intervalMs === "number")) { 25 | throw new TypeError("Second argument is not a number"); 26 | } 27 | return SetIntervalAsyncTimer.startTimer( 28 | "dynamic", 29 | handler, 30 | intervalMs, 31 | ...handlerArgs 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/dynamic/set-interval-async.mts: -------------------------------------------------------------------------------- 1 | import { clearIntervalAsync } from "../clear-interval-async.cjs"; 2 | import { setIntervalAsync } from "./set-interval-async.cjs"; 3 | import { SetIntervalAsyncHandler } from "../set-interval-async-handler.cjs"; 4 | import type { SetIntervalAsyncTimer } from "../set-interval-async-timer.cjs"; 5 | 6 | export { clearIntervalAsync }; 7 | export { setIntervalAsync }; 8 | export type { SetIntervalAsyncHandler }; 9 | export type { SetIntervalAsyncTimer }; 10 | -------------------------------------------------------------------------------- /src/fixed/set-interval-async.cts: -------------------------------------------------------------------------------- 1 | import { clearIntervalAsync } from "../clear-interval-async.cjs"; 2 | import { SetIntervalAsyncHandler } from "../set-interval-async-handler.cjs"; 3 | import { SetIntervalAsyncTimer } from "../set-interval-async-timer.cjs"; 4 | 5 | export { clearIntervalAsync }; 6 | export type { SetIntervalAsyncHandler }; 7 | export type { SetIntervalAsyncTimer }; 8 | 9 | /** 10 | * Executes the given handler at fixed intervals, while preventing 11 | * multiple concurrent executions. The handler will never be executed 12 | * concurrently more than once in any given moment, providing a fixed 13 | * time interval between the end of a given execution and the start of 14 | * the following one. 15 | */ 16 | export function setIntervalAsync( 17 | handler: SetIntervalAsyncHandler, 18 | intervalMs: number, 19 | ...handlerArgs: HandlerArgs 20 | ): SetIntervalAsyncTimer { 21 | if (!(typeof handler === "function")) { 22 | throw new TypeError("First argument is not a function"); 23 | } 24 | if (!(typeof intervalMs === "number")) { 25 | throw new TypeError("Second argument is not a number"); 26 | } 27 | return SetIntervalAsyncTimer.startTimer( 28 | "fixed", 29 | handler, 30 | intervalMs, 31 | ...handlerArgs 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/fixed/set-interval-async.mts: -------------------------------------------------------------------------------- 1 | import { clearIntervalAsync } from "../clear-interval-async.cjs"; 2 | import { setIntervalAsync } from "./set-interval-async.cjs"; 3 | import { SetIntervalAsyncHandler } from "../set-interval-async-handler.cjs"; 4 | import type { SetIntervalAsyncTimer } from "../set-interval-async-timer.cjs"; 5 | 6 | export { clearIntervalAsync }; 7 | export { setIntervalAsync }; 8 | export type { SetIntervalAsyncHandler }; 9 | export type { SetIntervalAsyncTimer }; 10 | -------------------------------------------------------------------------------- /src/set-interval-async-handler.cts: -------------------------------------------------------------------------------- 1 | export type SetIntervalAsyncHandler = ( 2 | ...handlerArgs: HandlerArgs 3 | ) => void | Promise; 4 | -------------------------------------------------------------------------------- /src/set-interval-async-strategy.cts: -------------------------------------------------------------------------------- 1 | export type SetIntervalAsyncStrategy = "dynamic" | "fixed"; 2 | -------------------------------------------------------------------------------- /src/set-interval-async-timer.cts: -------------------------------------------------------------------------------- 1 | import { SetIntervalAsyncStrategy } from "./set-interval-async-strategy.cjs"; 2 | import { SetIntervalAsyncHandler } from "./set-interval-async-handler.cjs"; 3 | 4 | declare type NativeTimeout = unknown; 5 | 6 | declare function setTimeout( 7 | handler: (...args: unknown[]) => void, 8 | delayMs: number, 9 | ...args: unknown[] 10 | ): NativeTimeout; 11 | 12 | declare function clearTimeout(timeout: NativeTimeout): void; 13 | 14 | const MIN_INTERVAL_MS = 10; 15 | const MAX_INTERVAL_MS = 2147483647; 16 | 17 | export class SetIntervalAsyncTimer { 18 | #timeout: NativeTimeout | undefined = undefined; 19 | #promise: Promise | undefined = undefined; 20 | #stopped = false; 21 | 22 | static startTimer( 23 | strategy: SetIntervalAsyncStrategy, 24 | handler: SetIntervalAsyncHandler, 25 | intervalMs: number, 26 | ...handlerArgs: HandlerArgs 27 | ): SetIntervalAsyncTimer { 28 | intervalMs = Math.min( 29 | Math.max(Math.trunc(intervalMs), MIN_INTERVAL_MS), 30 | MAX_INTERVAL_MS 31 | ); 32 | const timer = new SetIntervalAsyncTimer(); 33 | timer.#scheduleTimeout( 34 | strategy, 35 | handler, 36 | intervalMs, 37 | intervalMs, 38 | ...handlerArgs 39 | ); 40 | return timer; 41 | } 42 | 43 | static async stopTimer( 44 | timer: SetIntervalAsyncTimer 45 | ): Promise { 46 | timer.#stopped = true; 47 | if (timer.#timeout) { 48 | clearTimeout(timer.#timeout); 49 | } 50 | if (timer.#promise) { 51 | await timer.#promise; 52 | } 53 | } 54 | 55 | #scheduleTimeout( 56 | strategy: SetIntervalAsyncStrategy, 57 | handler: SetIntervalAsyncHandler, 58 | intervalMs: number, 59 | delayMs: number, 60 | ...handlerArgs: HandlerArgs 61 | ): void { 62 | this.#timeout = setTimeout(async () => { 63 | this.#timeout = undefined; 64 | this.#promise = this.#runHandlerAndScheduleTimeout( 65 | strategy, 66 | handler, 67 | intervalMs, 68 | ...handlerArgs 69 | ); 70 | await this.#promise; 71 | this.#promise = undefined; 72 | }, delayMs); 73 | } 74 | 75 | async #runHandlerAndScheduleTimeout( 76 | strategy: SetIntervalAsyncStrategy, 77 | handler: SetIntervalAsyncHandler, 78 | intervalMs: number, 79 | ...handlerArgs: HandlerArgs 80 | ): Promise { 81 | const startTimeMs = new Date().getTime(); 82 | try { 83 | await handler(...handlerArgs); 84 | } finally { 85 | if (!this.#stopped) { 86 | const executionTimeMs = new Date().getTime() - startTimeMs; 87 | const delayMs = 88 | strategy === "dynamic" 89 | ? intervalMs > executionTimeMs 90 | ? intervalMs - executionTimeMs 91 | : 0 92 | : intervalMs; 93 | this.#scheduleTimeout( 94 | strategy, 95 | handler, 96 | intervalMs, 97 | delayMs, 98 | ...handlerArgs 99 | ); 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/clear-interval-async.spec.ts: -------------------------------------------------------------------------------- 1 | import { install, InstalledClock } from "@sinonjs/fake-timers"; 2 | import sinon from "sinon"; 3 | import { strict as assert } from "assert"; 4 | import { 5 | setIntervalAsync as setIntervalAsyncDynamic, 6 | clearIntervalAsync as clearIntervalAsyncDynamic, 7 | SetIntervalAsyncTimer, 8 | } from "set-interval-async/dynamic"; 9 | import { 10 | setIntervalAsync as setIntervalAsyncFixed, 11 | clearIntervalAsync as clearIntervalAsyncFixed, 12 | } from "set-interval-async/fixed"; 13 | 14 | for (const [strategy, setIntervalAsync, clearIntervalAsync] of [ 15 | ["Dynamic", setIntervalAsyncDynamic, clearIntervalAsyncDynamic], 16 | ["Fixed", setIntervalAsyncFixed, clearIntervalAsyncFixed], 17 | ] as const) { 18 | describe(`[${strategy}] clearIntervalAsync`, () => { 19 | let clock: InstalledClock; 20 | 21 | beforeEach(() => { 22 | clock = install(); 23 | }); 24 | 25 | afterEach(() => { 26 | clock.uninstall(); 27 | }); 28 | 29 | it("should fail if timer is not an instance of SetIntervalAsyncTimer", async () => { 30 | const invalidTimers = [null, undefined, 0, "str", {}, []]; 31 | for (const invalidTimer of invalidTimers) { 32 | try { 33 | await clearIntervalAsync(invalidTimer as SetIntervalAsyncTimer<[]>); 34 | assert.fail("Did not throw"); 35 | } catch (err: unknown) { 36 | assert.ok(err instanceof TypeError); 37 | } 38 | } 39 | }); 40 | 41 | it(`should stop running successfully before the first iteration`, async () => { 42 | const intervalMs = 100; 43 | const handler = sinon.fake(async () => { 44 | /* empty */ 45 | }); 46 | const timer = setIntervalAsync(handler, intervalMs); 47 | await clearIntervalAsync(timer); 48 | assert.equal(handler.callCount, 0); 49 | }); 50 | 51 | it(`should stop running successfully after the first iteration`, async () => { 52 | const iterationCount = 10; 53 | const intervalMs = 100; 54 | const handler = sinon.fake(async () => { 55 | /* empty */ 56 | }); 57 | const timer = setIntervalAsync(handler, intervalMs); 58 | for (let iteration = 1; iteration <= iterationCount; ++iteration) { 59 | await clock.nextAsync(); 60 | } 61 | await clearIntervalAsync(timer); 62 | assert.equal(handler.callCount, iterationCount); 63 | await clock.nextAsync(); 64 | assert.equal(handler.callCount, iterationCount); 65 | }); 66 | 67 | it(`should stop running successfully in the middle of an iteration`, async () => { 68 | const intervalMs = 1000; 69 | const handler = sinon.fake(async () => { 70 | await new Promise((resolve) => clock.setTimeout(resolve, 100)); 71 | await new Promise((resolve) => clock.setTimeout(resolve, 100)); 72 | }); 73 | const timer = setIntervalAsync(handler, intervalMs); 74 | await clock.nextAsync(); 75 | await clock.nextAsync(); 76 | const cleared = clearIntervalAsync(timer); 77 | await clock.nextAsync(); 78 | await cleared; 79 | }); 80 | 81 | it(`should throw if the last iteration throws`, async () => { 82 | const intervalMs = 100; 83 | const handler = sinon.fake(async () => { 84 | throw new Error("Some Error"); 85 | }); 86 | const timer = setIntervalAsync(handler, intervalMs); 87 | await clock.nextAsync(); 88 | try { 89 | await clearIntervalAsync(timer); 90 | assert.fail("Did not throw."); 91 | } catch (_) { 92 | /* empty */ 93 | } 94 | }); 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /test/dynamic/set-interval-async.spec.ts: -------------------------------------------------------------------------------- 1 | import { install, InstalledClock } from "@sinonjs/fake-timers"; 2 | import { 3 | setIntervalAsync, 4 | SetIntervalAsyncHandler, 5 | } from "set-interval-async/dynamic"; 6 | import { strict as assert } from "assert"; 7 | import sinon from "sinon"; 8 | 9 | describe("[Dynamic] setIntervalAsync", () => { 10 | let clock: InstalledClock; 11 | 12 | beforeEach(() => { 13 | clock = install(); 14 | }); 15 | 16 | afterEach(() => { 17 | clock.uninstall(); 18 | }); 19 | 20 | it("should fail if handler is not a function", async () => { 21 | const invalidHandlers = [null, undefined, 0, "str", {}, []]; 22 | const intervalMs = 1000; 23 | for (const invalidHandler of invalidHandlers) { 24 | try { 25 | setIntervalAsync( 26 | invalidHandler as unknown as SetIntervalAsyncHandler<[]>, 27 | intervalMs 28 | ); 29 | assert.fail("Did not throw"); 30 | } catch (err: unknown) { 31 | assert.ok(err instanceof TypeError); 32 | } 33 | } 34 | }); 35 | 36 | it("should fail if interval is not a number", async () => { 37 | const handlerDurationMs = 100; 38 | const handler = createHandlerForTest(handlerDurationMs); 39 | const invalidIntervalMss = [null, undefined, "str", {}, [], new Function()]; 40 | for (const invalidIntervalMs of invalidIntervalMss) { 41 | try { 42 | setIntervalAsync(handler, invalidIntervalMs as number); 43 | assert.fail("Did not throw"); 44 | } catch (err: unknown) { 45 | assert.ok(err instanceof TypeError); 46 | } 47 | } 48 | }); 49 | 50 | it("should run every intervalMs when intervalMs > handlerDurationMs", async () => { 51 | const iterationCount = 100; 52 | const intervalMs = 1000; 53 | const handlerDurationMs = 100; 54 | const success = await runTest( 55 | iterationCount, 56 | intervalMs, 57 | handlerDurationMs 58 | ); 59 | assert.ok(success); 60 | }); 61 | 62 | it("should run every intervalMs when intervalMs = handlerDurationMs", async () => { 63 | const iterationCount = 100; 64 | const intervalMs = 100; 65 | const handlerDurationMs = 100; 66 | const success = await runTest( 67 | iterationCount, 68 | intervalMs, 69 | handlerDurationMs 70 | ); 71 | assert.ok(success); 72 | }); 73 | 74 | it("should run every handlerDurationMs when intervalMs < handlerDurationMs", async () => { 75 | const iterationCount = 100; 76 | const intervalMs = 100; 77 | const handlerDurationMs = 1000; 78 | const handler = createHandlerForTest(handlerDurationMs); 79 | setIntervalAsync(handler, intervalMs); 80 | await clock.nextAsync(); 81 | assert.equal(getCurrentTime(), intervalMs); 82 | await clock.nextAsync(); 83 | assert.equal(getCurrentTime(), intervalMs + handlerDurationMs); 84 | for (let iteration = 2; iteration <= iterationCount; ++iteration) { 85 | await clock.nextAsync(); 86 | assert.equal( 87 | getCurrentTime(), 88 | intervalMs + (iteration - 1) * handlerDurationMs 89 | ); 90 | await clock.nextAsync(); 91 | assert.equal( 92 | getCurrentTime(), 93 | intervalMs + iteration * handlerDurationMs 94 | ); 95 | } 96 | assert.equal(handler.callCount, iterationCount); 97 | }); 98 | 99 | it("should continue running even if handler throws error", async () => { 100 | const iterationCount = 100; 101 | const intervalMs = 100; 102 | const handler = sinon.fake(async () => { 103 | throw new Error("Some Error"); 104 | }); 105 | setIntervalAsync(handler, intervalMs); 106 | for (let iteration = 1; iteration <= iterationCount; ++iteration) { 107 | await clock.nextAsync(); 108 | } 109 | assert.equal(handler.callCount, iterationCount); 110 | }); 111 | 112 | async function runTest( 113 | iterationCount: number, 114 | intervalMs: number, 115 | handlerDurationMs: number 116 | ) { 117 | const handler = createHandlerForTest(handlerDurationMs); 118 | setIntervalAsync(handler, intervalMs); 119 | for (let iteration = 1; iteration <= iterationCount; ++iteration) { 120 | await clock.nextAsync(); 121 | assert.equal(getCurrentTime(), iteration * intervalMs); 122 | await clock.nextAsync(); 123 | assert.equal( 124 | getCurrentTime(), 125 | iteration * intervalMs + handlerDurationMs 126 | ); 127 | } 128 | assert.equal(handler.callCount, iterationCount); 129 | return true; 130 | } 131 | 132 | function createHandlerForTest( 133 | handlerDurationMs: number 134 | ): sinon.SinonSpy<[], Promise> { 135 | return sinon.fake(async () => { 136 | await new Promise((resolve) => 137 | clock.setTimeout(resolve, handlerDurationMs) 138 | ); 139 | }); 140 | } 141 | 142 | function getCurrentTime(): number { 143 | return new clock.Date().getTime(); 144 | } 145 | }); 146 | -------------------------------------------------------------------------------- /test/exports.spec.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from "assert"; 2 | import { setIntervalAsync, clearIntervalAsync } from "set-interval-async"; 3 | import { 4 | setIntervalAsync as setIntervalAsyncDynamic, 5 | clearIntervalAsync as clearIntervalAsyncDynamic, 6 | } from "set-interval-async/dynamic"; 7 | import { 8 | setIntervalAsync as setIntervalAsyncFixed, 9 | clearIntervalAsync as clearIntervalAsyncFixed, 10 | } from "set-interval-async/fixed"; 11 | 12 | describe("Exports", () => { 13 | it("should export setIntervalAsync and clearIntervalAsync from set-interval-async/dynamic ", () => { 14 | assert.ok(setIntervalAsyncDynamic); 15 | assert.ok(clearIntervalAsyncDynamic); 16 | }); 17 | 18 | it("should export setIntervalAsync and clearIntervalAsync from set-interval-async/fixed ", () => { 19 | assert.ok(setIntervalAsyncFixed); 20 | assert.ok(clearIntervalAsyncFixed); 21 | }); 22 | 23 | it("should export dynamic setIntervalAsync and clearIntervalAsync from set-interval-async ", () => { 24 | assert.equal(setIntervalAsync, setIntervalAsyncDynamic); 25 | assert.equal(clearIntervalAsync, clearIntervalAsyncDynamic); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/fixed/set-interval-async.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | setIntervalAsync, 3 | SetIntervalAsyncHandler, 4 | } from "set-interval-async/fixed"; 5 | import { install, InstalledClock } from "@sinonjs/fake-timers"; 6 | import { strict as assert } from "assert"; 7 | import sinon from "sinon"; 8 | 9 | describe("[Fixed] setIntervalAsync", () => { 10 | let clock: InstalledClock; 11 | 12 | beforeEach(() => { 13 | clock = install(); 14 | }); 15 | 16 | afterEach(() => { 17 | clock.uninstall(); 18 | }); 19 | 20 | it("should fail if handler is not a function", async () => { 21 | const invalidHandlers = [null, undefined, 0, "str", {}, []]; 22 | const intervalMs = 1000; 23 | for (const invalidHandler of invalidHandlers) { 24 | try { 25 | setIntervalAsync( 26 | invalidHandler as unknown as SetIntervalAsyncHandler<[]>, 27 | intervalMs 28 | ); 29 | assert.fail("Did not throw"); 30 | } catch (err: unknown) { 31 | assert.ok(err instanceof TypeError); 32 | } 33 | } 34 | }); 35 | 36 | it("should fail if interval is not a number", async () => { 37 | const handlerDurationMs = 100; 38 | const handler = createHandlerForTest(handlerDurationMs); 39 | const invalidIntervalMss = [null, undefined, "str", {}, [], new Function()]; 40 | for (const invalidIntervalMs of invalidIntervalMss) { 41 | try { 42 | setIntervalAsync(handler, invalidIntervalMs as number); 43 | assert.fail("Did not throw"); 44 | } catch (err: unknown) { 45 | assert.ok(err instanceof TypeError); 46 | } 47 | } 48 | }); 49 | 50 | it("should run every intervalMs when intervalMs > handlerDurationMs", async () => { 51 | const iterationCount = 100; 52 | const intervalMs = 1000; 53 | const handlerDurationMs = 100; 54 | const success = await runTest( 55 | iterationCount, 56 | intervalMs, 57 | handlerDurationMs 58 | ); 59 | assert.ok(success); 60 | }); 61 | 62 | it("should run every intervalMs when intervalMs = handlerDurationMs", async () => { 63 | const iterationCount = 100; 64 | const intervalMs = 100; 65 | const handlerDurationMs = 100; 66 | const success = await runTest( 67 | iterationCount, 68 | intervalMs, 69 | handlerDurationMs 70 | ); 71 | assert.ok(success); 72 | }); 73 | 74 | it("should run every intervalMs when intervalMs < handlerDurationMs", async () => { 75 | const iterationCount = 100; 76 | const intervalMs = 100; 77 | const handlerDurationMs = 1000; 78 | const success = await runTest( 79 | iterationCount, 80 | intervalMs, 81 | handlerDurationMs 82 | ); 83 | assert.ok(success); 84 | }); 85 | 86 | it("should continue running even if handler throws error", async () => { 87 | const iterationCount = 100; 88 | const intervalMs = 100; 89 | const handler = sinon.fake(async () => { 90 | throw new Error("Some Error"); 91 | }); 92 | setIntervalAsync(handler, intervalMs); 93 | for (let iteration = 1; iteration <= iterationCount; ++iteration) { 94 | await clock.nextAsync(); 95 | } 96 | assert.equal(handler.callCount, iterationCount); 97 | }); 98 | 99 | async function runTest( 100 | iterationCount: number, 101 | intervalMs: number, 102 | handlerDurationMs: number 103 | ) { 104 | const handler = createHandlerForTest(handlerDurationMs); 105 | setIntervalAsync(handler, intervalMs); 106 | for (let iteration = 1; iteration <= iterationCount; ++iteration) { 107 | await clock.nextAsync(); 108 | assert.equal( 109 | getCurrentTime(), 110 | iteration * intervalMs + (iteration - 1) * handlerDurationMs 111 | ); 112 | await clock.nextAsync(); 113 | assert.equal( 114 | getCurrentTime(), 115 | iteration * (intervalMs + handlerDurationMs) 116 | ); 117 | } 118 | assert.equal(handler.callCount, iterationCount); 119 | return true; 120 | } 121 | 122 | function createHandlerForTest( 123 | handlerDurationMs: number 124 | ): sinon.SinonSpy<[], Promise> { 125 | return sinon.fake(async () => { 126 | await new Promise((resolve) => 127 | clock.setTimeout(resolve, handlerDurationMs) 128 | ); 129 | }); 130 | } 131 | 132 | function getCurrentTime(): number { 133 | return new clock.Date().getTime(); 134 | } 135 | }); 136 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src" 5 | }, 6 | "exclude": [ 7 | "test/**/*" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext"], 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "declaration": true, 8 | "outDir": "./dist", 9 | "isolatedModules": true, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "strictFunctionTypes": true, 16 | "strictBindCallApply": true, 17 | "strictPropertyInitialization": true, 18 | "noImplicitThis": true, 19 | "useUnknownInCatchVariables": true, 20 | "alwaysStrict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "exactOptionalPropertyTypes": true, 24 | "noImplicitReturns": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "noUncheckedIndexedAccess": true, 27 | "noImplicitOverride": true, 28 | "noPropertyAccessFromIndexSignature": true, 29 | "skipLibCheck": true 30 | }, 31 | "include": ["src/**/*", "test/**/*"], 32 | "exclude": ["node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const fs = require('fs'); 4 | 5 | module.exports = { 6 | mode: "production", 7 | devtool: false, 8 | entry: './dist/browser/set-interval-async.cjs', 9 | output: { 10 | iife: true, 11 | library: 'SetIntervalAsync', 12 | filename: 'set-interval-async.iife.js', 13 | path: path.resolve(__dirname, 'dist', 'browser'), 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.cjs$/, 19 | exclude: /(node_modules)/, 20 | use: { 21 | loader: 'babel-loader', 22 | options: { 23 | presets: [ 24 | ['@babel/preset-env', { 25 | targets: '> 0.4%, not dead', 26 | }], 27 | ], 28 | plugins: [ 29 | '@babel/plugin-transform-modules-commonjs', 30 | '@babel/transform-runtime', 31 | ], 32 | }, 33 | }, 34 | } 35 | ], 36 | }, 37 | plugins: [ 38 | new webpack.BannerPlugin( 39 | fs.readFileSync( 40 | path.resolve(__dirname, "LICENSE"), 41 | ).toString(), 42 | ), 43 | ], 44 | }; 45 | --------------------------------------------------------------------------------