├── .editorconfig ├── .gitattributes ├── .github ├── funding.yml ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── index.d.ts ├── index.js ├── index.test-d.ts ├── 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/funding.yml: -------------------------------------------------------------------------------- 1 | github: sindresorhus 2 | open_collective: sindresorhus 3 | tidelift: npm/delay 4 | custom: https://sindresorhus.com/donate 5 | -------------------------------------------------------------------------------- /.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 | - 16 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type Options = { 2 | /** 3 | A value to resolve in the returned promise. 4 | 5 | @example 6 | ``` 7 | import delay from 'delay'; 8 | 9 | const result = await delay(100, {value: '🦄'}); 10 | 11 | // Executed after 100 milliseconds 12 | console.log(result); 13 | //=> '🦄' 14 | ``` 15 | */ 16 | value?: T; 17 | 18 | /** 19 | An `AbortSignal` to abort the delay. 20 | 21 | The returned promise will be rejected with an `AbortError` if the signal is aborted. 22 | 23 | @example 24 | ``` 25 | import delay from 'delay'; 26 | 27 | const abortController = new AbortController(); 28 | 29 | setTimeout(() => { 30 | abortController.abort(); 31 | }, 500); 32 | 33 | try { 34 | await delay(1000, {signal: abortController.signal}); 35 | } catch (error) { 36 | // 500 milliseconds later 37 | console.log(error.name) 38 | //=> 'AbortError' 39 | } 40 | ``` 41 | */ 42 | signal?: AbortSignal; 43 | }; 44 | 45 | /** 46 | Create a promise which resolves after the specified `milliseconds`. 47 | 48 | @param milliseconds - Milliseconds to delay the promise. 49 | @returns A promise which resolves after the specified `milliseconds`. 50 | 51 | @example 52 | ``` 53 | import delay from 'delay'; 54 | 55 | bar(); 56 | 57 | await delay(100); 58 | 59 | // Executed 100 milliseconds later 60 | baz(); 61 | ``` 62 | */ 63 | export default function delay( 64 | milliseconds: number, 65 | options?: Options 66 | ): Promise; 67 | 68 | /** 69 | Create a promise which resolves after a random amount of milliseconds between `minimum` and `maximum` has passed. 70 | 71 | Useful for tests and web scraping since they can have unpredictable performance. For example, if you have a test that asserts a method should not take longer than a certain amount of time, and then run it on a CI, it could take longer. So with this method, you could give it a threshold instead. 72 | 73 | @param minimum - Minimum amount of milliseconds to delay the promise. 74 | @param maximum - Maximum amount of milliseconds to delay the promise. 75 | @returns A promise which resolves after a random amount of milliseconds between `maximum` and `maximum` has passed. 76 | */ 77 | export function rangeDelay( 78 | minimum: number, 79 | maximum: number, 80 | options?: Options 81 | ): Promise; 82 | 83 | /** 84 | Clears the delay and settles the promise. 85 | 86 | If you pass in a promise that is already cleared or a promise coming from somewhere else, it does nothing. 87 | 88 | @example 89 | ``` 90 | import delay, {clearDelay} from 'delay'; 91 | 92 | const delayedPromise = delay(1000, {value: 'Done'}); 93 | 94 | setTimeout(() => { 95 | clearDelay(delayedPromise); 96 | }, 500); 97 | 98 | // 500 milliseconds later 99 | console.log(await delayedPromise); 100 | //=> 'Done' 101 | ``` 102 | */ 103 | export function clearDelay(delayPromise: Promise): void; 104 | 105 | // The types are intentionally loose to make it work with both Node.js and browser versions of these methods. 106 | /** 107 | Creates a new `delay` instance using the provided functions for clearing and setting timeouts. Useful if you're about to stub timers globally, but you still want to use `delay` to manage your tests. 108 | 109 | @example 110 | ``` 111 | import {createDelay} from 'delay'; 112 | 113 | const customDelay = createDelay({clearTimeout, setTimeout}); 114 | 115 | const result = await customDelay(100, {value: '🦄'}); 116 | 117 | // Executed after 100 milliseconds 118 | console.log(result); 119 | //=> '🦄' 120 | ``` 121 | */ 122 | export function createDelay(timers: { 123 | clearTimeout: (timeoutId: any) => void; 124 | setTimeout: (callback: (...args: any[]) => void, milliseconds: number, ...args: any[]) => unknown; 125 | }): typeof delay; 126 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // From https://github.com/sindresorhus/random-int/blob/c37741b56f76b9160b0b63dae4e9c64875128146/index.js#L13-L15 2 | const randomInteger = (minimum, maximum) => Math.floor((Math.random() * (maximum - minimum + 1)) + minimum); 3 | 4 | const createAbortError = () => { 5 | const error = new Error('Delay aborted'); 6 | error.name = 'AbortError'; 7 | return error; 8 | }; 9 | 10 | const clearMethods = new WeakMap(); 11 | 12 | export function createDelay({clearTimeout: defaultClear, setTimeout: defaultSet} = {}) { 13 | // We cannot use `async` here as we need the promise identity. 14 | return (milliseconds, {value, signal} = {}) => { 15 | // TODO: Use `signal?.throwIfAborted()` when targeting Node.js 18. 16 | if (signal?.aborted) { 17 | return Promise.reject(createAbortError()); 18 | } 19 | 20 | let timeoutId; 21 | let settle; 22 | let rejectFunction; 23 | const clear = defaultClear ?? clearTimeout; 24 | 25 | const signalListener = () => { 26 | clear(timeoutId); 27 | rejectFunction(createAbortError()); 28 | }; 29 | 30 | const cleanup = () => { 31 | if (signal) { 32 | signal.removeEventListener('abort', signalListener); 33 | } 34 | }; 35 | 36 | const delayPromise = new Promise((resolve, reject) => { 37 | settle = () => { 38 | cleanup(); 39 | resolve(value); 40 | }; 41 | 42 | rejectFunction = reject; 43 | timeoutId = (defaultSet ?? setTimeout)(settle, milliseconds); 44 | }); 45 | 46 | if (signal) { 47 | signal.addEventListener('abort', signalListener, {once: true}); 48 | } 49 | 50 | clearMethods.set(delayPromise, () => { 51 | clear(timeoutId); 52 | timeoutId = null; 53 | settle(); 54 | }); 55 | 56 | return delayPromise; 57 | }; 58 | } 59 | 60 | const delay = createDelay(); 61 | 62 | export default delay; 63 | 64 | export async function rangeDelay(minimum, maximum, options = {}) { 65 | return delay(randomInteger(minimum, maximum), options); 66 | } 67 | 68 | export function clearDelay(promise) { 69 | clearMethods.get(promise)?.(); 70 | } 71 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import delay, {rangeDelay, createDelay} from './index.js'; 3 | 4 | expectType>(delay(200)); 5 | 6 | expectType>(delay(200, {value: '🦄'})); 7 | expectType>(delay(200, {value: 0})); 8 | expectType>( 9 | delay(200, {signal: new AbortController().signal}), 10 | ); 11 | 12 | expectType>(rangeDelay(50, 200, {value: 0})); 13 | 14 | const customDelay = createDelay({clearTimeout, setTimeout}); 15 | expectType>(customDelay(200)); 16 | 17 | expectType>(customDelay(200, {value: '🦄'})); 18 | expectType>(customDelay(200, {value: 0})); 19 | 20 | const unrefDelay = createDelay({ 21 | clearTimeout, 22 | setTimeout(...arguments_) { 23 | return setTimeout(...arguments_).unref(); 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delay", 3 | "version": "6.0.0", 4 | "description": "Delay a promise a specified amount of time", 5 | "license": "MIT", 6 | "repository": "sindresorhus/delay", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "engines": { 19 | "node": ">=16" 20 | }, 21 | "scripts": { 22 | "test": "xo && ava && tsd" 23 | }, 24 | "files": [ 25 | "index.js", 26 | "index.d.ts" 27 | ], 28 | "keywords": [ 29 | "promise", 30 | "resolve", 31 | "delay", 32 | "defer", 33 | "wait", 34 | "stall", 35 | "timeout", 36 | "settimeout", 37 | "event", 38 | "loop", 39 | "next", 40 | "tick", 41 | "delay", 42 | "async", 43 | "await", 44 | "promises", 45 | "bluebird", 46 | "threshold", 47 | "range", 48 | "random" 49 | ], 50 | "devDependencies": { 51 | "ava": "5.2.0", 52 | "in-range": "^3.0.0", 53 | "time-span": "^5.1.0", 54 | "tsd": "^0.28.1", 55 | "xo": "^0.54.2" 56 | }, 57 | "ava": { 58 | "serial": true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # delay 2 | 3 | > Delay a promise a specified amount of time 4 | 5 | *If you target Node.js only, you can use `import {setTimeout} from 'node:timers/promises'; await setTimeout(1000);` instead. This package can still be useful if you need browser support or the extra features.* 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install delay 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | import delay from 'delay'; 17 | 18 | bar(); 19 | 20 | await delay(100); 21 | 22 | // Executed 100 milliseconds later 23 | baz(); 24 | ``` 25 | 26 | ## API 27 | 28 | ### delay(milliseconds, options?) default import 29 | 30 | Create a promise which resolves after the specified `milliseconds`. 31 | 32 | ### rangeDelay(minimum, maximum, options?) 33 | 34 | Create a promise which resolves after a random amount of milliseconds between `minimum` and `maximum` has passed. 35 | 36 | Useful for tests and web scraping since they can have unpredictable performance. For example, if you have a test that asserts a method should not take longer than a certain amount of time, and then run it on a CI, it could take longer. So with this method, you could give it a threshold instead. 37 | 38 | #### milliseconds 39 | #### mininum 40 | #### maximum 41 | 42 | Type: `number` 43 | 44 | Milliseconds to delay the promise. 45 | 46 | #### options 47 | 48 | Type: `object` 49 | 50 | ##### value 51 | 52 | Type: `unknown` 53 | 54 | A value to resolve in the returned promise. 55 | 56 | ```js 57 | import delay from 'delay'; 58 | 59 | const result = await delay(100, {value: '🦄'}); 60 | 61 | // Executed after 100 milliseconds 62 | console.log(result); 63 | //=> '🦄' 64 | ``` 65 | 66 | ##### signal 67 | 68 | Type: [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) 69 | 70 | The returned promise will be rejected with an `AbortError` if the signal is aborted. 71 | 72 | ```js 73 | import delay from 'delay'; 74 | 75 | const abortController = new AbortController(); 76 | 77 | setTimeout(() => { 78 | abortController.abort(); 79 | }, 500); 80 | 81 | try { 82 | await delay(1000, {signal: abortController.signal}); 83 | } catch (error) { 84 | // 500 milliseconds later 85 | console.log(error.name) 86 | //=> 'AbortError' 87 | } 88 | ``` 89 | 90 | ### clearDelay(delayPromise) 91 | 92 | Clears the delay and settles the promise. 93 | 94 | If you pass in a promise that is already cleared or a promise coming from somewhere else, it does nothing. 95 | 96 | ```js 97 | import delay, {clearDelay} from 'delay'; 98 | 99 | const delayedPromise = delay(1000, {value: 'Done'}); 100 | 101 | setTimeout(() => { 102 | clearDelay(delayedPromise); 103 | }, 500); 104 | 105 | // 500 milliseconds later 106 | console.log(await delayedPromise); 107 | //=> 'Done' 108 | ``` 109 | 110 | ### createDelay({clearTimeout, setTimeout}) 111 | 112 | Creates a new `delay` instance using the provided functions for clearing and setting timeouts. Useful if you're about to stub timers globally, but you still want to use `delay` to manage your tests. 113 | 114 | ```js 115 | import {createDelay} from 'delay'; 116 | 117 | const customDelay = createDelay({clearTimeout, setTimeout}); 118 | 119 | const result = await customDelay(100, {value: '🦄'}); 120 | 121 | // Executed after 100 milliseconds 122 | console.log(result); 123 | //=> '🦄' 124 | ``` 125 | 126 | ## Related 127 | 128 | - [delay-cli](https://github.com/sindresorhus/delay-cli) - CLI for this module 129 | - [p-cancelable](https://github.com/sindresorhus/p-cancelable) - Create a promise that can be canceled 130 | - [p-min-delay](https://github.com/sindresorhus/p-min-delay) - Delay a promise a minimum amount of time 131 | - [p-immediate](https://github.com/sindresorhus/p-immediate) - Returns a promise resolved in the next event loop - think `setImmediate()` 132 | - [p-timeout](https://github.com/sindresorhus/p-timeout) - Timeout a promise after a specified amount of time 133 | - [More…](https://github.com/sindresorhus/promise-fun) 134 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import timeSpan from 'time-span'; 3 | import inRange from 'in-range'; 4 | import delay, {clearDelay, rangeDelay, createDelay} from './index.js'; 5 | 6 | test('returns a resolved promise', async t => { 7 | const end = timeSpan(); 8 | await delay(50); 9 | t.true(inRange(end(), {start: 30, end: 70}), 'is delayed'); 10 | }); 11 | 12 | test('able to resolve a falsy value', async t => { 13 | t.is( 14 | await delay(50, {value: 0}), 15 | 0, 16 | ); 17 | }); 18 | 19 | test('delay defaults to 0 ms', async t => { 20 | const end = timeSpan(); 21 | await delay(); 22 | t.true(end() < 30); 23 | }); 24 | 25 | test('can clear a delayed resolution', async t => { 26 | const end = timeSpan(); 27 | const delayPromise = delay(1000, {value: 'success!'}); 28 | 29 | clearDelay(delayPromise); 30 | const success = await delayPromise; 31 | 32 | t.true(end() < 30); 33 | t.is(success, 'success!'); 34 | }); 35 | 36 | test('resolution can be aborted with an AbortSignal', async t => { 37 | const end = timeSpan(); 38 | const abortController = new AbortController(); 39 | 40 | setTimeout(() => { 41 | abortController.abort(); 42 | }, 1); 43 | 44 | await t.throwsAsync( 45 | delay(1000, {signal: abortController.signal}), 46 | {name: 'AbortError'}, 47 | ); 48 | 49 | t.true(end() < 30); 50 | }); 51 | 52 | test('resolution can be aborted with an AbortSignal if a value is passed', async t => { 53 | const end = timeSpan(); 54 | const abortController = new AbortController(); 55 | 56 | setTimeout(() => { 57 | abortController.abort(); 58 | }, 1); 59 | 60 | await t.throwsAsync( 61 | delay(1000, {value: 123, signal: abortController.signal}), 62 | {name: 'AbortError'}, 63 | ); 64 | 65 | t.true(end() < 30); 66 | }); 67 | 68 | test('rejects with AbortError if AbortSignal is already aborted', async t => { 69 | const end = timeSpan(); 70 | 71 | const abortController = new AbortController(); 72 | abortController.abort(); 73 | 74 | await t.throwsAsync( 75 | delay(1000, {signal: abortController.signal}), 76 | {name: 'AbortError'}, 77 | ); 78 | 79 | t.true(end() < 30); 80 | }); 81 | 82 | test('returns a promise that is resolved in a random range of time', async t => { 83 | const end = timeSpan(); 84 | await rangeDelay(50, 150); 85 | t.true(inRange(end(), {start: 30, end: 170}), 'is delayed'); 86 | }); 87 | 88 | test('can create a new instance with fixed timeout methods', async t => { 89 | const cleared = []; 90 | const callbacks = []; 91 | 92 | const custom = createDelay({ 93 | clearTimeout(handle) { 94 | cleared.push(handle); 95 | }, 96 | 97 | setTimeout(callback, ms) { 98 | const handle = Symbol('handle'); 99 | callbacks.push({callback, handle, ms}); 100 | return handle; 101 | }, 102 | }); 103 | 104 | const first = custom(50, {value: 'first'}); 105 | t.is(callbacks.length, 1); 106 | t.is(callbacks[0].ms, 50); 107 | callbacks[0].callback(); 108 | t.is(await first, 'first'); 109 | 110 | const second = custom(40, {value: 'second'}); 111 | t.is(callbacks.length, 2); 112 | t.is(callbacks[1].ms, 40); 113 | callbacks[1].callback(); 114 | t.is(await second, 'second'); 115 | 116 | const third = custom(60); 117 | t.is(callbacks.length, 3); 118 | t.is(callbacks[2].ms, 60); 119 | clearDelay(third); 120 | t.is(cleared.length, 1); 121 | t.is(cleared[0], callbacks[2].handle); 122 | }); 123 | --------------------------------------------------------------------------------