From b2bc3354ac22e76e129ef8ae5b9656fa555fa061 Mon Sep 17 00:00:00 2001 From: Nick Uraltsev Date: Thu, 15 Sep 2016 21:06:32 -0700 Subject: [PATCH] Adding Cancel and CancelToken classes --- lib/cancel/Cancel.js | 17 ++++++ lib/cancel/CancelToken.js | 57 ++++++++++++++++++ test/specs/cancel/Cancel.spec.js | 15 +++++ test/specs/cancel/CancelToken.spec.js | 87 +++++++++++++++++++++++++++ 4 files changed, 176 insertions(+) create mode 100644 lib/cancel/Cancel.js create mode 100644 lib/cancel/CancelToken.js create mode 100644 test/specs/cancel/Cancel.spec.js create mode 100644 test/specs/cancel/CancelToken.spec.js diff --git a/lib/cancel/Cancel.js b/lib/cancel/Cancel.js new file mode 100644 index 00000000..99d01ffe --- /dev/null +++ b/lib/cancel/Cancel.js @@ -0,0 +1,17 @@ +'use strict'; + +/** + * A `Cancel` is an object that is thrown when an operation is canceled. + * + * @class + * @param {string=} message The message. + */ +function Cancel(message) { + this.message = message; +} + +Cancel.prototype.toString = function toString() { + return 'Cancel' + (this.message ? ': ' + this.message : ''); +}; + +module.exports = Cancel; diff --git a/lib/cancel/CancelToken.js b/lib/cancel/CancelToken.js new file mode 100644 index 00000000..6b46e666 --- /dev/null +++ b/lib/cancel/CancelToken.js @@ -0,0 +1,57 @@ +'use strict'; + +var Cancel = require('./Cancel'); + +/** + * A `CancelToken` is an object that can be used to request cancellation of an operation. + * + * @class + * @param {Function} executor The executor function. + */ +function CancelToken(executor) { + if (typeof executor !== 'function') { + throw new TypeError('executor must be a function.'); + } + + var resolvePromise; + this.promise = new Promise(function promiseExecutor(resolve) { + resolvePromise = resolve; + }); + + var token = this; + executor(function cancel(message) { + if (token.reason) { + // Cancellation has already been requested + return; + } + + token.reason = new Cancel(message); + resolvePromise(token.reason); + }); +} + +/** + * Throws a `Cancel` if cancellation has been requested. + */ +CancelToken.prototype.throwIfRequested = function throwIfRequested() { + if (this.reason) { + throw this.reason; + } +}; + +/** + * Returns an object that contains a new `CancelToken` and a function that, when called, + * cancels the `CancelToken`. + */ +CancelToken.source = function source() { + var cancel; + var token = new CancelToken(function executor(c) { + cancel = c; + }); + return { + token: token, + cancel: cancel + }; +}; + +module.exports = CancelToken; diff --git a/test/specs/cancel/Cancel.spec.js b/test/specs/cancel/Cancel.spec.js new file mode 100644 index 00000000..0e0de805 --- /dev/null +++ b/test/specs/cancel/Cancel.spec.js @@ -0,0 +1,15 @@ +var Cancel = require('../../../lib/cancel/Cancel'); + +describe('Cancel', function() { + describe('toString', function() { + it('returns correct result when message is not specified', function() { + var cancel = new Cancel(); + expect(cancel.toString()).toBe('Cancel'); + }); + + it('returns correct result when message is specified', function() { + var cancel = new Cancel('Operation has been canceled.'); + expect(cancel.toString()).toBe('Cancel: Operation has been canceled.'); + }); + }); +}); diff --git a/test/specs/cancel/CancelToken.spec.js b/test/specs/cancel/CancelToken.spec.js new file mode 100644 index 00000000..dd723271 --- /dev/null +++ b/test/specs/cancel/CancelToken.spec.js @@ -0,0 +1,87 @@ +var CancelToken = require('../../../lib/cancel/CancelToken'); +var Cancel = require('../../../lib/cancel/Cancel'); + +describe('CancelToken', function() { + describe('constructor', function() { + it('throws when executor is not specified', function() { + expect(function() { + new CancelToken(); + }).toThrowError(TypeError, 'executor must be a function.'); + }); + + it('throws when executor is not a function', function() { + expect(function() { + new CancelToken(123); + }).toThrowError(TypeError, 'executor must be a function.'); + }); + }); + + describe('reason', function() { + it('returns a Cancel if cancellation has been requested', function() { + var cancel; + var token = new CancelToken(function(c) { + cancel = c; + }); + cancel('Operation has been canceled.'); + expect(token.reason).toEqual(jasmine.any(Cancel)); + expect(token.reason.message).toBe('Operation has been canceled.'); + }); + + it('returns undefined if cancellation has not been requested', function() { + var token = new CancelToken(function() {}); + expect(token.reason).toBeUndefined(); + }); + }); + + describe('promise', function() { + it('returns a Promise that resolves when cancellation is requested', function(done) { + var cancel; + var token = new CancelToken(function(c) { + cancel = c; + }); + token.promise.then(function onFulfilled(value) { + expect(value).toEqual(jasmine.any(Cancel)); + expect(value.message).toBe('Operation has been canceled.'); + done(); + }); + cancel('Operation has been canceled.'); + }); + }); + + describe('throwIfRequested', function() { + it('throws if cancellation has been requested', function() { + // Note: we cannot use expect.toThrowError here as Cancel does not inherit from Error + var cancel; + var token = new CancelToken(function(c) { + cancel = c; + }); + cancel('Operation has been canceled.'); + try { + token.throwIfRequested(); + fail('Expected throwIfRequested to throw.'); + } catch (thrown) { + if (!(thrown instanceof Cancel)) { + fail('Expected throwIfRequested to throw a Cancel, but it threw ' + thrown + '.'); + } + expect(thrown.message).toBe('Operation has been canceled.'); + } + }); + + it('does not throw if cancellation has not been requested', function() { + var token = new CancelToken(function() {}); + token.throwIfRequested(); + }); + }); + + describe('source', function() { + it('returns an object containing token and cancel function', function() { + var source = CancelToken.source(); + expect(source.token).toEqual(jasmine.any(CancelToken)); + expect(source.cancel).toEqual(jasmine.any(Function)); + expect(source.token.reason).toBeUndefined(); + source.cancel('Operation has been canceled.'); + expect(source.token.reason).toEqual(jasmine.any(Cancel)); + expect(source.token.reason.message).toBe('Operation has been canceled.'); + }); + }); +});