Skip to content

Commit

Permalink
Require Node.js 16 and move to ESM
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus committed May 21, 2023
1 parent 584ad84 commit b2edac7
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 353 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/main.yml
Expand Up @@ -10,12 +10,12 @@ jobs:
fail-fast: false
matrix:
node-version:
- 14
- 12
- 10
- 20
- 18
- 16
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm install
Expand Down
198 changes: 108 additions & 90 deletions index.d.ts
@@ -1,107 +1,125 @@
declare namespace delay {
interface ClearablePromise<T> extends Promise<T> {
/**
Clears the delay and settles the promise.
*/
clear(): void;
}

export type Options<T> = {
/**
Minimal subset of `AbortSignal` that delay will use if passed.
This avoids a dependency on dom.d.ts.
The dom.d.ts `AbortSignal` is compatible with this one.
*/
interface AbortSignal {
readonly aborted: boolean;
addEventListener(
type: 'abort',
listener: () => void,
options?: {once?: boolean}
): void;
removeEventListener(type: 'abort', listener: () => void): void;
}
A value to resolve in the returned promise.
interface Options {
/**
An optional AbortSignal to abort the delay.
If aborted, the Promise will be rejected with an AbortError.
*/
signal?: AbortSignal;
}
}
@example
```
import delay from 'delay';
type Delay = {
/**
Create a promise which resolves after the specified `milliseconds`.
const result = await delay(100, {value: '🦄'});
@param milliseconds - Milliseconds to delay the promise.
@returns A promise which resolves after the specified `milliseconds`.
// Executed after 100 milliseconds
console.log(result);
//=> '🦄'
```
*/
(milliseconds: number, options?: delay.Options): delay.ClearablePromise<void>;
value?: T;

/**
Create a promise which resolves after the specified `milliseconds`.
An `AbortSignal` to abort the delay.
@param milliseconds - Milliseconds to delay the promise.
@returns A promise which resolves after the specified `milliseconds`.
*/
<T>(
milliseconds: number,
options?: delay.Options & {
/**
Value to resolve in the returned promise.
*/
value: T;
}
): delay.ClearablePromise<T>;
The returned promise will be rejected with an `AbortError` if the signal is aborted.
/**
Create a promise which resolves after a random amount of milliseconds between `minimum` and `maximum` has passed.
@example
```
import delay from 'delay';
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 `.range()`, you could give it a threshold instead.
const abortController = new AbortController();
@param minimum - Minimum amount of milliseconds to delay the promise.
@param maximum - Maximum amount of milliseconds to delay the promise.
@returns A promise which resolves after a random amount of milliseconds between `maximum` and `maximum` has passed.
*/
range<T>(
minimum: number,
maximum: number,
options?: delay.Options & {
/**
Value to resolve in the returned promise.
*/
value: T;
}
): delay.ClearablePromise<T>;

// TODO: Allow providing value type after https://github.com/Microsoft/TypeScript/issues/5413 is resolved.
/**
Create a promise which rejects after the specified `milliseconds`.
setTimeout(() => {
abortController.abort();
}, 500);
@param milliseconds - Milliseconds to delay the promise.
@returns A promise which rejects after the specified `milliseconds`.
try {
await delay(1000, {signal: abortController.signal});
} catch (error) {
// 500 milliseconds later
console.log(error.name)
//=> 'AbortError'
}
```
*/
reject(
milliseconds: number,
options?: delay.Options & {
/**
Value to reject in the returned promise.
*/
value?: unknown;
}
): delay.ClearablePromise<never>;
signal?: AbortSignal;
};

