Move the secondary parameters into the options parameter (#29)

This commit is contained in:
Gyubong 2022-07-26 01:16:27 +09:00 committed by GitHub
parent de29542ac4
commit d7920cae0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 130 additions and 101 deletions

97
index.d.ts vendored
View File

@ -1,18 +1,54 @@
/* eslint-disable import/export */
export class TimeoutError extends Error { export class TimeoutError extends Error {
readonly name: 'TimeoutError'; readonly name: 'TimeoutError';
constructor(message?: string); constructor(message?: string);
} }
export interface ClearablePromise<T> extends Promise<T>{ export interface ClearablePromise<T> extends Promise<T> {
/** /**
Clear the timeout. Clear the timeout.
*/ */
clear: () => void; 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 '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. Custom implementations for the `setTimeout` and `clearTimeout` functions.
@ -29,7 +65,8 @@ export type Options = {
sinon.useFakeTimers(); sinon.useFakeTimers();
// Use `pTimeout` without being affected by `sinon.useFakeTimers()`: // Use `pTimeout` without being affected by `sinon.useFakeTimers()`:
await pTimeout(doSomething(), 2000, undefined, { await pTimeout(doSomething(), {
milliseconds: 2000,
customTimers: { customTimers: {
setTimeout: originalSetTimeout, setTimeout: originalSetTimeout,
clearTimeout: originalClearTimeout clearTimeout: originalClearTimeout
@ -38,8 +75,8 @@ export type Options = {
``` ```
*/ */
readonly customTimers?: { readonly customTimers?: {
setTimeout: typeof global.setTimeout; setTimeout: typeof globalThis.setTimeout;
clearTimeout: typeof global.clearTimeout; clearTimeout: typeof globalThis.clearTimeout;
}; };
/** /**
@ -60,7 +97,8 @@ export type Options = {
abortController.abort(); abortController.abort();
}, 100); }, 100);
await pTimeout(delayedPromise, 2000, undefined, { await pTimeout(delayedPromise, {
milliseconds: 2000,
signal: abortController.signal signal: abortController.signal
}); });
``` ```
@ -74,36 +112,6 @@ 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. 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 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. @returns A decorated `input` that times out after `milliseconds` time. It has a `.clear()` method that clears the timeout.
@example @example
@ -113,14 +121,15 @@ import pTimeout from 'p-timeout';
const delayedPromise = () => setTimeout(200); const delayedPromise = () => setTimeout(200);
await pTimeout(delayedPromise(), 50, () => { await pTimeout(delayedPromise(), {
return pTimeout(delayedPromise(), 300); milliseconds: 50,
fallback: () => {
return pTimeout(delayedPromise(), 300);
}
}); });
``` ```
*/ */
export default function pTimeout<ValueType, ReturnType>( export default function pTimeout<ValueType, ReturnType = ValueType>(
input: PromiseLike<ValueType>, input: PromiseLike<ValueType>,
milliseconds: number, options: Options<ReturnType>
fallback: () => ReturnType | Promise<ReturnType>,
options?: Options
): ClearablePromise<ValueType | ReturnType>; ): ClearablePromise<ValueType | ReturnType>;

View File

@ -35,10 +35,12 @@ const getAbortedReason = signal => {
return reason instanceof Error ? reason : getDOMException(reason); return reason instanceof Error ? reason : getDOMException(reason);
}; };
export default function pTimeout(promise, milliseconds, fallback, options) { export default function pTimeout(promise, options) {
let timer; let timer;
const cancelablePromise = new Promise((resolve, reject) => { const cancelablePromise = new Promise((resolve, reject) => {
const {milliseconds, fallback, message} = options;
if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) { if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) {
throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``); throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``);
} }
@ -65,7 +67,7 @@ export default function pTimeout(promise, milliseconds, fallback, options) {
} }
timer = options.customTimers.setTimeout.call(undefined, () => { timer = options.customTimers.setTimeout.call(undefined, () => {
if (typeof fallback === 'function') { if (fallback) {
try { try {
resolve(fallback()); resolve(fallback());
} catch (error) { } catch (error) {
@ -75,8 +77,8 @@ export default function pTimeout(promise, milliseconds, fallback, options) {
return; return;
} }
const message = typeof fallback === 'string' ? fallback : `Promise timed out after ${milliseconds} milliseconds`; const errorMessage = typeof message === 'string' ? message : `Promise timed out after ${milliseconds} milliseconds`;
const timeoutError = fallback instanceof Error ? fallback : new TimeoutError(message); const timeoutError = message instanceof Error ? message : new TimeoutError(errorMessage);
if (typeof promise.cancel === 'function') { if (typeof promise.cancel === 'function') {
promise.cancel(); promise.cancel();

View File

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

View File

@ -16,14 +16,15 @@ import pTimeout from 'p-timeout';
const delayedPromise = setTimeout(200); const delayedPromise = setTimeout(200);
await pTimeout(delayedPromise, 50); await pTimeout(delayedPromise, {
milliseconds: 50,
});
//=> [TimeoutError: Promise timed out after 50 milliseconds] //=> [TimeoutError: Promise timed out after 50 milliseconds]
``` ```
## API ## API
### pTimeout(input, milliseconds, message?, options?) ### pTimeout(input, options)
### pTimeout(input, milliseconds, fallback?, options?)
Returns a decorated `input` that times out after `milliseconds` time. It has a `.clear()` method that clears the timeout. 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. Promise to decorate.
#### milliseconds #### options
Type: `object`
##### milliseconds
Type: `number` Type: `number`
@ -43,7 +48,7 @@ Milliseconds before timing out.
Passing `Infinity` will cause it to never time out. Passing `Infinity` will cause it to never time out.
#### message ##### message
Type: `string | Error`\ Type: `string | Error`\
Default: `'Promise timed out after 50 milliseconds'` 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`. If you do a custom error, it's recommended to sub-class `pTimeout.TimeoutError`.
#### fallback ##### fallback
Type: `Function` Type: `Function`
@ -66,15 +71,14 @@ import pTimeout from 'p-timeout';
const delayedPromise = () => setTimeout(200); const delayedPromise = () => setTimeout(200);
await pTimeout(delayedPromise(), 50, () => { await pTimeout(delayedPromise(), {
return pTimeout(delayedPromise(), 300); milliseconds: 50,
fallback: () => {
return pTimeout(delayedPromise(), 300);
},
}); });
``` ```
#### options
Type: `object`
##### customTimers ##### customTimers
Type: `object` with function properties `setTimeout` and `clearTimeout` Type: `object` with function properties `setTimeout` and `clearTimeout`
@ -95,7 +99,8 @@ const originalClearTimeout = clearTimeout;
sinon.useFakeTimers(); sinon.useFakeTimers();
// Use `pTimeout` without being affected by `sinon.useFakeTimers()`: // Use `pTimeout` without being affected by `sinon.useFakeTimers()`:
await pTimeout(doSomething(), 2000, undefined, { await pTimeout(doSomething(), {
milliseconds: 2000,
customTimers: { customTimers: {
setTimeout: originalSetTimeout, setTimeout: originalSetTimeout,
clearTimeout: originalClearTimeout clearTimeout: originalClearTimeout
@ -123,7 +128,8 @@ setTimeout(() => {
abortController.abort(); abortController.abort();
}, 100); }, 100);
await pTimeout(delayedPromise, 2000, undefined, { await pTimeout(delayedPromise, {
milliseconds: 2000,
signal: abortController.signal signal: abortController.signal
}); });
``` ```

39
test.js
View File

@ -9,43 +9,45 @@ const fixture = Symbol('fixture');
const fixtureError = new Error('fixture'); const fixtureError = new Error('fixture');
test('resolves before timeout', async t => { 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 => { 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 => { 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 => { test('throws when milliseconds is NaN', async t => {
await t.throwsAsync(pTimeout(delay(50), Number.NaN), {instanceOf: TypeError}); await t.throwsAsync(pTimeout(delay(50), {milliseconds: Number.NaN}), {instanceOf: TypeError});
}); });
test('handles milliseconds being `Infinity`', async t => { test('handles milliseconds being `Infinity`', async t => {
t.is( t.is(
await pTimeout(delay(50, {value: fixture}), Number.POSITIVE_INFINITY), await pTimeout(delay(50, {value: fixture}), {milliseconds: Number.POSITIVE_INFINITY}),
fixture fixture
); );
}); });
test('rejects after timeout', async t => { 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 => { 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 => { test('fallback argument', async t => {
await t.throwsAsync(pTimeout(delay(200), 50, 'rainbow'), {message: 'rainbow'}); await t.throwsAsync(pTimeout(delay(200), {milliseconds: 50, message: 'rainbow'}), {message: 'rainbow'});
await t.throwsAsync(pTimeout(delay(200), 50, new RangeError('cake')), {instanceOf: RangeError}); await t.throwsAsync(pTimeout(delay(200), {milliseconds: 50, message: new RangeError('cake')}), {instanceOf: RangeError});
await t.throwsAsync(pTimeout(delay(200), 50, () => Promise.reject(fixtureError)), {message: fixtureError.message}); await t.throwsAsync(pTimeout(delay(200), {milliseconds: 50, fallback: () => Promise.reject(fixtureError)}), {message: fixtureError.message});
await t.throwsAsync(pTimeout(delay(200), 50, () => { await t.throwsAsync(pTimeout(delay(200), {milliseconds: 50, fallback() {
throw new RangeError('cake'); throw new RangeError('cake');
}), {instanceOf: RangeError}); }}), {instanceOf: RangeError});
}); });
test('calls `.cancel()` on promise when it exists', async t => { test('calls `.cancel()` on promise when it exists', async t => {
@ -58,14 +60,15 @@ test('calls `.cancel()` on promise when it exists', async t => {
resolve(); resolve();
}); });
await t.throwsAsync(pTimeout(promise, 50), {instanceOf: TimeoutError}); await t.throwsAsync(pTimeout(promise, {milliseconds: 50}), {instanceOf: TimeoutError});
t.true(promise.isCanceled); t.true(promise.isCanceled);
}); });
test('accepts `customTimers` option', async t => { test('accepts `customTimers` option', async t => {
t.plan(2); t.plan(2);
await pTimeout(delay(50), 123, undefined, { await pTimeout(delay(50), {
milliseconds: 123,
customTimers: { customTimers: {
setTimeout(fn, milliseconds) { setTimeout(fn, milliseconds) {
t.is(milliseconds, 123); t.is(milliseconds, 123);
@ -81,7 +84,7 @@ test('accepts `customTimers` option', async t => {
test('`.clear()` method', async t => { test('`.clear()` method', async t => {
const end = timeSpan(); const end = timeSpan();
const promise = pTimeout(delay(300), 200); const promise = pTimeout(delay(300), {milliseconds: 200});
promise.clear(); promise.clear();
@ -96,7 +99,8 @@ if (globalThis.AbortController !== undefined) {
test('rejects when calling `AbortController#abort()`', async t => { test('rejects when calling `AbortController#abort()`', async t => {
const abortController = new AbortController(); const abortController = new AbortController();
const promise = pTimeout(delay(3000), 2000, undefined, { const promise = pTimeout(delay(3000), {
milliseconds: 2000,
signal: abortController.signal signal: abortController.signal
}); });
@ -112,7 +116,8 @@ if (globalThis.AbortController !== undefined) {
abortController.abort(); abortController.abort();
await t.throwsAsync(pTimeout(delay(3000), 2000, undefined, { await t.throwsAsync(pTimeout(delay(3000), {
milliseconds: 2000,
signal: abortController.signal signal: abortController.signal
}), { }), {
name: 'AbortError' name: 'AbortError'