diff --git a/.gitattributes b/.gitattributes index 391f0a4..6313b56 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1 @@ -* text=auto -*.js text eol=lf +* text=auto eol=lf diff --git a/.travis.yml b/.travis.yml index 7d69d74..f3fa8cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ language: node_js node_js: + - '10' - '8' - - '6' - - '4' diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..e0971a1 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,47 @@ +/** + * 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 ms - 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 `ms` time. + */ +export default function pTimeout( + input: PromiseLike, + ms: number, + message?: string | Error +): Promise; + +/** + * 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 ms - Milliseconds before timing 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 `ms` time. + * + * @example + * + * import delay from 'delay'; + * import pTimeout from 'p-timeout'; + * + * const delayedPromise = () => delay(200); + * + * pTimeout(delayedPromise(), 50, () => { + * return pTimeout(delayedPromise(), 300); + * }); + */ +export default function pTimeout( + input: PromiseLike, + ms: number, + fallback: () => ReturnType | Promise +): Promise; + +export class TimeoutError extends Error { + readonly name: 'TimeoutError'; + constructor(message?: string); +} diff --git a/index.js b/index.js index 8393646..7e5a678 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ 'use strict'; + const pFinally = require('p-finally'); class TimeoutError extends Error { @@ -8,7 +9,7 @@ class TimeoutError extends Error { } } -module.exports = (promise, ms, fallback) => new Promise((resolve, reject) => { +const pTimeout = (promise, ms, fallback) => new Promise((resolve, reject) => { if (typeof ms !== 'number' || ms < 0) { throw new TypeError('Expected `ms` to be a positive number'); } @@ -17,23 +18,25 @@ module.exports = (promise, ms, fallback) => new Promise((resolve, reject) => { if (typeof fallback === 'function') { try { resolve(fallback()); - } catch (err) { - reject(err); + } catch (error) { + reject(error); } + return; } const message = typeof fallback === 'string' ? fallback : `Promise timed out after ${ms} milliseconds`; - const err = fallback instanceof Error ? fallback : new TimeoutError(message); + const timeoutError = fallback instanceof Error ? fallback : new TimeoutError(message); if (typeof promise.cancel === 'function') { promise.cancel(); } - reject(err); + reject(timeoutError); }, ms); pFinally( + // eslint-disable-next-line promise/prefer-await-to-then promise.then(resolve, reject), () => { clearTimeout(timer); @@ -41,4 +44,7 @@ module.exports = (promise, ms, fallback) => new Promise((resolve, reject) => { ); }); +module.exports = pTimeout; +module.exports.default = pTimeout; + module.exports.TimeoutError = TimeoutError; diff --git a/index.test-d.ts b/index.test-d.ts new file mode 100644 index 0000000..e030706 --- /dev/null +++ b/index.test-d.ts @@ -0,0 +1,25 @@ +import {expectType} from 'tsd-check'; +import pTimeout, {TimeoutError} from '.'; + +const delayedPromise: () => Promise = () => + new Promise(resolve => setTimeout(() => resolve('foo'), 200)); + +pTimeout(delayedPromise(), 50).then(() => 'foo'); +pTimeout(delayedPromise(), 50, () => { + return pTimeout(delayedPromise(), 300); +}); +pTimeout(delayedPromise(), 50).then(value => expectType(value)); +pTimeout(delayedPromise(), 50, 'error').then(value => + expectType(value) +); +pTimeout(delayedPromise(), 50, new Error('error')).then(value => + expectType(value) +); +pTimeout(delayedPromise(), 50, async () => 10).then(value => { + expectType(value); +}); +pTimeout(delayedPromise(), 50, () => 10).then(value => { + expectType(value); +}); + +expectType(TimeoutError); diff --git a/package.json b/package.json index 48cb322..ed376c2 100644 --- a/package.json +++ b/package.json @@ -1,43 +1,45 @@ { - "name": "p-timeout", - "version": "2.0.1", - "description": "Timeout a promise after a specified amount of time", - "license": "MIT", - "repository": "sindresorhus/p-timeout", - "author": { - "name": "Sindre Sorhus", - "email": "sindresorhus@gmail.com", - "url": "sindresorhus.com" - }, - "engines": { - "node": ">=4" - }, - "scripts": { - "test": "xo && ava" - }, - "files": [ - "index.js" - ], - "keywords": [ - "promise", - "timeout", - "error", - "invalidate", - "async", - "await", - "promises", - "time", - "out", - "cancel", - "bluebird" - ], - "dependencies": { - "p-finally": "^1.0.0" - }, - "devDependencies": { - "ava": "*", - "delay": "^2.0.0", - "p-cancelable": "^0.3.0", - "xo": "*" - } + "name": "p-timeout", + "version": "2.0.1", + "description": "Timeout a promise after a specified amount of time", + "license": "MIT", + "repository": "sindresorhus/p-timeout", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "sindresorhus.com" + }, + "engines": { + "node": ">=8" + }, + "scripts": { + "test": "xo && ava && tsd-check" + }, + "files": [ + "index.js", + "index.d.ts" + ], + "keywords": [ + "promise", + "timeout", + "error", + "invalidate", + "async", + "await", + "promises", + "time", + "out", + "cancel", + "bluebird" + ], + "dependencies": { + "p-finally": "^1.0.0" + }, + "devDependencies": { + "ava": "^1.3.1", + "delay": "^4.1.0", + "p-cancelable": "^1.1.0", + "tsd-check": "^0.3.0", + "xo": "^0.24.0" + } } diff --git a/test.js b/test.js index b54cac1..fe7bdc1 100644 --- a/test.js +++ b/test.js @@ -1,49 +1,50 @@ import test from 'ava'; import delay from 'delay'; import PCancelable from 'p-cancelable'; -import m from '.'; +import pTimeout from '.'; const fixture = Symbol('fixture'); const fixtureErr = new Error('fixture'); test('resolves before timeout', async t => { - t.is(await m(delay(50).then(() => fixture), 200), fixture); + t.is(await pTimeout(delay(50).then(() => fixture), 200), fixture); }); test('throws when ms is not number', async t => { - await t.throws(m(delay(50), '200'), TypeError); + await t.throwsAsync(pTimeout(delay(50), '200'), TypeError); }); test('throws when ms is negative number', async t => { - await t.throws(m(delay(50), -1), TypeError); + await t.throwsAsync(pTimeout(delay(50), -1), TypeError); }); test('rejects after timeout', async t => { - await t.throws(m(delay(200), 50), m.TimeoutError); + await t.throwsAsync(pTimeout(delay(200), 50), pTimeout.TimeoutError); }); test('rejects before timeout if specified promise rejects', async t => { - await t.throws(m(delay(50).then(() => Promise.reject(fixtureErr)), 200), fixtureErr.message); + await t.throwsAsync(pTimeout(delay(50).then(() => Promise.reject(fixtureErr)), 200), fixtureErr.message); }); test('fallback argument', async t => { - await t.throws(m(delay(200), 50, 'rainbow'), 'rainbow'); - await t.throws(m(delay(200), 50, new RangeError('cake')), RangeError); - await t.throws(m(delay(200), 50, () => Promise.reject(fixtureErr)), fixtureErr.message); - await t.throws(m(delay(200), 50, () => { + await t.throwsAsync(pTimeout(delay(200), 50, 'rainbow'), 'rainbow'); + await t.throwsAsync(pTimeout(delay(200), 50, new RangeError('cake')), RangeError); + await t.throwsAsync(pTimeout(delay(200), 50, () => Promise.reject(fixtureErr)), fixtureErr.message); + await t.throwsAsync(pTimeout(delay(200), 50, () => { throw new RangeError('cake'); }), RangeError); }); test('calls `.cancel()` on promise when it exists', async t => { - const p = new PCancelable(onCancel => { + const promise = new PCancelable(async (resolve, reject, onCancel) => { onCancel(() => { t.pass(); }); - return delay(200); + await delay(200); + resolve(); }); - await t.throws(m(p, 50), m.TimeoutError); - t.true(p.canceled); + await t.throwsAsync(pTimeout(promise, 50), pTimeout.TimeoutError); + t.true(promise.isCanceled); });