Skip to content

Commit

Permalink
Improved cancellation logic to work properly with multi leaves chains;
Browse files Browse the repository at this point in the history
  • Loading branch information
DigitalBrainJS committed Nov 29, 2020
1 parent 99f3798 commit cef6c54
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 37 deletions.
34 changes: 25 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
![npm](https://img.shields.io/npm/dm/c-promise2)
![npm bundle size](https://img.shields.io/bundlephobia/minzip/c-promise2)
![David](https://img.shields.io/david/DigitalBrainJS/c-promise)
![Stars](https://badgen.net/github/stars/DigitalBrainJS/c-promise)

## Table of contents
- [SYNOPSIS](#synopsis-sparkles)
Expand Down Expand Up @@ -495,8 +496,8 @@ CPromise class
* [.reject(err)](#module_CPromise..CPromise+reject) ⇒ <code>CPromise</code>
* [.pause()](#module_CPromise..CPromise+pause) ⇒ <code>Boolean</code>
* [.resume()](#module_CPromise..CPromise+resume) ⇒ <code>Boolean</code>
* [.cancel([reason])](#module_CPromise..CPromise+cancel)
* [.emitSignal([data], type, [handler])](#module_CPromise..CPromise+emitSignal) ⇒ <code>Boolean</code>
* [.cancel([reason], [force])](#module_CPromise..CPromise+cancel)
* [.emitSignal([data], type, [handler], [validator])](#module_CPromise..CPromise+emitSignal) ⇒ <code>Boolean</code>
* [.delay(ms)](#module_CPromise..CPromise+delay) ⇒ <code>CPromise</code>
* [.then(onFulfilled, [onRejected])](#module_CPromise..CPromise+then) ⇒ <code>CPromise</code>
* [.catch(onRejected, [filter])](#module_CPromise..CPromise+catch) ⇒ <code>CPromise</code>
Expand Down Expand Up @@ -700,18 +701,19 @@ Resume promise
**Kind**: instance method of [<code>CPromise</code>](#module_CPromise..CPromise)
<a name="module_CPromise..CPromise+cancel"></a>

#### cPromise.cancel([reason])
#### cPromise.cancel([reason], [force])
throws the CanceledError that cause promise chain cancellation

**Kind**: instance method of [<code>CPromise</code>](#module_CPromise..CPromise)

| Param | Type |
| --- | --- |
| [reason] | <code>String</code> \| <code>Error</code> |
| Param | Type | Default |
| --- | --- | --- |
| [reason] | <code>String</code> \| <code>Error</code> | |
| [force] | <code>Boolean</code> | <code>false</code> |

<a name="module_CPromise..CPromise+emitSignal"></a>

#### cPromise.emitSignal([data], type, [handler]) ⇒ <code>Boolean</code>
#### cPromise.emitSignal([data], type, [handler], [validator]) ⇒ <code>Boolean</code>
Emit a signal of the specific type

**Kind**: instance method of [<code>CPromise</code>](#module_CPromise..CPromise)
Expand All @@ -720,7 +722,8 @@ Emit a signal of the specific type
| --- | --- |
| [data] | <code>\*</code> |
| type | <code>Signal</code> |
| [handler] | <code>function</code> |
| [handler] | <code>SignalHandler</code> |
| [validator] | <code>SignalValidator</code> |

<a name="module_CPromise..CPromise+delay"></a>

Expand Down Expand Up @@ -989,7 +992,7 @@ If value is a number it will be considered as the value for timeout option If va
**Kind**: inner typedef of [<code>CPromise</code>](#module_CPromise)
<a name="module_CPromise..Signal"></a>

### CPromise~Signal : <code>String</code> \| <code>Signal</code>
### CPromise~Signal : <code>String</code> \| <code>Symbol</code>
**Kind**: inner typedef of [<code>CPromise</code>](#module_CPromise)
<a name="module_CPromise..SignalHandler"></a>

Expand All @@ -999,9 +1002,22 @@ If value is a number it will be considered as the value for timeout option If va

| Param | Type |
| --- | --- |
| data | <code>\*</code> |
| type | <code>Signal</code> |
| scope | <code>CPromise</code> |

<a name="module_CPromise..SignalValidator"></a>

### CPromise~SignalValidator ⇒ <code>Boolean</code>
**Kind**: inner typedef of [<code>CPromise</code>](#module_CPromise)
**this**: <code>{CPromise}</code>

| Param | Type |
| --- | --- |
| data | <code>\*</code> |
| type | <code>Signal</code> |
| scope | <code>CPromise</code> |
| isRoot | <code>Boolean</code> |

<a name="module_CPromise..AllOptions"></a>

Expand Down
1 change: 1 addition & 0 deletions jsdoc2md/README.hbs.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
![npm](https://img.shields.io/npm/dm/c-promise2)
![npm bundle size](https://img.shields.io/bundlephobia/minzip/c-promise2)
![David](https://img.shields.io/david/DigitalBrainJS/c-promise)
![Stars](https://badgen.net/github/stars/DigitalBrainJS/c-promise)

## Table of contents
- [SYNOPSIS](#synopsis-sparkles)
Expand Down
63 changes: 55 additions & 8 deletions lib/c-promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ function resolveGenerator(generatorFn) {
scope.on('signal', (type, data) => {
switch (type) {
case SIGNAL_CANCEL:
if (!promise.cancel(data)) {
onRejected(data);
if (!promise.cancel(data.err, data.force)) {
onRejected(data.err);
}
return true;
case SIGNAL_PAUSE:
Expand Down Expand Up @@ -189,6 +189,7 @@ class CPromise extends Promise {
const shadow = this[_shadow] = {
resolve,
reject,
leafsCount: 0,
paused: false,
timestamp: -1,
innerChain: null,
Expand Down Expand Up @@ -668,6 +669,7 @@ class CPromise extends Promise {
}
shadow.isPending = false;
shadow.innerChain = null;
shadow.parent && shadow.parent[_shadow].leafsCount--;
shadow.parent = null;
this[_events] = null;
}
Expand Down Expand Up @@ -777,24 +779,37 @@ class CPromise extends Promise {
/**
* throws the CanceledError that cause promise chain cancellation
* @param {String|Error} [reason]
* @param {Boolean} [force]
*/

cancel(reason) {
return this.emitSignal(SIGNAL_CANCEL, CanceledError.from(reason), function (data) {
this.reject(data);
cancel(reason, force = false) {
return this.emitSignal(SIGNAL_CANCEL, {err: CanceledError.from(reason), force}, function ({err}) {
this.reject(err);
return true;
}, ({force}, type, scope, isRoot)=>{
return (!isRoot && !force && scope[_shadow].leafsCount > 1);
});
}

/**
* @typedef {String|Signal} Signal
* @typedef {String|Symbol} Signal
*/

/**
* @typedef {Function} SignalHandler
* @param {*} data
* @param {Signal} type
* @param {CPromise} scope
* @this {CPromise}
* @returns {Boolean}
*/

/**
* @typedef {Function} SignalValidator
* @param {*} data
* @param {Signal} type
* @param {CPromise} scope
* @param {Boolean} isRoot
* @this {CPromise}
* @returns {Boolean}
*/
Expand All @@ -803,14 +818,44 @@ class CPromise extends Promise {
* Emit a signal of the specific type
* @param {*} [data]
* @param {Signal} type
* @param {Function} [handler]
* @param {SignalHandler} [handler]
* @param {SignalValidator} [validator]
* @returns {Boolean}
*/

emitSignal(type, data, handler) {
emitSignal(type, data, handler, validator) {
const emit= (scope, isRoot)=>{
const shadow = scope[_shadow];
if (!shadow.isPending) return false;

if(validator && validator.call(scope, data, type, scope, isRoot)){
return false;
}

let {parent, innerChain} = shadow;

if (parent && emit(parent, false)) {
return true;
}

if (innerChain && emit(innerChain, false)) {
return true;
}

return !!(scope.emitHook('signal', type, data) || handler && handler.call(scope, data, type, scope));
}

return emit(this, true);
}

emitSignal2(type, data, handler, selector) {
const shadow = this[_shadow];
if (!shadow.isPending) return false;

if(selector && selector.call(this, data, type, this)){

}

let {parent, innerChain} = shadow;

if (parent && parent.emitSignal(type, data, handler)) {
Expand Down Expand Up @@ -859,6 +904,8 @@ class CPromise extends Promise {

promise[_shadow].parent = this;

this[_shadow].leafsCount++;

this.on('propagate', (type, scope, data) => {
promise.emit('propagate', type, scope, data);
});
Expand Down
27 changes: 13 additions & 14 deletions lib/canceled-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,21 @@ class CanceledError extends Error{
* @returns {CanceledError}
*/

static from(thing){
switch (typeof(thing)){
case 'object':
if(thing instanceof Error){
if(thing instanceof CanceledError){
return thing;
}
static from(thing) {
const type = typeof thing;

if(thing.name==='AbortError'){
return new CanceledError(thing.message);
}
if (type === 'string' || thing == null) {
return new CanceledError(thing || 'canceled');
} else if (type === 'object') {
if (thing instanceof Error) {
if (thing instanceof CanceledError) {
return thing;
}
break;
case 'string':
case 'undefined':
return new CanceledError(thing || 'canceled');

if (thing.name === 'AbortError') {
return new CanceledError(thing.message);
}
}
}

throw TypeError(`unable convert ${thing} to a CanceledError`);
Expand Down
77 changes: 71 additions & 6 deletions test/tests/CPromise.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ const {CanceledError} = CPromise;

const delay = (ms, value, options) => new CPromise(resolve => setTimeout(() => resolve(value), ms), options);

const makePromise = (ms, value, onCancelCb) => {
return new CPromise((resolve, reject, {onCancel}) => {
setTimeout(resolve, ms, value);
onCancelCb && onCancel(onCancelCb);
const makePromise = (ms, value, handler) => {
return new CPromise((resolve, reject, scope) => {
const timer = setTimeout(resolve, ms, value);
scope.onCancel(() => {
clearTimeout(timer);
handler && handler(scope);
})
});
}
};

module.exports = {
constructor: {
Expand All @@ -21,7 +24,7 @@ module.exports = {
}
},

'should support cancelation by the external signal': async function () {
'should support cancellation by the external signal': async function () {
const controller = new CPromise.AbortController();

const timestamp = Date.now();
Expand Down Expand Up @@ -141,6 +144,68 @@ module.exports = {
})
}
},

'should cancel only isolated leaves': async function () {
let rootCanceled = false;
let firstCanceled = false;
let secondCanceled = false;

const root = makePromise(1000, null, () => {
rootCanceled = true;
});

const firstLeaf = root.then(() => makePromise(1000)).then(() => {
assert.fail('first promise leaf was not canceled');
}).canceled(() => {
firstCanceled = true;
});

const secondLeaf = root.then(() => makePromise(1000)).then(() => {
assert.fail('second promise leaf was not canceled');
}).canceled(() => {
secondCanceled = true;
});

firstLeaf.cancel();
await firstLeaf;
assert.equal(firstCanceled, true);
assert.equal(secondCanceled, false);
assert.equal(rootCanceled, false);
secondLeaf.cancel();
await secondLeaf;
assert.equal(firstCanceled, true);
assert.equal(secondCanceled, true);
assert.equal(rootCanceled, true);
await root.canceled();
},

'should cancel all leaves if the force option is set to true': async function () {
let rootCanceled = false;
let firstCanceled = false;
let secondCanceled = false;

const root = makePromise(1000, null, () => {
rootCanceled = true;
});

const firstLeaf = root.then(() => makePromise(1000)).then(() => {
assert.fail('first promise leaf was not canceled');
}).canceled(() => {
firstCanceled = true;
});

const secondLeaf = root.then(() => makePromise(1000)).then(() => {
assert.fail('second promise leaf was not canceled');
}).canceled(() => {
secondCanceled = true;
});

firstLeaf.cancel('', true);
await firstLeaf;
assert.equal(firstCanceled, true);
assert.equal(secondCanceled, true);
assert.equal(rootCanceled, true);
}
},

'progress capturing': {
Expand Down

0 comments on commit cef6c54

Please sign in to comment.