From e26f08d43fe0a8f58c169b60feaac863041e4e3e Mon Sep 17 00:00:00 2001 From: peja Date: Sat, 26 Dec 2020 13:42:06 +0300 Subject: [PATCH] Make timeout clearable (#15) Co-authored-by: Sindre Sorhus --- index.d.ts | 15 ++++++--- index.js | 88 +++++++++++++++++++++++++++++----------------------- package.json | 4 ++- readme.md | 2 +- test.js | 12 +++++++ 5 files changed, 76 insertions(+), 45 deletions(-) diff --git a/index.d.ts b/index.d.ts index a9c1d37..480eb0c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -40,6 +40,13 @@ declare namespace pTimeout { }; } +interface ClearablePromise extends Promise{ + /** + Clear the timeout. + */ + clear: () => void; +} + declare const pTimeout: { TimeoutError: typeof TimeoutErrorClass; @@ -53,7 +60,7 @@ declare const pTimeout: { @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. + @returns A decorated `input` that times out after `milliseconds` time. It has a `.clear()` method that clears the timeout. @example ``` @@ -71,7 +78,7 @@ declare const pTimeout: { milliseconds: number, message?: string | Error, options?: pTimeout.Options - ): Promise; + ): ClearablePromise; /** Timeout a promise after a specified amount of time. @@ -81,7 +88,7 @@ declare const pTimeout: { @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. + @returns A decorated `input` that times out after `milliseconds` time. It has a `.clear()` method that clears the timeout. @example ``` @@ -100,7 +107,7 @@ declare const pTimeout: { milliseconds: number, fallback: () => ReturnType | Promise, options?: pTimeout.Options - ): Promise; + ): ClearablePromise; }; export = pTimeout; diff --git a/index.js b/index.js index 76a0795..3cbd95e 100644 --- a/index.js +++ b/index.js @@ -7,52 +7,62 @@ class TimeoutError extends Error { } } -const pTimeout = (promise, milliseconds, fallback, options) => new Promise((resolve, reject) => { - if (typeof milliseconds !== 'number' || milliseconds < 0) { - throw new TypeError('Expected `milliseconds` to be a positive number'); - } - - if (milliseconds === Infinity) { - resolve(promise); - return; - } - - options = { - customTimers: {setTimeout, clearTimeout}, - ...options - }; - - const timer = options.customTimers.setTimeout.call(undefined, () => { - if (typeof fallback === 'function') { - try { - resolve(fallback()); - } catch (error) { - reject(error); - } +const 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 === Infinity) { + resolve(promise); return; } - const message = typeof fallback === 'string' ? fallback : `Promise timed out after ${milliseconds} milliseconds`; - const timeoutError = fallback instanceof Error ? fallback : new TimeoutError(message); + options = { + customTimers: {setTimeout, clearTimeout}, + ...options + }; - if (typeof promise.cancel === 'function') { - promise.cancel(); - } + timer = options.customTimers.setTimeout.call(undefined, () => { + if (typeof fallback === 'function') { + try { + resolve(fallback()); + } catch (error) { + reject(error); + } - reject(timeoutError); - }, milliseconds); + return; + } - (async () => { - try { - resolve(await promise); - } catch (error) { - reject(error); - } finally { - options.customTimers.clearTimeout.call(undefined, timer); - } - })(); -}); + 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; +}; module.exports = pTimeout; // TODO: Remove this for the next major release diff --git a/package.json b/package.json index a047898..0c2e390 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,8 @@ "delay": "^4.4.0", "p-cancelable": "^2.0.0", "tsd": "^0.13.1", - "xo": "^0.35.0" + "xo": "^0.35.0", + "in-range": "^2.0.0", + "time-span": "^4.0.0" } } diff --git a/readme.md b/readme.md index 593d067..4000012 100644 --- a/readme.md +++ b/readme.md @@ -25,7 +25,7 @@ pTimeout(delayedPromise, 50).then(() => 'foo'); ### pTimeout(input, milliseconds, message?, options?) ### pTimeout(input, milliseconds, fallback?, options?) -Returns a decorated `input` that times out after `milliseconds` time. +Returns a decorated `input` that times out after `milliseconds` time. It has a `.clear()` method that clears the timeout. 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. diff --git a/test.js b/test.js index aa1ac30..cfc47ed 100644 --- a/test.js +++ b/test.js @@ -1,6 +1,8 @@ import test from 'ava'; import delay from 'delay'; import PCancelable from 'p-cancelable'; +import inRange from 'in-range'; +import timeSpan from 'time-span'; import pTimeout from '.'; const fixture = Symbol('fixture'); @@ -72,3 +74,13 @@ test('accepts `customTimers` option', async t => { } }); }); + +test('`.clear()` method', async t => { + const end = timeSpan(); + const promise = pTimeout(delay(300), 200); + + promise.clear(); + + await promise; + t.true(inRange(end(), {start: 0, end: 350})); +});