declare const delay: Delay & {
// The types are intentionally loose to make it work with both Node.js and browser versions of these methods.
createWithTimers(timers: {
clearTimeout: (timeoutId: any) => void;
setTimeout: (callback: (...args: any[]) => void, milliseconds: number, ...args: any[]) => unknown;
}): Delay;
/**
Create a promise which resolves after the specified `milliseconds`.
// TODO: Remove this for the next major release.
default: typeof delay;
};
@param milliseconds - Milliseconds to delay the promise.
@returns A promise which resolves after the specified `milliseconds`.
@example
```
import delay from 'delay';
bar();
await delay(100);
// Executed 100 milliseconds later
baz();
```
*/
export default function delay<T>(
milliseconds: number,
options?: Options<T>
): Promise<T>;

/**
Create a promise which resolves after a random amount of milliseconds between `minimum` and `maximum` has passed.
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.
@param minimum - Minimum amount of milliseconds to delay the promise.
@param maximum - Maximum amount of milliseconds to delay the promise.
@returns A promise which resolves after a random amount of milliseconds between `maximum` and `maximum` has passed.
*/
export function rangeDelay<T>(
minimum: number,
maximum: number,
options?: Options<T>
): Promise<T>;

/**
Clears the delay and settles the promise.
If you pass in a promise that is already cleared or a promise coming from somewhere else, it does nothing.
@example
```
import delay, {clearDelay} from 'delay';
const delayedPromise = delay(1000, {value: 'Done'});
setTimeout(() => {
clearDelay(delayedPromise);
}, 500);
// 500 milliseconds later
console.log(await delayedPromise);
//=> 'Done'
```
*/
export function clearDelay(delayPromise: Promise<unknown>): void;

// The types are intentionally loose to make it work with both Node.js and browser versions of these methods.
/**
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.
@example
```
import {createDelay} from 'delay';
const customDelay = createDelay({clearTimeout, setTimeout});
const result = await customDelay(100, {value: '🦄'});
export = delay;
// Executed after 100 milliseconds
console.log(result);
//=> '🦄'
```
*/
export function createDelay(timers: {
clearTimeout: (timeoutId: any) => void;
setTimeout: (callback: (...args: any[]) => void, milliseconds: number, ...args: any[]) => unknown;
}): typeof delay;
96 changes: 47 additions & 49 deletions index.js
@@ -1,5 +1,3 @@
'use strict';

// From https://github.com/sindresorhus/random-int/blob/c37741b56f76b9160b0b63dae4e9c64875128146/index.js#L13-L15
const randomInteger = (minimum, maximum) => Math.floor((Math.random() * (maximum - minimum + 1)) + minimum);

Expand All @@ -9,64 +7,64 @@ const createAbortError = () => {
return error;
};

const createDelay = ({clearTimeout: defaultClear, setTimeout: set, willResolve}) => (ms, {value, signal} = {}) => {
if (signal && signal.aborted) {
return Promise.reject(createAbortError());
}
const clearMethods = new WeakMap();

let timeoutId;
let settle;
let rejectFn;
const clear = defaultClear || clearTimeout;
export function createDelay({clearTimeout: defaultClear, setTimeout: defaultSet} = {}) {
// We cannot use `async` here as we need the promise identity.
return (milliseconds, {value, signal} = {}) => {
// TODO: Use `signal?.throwIfAborted()` when targeting Node.js 18.
if (signal?.aborted) {
return Promise.reject(createAbortError());
}

const signalListener = () => {
clear(timeoutId);
rejectFn(createAbortError());
};
let timeoutId;
let settle;
let rejectFunction;
const clear = defaultClear ?? clearTimeout;

const cleanup = () => {
if (signal) {
signal.removeEventListener('abort', signalListener);
}
};
const signalListener = () => {
clear(timeoutId);
rejectFunction(createAbortError());
};

const delayPromise = new Promise((resolve, reject) => {
settle = () => {
cleanup();
if (willResolve) {
resolve(value);
} else {
reject(value);
const cleanup = () => {
if (signal) {
signal.removeEventListener('abort', signalListener);
}
};

rejectFn = reject;
timeoutId = (set || setTimeout)(settle, ms);
});
const delayPromise = new Promise((resolve, reject) => {
settle = () => {
cleanup();
resolve(value);
};

rejectFunction = reject;
timeoutId = (defaultSet ?? setTimeout)(settle, milliseconds);
});

if (signal) {
signal.addEventListener('abort', signalListener, {once: true});
}
if (signal) {
signal.addEventListener('abort', signalListener, {once: true});
}

delayPromise.clear = () => {
clear(timeoutId);
timeoutId = null;
settle();
clearMethods.set(delayPromise, () => {
clear(timeoutId);
timeoutId = null;
settle();
});

return delayPromise;
};
}

return delayPromise;
};
const delay = createDelay();

const createWithTimers = clearAndSet => {
const delay = createDelay({...clearAndSet, willResolve: true});
delay.reject = createDelay({...clearAndSet, willResolve: false});
delay.range = (minimum, maximum, options) => delay(randomInteger(minimum, maximum), options);
return delay;
};
export default delay;

const delay = createWithTimers();
delay.createWithTimers = createWithTimers;
export async function rangeDelay(minimum, maximum, options = {}) {
return delay(randomInteger(minimum, maximum), options);
}

module.exports = delay;
// TODO: Remove this for the next major release
module.exports.default = delay;
export function clearDelay(promise) {
clearMethods.get(promise)?.();
}
36 changes: 14 additions & 22 deletions index.test-d.ts
@@ -1,33 +1,25 @@
/// <reference lib="dom"/>
import {expectType} from 'tsd';
import delay = require('.');
import {ClearablePromise} from '.';
import delay, {rangeDelay, createDelay} from './index.js';

expectType<ClearablePromise<void>>(delay(200));
expectType<Promise<void>>(delay(200));

expectType<ClearablePromise<string>>(delay(200, {value: '🦄'}));
expectType<ClearablePromise<number>>(delay(200, {value: 0}));
expectType<ClearablePromise<void>>(
delay(200, {signal: new AbortController().signal})
expectType<Promise<string>>(delay(200, {value: '🦄'}));
expectType<Promise<number>>(delay(200, {value: 0}));
expectType<Promise<void>>(
delay(200, {signal: new AbortController().signal}),
);

expectType<ClearablePromise<number>>(delay.range(50, 200, {value: 0}));
expectType<Promise<number>>(rangeDelay(50, 200, {value: 0}));

expectType<ClearablePromise<never>>(delay.reject(200, {value: '🦄'}));
expectType<ClearablePromise<never>>(delay.reject(200, {value: 0}));
const customDelay = createDelay({clearTimeout, setTimeout});
expectType<Promise<void>>(customDelay(200));

const customDelay = delay.createWithTimers({clearTimeout, setTimeout});
expectType<ClearablePromise<void>>(customDelay(200));
expectType<Promise<string>>(customDelay(200, {value: '🦄'}));
expectType<Promise<number>>(customDelay(200, {value: 0}));

expectType<ClearablePromise<string>>(customDelay(200, {value: '🦄'}));
expectType<ClearablePromise<number>>(customDelay(200, {value: 0}));

expectType<ClearablePromise<never>>(customDelay.reject(200, {value: '🦄'}));
expectType<ClearablePromise<never>>(customDelay.reject(200, {value: 0}));

const unrefDelay = delay.createWithTimers({
const unrefDelay = createDelay({
clearTimeout,
setTimeout(...args) {
return setTimeout(...args).unref()
setTimeout(...arguments_) {
return setTimeout(...arguments_).unref();
},
});

0 comments on commit b2edac7

Please sign in to comment.