From 41b0b910d2b052025ae9b2d027cbffd4f2185c62 Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Sun, 24 Jul 2022 21:06:13 +0900 Subject: [PATCH] WIP --- index.d.ts | 91 ++++++++++++++++++++++++++----------------------- index.js | 10 +++--- index.test-d.ts | 42 +++++++++++------------ readme.md | 34 ++++++++++-------- test.js | 39 ++++++++++++--------- 5 files changed, 117 insertions(+), 99 deletions(-) diff --git a/index.d.ts b/index.d.ts index a6e8a93..2952a2a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,18 +1,52 @@ -/* eslint-disable import/export */ export class TimeoutError extends Error { readonly name: 'TimeoutError'; constructor(message?: string); } -export interface ClearablePromise extends Promise{ +export interface ClearablePromise extends Promise { /** Clear the timeout. */ clear: () => void; } -export type Options = { +export type Options = { + /** + Milliseconds before timing out. + + Passing `Infinity` will cause it to never time out. + @default Infinity + */ + milliseconds?: number; + + /** + Do something other than rejecting with an error on timeout. + + @example + ``` + import {setTimeout} from 'timers/promises'; + import pTimeout from 'p-timeout'; + + const delayedPromise = () => setTimeout(200); + + await pTimeout(delayedPromise(), { + milliseconds: 50, + fallback: () => { + return pTimeout(delayedPromise(), 300); + }, + }); + ``` + */ + fallback?: () => ReturnType | Promise; + + /** + 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 +63,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,8 +73,8 @@ export type Options = { ``` */ readonly customTimers?: { - setTimeout: typeof global.setTimeout; - clearTimeout: typeof global.clearTimeout; + setTimeout?: typeof global.setTimeout; + clearTimeout?: typeof global.clearTimeout; }; /** @@ -60,7 +95,8 @@ export type Options = { abortController.abort(); }, 100); - await pTimeout(delayedPromise, 2000, undefined, { + await pTimeout(delayedPromise, { + milliseconds: 2000, signal: abortController.signal }); ``` @@ -74,36 +110,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. @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( - input: PromiseLike, - milliseconds: number, - message?: string | Error, - options?: Options -): ClearablePromise; - -/** -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 @@ -113,14 +119,15 @@ 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( input: PromiseLike, - milliseconds: number, - fallback: () => ReturnType | Promise, - options?: Options + options?: Options ): ClearablePromise; diff --git a/index.js b/index.js index 2c50263..648ce7e 100644 --- a/index.js +++ b/index.js @@ -35,10 +35,12 @@ const getAbortedReason = signal => { return reason instanceof Error ? reason : getDOMException(reason); }; -export default function pTimeout(promise, milliseconds, fallback, options) { +export default function pTimeout(promise, options) { let timer; const cancelablePromise = new Promise((resolve, reject) => { + const {milliseconds, fallback, message} = options; + if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) { 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, () => { - if (typeof fallback === 'function') { + if (fallback) { try { resolve(fallback()); } catch (error) { @@ -75,8 +77,8 @@ export default function pTimeout(promise, milliseconds, fallback, options) { return; } - const message = typeof fallback === 'string' ? fallback : `Promise timed out after ${milliseconds} milliseconds`; - const timeoutError = fallback instanceof Error ? fallback : new TimeoutError(message); + 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(); diff --git a/index.test-d.ts b/index.test-d.ts index 9a968ef..7a2807d 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -2,40 +2,38 @@ import {expectType, expectError} from 'tsd'; import pTimeout, {TimeoutError} from './index.js'; -const delayedPromise: () => Promise = 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 = async () => new Promise(resolve => { + setTimeout(() => { + resolve('foo'); + }, 200); }); -pTimeout(delayedPromise(), 50).then(value => expectType(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(value)); +pTimeout(delayedPromise(), {milliseconds: 50, message: 'error'}).then(value => expectType(value) ); -pTimeout(delayedPromise(), 50, new Error('error')).then(value => +pTimeout(delayedPromise(), {milliseconds: 50, message: new Error('error')}).then(value => expectType(value) ); -pTimeout(delayedPromise(), 50, async () => 10).then(value => { +pTimeout(delayedPromise(), {milliseconds: 50, fallback: async () => 10}).then(value => { expectType(value); }); -pTimeout(delayedPromise(), 50, () => 10).then(value => { +pTimeout(delayedPromise(), {milliseconds: 50, fallback: () => 10}).then(value => { expectType(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 diff --git a/readme.md b/readme.md index a6ddae5..19ad7db 100644 --- a/readme.md +++ b/readme.md @@ -16,14 +16,15 @@ 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` @@ -66,15 +71,14 @@ 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` @@ -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 @@ -123,7 +128,8 @@ setTimeout(() => { abortController.abort(); }, 100); -await pTimeout(delayedPromise, 2000, undefined, { +await pTimeout(delayedPromise, { + milliseconds: 2000, signal: abortController.signal }); ``` diff --git a/test.js b/test.js index c9f31fb..0488bdd 100644 --- a/test.js +++ b/test.js @@ -9,43 +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), Number.NaN), {instanceOf: TypeError}); + 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), + 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 => { @@ -58,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); @@ -81,7 +84,7 @@ test('accepts `customTimers` option', async t => { test('`.clear()` method', async t => { const end = timeSpan(); - const promise = pTimeout(delay(300), 200); + const promise = pTimeout(delay(300), {milliseconds: 200}); promise.clear(); @@ -96,7 +99,8 @@ if (globalThis.AbortController !== undefined) { test('rejects when calling `AbortController#abort()`', async t => { const abortController = new AbortController(); - const promise = pTimeout(delay(3000), 2000, undefined, { + const promise = pTimeout(delay(3000), { + milliseconds: 2000, signal: abortController.signal }); @@ -112,7 +116,8 @@ if (globalThis.AbortController !== undefined) { abortController.abort(); - await t.throwsAsync(pTimeout(delay(3000), 2000, undefined, { + await t.throwsAsync(pTimeout(delay(3000), { + milliseconds: 2000, signal: abortController.signal }), { name: 'AbortError'