diff --git a/addon/decorators/stateful-function.js b/addon/decorators/stateful-function.js index 98557c1..1edd0a0 100644 --- a/addon/decorators/stateful-function.js +++ b/addon/decorators/stateful-function.js @@ -1,5 +1,6 @@ import { StatefulPromise } from 'ember-stateful-promise/utils/stateful-promise'; import { CanceledPromise } from 'ember-stateful-promise/utils/canceled-promise'; +import { DestroyableCanceledPromise } from 'ember-stateful-promise/utils/destroyable-canceled-promise'; import { tracked } from '@glimmer/tracking'; class Handler { @@ -13,6 +14,15 @@ class Handler { cancelPromise = false; + reset() { + this.isCanceled = false; + this.isResolved = false; + this.isRunning = false; + this.isError = false; + + this.performCount = 0; + } + cancel() { this.cancelPromise = true; } @@ -39,10 +49,10 @@ class Handler { export function statefulFunction(options) { const throttle = options.throttle; let ctx; - const decorator = function (target, _property, descriptor) { + const decorator = function (_target, _property, descriptor) { const actualFunc = descriptor.value; - const handler = new Handler(); + let handler = new Handler(); let rej; const _statefulFunc = function (...args) { @@ -61,7 +71,7 @@ export function statefulFunction(options) { let maybePromise = actualFunc.call(ctx, ...args); // wrapping the promise in a StatefulPromise - const sp = new StatefulPromise().create(target, (resolveFn, rejectFn) => { + const sp = new StatefulPromise().create(ctx, (resolveFn, rejectFn) => { // store away in case we need to cancel rej = rejectFn; @@ -82,6 +92,9 @@ export function statefulFunction(options) { } }) .catch((e) => { + if (e instanceof DestroyableCanceledPromise) { + handler.reset(); + } rej(e); }) .finally(() => { @@ -98,7 +111,12 @@ export function statefulFunction(options) { sp.catch((e) => { // ensure no unhandledrejection if canceled - if (!(e instanceof CanceledPromise)) { + if ( + !( + e instanceof CanceledPromise || + e instanceof DestroyableCanceledPromise + ) + ) { throw e; } }); diff --git a/addon/utils/destroyable-canceled-promise.js b/addon/utils/destroyable-canceled-promise.js new file mode 100644 index 0000000..6194bca --- /dev/null +++ b/addon/utils/destroyable-canceled-promise.js @@ -0,0 +1,7 @@ +export class DestroyableCanceledPromise extends Error { + constructor(msg) { + super(msg); + + Object.setPrototypeOf(this, DestroyableCanceledPromise.prototype); + } +} diff --git a/addon/utils/stateful-promise.js b/addon/utils/stateful-promise.js index 603d1b2..50c775e 100644 --- a/addon/utils/stateful-promise.js +++ b/addon/utils/stateful-promise.js @@ -1,6 +1,7 @@ import { tracked } from '@glimmer/tracking'; import { registerDestructor, isDestroying } from '@ember/destroyable'; import { CanceledPromise } from 'ember-stateful-promise/utils/canceled-promise'; +import { DestroyableCanceledPromise } from 'ember-stateful-promise/utils/destroyable-canceled-promise'; export class StatefulPromise extends Promise { /** @@ -71,7 +72,7 @@ export class StatefulPromise extends Promise { if (this._state === 'RUNNING') { this._state = 'CANCELED'; this._reject( - new CanceledPromise( + new DestroyableCanceledPromise( 'The object this promise was attached to was destroyed' ) ); @@ -93,7 +94,7 @@ export class StatefulPromise extends Promise { this._state = 'CANCELED'; this._reject( new CanceledPromise( - 'The object this promise was attached to was destroyed' + 'The object this promise was attached to was destroyed while the promise was still outstanding' ) ); } else { @@ -104,6 +105,8 @@ export class StatefulPromise extends Promise { .catch((e) => { if (e instanceof CanceledPromise) { this._state = 'CANCELED'; + } else if (e instanceof DestroyableCanceledPromise) { + this._state = 'CANCELED'; } else { this._state = 'ERROR'; } @@ -116,7 +119,7 @@ export class StatefulPromise extends Promise { if (isDestroying(destroyable)) { this._state = 'CANCELED'; this._reject( - new CanceledPromise( + new DestroyableCanceledPromise( 'The object this promise was attached to was destroyed' ) ); @@ -129,6 +132,8 @@ export class StatefulPromise extends Promise { (err) => { if (err instanceof CanceledPromise) { this._state = 'CANCELED'; + } else if (err instanceof DestroyableCanceledPromise) { + this._state = 'CANCELED'; } else { this._state = 'ERROR'; } diff --git a/app/utils/destroyable-canceled-promise.js b/app/utils/destroyable-canceled-promise.js new file mode 100644 index 0000000..21e6405 --- /dev/null +++ b/app/utils/destroyable-canceled-promise.js @@ -0,0 +1 @@ +export { default } from 'ember-stateful-promise/utils/destroyable-canceled-promise'; diff --git a/tests/integration/components/playground-test.js b/tests/integration/components/playground-test.js index 05d5018..28cda32 100644 --- a/tests/integration/components/playground-test.js +++ b/tests/integration/components/playground-test.js @@ -6,6 +6,24 @@ import { hbs } from 'ember-cli-htmlbars'; module('Integration | Component | playground', function (hooks) { setupRenderingTest(hooks); + test('it renders - setup state', async function (assert) { + await render(hbs``); + assert + .dom('[data-test-playground-perform-count]') + .hasText('Perform Count: 0'); + + click('[data-test-playground-button]'); + click('[data-test-playground-button]'); + + await waitFor('[data-test-playground-button]:is([disabled])'); + assert.dom('[data-test-playground-button]').hasAttribute('disabled'); + + assert + .dom('[data-test-playground-perform-count]') + .hasText('Perform Count: 2'); + // button is disabled and promise needs to be cancelled when component is destroyed + }); + test('it renders', async function (assert) { await render(hbs``); assert