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 4 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
66 changes: 53 additions & 13 deletions addon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -145,38 +150,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 +227,8 @@ function createDecorator(
);
}

const taskDecorator = createDecorator(createTaskFromDescriptor);

/**
* Turns the decorated generator function into a task.
*
Expand All @@ -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<T extends TaskFunction>(
taskFn: T
): Task<TaskFunctionReturnType<T>, TaskFunctionArgs<T>>;
export function task<T extends EncapsulatedTask>(
taskFn: T
): Task<
EncapsulatedTaskDescriptorReturnType<T>,
EncapsulatedTaskDescriptorArgs<T>
>;
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<unknown, unknown[]> {
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
99 changes: 98 additions & 1 deletion tests/unit/decorators-test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -76,6 +77,72 @@ 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();
});
// 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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When machty/ember-concurrency#363 lands, we can remove this ugly special-casing for the 2.18 try case and just use, e.g.

assert.equal(subject.get('doStuff').get('last')?.value, 123);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't resist: b5dbcb4

} 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.
test('Encapsulated tasks', function(assert) {
assert.expect(1);
Expand All @@ -99,4 +166,34 @@ 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();
});
// 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);
}
});
});
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'
);
});
});