diff --git a/addon/index.ts b/addon/index.ts index 96b03aa..81c0893 100644 --- a/addon/index.ts +++ b/addon/index.ts @@ -3,6 +3,8 @@ import { DecoratorDescriptor } 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, @@ -10,7 +12,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'; @@ -145,19 +152,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> @@ -165,18 +172,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< @@ -222,6 +229,8 @@ function createDecorator( ); } +const taskDecorator = createDecorator(createTaskFromDescriptor); + /** * Turns the decorated generator function into a task. * @@ -245,7 +254,44 @@ 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; +// TODO: remove & EmberObject when https://github.com/machty/ember-concurrency/pull/363 lands +export function task( + taskFn: T +): ComputedProperty< + Task, TaskFunctionArgs> & EmberObject +>; +export function task( + taskFn: T +): ComputedProperty< + Task< + EncapsulatedTaskDescriptorReturnType, + EncapsulatedTaskDescriptorArgs + > & + EmberObject +>; +export function task( + ...args: + | [object, string | symbol] + | [object, string | symbol, PropertyDescriptor] + | [TaskOptions] + | [TaskFunction] + | [EncapsulatedTask] +): + | PropertyDecorator // needed for overload compatibility + | TaskFunction + | EncapsulatedTask + | PropertyDescriptor + | void { + 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 diff --git a/tests/unit/decorators-test.ts b/tests/unit/decorators-test.ts index 5b03fab..423b35a 100644 --- a/tests/unit/decorators-test.ts +++ b/tests/unit/decorators-test.ts @@ -1,5 +1,5 @@ /* 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'; @@ -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').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. 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').get('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' + ); + }); });