From 4c0f11af4620c822b19f0a8590e7a7814b054a25 Mon Sep 17 00:00:00 2001 From: Jelle De Loecker Date: Sat, 17 Feb 2024 13:44:43 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Allow=20`Pledge`=20instances=20to?= =?UTF-8?q?=20be=20cancelled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + lib/pledge.js | 154 +++++++++++++++++++++++++++++++++++++++++----- test/pledge.js | 163 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 301 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0192b73..c3d344f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.9.0 (WIP) * Rewrite JavaScript tokenizer to no longer use regexes +* Allow `Pledge` instances to be cancelled ## 0.9.0 (2024-02-15) diff --git a/lib/pledge.js b/lib/pledge.js index f47f057..2c8bc4e 100644 --- a/lib/pledge.js +++ b/lib/pledge.js @@ -1,14 +1,18 @@ const PENDING = 0, RESOLVED = 1, - REJECTED = 2; + REJECTED = 2, + CANCELLED = 3; const REJECTED_REASON = Symbol('rejected_reason'), RESOLVED_VALUE = Symbol('resolved_value'), START_EXECUTOR = Symbol('start_executor'), ON_FULFILLED = Symbol('on_fulfilled'), - SUB_PLEDGES = Symbol('sub_pledges'), + ON_CANCELLED = Symbol('on_cancelled'), + SUB_PLEDGES = Symbol('sub_pledges'), ON_REJECTED = Symbol('on_rejected'), DO_RESOLVE = Symbol('do_resolve'), + ON_FINALLY = Symbol('on_finally'), + DO_FINALLY = Symbol('do_finally'), DO_REJECT = Symbol('do_reject'), EXECUTOR = Symbol('executor'), UPDATED = Symbol('updated'), @@ -92,6 +96,15 @@ AbstractPledge.setProperty({ */ [ON_REJECTED]: null, + /** + * An array of tasks to perform when this pledge is cancelled + * + * @author Jelle De Loecker + * @since 0.9.1 + * @version 0.9.1 + */ + [ON_CANCELLED]: null, + /** * The eventual resolved value * @@ -132,17 +145,6 @@ AbstractPledge.setProperty({ */ _durations: null, - /** - * Property that could be a function to cancel the pledge - * - * @author Jelle De Loecker - * @since 0.7.0 - * @version 0.7.0 - * - * @type {Function} - */ - cancel: null, - /** * Warn when an error is not caught * @@ -519,6 +521,111 @@ AbstractPledge.setMethod(function reportProgressPart(parts) {}); */ AbstractPledge.setMethod(function _addProgressPledge(pledge) {}); +/** + * Cancel the pledge + * + * @author Jelle De Loecker + * @since 0.9.1 + * @version 0.9.1 + */ +AbstractPledge.setMethod(function cancel() { + + if (!this.isPending()) { + return; + } + + this[STATE] = CANCELLED; + + // Always do the on-cancel tasks as swiftly as possible + return Swift.all(this[ON_CANCELLED]).finally(() => this[DO_FINALLY]()); +}); + +/** + * Do the given task when this pledge is cancelled + * + * @author Jelle De Loecker + * @since 0.9.1 + * @version 0.9.1 + */ +AbstractPledge.setMethod(function onCancelled(task) { + + if (typeof task != 'function') { + return; + } + + if (!this.isPending()) { + + if (this.isCancelled() && task) { + task(); + } + + return; + } + + if (!this[ON_CANCELLED]) { + this[ON_CANCELLED] = []; + } + + this[ON_CANCELLED].push(next => Swift.done(task(), next)); +}); + +/** + * Do all the finally callbacks + * + * @author Jelle De Loecker + * @since 0.9.1 + * @version 0.9.1 + */ +AbstractPledge.setMethod(DO_FINALLY, function doFinally() { + while (this[ON_FINALLY]?.length) { + this[ON_FINALLY].shift()(); + } +}); + +/** + * Has this pledge been resolved? + * + * @author Jelle De Loecker + * @since 0.9.1 + * @version 0.9.1 + */ +AbstractPledge.setMethod(function isResolved() { + return this[STATE] === RESOLVED; +}); + +/** + * Has this pledge been rejected? + * + * @author Jelle De Loecker + * @since 0.9.1 + * @version 0.9.1 + */ +AbstractPledge.setMethod(function isRejected() { + return this[STATE] === REJECTED; +}); + +/** + * Has this pledge been cancelled? + * + * @author Jelle De Loecker + * @since 0.9.1 + * @version 0.9.1 + */ +AbstractPledge.setMethod(function isCancelled() { + return this[STATE] === CANCELLED; +}); + +/** + * Is this pledge still pending? + * + * @author Jelle De Loecker + * @since 0.9.1 + * @version 0.9.1 + */ +AbstractPledge.setMethod(function isPending() { + return this[STATE] === PENDING; +}); + /** * The BasePledge Class * @@ -1075,7 +1182,7 @@ Pledge.setMethod('catch', function _catch(on_rejected) { * * @author Jelle De Loecker * @since 0.5.6 - * @version 0.5.6 + * @version 0.9.1 * * @param {Function} on_finally * @@ -1083,7 +1190,20 @@ Pledge.setMethod('catch', function _catch(on_rejected) { */ Pledge.setMethod('finally', function _finally(on_finally) { - var constructor = this.constructor; + let constructor = this.constructor; + + if (this.isPending() || this.isCancelled()) { + if (!this[ON_FINALLY]) { + this[ON_FINALLY] = []; + } + + // We keep this in an array in case the pledge gets cancelled + this[ON_FINALLY].push(on_finally); + + if (this.isCancelled()) { + return this[DO_FINALLY](); + } + } return this.then( function afterResolved(value) { @@ -1374,8 +1494,9 @@ Swift.setMethod(DO_RESOLVE, function _doResolve(value) { this[ON_REJECTED].length = 0; } -}); + this[DO_FINALLY](); +}); /** * Reject with the given reason @@ -1393,6 +1514,7 @@ Swift.setMethod(function reject(reason) { } this[DO_REJECT](reason); + this[DO_FINALLY](); }); /** diff --git a/test/pledge.js b/test/pledge.js index 08b25c0..54e07a7 100644 --- a/test/pledge.js +++ b/test/pledge.js @@ -418,6 +418,10 @@ describe('Pledge', function() { let x = await Pledge.all(['hello', Pledge.resolve('world')]); assert.deepEqual(x, ['hello', 'world']); }); + + it('should do the tasks in order', async () => { + pledgeAllTestOne(Pledge, true); + }); }); describe('.race', function () { @@ -668,6 +672,16 @@ describe('Pledge', function() { }); }); + describe('#cancel()', () => { + it('should cancel the pledge and call `finally`', async () => { + return pledgeCancelTestOne(Pledge); + }); + + it('should call the onCancel queued tasks first', async () => { + return pledgeCancelTestTwo(Pledge); + }); + }); + describe('#handleCallback(callback)', function() { it('should call the callback when resolving or rejecting', function() { @@ -876,5 +890,152 @@ describe('TimeoutPledge', function() { }); }); }); +}); + +describe('Swift', function() { + + describe('.all(tasks)', () => { + it('should do the tasks immediately', async () => { + pledgeAllTestOne(Pledge.Swift, false); + }); + }); + + describe('#cancel()', () => { + it('should cancel the pledge and call `finally`', async () => { + return pledgeCancelTestOne(Pledge.Swift); + }); + + it('should call the onCancel queued tasks first', async () => { + return pledgeCancelTestTwo(Pledge.Swift); + }); + }); + +}); + +async function pledgeAllTestOne(constructor, do_wait = true) { + + let finished_one = false, + finished_two = false, + finished_three = false; + + let counter = 1; + + let tasks = []; + tasks.push((next) => { + finished_one = counter++; + }); + + tasks.push((next) => { + finished_two = counter++; + }); + + tasks.push((next) => { + finished_three = counter++; + }); + + constructor.all(tasks); + + if (do_wait) { + await Pledge.after(3); + } + + assert.strictEqual(finished_one, 1); + assert.strictEqual(finished_two, 2); + assert.strictEqual(finished_three, 3); +} + +async function pledgeCancelTestOne(constructor) { + + let pledge = new constructor(); + let then_called = false, + catch_called = false, + finally_called = false; + + pledge.then(() => { + then_called = true; + }); + + pledge.catch(() => { + catch_called = true; + }); + + pledge.finally(() => { + finally_called = true; + }); + + pledge.cancel(); + assert.strictEqual(pledge.isCancelled(), true); + + pledge.resolve(false); + assert.strictEqual(pledge.isCancelled(), true); + + await Pledge.after(3); + + assert.strictEqual(then_called, false); + assert.strictEqual(catch_called, false); + assert.strictEqual(finally_called, true); +} + +async function pledgeCancelTestTwo(constructor) { + let pledge = new constructor(); + + let then_called = false, + catch_called = false, + finally_called = false, + cancel_called = false, + cancel_two_called = false, + cancel_two_pledge = new Pledge.Swift(), + finally_pledge = new Pledge.Swift(); + + let counter = 1; + + pledge.then(() => { + then_called = counter++; + }); + + pledge.catch(() => { + catch_called = counter++; + }); + + pledge.finally(() => { + finally_called = counter++; + finally_pledge.resolve(); + }); + + pledge.onCancelled(() => { + cancel_called = counter++; + }); + + pledge.onCancelled(async () => { + cancel_two_called = counter++; + cancel_two_pledge.resolve(); + }); + + pledge.cancel(); + assert.strictEqual(pledge.isCancelled(), true); + + pledge.resolve(false); + assert.strictEqual(pledge.isCancelled(), true); + + assert.strictEqual(then_called, false); + assert.strictEqual(catch_called, false); + assert.strictEqual(cancel_called, 1); + + await cancel_two_pledge; + assert.strictEqual(cancel_two_called, 2); + + await finally_pledge; + assert.strictEqual(finally_called, 3); + + let final_finally = false, + final_pledge = new Pledge.Swift(); + + pledge.finally(() => { + final_finally = counter++; + final_pledge.resolve(); + }); + + await final_pledge; -}) \ No newline at end of file + assert.strictEqual(final_finally, 4); +} \ No newline at end of file