Compare commits

...

16 Commits
v5.0.0 ... main

Author SHA1 Message Date
Derrick Hammer 4d4bee14a2
*Update dist 2022-12-15 06:58:16 -05:00
Derrick Hammer cc29396abb
*Switch to commonjs 2022-12-15 06:57:55 -05:00
Derrick Hammer a3434a77f7
*Add dist 2022-12-15 06:54:55 -05:00
Derrick Hammer 0076c9fb03
*Initial refactor to use typescript 2022-12-15 06:54:28 -05:00
Sindre Sorhus 200df03007 Fix CI 2022-07-25 18:34:28 +02:00
Sindre Sorhus 46acd4b511 6.0.0 2022-07-25 18:29:37 +02:00
Sindre Sorhus eb1730ba0c Require Node.js 14
Fixes #27
2022-07-25 18:26:31 +02:00
Gyubong d7920cae0a
Move the secondary parameters into the options parameter (#29) 2022-07-25 18:16:27 +02:00
Sindre Sorhus de29542ac4 Meta tweaks 2022-07-08 14:43:39 +02:00
Sindre Sorhus 56e22052c6 5.1.0 2022-05-26 23:58:36 +07:00
Gyubong 1bf6679148
Support `AbortController` (#26)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
2022-05-26 23:57:52 +07:00
Sindre Sorhus 0c28612eae 5.0.2 2021-10-23 21:00:37 +07:00
Kiko Beats 6b6c4e85c3
Ensure milliseconds argument is not NaN (#25) 2021-10-23 20:54:29 +07:00
Sindre Sorhus 7dd311757e 5.0.1 2021-10-08 13:16:52 +07:00
Kiko Beats bf5d1649cc
Improve timeout error message (#24)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
2021-10-08 13:15:55 +07:00
Sindre Sorhus 495a0bb923
Fix readme typo 2021-07-09 16:56:27 +07:00
15 changed files with 814 additions and 220 deletions

4
.github/funding.yml vendored Normal file
View File

@ -0,0 +1,4 @@
github: sindresorhus
open_collective: sindresorhus
tidelift: npm/p-timeout
custom: https://sindresorhus.com/donate

3
.github/security.md vendored Normal file
View File

@ -0,0 +1,3 @@
# Security Policy
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.

View File

@ -10,11 +10,11 @@ jobs:
fail-fast: false
matrix:
node-version:
- 14
- 12
- 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

1
.yarnrc.yml Normal file
View File

@ -0,0 +1 @@
nodeLinker: node-modules

135
dist/index.d.ts vendored Normal file
View File

@ -0,0 +1,135 @@
import PCancelable from "p-cancelable";
export interface ClearablePromise<T> extends Promise<T> {
/**
Clear the timeout.
*/
clear: () => void;
}
export type Options<ReturnType> = {
/**
Milliseconds before timing out.
Passing `Infinity` will cause it to never time out.
*/
milliseconds: number;
/**
Do something other than rejecting with an error on timeout.
You could for example retry:
@example
```
import {setTimeout} from 'node:timers/promises';
import pTimeout from 'p-timeout';
const delayedPromise = () => setTimeout(200);
await pTimeout(delayedPromise(), {
milliseconds: 50,
fallback: () => {
return pTimeout(delayedPromise(), {
milliseconds: 300
});
},
});
```
*/
fallback?: () => ReturnType | Promise<ReturnType>;
/**
Specify a custom error message or error.
If you do a custom error, it's recommended to sub-class `pTimeout.TimeoutError`.
*/
message?: string | Error;
/**
Custom implementations for the `setTimeout` and `clearTimeout` functions.
Useful for testing purposes, in particular to work around [`sinon.useFakeTimers()`](https://sinonjs.org/releases/latest/fake-timers/).
@example
```
import pTimeout from 'p-timeout';
import sinon from 'sinon';
const originalSetTimeout = setTimeout;
const originalClearTimeout = clearTimeout;
sinon.useFakeTimers();
// Use `pTimeout` without being affected by `sinon.useFakeTimers()`:
await pTimeout(doSomething(), {
milliseconds: 2000,
customTimers: {
setTimeout: originalSetTimeout,
clearTimeout: originalClearTimeout
}
});
```
*/
readonly customTimers?: {
setTimeout: typeof globalThis.setTimeout;
clearTimeout: typeof globalThis.clearTimeout;
};
/**
You can abort the promise using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
_Requires Node.js 16 or later._
@example
```
import pTimeout from 'p-timeout';
import delay from 'delay';
const delayedPromise = delay(3000);
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, 100);
await pTimeout(delayedPromise, {
milliseconds: 2000,
signal: abortController.signal
});
```
*/
signal?: globalThis.AbortSignal;
};
/**
Timeout a promise after a specified amount of time.
If you pass in a cancelable promise, specifically a promise with a `.cancel()` method, that method will be called when the `pTimeout` promise times out.
@param input - Promise to decorate.
@returns A decorated `input` that times out after `milliseconds` time. It has a `.clear()` method that clears the timeout.
@example
```
import {setTimeout} from 'node:timers/promises';
import pTimeout from 'p-timeout';
const delayedPromise = () => setTimeout(200);
await pTimeout(delayedPromise(), {
milliseconds: 50,
fallback: () => {
return pTimeout(delayedPromise(), 300);
}
});
```
*/
export declare class TimeoutError extends Error {
readonly name: "TimeoutError";
constructor(message?: string);
}
/**
An error to be thrown when the request is aborted by AbortController.
DOMException is thrown instead of this Error when DOMException is available.
*/
export declare class AbortError extends Error {
readonly name: "AbortError";
constructor(message?: string);
}
export default function pTimeout<ValueType, ReturnType = ValueType>(promise: PromiseLike<ValueType> | PCancelable<ValueType>, options: Options<ReturnType>): ClearablePromise<ValueType | ReturnType>;
//# sourceMappingURL=index.d.ts.map

1
dist/index.d.ts.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,cAAc,CAAC;AAEvC,MAAM,WAAW,gBAAgB,CAAC,CAAC,CAAE,SAAQ,OAAO,CAAC,CAAC,CAAC;IACtD;;OAEG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,MAAM,OAAO,CAAC,UAAU,IAAI;IACjC;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,QAAQ,CAAC,EAAE,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAElD;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAEzB;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE;QACvB,UAAU,EAAE,OAAO,UAAU,CAAC,UAAU,CAAC;QACzC,YAAY,EAAE,OAAO,UAAU,CAAC,YAAY,CAAC;KAC7C,CAAC;IAEF;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC,WAAW,CAAC;CAChC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,qBAAa,YAAa,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;gBAClB,OAAO,CAAC,EAAE,MAAM;CAI5B;AAED;;;EAGE;AACF,qBAAa,UAAW,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;gBAChB,OAAO,CAAC,EAAE,MAAM;CAK5B;AAsBD,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,SAAS,EAAE,UAAU,GAAG,SAAS,EACjE,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,GAAG,WAAW,CAAC,SAAS,CAAC,EACxD,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,GAC1B,gBAAgB,CAAC,SAAS,GAAG,UAAU,CAAC,CAgF1C"}

121
dist/index.js vendored Normal file
View File

@ -0,0 +1,121 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AbortError = exports.TimeoutError = void 0;
/**
Timeout a promise after a specified amount of time.
If you pass in a cancelable promise, specifically a promise with a `.cancel()` method, that method will be called when the `pTimeout` promise times out.
@param input - Promise to decorate.
@returns A decorated `input` that times out after `milliseconds` time. It has a `.clear()` method that clears the timeout.
@example
```
import {setTimeout} from 'node:timers/promises';
import pTimeout from 'p-timeout';
const delayedPromise = () => setTimeout(200);
await pTimeout(delayedPromise(), {
milliseconds: 50,
fallback: () => {
return pTimeout(delayedPromise(), 300);
}
});
```
*/
class TimeoutError extends Error {
name;
constructor(message) {
super(message);
this.name = "TimeoutError";
}
}
exports.TimeoutError = TimeoutError;
/**
An error to be thrown when the request is aborted by AbortController.
DOMException is thrown instead of this Error when DOMException is available.
*/
class AbortError extends Error {
name;
constructor(message) {
super();
this.name = "AbortError";
this.message = message;
}
}
exports.AbortError = AbortError;
/**
TODO: Remove AbortError and just throw DOMException when targeting Node 18.
*/
const getDOMException = (errorMessage) => globalThis.DOMException === undefined
? new AbortError(errorMessage)
: new DOMException(errorMessage);
/**
TODO: Remove below function and just 'reject(signal.reason)' when targeting Node 18.
*/
const getAbortedReason = (signal) => {
const reason = signal.reason === undefined
? getDOMException("This operation was aborted.")
: signal.reason;
return reason instanceof Error ? reason : getDOMException(reason);
};
function pTimeout(promise, options) {
const { milliseconds, fallback, message, customTimers = { setTimeout, clearTimeout }, } = options;
let timer;
const cancelablePromise = new Promise((resolve, reject) => {
if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) {
throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``);
}
if (milliseconds === Number.POSITIVE_INFINITY) {
resolve(promise);
return;
}
if (options.signal) {
const { signal } = options;
if (signal.aborted) {
reject(getAbortedReason(signal));
}
signal.addEventListener("abort", () => {
reject(getAbortedReason(signal));
});
}
timer = customTimers.setTimeout.call(undefined, () => {
if (fallback) {
try {
resolve(fallback());
}
catch (error) {
reject(error);
}
return;
}
const errorMessage = typeof message === "string"
? message
: `Promise timed out after ${milliseconds} milliseconds`;
const timeoutError = message instanceof Error ? message : new TimeoutError(errorMessage);
if (typeof promise?.cancel === "function") {
promise?.cancel();
}
reject(timeoutError);
}, milliseconds);
(async () => {
try {
resolve(await promise);
}
catch (error) {
reject(error);
}
finally {
customTimers.clearTimeout.call(undefined, timer);
}
})();
});
cancelablePromise.clear =
() => {
customTimers.clearTimeout.call(undefined, timer);
timer = undefined;
};
return cancelablePromise;
}
exports.default = pTimeout;

122
index.d.ts vendored
View File

@ -1,18 +1,54 @@
/* eslint-disable import/export */
export class TimeoutError extends Error {
readonly name: 'TimeoutError';
constructor(message?: string);
}
export interface ClearablePromise<T> extends Promise<T>{
export interface ClearablePromise<T> extends Promise<T> {
/**
Clear the timeout.
*/
clear: () => void;
}
export type Options = {
export type Options<ReturnType> = {
/**
Milliseconds before timing out.
Passing `Infinity` will cause it to never time out.
*/
milliseconds: number;
/**
Do something other than rejecting with an error on timeout.
You could for example retry:
@example
```
import {setTimeout} from 'node:timers/promises';
import pTimeout from 'p-timeout';
const delayedPromise = () => setTimeout(200);
await pTimeout(delayedPromise(), {
milliseconds: 50,
fallback: () => {
return pTimeout(delayedPromise(), {
milliseconds: 300
});
},
});
```
*/
fallback?: () => ReturnType | Promise<ReturnType>;
/**
Specify a custom error message or error.
If you do a custom error, it's recommended to sub-class `pTimeout.TimeoutError`.
*/
message?: string | Error;
/**
Custom implementations for the `setTimeout` and `clearTimeout` functions.
@ -29,7 +65,8 @@ export type Options = {
sinon.useFakeTimers();
// Use `pTimeout` without being affected by `sinon.useFakeTimers()`:
await pTimeout(doSomething(), 2000, undefined, {
await pTimeout(doSomething(), {
milliseconds: 2000,
customTimers: {
setTimeout: originalSetTimeout,
clearTimeout: originalClearTimeout
@ -38,9 +75,35 @@ export type Options = {
```
*/
readonly customTimers?: {
setTimeout: typeof global.setTimeout;
clearTimeout: typeof global.clearTimeout;
setTimeout: typeof globalThis.setTimeout;
clearTimeout: typeof globalThis.clearTimeout;
};
/**
You can abort the promise using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
_Requires Node.js 16 or later._
@example
```
import pTimeout from 'p-timeout';
import delay from 'delay';
const delayedPromise = delay(3000);
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, 100);
await pTimeout(delayedPromise, {
milliseconds: 2000,
signal: abortController.signal
});
```
*/
signal?: globalThis.AbortSignal;
};
/**
@ -49,53 +112,24 @@ Timeout a promise after a specified amount of time.
If you pass in a cancelable promise, specifically a promise with a `.cancel()` method, that method will be called when the `pTimeout` promise times out.
@param input - Promise to decorate.
@param milliseconds - Milliseconds before timing out.
@param message - Specify a custom error message or error. If you do a custom error, it's recommended to sub-class `pTimeout.TimeoutError`. Default: `'Promise timed out after 50 milliseconds'`.
@returns A decorated `input` that times out after `milliseconds` time. It has a `.clear()` method that clears the timeout.
@example
```
import {setTimeout} from 'timers/promises';
import pTimeout from 'p-timeout';
const delayedPromise = setTimeout(200);
await pTimeout(delayedPromise, 50);
//=> [TimeoutError: Promise timed out after 50 milliseconds]
```
*/
export default function pTimeout<ValueType>(
input: PromiseLike<ValueType>,
milliseconds: number,
message?: string | Error,
options?: Options
): ClearablePromise<ValueType>;
/**
Timeout a promise after a specified amount of time.
If you pass in a cancelable promise, specifically a promise with a `.cancel()` method, that method will be called when the `pTimeout` promise times out.
@param input - Promise to decorate.
@param milliseconds - Milliseconds before timing out. Passing `Infinity` will cause it to never time out.
@param fallback - Do something other than rejecting with an error on timeout. You could for example retry.
@returns A decorated `input` that times out after `milliseconds` time. It has a `.clear()` method that clears the timeout.
@example
```
import {setTimeout} from 'timers/promises';
import {setTimeout} from 'node:timers/promises';
import pTimeout from 'p-timeout';
const delayedPromise = () => setTimeout(200);
await pTimeout(delayedPromise(), 50, () => {
return pTimeout(delayedPromise(), 300);
await pTimeout(delayedPromise(), {
milliseconds: 50,
fallback: () => {
return pTimeout(delayedPromise(), 300);
}
});
```
*/
export default function pTimeout<ValueType, ReturnType>(
export default function pTimeout<ValueType, ReturnType = ValueType>(
input: PromiseLike<ValueType>,
milliseconds: number,
fallback: () => ReturnType | Promise<ReturnType>,
options?: Options
options: Options<ReturnType>
): ClearablePromise<ValueType | ReturnType>;

View File

@ -1,63 +0,0 @@
export class TimeoutError extends Error {
constructor(message) {
super(message);
this.name = 'TimeoutError';
}
}
export default function pTimeout(promise, milliseconds, fallback, options) {
let timer;
const cancelablePromise = new Promise((resolve, reject) => {
if (typeof milliseconds !== 'number' || milliseconds < 0) {
throw new TypeError('Expected `milliseconds` to be a positive number');
}
if (milliseconds === Number.POSITIVE_INFINITY) {
resolve(promise);
return;
}
options = {
customTimers: {setTimeout, clearTimeout},
...options
};
timer = options.customTimers.setTimeout.call(undefined, () => {
if (typeof fallback === 'function') {
try {
resolve(fallback());
} catch (error) {
reject(error);
}
return;
}
const message = typeof fallback === 'string' ? fallback : `Promise timed out after ${milliseconds} milliseconds`;
const timeoutError = fallback instanceof Error ? fallback : new TimeoutError(message);
if (typeof promise.cancel === 'function') {
promise.cancel();
}
reject(timeoutError);
}, milliseconds);
(async () => {
try {
resolve(await promise);
} catch (error) {
reject(error);
} finally {
options.customTimers.clearTimeout.call(undefined, timer);
}
})();
});
cancelablePromise.clear = () => {
clearTimeout(timer);
timer = undefined;
};
return cancelablePromise;
}

View File

@ -2,45 +2,54 @@
import {expectType, expectError} from 'tsd';
import pTimeout, {TimeoutError} from './index.js';
const delayedPromise: () => Promise<string> = async () => {
return new Promise(resolve => {
setTimeout(() => {
resolve('foo');
}, 200);
});
};
pTimeout(delayedPromise(), 50).then(() => 'foo');
pTimeout(delayedPromise(), 50, async () => {
return pTimeout(delayedPromise(), 300);
const delayedPromise: () => Promise<string> = async () => new Promise(resolve => {
setTimeout(() => {
resolve('foo');
}, 200);
});
pTimeout(delayedPromise(), 50).then(value => expectType<string>(value));
pTimeout(delayedPromise(), 50, 'error').then(value =>
expectType<string>(value)
);
pTimeout(delayedPromise(), 50, new Error('error')).then(value =>
expectType<string>(value)
);
pTimeout(delayedPromise(), 50, async () => 10).then(value => {
pTimeout(delayedPromise(), {milliseconds: 50}).then(() => 'foo');
pTimeout(delayedPromise(), {milliseconds: 50, fallback: async () => pTimeout(delayedPromise(), {milliseconds: 300})});
pTimeout(delayedPromise(), {milliseconds: 50}).then(value => {
expectType<string>(value);
});
pTimeout(delayedPromise(), {milliseconds: 50, message: 'error'}).then(value => {
expectType<string>(value);
});
pTimeout(delayedPromise(), {milliseconds: 50, message: new Error('error')}).then(value => {
expectType<string>(value);
});
pTimeout(delayedPromise(), {milliseconds: 50, fallback: async () => 10}).then(value => {
expectType<string | number>(value);
});
pTimeout(delayedPromise(), 50, () => 10).then(value => {
pTimeout(delayedPromise(), {milliseconds: 50, fallback: () => 10}).then(value => {
expectType<string | number>(value);
});
const customTimers = {setTimeout, clearTimeout};
pTimeout(delayedPromise(), 50, undefined, {customTimers});
pTimeout(delayedPromise(), 50, 'foo', {customTimers});
pTimeout(delayedPromise(), 50, new Error('error'), {customTimers});
pTimeout(delayedPromise(), 50, () => 10, {});
pTimeout(delayedPromise(), {milliseconds: 50, customTimers});
pTimeout(delayedPromise(), {milliseconds: 50, message: 'foo', customTimers});
pTimeout(delayedPromise(), {milliseconds: 50, message: new Error('error'), customTimers});
pTimeout(delayedPromise(), {milliseconds: 50, fallback: () => 10});
expectError(pTimeout(delayedPromise(), 50, () => 10, {customTimers: {setTimeout}}));
expectError(pTimeout(delayedPromise(), 50, () => 10, {
expectError(pTimeout(delayedPromise(), {
milliseconds: 50,
fallback: () => 10,
customTimers: {
setTimeout,
},
}));
expectError(pTimeout(delayedPromise(), {
milliseconds: 50,
fallback: () => 10,
customTimers: {
setTimeout: () => 42, // Invalid `setTimeout` implementation
clearTimeout
}
clearTimeout,
},
}));
expectError(pTimeout(delayedPromise(), {})); // `milliseconds` is required
const timeoutError = new TimeoutError();
expectType<TimeoutError>(timeoutError);

View File

@ -1,47 +1,48 @@
{
"name": "p-timeout",
"version": "5.0.0",
"description": "Timeout a promise after a specified amount of time",
"license": "MIT",
"repository": "sindresorhus/p-timeout",
"funding": "https://github.com/sponsors/sindresorhus",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "https://sindresorhus.com"
},
"type": "module",
"exports": "./index.js",
"engines": {
"node": ">=12"
},
"scripts": {
"test": "xo && ava && tsd"
},
"files": [
"index.js",
"index.d.ts"
],
"keywords": [
"promise",
"timeout",
"error",
"invalidate",
"async",
"await",
"promises",
"time",
"out",
"cancel",
"bluebird"
],
"devDependencies": {
"ava": "^3.15.0",
"delay": "^5.0.0",
"in-range": "^3.0.0",
"p-cancelable": "^2.1.0",
"time-span": "^4.0.0",
"tsd": "^0.14.0",
"xo": "^0.38.2"
}
"name": "p-timeout",
"version": "6.0.0",
"main": "dist/index.js",
"description": "Timeout a promise after a specified amount of time",
"license": "MIT",
"repository": "sindresorhus/p-timeout",
"funding": "https://github.com/sponsors/sindresorhus",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "https://sindresorhus.com"
},
"type": "commonjs",
"engines": {
"node": ">=14.16"
},
"scripts": {
"test": "xo && ava && tsd"
},
"files": [
"src/index.js",
"index.d.ts"
],
"keywords": [
"promise",
"timeout",
"error",
"invalidate",
"async",
"await",
"promises",
"time",
"out",
"cancel",
"bluebird"
],
"devDependencies": {
"ava": "^4.3.1",
"delay": "^5.0.0",
"in-range": "^3.0.0",
"p-cancelable": "^4.0.1",
"prettier": "^2.8.1",
"time-span": "^5.1.0",
"tsd": "^0.22.0",
"xo": "^0.51.0"
}
}

View File

@ -4,26 +4,27 @@
## Install
```
$ npm install p-timeout
```sh
npm install p-timeout
```
## Usage
```js
import {setTimeout} from 'timers/promises';
import {setTimeout} from 'node:timers/promises';
import pTimeout from 'p-timeout';
const delayedPromise = setTimeout(200);
await pTimeout(delayedPromise, 50);
await pTimeout(delayedPromise, {
milliseconds: 50,
});
//=> [TimeoutError: Promise timed out after 50 milliseconds]
```
## API
### pTimeout(input, milliseconds, message?, options?)
### pTimeout(input, milliseconds, fallback?, options?)
### pTimeout(input, options)
Returns a decorated `input` that times out after `milliseconds` time. It has a `.clear()` method that clears the timeout.
@ -35,7 +36,11 @@ Type: `Promise`
Promise to decorate.
#### milliseconds
#### options
Type: `object`
##### milliseconds
Type: `number`
@ -43,7 +48,7 @@ Milliseconds before timing out.
Passing `Infinity` will cause it to never time out.
#### message
##### message
Type: `string | Error`\
Default: `'Promise timed out after 50 milliseconds'`
@ -52,7 +57,7 @@ Specify a custom error message or error.
If you do a custom error, it's recommended to sub-class `pTimeout.TimeoutError`.
#### fallback
##### fallback
Type: `Function`
@ -61,20 +66,19 @@ Do something other than rejecting with an error on timeout.
You could for example retry:
```js
import {setTimeout} from 'timers/promises';
import {setTimeout} from 'node:timers/promises';
import pTimeout from 'p-timeout';
const delayedPromise = () => setTimeout(200);
await pTimeout(delayedPromise(), 50, () => {
return pTimeout(delayedPromise(), 300);
await pTimeout(delayedPromise(), {
milliseconds: 50,
fallback: () => {
return pTimeout(delayedPromise(), 300);
},
});
```
#### options
Type: `object`
##### customTimers
Type: `object` with function properties `setTimeout` and `clearTimeout`
@ -86,7 +90,7 @@ Useful for testing purposes, in particular to work around [`sinon.useFakeTimers(
Example:
```js
import {setTimeout} from 'timers/promises';
import {setTimeout} from 'node:timers/promises';
import pTimeout from 'p-timeout';
const originalSetTimeout = setTimeout;
@ -95,7 +99,8 @@ const originalClearTimeout = clearTimeout;
sinon.useFakeTimers();
// Use `pTimeout` without being affected by `sinon.useFakeTimers()`:
await pTimeout(doSomething(), 2000, undefined, {
await pTimeout(doSomething(), {
milliseconds: 2000,
customTimers: {
setTimeout: originalSetTimeout,
clearTimeout: originalClearTimeout
@ -103,7 +108,33 @@ await pTimeout(doSomething(), 2000, undefined, {
});
```
### pTimeout.TimeoutError
#### signal
Type: [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
You can abort the promise using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
*Requires Node.js 16 or later.*
```js
import pTimeout from 'p-timeout';
import delay from 'delay';
const delayedPromise = delay(3000);
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, 100);
await pTimeout(delayedPromise, {
milliseconds: 2000,
signal: abortController.signal
});
```
### TimeoutError
Exposed for instance checking and sub-classing.

254
src/index.ts Normal file
View File

@ -0,0 +1,254 @@
import PCancelable from "p-cancelable";
export interface ClearablePromise<T> extends Promise<T> {
/**
Clear the timeout.
*/
clear: () => void;
}
export type Options<ReturnType> = {
/**
Milliseconds before timing out.
Passing `Infinity` will cause it to never time out.
*/
milliseconds: number;
/**
Do something other than rejecting with an error on timeout.
You could for example retry:
@example
```
import {setTimeout} from 'node:timers/promises';
import pTimeout from 'p-timeout';
const delayedPromise = () => setTimeout(200);
await pTimeout(delayedPromise(), {
milliseconds: 50,
fallback: () => {
return pTimeout(delayedPromise(), {
milliseconds: 300
});
},
});
```
*/
fallback?: () => ReturnType | Promise<ReturnType>;
/**
Specify a custom error message or error.
If you do a custom error, it's recommended to sub-class `pTimeout.TimeoutError`.
*/
message?: string | Error;
/**
Custom implementations for the `setTimeout` and `clearTimeout` functions.
Useful for testing purposes, in particular to work around [`sinon.useFakeTimers()`](https://sinonjs.org/releases/latest/fake-timers/).
@example
```
import pTimeout from 'p-timeout';
import sinon from 'sinon';
const originalSetTimeout = setTimeout;
const originalClearTimeout = clearTimeout;
sinon.useFakeTimers();
// Use `pTimeout` without being affected by `sinon.useFakeTimers()`:
await pTimeout(doSomething(), {
milliseconds: 2000,
customTimers: {
setTimeout: originalSetTimeout,
clearTimeout: originalClearTimeout
}
});
```
*/
readonly customTimers?: {
setTimeout: typeof globalThis.setTimeout;
clearTimeout: typeof globalThis.clearTimeout;
};
/**
You can abort the promise using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
_Requires Node.js 16 or later._
@example
```
import pTimeout from 'p-timeout';
import delay from 'delay';
const delayedPromise = delay(3000);
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, 100);
await pTimeout(delayedPromise, {
milliseconds: 2000,
signal: abortController.signal
});
```
*/
signal?: globalThis.AbortSignal;
};
/**
Timeout a promise after a specified amount of time.
If you pass in a cancelable promise, specifically a promise with a `.cancel()` method, that method will be called when the `pTimeout` promise times out.
@param input - Promise to decorate.
@returns A decorated `input` that times out after `milliseconds` time. It has a `.clear()` method that clears the timeout.
@example
```
import {setTimeout} from 'node:timers/promises';
import pTimeout from 'p-timeout';
const delayedPromise = () => setTimeout(200);
await pTimeout(delayedPromise(), {
milliseconds: 50,
fallback: () => {
return pTimeout(delayedPromise(), 300);
}
});
```
*/
export class TimeoutError extends Error {
readonly name: "TimeoutError";
constructor(message?: string) {
super(message);
this.name = "TimeoutError";
}
}
/**
An error to be thrown when the request is aborted by AbortController.
DOMException is thrown instead of this Error when DOMException is available.
*/
export class AbortError extends Error {
readonly name: "AbortError";
constructor(message?: string) {
super();
this.name = "AbortError";
this.message = message as string;
}
}
/**
TODO: Remove AbortError and just throw DOMException when targeting Node 18.
*/
const getDOMException = (errorMessage: string | undefined) =>
globalThis.DOMException === undefined
? new AbortError(errorMessage)
: new DOMException(errorMessage);
/**
TODO: Remove below function and just 'reject(signal.reason)' when targeting Node 18.
*/
const getAbortedReason = (signal: AbortSignal) => {
const reason =
signal.reason === undefined
? getDOMException("This operation was aborted.")
: signal.reason;
return reason instanceof Error ? reason : getDOMException(reason);
};
export default function pTimeout<ValueType, ReturnType = ValueType>(
promise: PromiseLike<ValueType> | PCancelable<ValueType>,
options: Options<ReturnType>
): ClearablePromise<ValueType | ReturnType> {
const {
milliseconds,
fallback,
message,
customTimers = { setTimeout, clearTimeout },
} = options;
let timer: undefined | number;
const cancelablePromise = new Promise((resolve, reject) => {
if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) {
throw new TypeError(
`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``
);
}
if (milliseconds === Number.POSITIVE_INFINITY) {
resolve(promise);
return;
}
if (options.signal) {
const { signal } = options;
if (signal.aborted) {
reject(getAbortedReason(signal));
}
signal.addEventListener("abort", () => {
reject(getAbortedReason(signal));
});
}
timer = customTimers.setTimeout.call(
undefined,
() => {
if (fallback) {
try {
resolve(fallback());
} catch (error) {
reject(error);
}
return;
}
const errorMessage =
typeof message === "string"
? message
: `Promise timed out after ${milliseconds} milliseconds`;
const timeoutError =
message instanceof Error ? message : new TimeoutError(errorMessage);
if (typeof (promise as PCancelable<any>)?.cancel === "function") {
(promise as PCancelable<any>)?.cancel();
}
reject(timeoutError);
},
milliseconds
);
(async () => {
try {
resolve(await promise);
} catch (error) {
reject(error);
} finally {
customTimers.clearTimeout.call(undefined, timer);
}
})();
});
(cancelablePromise as ClearablePromise<ValueType | ReturnType>).clear =
() => {
customTimers.clearTimeout.call(undefined, timer);
timer = undefined;
};
return cancelablePromise as ClearablePromise<ValueType | ReturnType>;
}

74
test.js
View File

@ -9,39 +9,45 @@ const fixture = Symbol('fixture');
const fixtureError = new Error('fixture');
test('resolves before timeout', async t => {
t.is(await pTimeout(delay(50).then(() => fixture), 200), fixture);
t.is(await pTimeout(delay(50).then(() => fixture), {milliseconds: 200}), fixture);
});
test('throws when milliseconds is not number', async t => {
await t.throwsAsync(pTimeout(delay(50), '200'), {instanceOf: TypeError});
await t.throwsAsync(pTimeout(delay(50), {milliseconds: '200'}), {instanceOf: TypeError});
});
test('throws when milliseconds is negative number', async t => {
await t.throwsAsync(pTimeout(delay(50), -1), {instanceOf: TypeError});
await t.throwsAsync(pTimeout(delay(50), {milliseconds: -1}), {instanceOf: TypeError});
});
test('throws when milliseconds is NaN', async t => {
await t.throwsAsync(pTimeout(delay(50), {milliseconds: Number.NaN}), {instanceOf: TypeError});
});
test('handles milliseconds being `Infinity`', async t => {
t.is(
await pTimeout(delay(50, {value: fixture}), Number.POSITIVE_INFINITY),
fixture
await pTimeout(delay(50, {value: fixture}), {milliseconds: Number.POSITIVE_INFINITY}),
fixture,
);
});
test('rejects after timeout', async t => {
await t.throwsAsync(pTimeout(delay(200), 50), {instanceOf: TimeoutError});
await t.throwsAsync(pTimeout(delay(200), {milliseconds: 50}), {instanceOf: TimeoutError});
});
test('rejects before timeout if specified promise rejects', async t => {
await t.throwsAsync(pTimeout(delay(50).then(() => Promise.reject(fixtureError)), 200), {message: fixtureError.message});
await t.throwsAsync(pTimeout(delay(50).then(() => {
throw fixtureError;
}), {milliseconds: 200}), {message: fixtureError.message});
});
test('fallback argument', async t => {
await t.throwsAsync(pTimeout(delay(200), 50, 'rainbow'), {message: 'rainbow'});
await t.throwsAsync(pTimeout(delay(200), 50, new RangeError('cake')), {instanceOf: RangeError});
await t.throwsAsync(pTimeout(delay(200), 50, () => Promise.reject(fixtureError)), {message: fixtureError.message});
await t.throwsAsync(pTimeout(delay(200), 50, () => {
await t.throwsAsync(pTimeout(delay(200), {milliseconds: 50, message: 'rainbow'}), {message: 'rainbow'});
await t.throwsAsync(pTimeout(delay(200), {milliseconds: 50, message: new RangeError('cake')}), {instanceOf: RangeError});
await t.throwsAsync(pTimeout(delay(200), {milliseconds: 50, fallback: () => Promise.reject(fixtureError)}), {message: fixtureError.message});
await t.throwsAsync(pTimeout(delay(200), {milliseconds: 50, fallback() {
throw new RangeError('cake');
}), {instanceOf: RangeError});
}}), {instanceOf: RangeError});
});
test('calls `.cancel()` on promise when it exists', async t => {
@ -54,14 +60,15 @@ test('calls `.cancel()` on promise when it exists', async t => {
resolve();
});
await t.throwsAsync(pTimeout(promise, 50), {instanceOf: TimeoutError});
await t.throwsAsync(pTimeout(promise, {milliseconds: 50}), {instanceOf: TimeoutError});
t.true(promise.isCanceled);
});
test('accepts `customTimers` option', async t => {
t.plan(2);
await pTimeout(delay(50), 123, undefined, {
await pTimeout(delay(50), {
milliseconds: 123,
customTimers: {
setTimeout(fn, milliseconds) {
t.is(milliseconds, 123);
@ -70,17 +77,50 @@ test('accepts `customTimers` option', async t => {
clearTimeout(timeoutId) {
t.pass();
return clearTimeout(timeoutId);
}
}
},
},
});
});
test('`.clear()` method', async t => {
const end = timeSpan();
const promise = pTimeout(delay(300), 200);
const promise = pTimeout(delay(300), {milliseconds: 200});
promise.clear();
await promise;
t.true(inRange(end(), {start: 0, end: 350}));
});
/**
TODO: Remove if statement when targeting Node.js 16.
*/
if (globalThis.AbortController !== undefined) {
test('rejects when calling `AbortController#abort()`', async t => {
const abortController = new AbortController();
const promise = pTimeout(delay(3000), {
milliseconds: 2000,
signal: abortController.signal,
});
abortController.abort();
await t.throwsAsync(promise, {
name: 'AbortError',
});
});
test('already aborted signal', async t => {
const abortController = new AbortController();
abortController.abort();
await t.throwsAsync(pTimeout(delay(3000), {
milliseconds: 2000,
signal: abortController.signal,
}), {
name: 'AbortError',
});
});
}

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"declaration": true,
"strict": true,
"module": "commonjs",
"target": "esnext",
"esModuleInterop": true,
"sourceMap": false,
"rootDir": "src",
"outDir": "dist",
"typeRoots": [
"node_modules/@types",
],
"moduleResolution": "node",
"declarationMap": true,
"declarationDir": "dist",
"emitDeclarationOnly": false,
"allowJs": true
},
"include": [
"src"
]
}