diff --git a/.gitignore b/.gitignore index 2b2e74b..294c324 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /.idea/ /test/tests/decorators.js /test/tests/decorators.legacy.js +/playground/dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 901d1a2..bb4ed49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). For changes before version 0.4.0, please see the commit history +## [0.10.2] - 2020-12-08 + +### Added +- @canceled decorator + ## [0.10.1] - 2020-12-07 ### Updated diff --git a/README.md b/README.md index 5c96a29..c4af229 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ - [@async](#asynctimeout-number) - [@listen](#listensignal-abortsignalstringsymbol) - [@cancel](#cancelreason-string-signal-abortsignalstringsymbol) + - [@canceled](#canceledonrejectederr-scope-context-function) - [@timeout](#timeoutms-number) - [@innerWeight](#innerweightweight-number) - [@label](#labellabel-string) @@ -520,6 +521,9 @@ If this argument not specified or null, the internal default AbortController wil ### @cancel([reason: String], [signal: AbortSignal|String|Symbol]) Emits the cancel signal before the target function invoking. +### @canceled([onRejected(err, scope, context): Function]) +Catches rejections with CanceledError errors + ````javascript import cpFetch from "cpFetch"; @@ -634,6 +638,12 @@ innerWeight decorator ### CPromise.label : function label decorator +**Kind**: static property of [CPromise](#module_CPromise) + + +### CPromise.canceled : function +label decorator + **Kind**: static property of [CPromise](#module_CPromise) diff --git a/jsdoc2md/README.hbs.md b/jsdoc2md/README.hbs.md index 8c7266a..7d3432b 100644 --- a/jsdoc2md/README.hbs.md +++ b/jsdoc2md/README.hbs.md @@ -23,6 +23,7 @@ - [@async](#asynctimeout-number) - [@listen](#listensignal-abortsignalstringsymbol) - [@cancel](#cancelreason-string-signal-abortsignalstringsymbol) + - [@canceled](#canceledonrejectederr-scope-context-function) - [@timeout](#timeoutms-number) - [@innerWeight](#innerweightweight-number) - [@label](#labellabel-string) @@ -520,6 +521,9 @@ If this argument not specified or null, the internal default AbortController wil ### @cancel([reason: String], [signal: AbortSignal|String|Symbol]) Emits the cancel signal before the target function invoking. +### @canceled([onRejected(err, scope, context): Function]) +Catches rejections with CanceledError errors + ````javascript import cpFetch from "cpFetch"; diff --git a/lib/c-promise.js b/lib/c-promise.js index a7eedf4..442386e 100644 --- a/lib/c-promise.js +++ b/lib/c-promise.js @@ -7,7 +7,7 @@ * @typedef {String|Symbol} EventType */ const {CanceledError} = require('./canceled-error'); -const {E_REASON_CANCELED, E_REASON_TIMEOUT, E_REASON_DISPOSED}= CanceledError; +const {E_REASON_CANCELED, E_REASON_TIMEOUT, E_REASON_DISPOSED} = CanceledError; const {AbortController, AbortControllerEx, isAbortSignal, isAbortController} = require('./abort-controller'); const {validateOptions, validators} = require('./validator'); const { @@ -100,7 +100,7 @@ class CPromise extends Promise { } } - let {label, weight, timeout, signal, nativeController= false} = options || {}; + let {label, weight, timeout, signal, nativeController = false} = options || {}; let resolve, reject; @@ -469,10 +469,10 @@ class CPromise extends Promise { */ get signal() { - const shadow= this[_shadow]; + const shadow = this[_shadow]; if (this[_shadow].controller) return this[_shadow].controller.signal; - return (this[_shadow].controller = new (shadow.nativeController? AbortController : AbortControllerEx)()).signal; + return (this[_shadow].controller = new (shadow.nativeController ? AbortController : AbortControllerEx)()).signal; } /** @@ -588,7 +588,8 @@ class CPromise extends Promise { const resolve = () => { if (isRejected) { shadow.value = value; - shadow.isCanceled && !shadow.leafsCount && super.then(null, () => {}); + shadow.isCanceled && !shadow.leafsCount && super.then(null, () => { + }); this.emit('done', value); shadow.reject(value); } else { @@ -1621,8 +1622,8 @@ const decorators = { decorator.descriptor = { value: function (...args) { - let promise = CPromise.resolveGenerator(originalFn, { - context, + let promise = context.resolveGenerator(originalFn, { + context: this, args }); @@ -1701,6 +1702,26 @@ const decorators = { return fn.apply(this, arguments); } + return decorator; + }, + + canceled: (decorator, [arg0]) => { + if (arg0 != null && typeof arg0 !== 'function') { + throw TypeError(`@canceled decorator expects a function as the first argument`); + } + + const fn = decorator.descriptor.value; + + decorator.descriptor.value = function () { + const result = fn.apply(this, arguments); + + if (!(result instanceof CPromise)) { + throw TypeError(`@canceled decorator can only be used for async function only, that returns CPromise instance`); + } + + return result.canceled((err, scope)=> arg0.call(this, err, scope, this)); + } + return decorator; } }; @@ -1752,63 +1773,68 @@ Object.defineProperties(CPromise, { CPromise: {value: CPromise} }) -exports.CPromise= CPromise; +exports.CPromise = CPromise; /** * CanceledError class * @type {CanceledError} */ -exports.CanceledError= CanceledError; +exports.CanceledError = CanceledError; /** * Refers to the AbortController class (native if available) * @type {AbortController|AbortControllerEx} */ -exports.AbortController= AbortController; +exports.AbortController = AbortController; /** * AbortControllerEx class * @type {AbortControllerEx} */ -exports.AbortControllerEx= AbortControllerEx; +exports.AbortControllerEx = AbortControllerEx; /** * Generic cancellation reason */ -exports.E_REASON_CANCELED= E_REASON_CANCELED; +exports.E_REASON_CANCELED = E_REASON_CANCELED; /** * Cancellation reason for the case when the instance will be disposed */ -exports.E_REASON_DISPOSED= E_REASON_DISPOSED; +exports.E_REASON_DISPOSED = E_REASON_DISPOSED; /** * Timeout cancellation reason */ -exports.E_REASON_TIMEOUT= E_REASON_TIMEOUT; +exports.E_REASON_TIMEOUT = E_REASON_TIMEOUT; /** * async decorator * @type {Function} */ -exports.async= CPromise.async; +exports.async = CPromise.async; /** * listen decorator * @type {Function} */ -exports.listen= CPromise.listen; +exports.listen = CPromise.listen; /** * cancel decorator * @type {Function} */ -exports.cancel= CPromise.cancel; +exports.cancel = CPromise.cancel; /** * timeout decorator * @type {Function} */ -exports.timeout= CPromise.timeout; +exports.timeout = CPromise.timeout; /** * innerWeight decorator * @type {Function} */ -exports.innerWeight= CPromise.innerWeight; +exports.innerWeight = CPromise.innerWeight; +/** + * label decorator + * @type {Function} + */ +exports.label = CPromise.label; /** * label decorator * @type {Function} */ -exports.label= CPromise.label; +exports.canceled = CPromise.canceled; diff --git a/playground/dist/decorators.build.js b/playground/dist/decorators.build.js deleted file mode 100644 index a6491c3..0000000 --- a/playground/dist/decorators.build.js +++ /dev/null @@ -1,554 +0,0 @@ -'use strict'; - -function _toArray(arr) { - return _arrayWithHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableRest(); -} - -function _arrayWithHoles(arr) { - if (Array.isArray(arr)) return arr; -} - -function _iterableToArray(iter) { - if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); -} - -function _unsupportedIterableToArray(o, minLen) { - if (!o) return; - if (typeof o === "string") return _arrayLikeToArray(o, minLen); - var n = Object.prototype.toString.call(o).slice(8, -1); - if (n === "Object" && o.constructor) n = o.constructor.name; - if (n === "Map" || n === "Set") return Array.from(o); - if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); -} - -function _arrayLikeToArray(arr, len) { - if (len == null || len > arr.length) len = arr.length; - - for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; - - return arr2; -} - -function _nonIterableRest() { - throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); -} - -function _toPrimitive(input, hint) { - if (typeof input !== "object" || input === null) return input; - var prim = input[Symbol.toPrimitive]; - - if (prim !== undefined) { - var res = prim.call(input, hint || "default"); - if (typeof res !== "object") return res; - throw new TypeError("@@toPrimitive must return a primitive value."); - } - - return (hint === "string" ? String : Number)(input); -} - -function _toPropertyKey(arg) { - var key = _toPrimitive(arg, "string"); - - return typeof key === "symbol" ? key : String(key); -} - -function _decorate(decorators, factory, superClass, mixins) { - var api = _getDecoratorsApi(); - - if (mixins) { - for (var i = 0; i < mixins.length; i++) { - api = mixins[i](api); - } - } - - var r = factory(function initialize(O) { - api.initializeInstanceElements(O, decorated.elements); - }, superClass); - var decorated = api.decorateClass(_coalesceClassElements(r.d.map(_createElementDescriptor)), decorators); - api.initializeClassElements(r.F, decorated.elements); - return api.runClassFinishers(r.F, decorated.finishers); -} - -function _getDecoratorsApi() { - _getDecoratorsApi = function () { - return api; - }; - - var api = { - elementsDefinitionOrder: [["method"], ["field"]], - initializeInstanceElements: function (O, elements) { - ["method", "field"].forEach(function (kind) { - elements.forEach(function (element) { - if (element.kind === kind && element.placement === "own") { - this.defineClassElement(O, element); - } - }, this); - }, this); - }, - initializeClassElements: function (F, elements) { - var proto = F.prototype; - ["method", "field"].forEach(function (kind) { - elements.forEach(function (element) { - var placement = element.placement; - - if (element.kind === kind && (placement === "static" || placement === "prototype")) { - var receiver = placement === "static" ? F : proto; - this.defineClassElement(receiver, element); - } - }, this); - }, this); - }, - defineClassElement: function (receiver, element) { - var descriptor = element.descriptor; - - if (element.kind === "field") { - var initializer = element.initializer; - descriptor = { - enumerable: descriptor.enumerable, - writable: descriptor.writable, - configurable: descriptor.configurable, - value: initializer === void 0 ? void 0 : initializer.call(receiver) - }; - } - - Object.defineProperty(receiver, element.key, descriptor); - }, - decorateClass: function (elements, decorators) { - var newElements = []; - var finishers = []; - var placements = { - static: [], - prototype: [], - own: [] - }; - elements.forEach(function (element) { - this.addElementPlacement(element, placements); - }, this); - elements.forEach(function (element) { - if (!_hasDecorators(element)) return newElements.push(element); - var elementFinishersExtras = this.decorateElement(element, placements); - newElements.push(elementFinishersExtras.element); - newElements.push.apply(newElements, elementFinishersExtras.extras); - finishers.push.apply(finishers, elementFinishersExtras.finishers); - }, this); - - if (!decorators) { - return { - elements: newElements, - finishers: finishers - }; - } - - var result = this.decorateConstructor(newElements, decorators); - finishers.push.apply(finishers, result.finishers); - result.finishers = finishers; - return result; - }, - addElementPlacement: function (element, placements, silent) { - var keys = placements[element.placement]; - - if (!silent && keys.indexOf(element.key) !== -1) { - throw new TypeError("Duplicated element (" + element.key + ")"); - } - - keys.push(element.key); - }, - decorateElement: function (element, placements) { - var extras = []; - var finishers = []; - - for (var decorators = element.decorators, i = decorators.length - 1; i >= 0; i--) { - var keys = placements[element.placement]; - keys.splice(keys.indexOf(element.key), 1); - var elementObject = this.fromElementDescriptor(element); - var elementFinisherExtras = this.toElementFinisherExtras((0, decorators[i])(elementObject) || elementObject); - element = elementFinisherExtras.element; - this.addElementPlacement(element, placements); - - if (elementFinisherExtras.finisher) { - finishers.push(elementFinisherExtras.finisher); - } - - var newExtras = elementFinisherExtras.extras; - - if (newExtras) { - for (var j = 0; j < newExtras.length; j++) { - this.addElementPlacement(newExtras[j], placements); - } - - extras.push.apply(extras, newExtras); - } - } - - return { - element: element, - finishers: finishers, - extras: extras - }; - }, - decorateConstructor: function (elements, decorators) { - var finishers = []; - - for (var i = decorators.length - 1; i >= 0; i--) { - var obj = this.fromClassDescriptor(elements); - var elementsAndFinisher = this.toClassDescriptor((0, decorators[i])(obj) || obj); - - if (elementsAndFinisher.finisher !== undefined) { - finishers.push(elementsAndFinisher.finisher); - } - - if (elementsAndFinisher.elements !== undefined) { - elements = elementsAndFinisher.elements; - - for (var j = 0; j < elements.length - 1; j++) { - for (var k = j + 1; k < elements.length; k++) { - if (elements[j].key === elements[k].key && elements[j].placement === elements[k].placement) { - throw new TypeError("Duplicated element (" + elements[j].key + ")"); - } - } - } - } - } - - return { - elements: elements, - finishers: finishers - }; - }, - fromElementDescriptor: function (element) { - var obj = { - kind: element.kind, - key: element.key, - placement: element.placement, - descriptor: element.descriptor - }; - var desc = { - value: "Descriptor", - configurable: true - }; - Object.defineProperty(obj, Symbol.toStringTag, desc); - if (element.kind === "field") obj.initializer = element.initializer; - return obj; - }, - toElementDescriptors: function (elementObjects) { - if (elementObjects === undefined) return; - return _toArray(elementObjects).map(function (elementObject) { - var element = this.toElementDescriptor(elementObject); - this.disallowProperty(elementObject, "finisher", "An element descriptor"); - this.disallowProperty(elementObject, "extras", "An element descriptor"); - return element; - }, this); - }, - toElementDescriptor: function (elementObject) { - var kind = String(elementObject.kind); - - if (kind !== "method" && kind !== "field") { - throw new TypeError('An element descriptor\'s .kind property must be either "method" or' + ' "field", but a decorator created an element descriptor with' + ' .kind "' + kind + '"'); - } - - var key = _toPropertyKey(elementObject.key); - - var placement = String(elementObject.placement); - - if (placement !== "static" && placement !== "prototype" && placement !== "own") { - throw new TypeError('An element descriptor\'s .placement property must be one of "static",' + ' "prototype" or "own", but a decorator created an element descriptor' + ' with .placement "' + placement + '"'); - } - - var descriptor = elementObject.descriptor; - this.disallowProperty(elementObject, "elements", "An element descriptor"); - var element = { - kind: kind, - key: key, - placement: placement, - descriptor: Object.assign({}, descriptor) - }; - - if (kind !== "field") { - this.disallowProperty(elementObject, "initializer", "A method descriptor"); - } else { - this.disallowProperty(descriptor, "get", "The property descriptor of a field descriptor"); - this.disallowProperty(descriptor, "set", "The property descriptor of a field descriptor"); - this.disallowProperty(descriptor, "value", "The property descriptor of a field descriptor"); - element.initializer = elementObject.initializer; - } - - return element; - }, - toElementFinisherExtras: function (elementObject) { - var element = this.toElementDescriptor(elementObject); - - var finisher = _optionalCallableProperty(elementObject, "finisher"); - - var extras = this.toElementDescriptors(elementObject.extras); - return { - element: element, - finisher: finisher, - extras: extras - }; - }, - fromClassDescriptor: function (elements) { - var obj = { - kind: "class", - elements: elements.map(this.fromElementDescriptor, this) - }; - var desc = { - value: "Descriptor", - configurable: true - }; - Object.defineProperty(obj, Symbol.toStringTag, desc); - return obj; - }, - toClassDescriptor: function (obj) { - var kind = String(obj.kind); - - if (kind !== "class") { - throw new TypeError('A class descriptor\'s .kind property must be "class", but a decorator' + ' created a class descriptor with .kind "' + kind + '"'); - } - - this.disallowProperty(obj, "key", "A class descriptor"); - this.disallowProperty(obj, "placement", "A class descriptor"); - this.disallowProperty(obj, "descriptor", "A class descriptor"); - this.disallowProperty(obj, "initializer", "A class descriptor"); - this.disallowProperty(obj, "extras", "A class descriptor"); - - var finisher = _optionalCallableProperty(obj, "finisher"); - - var elements = this.toElementDescriptors(obj.elements); - return { - elements: elements, - finisher: finisher - }; - }, - runClassFinishers: function (constructor, finishers) { - for (var i = 0; i < finishers.length; i++) { - var newConstructor = (0, finishers[i])(constructor); - - if (newConstructor !== undefined) { - if (typeof newConstructor !== "function") { - throw new TypeError("Finishers must return a constructor."); - } - - constructor = newConstructor; - } - } - - return constructor; - }, - disallowProperty: function (obj, name, objectType) { - if (obj[name] !== undefined) { - throw new TypeError(objectType + " can't have a ." + name + " property."); - } - } - }; - return api; -} - -function _createElementDescriptor(def) { - var key = _toPropertyKey(def.key); - - var descriptor; - - if (def.kind === "method") { - descriptor = { - value: def.value, - writable: true, - configurable: true, - enumerable: false - }; - } else if (def.kind === "get") { - descriptor = { - get: def.value, - configurable: true, - enumerable: false - }; - } else if (def.kind === "set") { - descriptor = { - set: def.value, - configurable: true, - enumerable: false - }; - } else if (def.kind === "field") { - descriptor = { - configurable: true, - writable: true, - enumerable: true - }; - } - - var element = { - kind: def.kind === "field" ? "field" : "method", - key: key, - placement: def.static ? "static" : def.kind === "field" ? "own" : "prototype", - descriptor: descriptor - }; - if (def.decorators) element.decorators = def.decorators; - if (def.kind === "field") element.initializer = def.value; - return element; -} - -function _coalesceGetterSetter(element, other) { - if (element.descriptor.get !== undefined) { - other.descriptor.get = element.descriptor.get; - } else { - other.descriptor.set = element.descriptor.set; - } -} - -function _coalesceClassElements(elements) { - var newElements = []; - - var isSameElement = function (other) { - return other.kind === "method" && other.key === element.key && other.placement === element.placement; - }; - - for (var i = 0; i < elements.length; i++) { - var element = elements[i]; - var other; - - if (element.kind === "method" && (other = newElements.find(isSameElement))) { - if (_isDataDescriptor(element.descriptor) || _isDataDescriptor(other.descriptor)) { - if (_hasDecorators(element) || _hasDecorators(other)) { - throw new ReferenceError("Duplicated methods (" + element.key + ") can't be decorated."); - } - - other.descriptor = element.descriptor; - } else { - if (_hasDecorators(element)) { - if (_hasDecorators(other)) { - throw new ReferenceError("Decorators can't be placed on different accessors with for " + "the same property (" + element.key + ")."); - } - - other.decorators = element.decorators; - } - - _coalesceGetterSetter(element, other); - } - } else { - newElements.push(element); - } - } - - return newElements; -} - -function _hasDecorators(element) { - return element.decorators && element.decorators.length; -} - -function _isDataDescriptor(desc) { - return desc !== undefined && !(desc.value === undefined && desc.writable === undefined); -} - -function _optionalCallableProperty(obj, name) { - var value = obj[name]; - - if (value !== undefined && typeof value !== "function") { - throw new TypeError("Expected '" + name + "' to be a function"); - } - - return value; -} - -const CPromise = require('../../lib/c-promise'); - -const { - async, - listen, - cancel, - timeout, - E_REASON_DISPOSED -} = CPromise; -/* -class Test { - //@timeout(1000) - @listen - @async - *asyncTask(delay) { - const result1= yield CPromise.delay(delay, 123); - const result2= yield new CPromise((resolve, reject, {onCancel})=>{ - const timer= setTimeout(resolve, 1000); - onCancel((reason)=>{ - console.log(`Cancel inner promise due ${reason}`); - clearTimeout(timer); - }) - }) - return result1 + 1; - } - - //@timeout(1000) - @cancel(E_REASON_DISPOSED) - async asyncTask2(delay){ - return CPromise.delay(delay, 123); - } -} - -const test= new Test(); - -test.asyncTask(1000) - .then( - value => console.log(`Done: ${value}`), - err => console.warn(`Fail: ${err}`) - ); - -setTimeout(()=>{ - test.asyncTask2(1000); -}, 1100); -*/ - -/*let chain; - -CPromise.resolve().then(()=>{ - chain= CPromise.resolve().then(()=>{ - console.log('executed1'); - }).then(()=>{ - console.log('executed2'); - }) - console.log('willUnmount'); -}).then(()=>{ - console.log('cancel'); - chain.cancel(); -})*/ - -/*const chain2= new CPromise(resolve=>resolve(123)).label('first').then((value, scope)=> { - console.warn('executed', value) - //console.warn('executed', scope.isCanceled, chain2.isCanceled) -}).label('second').catch(err=> console.warn(`Rethrow ${err}`));*/ - -/*const chain3= new CPromise(resolve=>{ - resolve(123); -}).then(value=> { - console.log(`Done: ${value}`); - //return 456; -}, err=> console.warn(`Failed: ${err}`)); - -chain3.cancel();*/ -//chain3.parent.reject( new Error('test')); - -let Test = _decorate(null, function (_initialize) { - class Test { - constructor() { - _initialize(this); - } - - } - - return { - F: Test, - d: [{ - kind: "method", - decorators: [async], - key: "asyncMethod", - value: function* asyncMethod(x, y) { - const z = yield CPromise.delay(1000); - return x + y + z; - } - }] - }; -}); - -const test = new Test(); -const promise = test.asyncMethod(1, 2); -console.log(promise instanceof CPromise); // true - -promise.then(value => console.log(`Done: ${value}`), err => console.warn(`Fail: ${err}`)); -setTimeout(() => promise.cancel(), 500); diff --git a/playground/src/decorators.js b/playground/src/decorators.js index 434e717..ba67b70 100644 --- a/playground/src/decorators.js +++ b/playground/src/decorators.js @@ -41,3 +41,18 @@ test.asyncTask(1000) setTimeout(()=>{ test.asyncTask2(1000); }, 1100); + + +class Component{ + @canceled((err)=>{ + + }) + @async() + *test(){ + console.log('this', this); + } +} + +const c= new Component(); + +c.test().then(console.log); diff --git a/test/src/decorators.js b/test/src/decorators.js index 02ecdb9..7236414 100644 --- a/test/src/decorators.js +++ b/test/src/decorators.js @@ -7,6 +7,7 @@ const { timeout, innerWeight, label, + canceled, CanceledError, E_REASON_TIMEOUT } = require('../../lib/c-promise'); @@ -119,5 +120,43 @@ module.exports = { assert.fail('early cancellation detected'); } }) + }, + + "should support canceled decorator": async function () { + const time = measureTime(); + + let invoked = false; + + const klass = class { + @canceled(function (err, scope, context) { + assert.ok(this instanceof klass); + assert.ok(context instanceof klass); + assert.ok(err instanceof CanceledError); + assert.ok(scope instanceof CPromise); + invoked = true; + }) + @async + * generator() { + yield delay(1000, 123); + } + } + + const obj = new klass(); + + const thenable = obj.generator(); + + setTimeout(() => { + thenable.cancel(); + }, 100); + + return thenable.then(() => { + assert.ok(invoked, 'was not canceled'); + }, err => { + if(err instanceof CanceledError) { + assert.fail(`was not caught: ${err}`); + }else{ + throw err; + } + }) } }