Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow task to work as a "pass-through" wrapper for TS support #76

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 59 additions & 13 deletions addon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ 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,
taskGroup as createTaskGroupProperty,
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';
Expand Down Expand Up @@ -145,38 +152,38 @@ 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<Required<typeof options>>
]
) => {
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<
Expand Down Expand Up @@ -222,6 +229,8 @@ function createDecorator(
);
}

const taskDecorator = createDecorator(createTaskFromDescriptor);

/**
* Turns the decorated generator function into a task.
*
Expand All @@ -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<T extends TaskFunction>(
taskFn: T
): ComputedProperty<
Task<TaskFunctionReturnType<T>, TaskFunctionArgs<T>> & EmberObject
>;
export function task<T extends EncapsulatedTask>(
taskFn: T
): ComputedProperty<
Task<
EncapsulatedTaskDescriptorReturnType<T>,
EncapsulatedTaskDescriptorArgs<T>
> &
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
Expand Down
77 changes: 76 additions & 1 deletion tests/unit/decorators-test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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);
Expand All @@ -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);
});
});
57 changes: 57 additions & 0 deletions tests/unit/last-value-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
);
});
});