From b19179e5e749374f32b79f0ae0b9199f1b67fde4 Mon Sep 17 00:00:00 2001 From: "James C. Davis" Date: Thu, 18 Jun 2020 03:56:16 -0400 Subject: [PATCH 1/7] feat: allow task to be used as a "pass-through" wrapper for TS support --- addon/index.ts | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/addon/index.ts b/addon/index.ts index 96b03aa..44611d4 100644 --- a/addon/index.ts +++ b/addon/index.ts @@ -10,7 +10,12 @@ import { TaskFunction as GenericTaskFunction, TaskProperty as GenericTaskProperty, TaskGroupProperty as GenericTaskGroupProperty, - EncapsulatedTaskDescriptor as GenericEncapsulatedTask + EncapsulatedTaskDescriptor as GenericEncapsulatedTask, + Task, + TaskFunctionArgs, + TaskFunctionReturnType, + EncapsulatedTaskDescriptorArgs, + EncapsulatedTaskDescriptorReturnType } from 'ember-concurrency'; export { default as lastValue } from './last-value'; @@ -222,6 +227,8 @@ function createDecorator( ); } +const taskDecorator = createDecorator(createTaskFromDescriptor); + /** * Turns the decorated generator function into a task. * @@ -245,7 +252,40 @@ function createDecorator( * @param {object?} [options={}] * @return {TaskProperty} */ -export const task = createDecorator(createTaskFromDescriptor); +export function task(target: object, propertyKey: string | symbol): void; +export function task(options: TaskOptions): PropertyDecorator; +export function task( + taskFn: T +): Task, TaskFunctionArgs>; +export function task( + taskFn: T +): Task< + EncapsulatedTaskDescriptorReturnType, + EncapsulatedTaskDescriptorArgs +>; +export function task( + ...args: + | [object, string | symbol] + | [object, string | symbol, PropertyDescriptor] + | [TaskOptions] + | [TaskFunction] + | [EncapsulatedTask] +): + | TaskFunction + | EncapsulatedTask + | PropertyDescriptor + | void + // It doesn't *actuallly* ever return these, but they're needed for compatibility with the overloads. + | PropertyDecorator + | Task { + const [argument1, argument2, argument3] = args; + if (isTaskFunction(argument1) || isEncapsulatedTask(argument1)) { + return argument1; + } + if (argument2 && argument3) { + return taskDecorator(argument1, argument2, argument3); + } +} /** * Turns the decorated generator function into a task and applies the From daa1ce32fb079b58c337761504af0a68cf1f61e8 Mon Sep 17 00:00:00 2001 From: "James C. Davis" Date: Thu, 18 Jun 2020 03:57:41 -0400 Subject: [PATCH 2/7] refactor: rename vars to avoid shadowing --- addon/index.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/addon/index.ts b/addon/index.ts index 44611d4..66a511d 100644 --- a/addon/index.ts +++ b/addon/index.ts @@ -150,19 +150,19 @@ function createTaskGroupFromDescriptor( */ function applyOptions( options: TaskGroupOptions, - task: TaskGroupProperty + taskProperty: TaskGroupProperty ): TaskGroupProperty & Decorator; function applyOptions( options: TaskOptions, - task: TaskProperty + taskProperty: TaskProperty ): TaskProperty & Decorator; function applyOptions( options: TaskGroupOptions | TaskOptions, - task: TaskGroupProperty | TaskProperty + taskProperty: TaskGroupProperty | TaskProperty ): (TaskGroupProperty | TaskProperty) & Decorator { return Object.entries(options).reduce( ( - taskProperty, + optionTaskProperty, [key, value]: [ keyof typeof options, ObjectValues> @@ -170,18 +170,18 @@ function applyOptions( ) => { assert( `ember-concurrency-decorators: Option '${key}' is not a valid function`, - typeof taskProperty[key] === 'function' + typeof optionTaskProperty[key] === 'function' ); if (value === true) { - return (taskProperty[key] as () => typeof taskProperty)(); + return (optionTaskProperty[key] as () => typeof optionTaskProperty)(); } - return (taskProperty[key] as (o: typeof value) => typeof taskProperty)( - value - ); + return (optionTaskProperty[key] as ( + o: typeof value + ) => typeof optionTaskProperty)(value); }, - task + taskProperty // The CP decorator gets executed in `createDecorator` - ) as typeof task & Decorator; + ) as typeof taskProperty & Decorator; } type MethodOrPropertyDecoratorWithParams< From 19f49ec41b72fa0c4476a19046a4aca1feefaf1d Mon Sep 17 00:00:00 2001 From: "James C. Davis" Date: Thu, 18 Jun 2020 03:58:44 -0400 Subject: [PATCH 3/7] test: add tests for TS support wrapper --- tests/unit/decorators-test.ts | 75 +++++++++++++++++++++++++++++++++++ tests/unit/last-value-test.ts | 57 ++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/tests/unit/decorators-test.ts b/tests/unit/decorators-test.ts index 5b03fab..2738b5e 100644 --- a/tests/unit/decorators-test.ts +++ b/tests/unit/decorators-test.ts @@ -76,6 +76,57 @@ module('Unit | decorators', function() { assert.equal(subject.get('d.last.value'), 34); }); + test('Basic decorators functionality (using wrapper for TS support)', function(assert) { + assert.expect(5); + + class TestSubject extends EmberObject { + @task + doStuff = task(function*() { + yield; + return 123; + }); + + @restartableTask + a = task(function*() { + yield; + return 456; + }); + + @keepLatestTask + b = task(function*() { + yield; + return 789; + }); + + @dropTask + c = task(function*() { + yield; + return 12; + }); + + @enqueueTask + d = task(function*() { + yield; + return 34; + }); + } + + let subject!: TestSubject; + run(() => { + subject = TestSubject.create(); + subject.get('doStuff').perform(); + subject.get('a').perform(); + subject.get('b').perform(); + subject.get('c').perform(); + subject.get('d').perform(); + }); + assert.equal(subject.get('doStuff').last?.value, 123); + assert.equal(subject.get('a').last?.value, 456); + assert.equal(subject.get('b').last?.value, 789); + assert.equal(subject.get('c').last?.value, 12); + assert.equal(subject.get('d').last?.value, 34); + }); + // This has actually never worked. test('Encapsulated tasks', function(assert) { assert.expect(1); @@ -99,4 +150,28 @@ module('Unit | decorators', function() { // @ts-ignore assert.equal(subject.get('encapsulated.last.value'), 56); }); + + test('Encapsulated tasks (using wrapper for TS support)', function(assert) { + assert.expect(1); + + const ENCAPSULATED_TASK = { + privateState: 56, + *perform() { + yield; + return this.privateState; + } + }; + + class TestSubject extends EmberObject { + @task + encapsulated = task(ENCAPSULATED_TASK); + } + + let subject!: TestSubject; + run(() => { + subject = TestSubject.create(); + subject.get('encapsulated').perform(); + }); + assert.equal(subject.get('encapsulated').last?.value, 56); + }); }); diff --git a/tests/unit/last-value-test.ts b/tests/unit/last-value-test.ts index 54a62f6..ce28203 100644 --- a/tests/unit/last-value-test.ts +++ b/tests/unit/last-value-test.ts @@ -41,6 +41,34 @@ module('Unit | last-value', function(hooks) { ); }); + test('without a default value (using wrapper for TS support)', function(assert) { + class ObjectWithTask extends EmberObject { + @task + task = task(function*() { + return yield 'foo'; + }); + + @lastValue('task') + value?: 'foo'; + } + + const instance = ObjectWithTask.create(); + assert.strictEqual( + instance.get('value'), + undefined, + 'it returns nothing if the task has not been performed' + ); + + instance.get('task').perform(); + nextLoop(); + + assert.strictEqual( + instance.get('value'), + 'foo', + 'returning the last successful value' + ); + }); + test('with a default value', function(assert) { class ObjectWithTaskDefaultValue extends EmberObject { @task @@ -70,4 +98,33 @@ module('Unit | last-value', function(hooks) { 'returning the last successful value' ); }); + + test('with a default value (using wrapper for TS support)', function(assert) { + class ObjectWithTaskDefaultValue extends EmberObject { + @task + task = task(function*() { + return yield 'foo'; + }); + + @lastValue('task') + value = 'default value'; + } + + const instance = ObjectWithTaskDefaultValue.create(); + + assert.strictEqual( + instance.get('value'), + 'default value', + 'it returns the default value if the task has not been performed' + ); + + instance.get('task').perform(); + nextLoop(); + + assert.equal( + instance.get('value'), + 'foo', + 'returning the last successful value' + ); + }); }); From 3e44f371a56ec20ed7962ab974a37e054a79c55a Mon Sep 17 00:00:00 2001 From: "James C. Davis" Date: Fri, 19 Jun 2020 08:11:05 -0400 Subject: [PATCH 4/7] test: make tests work in Ember 2.18 --- tests/unit/decorators-test.ts | 36 ++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/tests/unit/decorators-test.ts b/tests/unit/decorators-test.ts index 2738b5e..a792b93 100644 --- a/tests/unit/decorators-test.ts +++ b/tests/unit/decorators-test.ts @@ -1,10 +1,11 @@ /* eslint-disable max-classes-per-file */ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/ban-ts-ignore */ import { module, test } from 'qunit'; import EmberObject from '@ember/object'; import { run } from '@ember/runloop'; +import Ember from 'ember'; import { task, @@ -120,11 +121,26 @@ module('Unit | decorators', function() { subject.get('c').perform(); subject.get('d').perform(); }); - assert.equal(subject.get('doStuff').last?.value, 123); - assert.equal(subject.get('a').last?.value, 456); - assert.equal(subject.get('b').last?.value, 789); - assert.equal(subject.get('c').last?.value, 12); - assert.equal(subject.get('d').last?.value, 34); + // eslint-disable-next-line ember/new-module-imports + if (/^2\./.test(Ember.VERSION)) { + // Have access computed properties with .get w/ Ember 2.x + // @ts-ignore + assert.equal(subject.get('doStuff.last.value'), 123); + // @ts-ignore + assert.equal(subject.get('a.last.value'), 456); + // @ts-ignore + assert.equal(subject.get('b.last.value'), 789); + // @ts-ignore + assert.equal(subject.get('c.last.value'), 12); + // @ts-ignore + assert.equal(subject.get('d.last.value'), 34); + } else { + assert.equal(subject.get('doStuff').last?.value, 123); + assert.equal(subject.get('a').last?.value, 456); + assert.equal(subject.get('b').last?.value, 789); + assert.equal(subject.get('c').last?.value, 12); + assert.equal(subject.get('d').last?.value, 34); + } }); // This has actually never worked. @@ -172,6 +188,12 @@ module('Unit | decorators', function() { subject = TestSubject.create(); subject.get('encapsulated').perform(); }); - assert.equal(subject.get('encapsulated').last?.value, 56); + // eslint-disable-next-line ember/new-module-imports + if (/^2\./.test(Ember.VERSION)) { + // @ts-ignore + assert.equal(subject.get('encapsulated.last.value'), 56); + } else { + assert.equal(subject.get('encapsulated').last?.value, 56); + } }); }); From b5dbcb40a0b86dfbf76bf96a10240e5befda1329 Mon Sep 17 00:00:00 2001 From: "James C. Davis" Date: Sat, 20 Jun 2020 02:53:20 -0400 Subject: [PATCH 5/7] refactor: fake Task extends EmberObject --- addon/index.ts | 7 +++++-- tests/unit/decorators-test.ts | 34 ++++++---------------------------- 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/addon/index.ts b/addon/index.ts index 66a511d..542fa10 100644 --- a/addon/index.ts +++ b/addon/index.ts @@ -3,6 +3,7 @@ import { DecoratorDescriptor } from '@ember-decorators/utils/decorator'; import { assert } from '@ember/debug'; +import EmberObject from '@ember/object'; import { task as createTaskProperty, @@ -254,15 +255,17 @@ const taskDecorator = createDecorator(createTaskFromDescriptor); */ export function task(target: object, propertyKey: string | symbol): void; export function task(options: TaskOptions): PropertyDecorator; +// TODO: remove & EmberObject when https://github.com/machty/ember-concurrency/pull/363 lands export function task( taskFn: T -): Task, TaskFunctionArgs>; +): Task, TaskFunctionArgs> & EmberObject; export function task( taskFn: T ): Task< EncapsulatedTaskDescriptorReturnType, EncapsulatedTaskDescriptorArgs ->; +> & + EmberObject; export function task( ...args: | [object, string | symbol] diff --git a/tests/unit/decorators-test.ts b/tests/unit/decorators-test.ts index a792b93..423b35a 100644 --- a/tests/unit/decorators-test.ts +++ b/tests/unit/decorators-test.ts @@ -5,7 +5,6 @@ import { module, test } from 'qunit'; import EmberObject from '@ember/object'; import { run } from '@ember/runloop'; -import Ember from 'ember'; import { task, @@ -121,26 +120,11 @@ module('Unit | decorators', function() { subject.get('c').perform(); subject.get('d').perform(); }); - // eslint-disable-next-line ember/new-module-imports - if (/^2\./.test(Ember.VERSION)) { - // Have access computed properties with .get w/ Ember 2.x - // @ts-ignore - assert.equal(subject.get('doStuff.last.value'), 123); - // @ts-ignore - assert.equal(subject.get('a.last.value'), 456); - // @ts-ignore - assert.equal(subject.get('b.last.value'), 789); - // @ts-ignore - assert.equal(subject.get('c.last.value'), 12); - // @ts-ignore - assert.equal(subject.get('d.last.value'), 34); - } else { - assert.equal(subject.get('doStuff').last?.value, 123); - assert.equal(subject.get('a').last?.value, 456); - assert.equal(subject.get('b').last?.value, 789); - assert.equal(subject.get('c').last?.value, 12); - assert.equal(subject.get('d').last?.value, 34); - } + assert.equal(subject.get('doStuff').get('last')?.value, 123); + assert.equal(subject.get('a').get('last')?.value, 456); + assert.equal(subject.get('b').get('last')?.value, 789); + assert.equal(subject.get('c').get('last')?.value, 12); + assert.equal(subject.get('d').get('last')?.value, 34); }); // This has actually never worked. @@ -188,12 +172,6 @@ module('Unit | decorators', function() { subject = TestSubject.create(); subject.get('encapsulated').perform(); }); - // eslint-disable-next-line ember/new-module-imports - if (/^2\./.test(Ember.VERSION)) { - // @ts-ignore - assert.equal(subject.get('encapsulated.last.value'), 56); - } else { - assert.equal(subject.get('encapsulated').last?.value, 56); - } + assert.equal(subject.get('encapsulated').get('last')?.value, 56); }); }); From 78a8d03e37664acc3356c8c802c0395decc3b96f Mon Sep 17 00:00:00 2001 From: "James C. Davis" Date: Sat, 20 Jun 2020 02:59:23 -0400 Subject: [PATCH 6/7] refactor: tasks are computed properties --- addon/index.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/addon/index.ts b/addon/index.ts index 542fa10..44634c6 100644 --- a/addon/index.ts +++ b/addon/index.ts @@ -4,6 +4,7 @@ import { } from '@ember-decorators/utils/decorator'; import { assert } from '@ember/debug'; import EmberObject from '@ember/object'; +import ComputedProperty from '@ember/object/computed'; import { task as createTaskProperty, @@ -258,14 +259,18 @@ export function task(options: TaskOptions): PropertyDecorator; // TODO: remove & EmberObject when https://github.com/machty/ember-concurrency/pull/363 lands export function task( taskFn: T -): Task, TaskFunctionArgs> & EmberObject; +): ComputedProperty< + Task, TaskFunctionArgs> & EmberObject +>; export function task( taskFn: T -): Task< - EncapsulatedTaskDescriptorReturnType, - EncapsulatedTaskDescriptorArgs -> & - EmberObject; +): ComputedProperty< + Task< + EncapsulatedTaskDescriptorReturnType, + EncapsulatedTaskDescriptorArgs + > & + EmberObject +>; export function task( ...args: | [object, string | symbol] From ee8b9f67e2c9c5aeed2769cd6aaebc5955ade653 Mon Sep 17 00:00:00 2001 From: "James C. Davis" Date: Sat, 20 Jun 2020 03:19:49 -0400 Subject: [PATCH 7/7] refactor: clean up task() return type --- addon/index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/addon/index.ts b/addon/index.ts index 44634c6..81c0893 100644 --- a/addon/index.ts +++ b/addon/index.ts @@ -279,13 +279,11 @@ export function task( | [TaskFunction] | [EncapsulatedTask] ): + | PropertyDecorator // needed for overload compatibility | TaskFunction | EncapsulatedTask | PropertyDescriptor - | void - // It doesn't *actuallly* ever return these, but they're needed for compatibility with the overloads. - | PropertyDecorator - | Task { + | void { const [argument1, argument2, argument3] = args; if (isTaskFunction(argument1) || isEncapsulatedTask(argument1)) { return argument1;