From 90d16f2402075b3432f0940258f016be4297c350 Mon Sep 17 00:00:00 2001 From: Tim De Pauw Date: Wed, 22 Mar 2017 11:49:54 +0100 Subject: [PATCH] Add `.cancel()` (#17) --- index.js | 44 +++++++++++++++++++++++++++++--------------- package.json | 3 +++ readme.md | 13 +++++++++++++ test.js | 13 ++++++++++++- 4 files changed, 57 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index c719584..27309f5 100644 --- a/index.js +++ b/index.js @@ -1,34 +1,48 @@ 'use strict'; +const defer = require('p-defer'); + +class CancelError extends Error { + constructor(message) { + super(message); + this.name = 'CancelError'; + } +} const generate = willResolve => function (ms, value) { ms = ms || 0; + const useValue = (arguments.length > 1); + let result = value; - // If supplied, the thunk will override promise results with `value`. - const useValue = arguments.length > 1; + const delaying = defer(); + const promise = delaying.promise; - let promise; + let timeout = setTimeout(() => { + const settle = willResolve ? delaying.resolve : delaying.reject; + settle(result); + }, ms); - const thunk = result => { - if (promise) { - // Prevent unhandled rejection errors if promise is never used, and only used as a thunk - promise.catch(() => {}); + const thunk = thunkResult => { + if (!useValue) { + result = thunkResult; } - return new Promise((resolve, reject) => { - setTimeout( - willResolve ? resolve : reject, - ms, - useValue ? value : result - ); - }); + return promise; }; - promise = thunk(value); thunk.then = promise.then.bind(promise); thunk.catch = promise.catch.bind(promise); thunk._actualPromise = promise; + thunk.cancel = () => { + if (timeout) { + clearTimeout(timeout); + timeout = null; + delaying.reject(new CancelError('Delay canceled')); + } + }; + return thunk; }; module.exports = generate(true); module.exports.reject = generate(false); +module.exports.CancelError = CancelError; diff --git a/package.json b/package.json index 1617d45..19b37a4 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,9 @@ "promises", "bluebird" ], + "dependencies": { + "p-defer": "^1.0.0" + }, "devDependencies": { "ava": "*", "bluebird": "^3.3.5", diff --git a/readme.md b/readme.md index f395a34..230c0cc 100644 --- a/readme.md +++ b/readme.md @@ -54,6 +54,19 @@ Promise.resolve('foo') // executed 100 milliseconds later // err === 'bar' }); + +// you can cancel the promise by calling .cancel() +async () => { + const delaying = delay(1000); + setTimeout(() => { + delaying.cancel(); + }, 500); + try { + await delaying; + } catch (err) { + // err is an instance of delay.CancelError + } +}(); ``` diff --git a/test.js b/test.js index 6a03a91..80206fc 100644 --- a/test.js +++ b/test.js @@ -3,7 +3,7 @@ import timeSpan from 'time-span'; import inRange from 'in-range'; // TODO: this is deprecated, switch it out with the `currently-unhandled` module import trackRejections from 'loud-rejection/api'; -import m from './'; +import m, {CancelError} from './'; // install core-js promise globally, because Node 0.12 native promises don't generate unhandledRejection events global.Promise = Promise; @@ -119,3 +119,14 @@ test.failing('rejected.then(rejectThunk).catch(handler) - should not create unha tracker.currentlyUnhandled().forEach(({promise}) => promise.catch(() => {})); }); + +test('can be canceled', async t => { + const delaying = m(1000); + delaying.cancel(); + try { + await delaying; + t.fail(); + } catch (err) { + t.true(err instanceof CancelError); + } +});