From 05cbb1a229dac91c61905fa08b48d84829697bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Buscht=C3=B6ns?= Date: Tue, 6 Aug 2019 21:25:45 +0200 Subject: [PATCH] refactor: PoC for TypeScript type support --- addon/ember-concurrency.d.ts | 139 ++++++++++++++++++++++++++++++++++ addon/index.ts | 46 +++++++++-- tests/unit/decorators-test.ts | 83 +++++++++++++------- tests/unit/last-value-test.ts | 10 +-- types/ember-concurrency.d.ts | 25 ------ 5 files changed, 237 insertions(+), 66 deletions(-) create mode 100644 addon/ember-concurrency.d.ts delete mode 100644 types/ember-concurrency.d.ts diff --git a/addon/ember-concurrency.d.ts b/addon/ember-concurrency.d.ts new file mode 100644 index 0000000..aff3cd4 --- /dev/null +++ b/addon/ember-concurrency.d.ts @@ -0,0 +1,139 @@ +declare module 'ember-concurrency' { + import EmberObject from '@ember/object'; + import { + UnwrapComputedPropertyGetter, + UnwrapComputedPropertyGetters + } from '@ember/object/-private/types'; + + import RSVP from 'rsvp'; + + // Lifted from @types/ember__object/observable.d.ts + interface Getter { + /** + * Retrieves the value of a property from the object. + */ + get(key: K): UnwrapComputedPropertyGetter; + /** + * To get the values of multiple properties at once, call `getProperties` + * with a list of strings or an array: + */ + getProperties( + list: K[] + ): Pick, K>; + getProperties( + ...list: K[] + ): Pick, K>; + } + + export type GeneratorFn = ( + ...args: Args + ) => IterableIterator; + + export const all: typeof Promise.all; + export const allSettled: typeof RSVP.allSettled; + export const hash: typeof RSVP.hash; + export const race: typeof Promise.race; + + export function timeout(ms: number): Promise; + + export function waitForEvent( + object: EmberObject | EventTarget, + eventName: string + ): Promise; + + export function waitForProperty( + object: T, + key: K, + callbackOrValue: (value: T[K]) => boolean + ): Promise; + + export function waitForQueue(queueName: string): Promise; + + export function task( + taskFn: GeneratorFn + ): Task>>; + export function task(encapsulatedTask: { + perform: GeneratorFn; + }): Task>>; + + export function taskGroup(): TaskGroupProperty; + + interface CommonTaskProperty { + restartable: () => TaskProperty; + drop: () => TaskProperty; + keepLatest: () => TaskProperty; + enqueue: () => TaskProperty; + maxConcurrency: (n: number) => TaskProperty; + cancelOn: (eventName: string) => TaskProperty; + group: (groupName: string) => TaskProperty; + } + + export interface TaskProperty extends CommonTaskProperty { + evented: () => TaskProperty; + debug: () => TaskProperty; + on: (eventName: string) => TaskProperty; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface TaskGroupProperty extends CommonTaskProperty {} + + // Based on https://github.com/CenterForOpenScience/ember-osf-web/blob/7933316efae805e00723789809bdeb58a96a286a/types/ember-concurrency/index.d.ts + + export enum TaskInstanceState { + Dropped = 'dropped', + Canceled = 'canceled', + Finished = 'finished', + Running = 'running', + Waiting = 'waiting' + } + + export interface TaskInstance extends PromiseLike, Getter { + readonly error?: unknown; + readonly hasStarted: boolean; + readonly isCanceled: boolean; + readonly isDropped: boolean; + readonly isError: boolean; + readonly isFinished: boolean; + readonly isRunning: boolean; + readonly isSuccessful: boolean; + readonly state: TaskInstanceState; + readonly value?: T; + cancel(): void; + catch(): RSVP.Promise; + finally(): RSVP.Promise; + then( + onfulfilled?: + | ((value: T) => TResult1 | RSVP.Promise) + | undefined + | null, + onrejected?: + | ((reason: any) => TResult2 | PromiseLike) + | undefined + | null + ): RSVP.Promise; + } + + export enum TaskState { + Running = 'running', + Queued = 'queued', + Idle = 'idle' + } + + export interface Task extends Getter { + readonly isIdle: boolean; + readonly isQueued: boolean; + readonly isRunning: boolean; + readonly last?: TaskInstance; + readonly lastCanceled?: TaskInstance; + readonly lastComplete?: TaskInstance; + readonly lastErrored?: TaskInstance; + readonly lastIncomplete?: TaskInstance; + readonly lastPerformed?: TaskInstance; + readonly lastRunning?: TaskInstance; + readonly lastSuccessful?: TaskInstance; + readonly performCount: number; + readonly state: TaskState; + perform(...args: Args): TaskInstance; + cancelAll(): void; + } +} diff --git a/addon/index.ts b/addon/index.ts index c08cd2f..07c8423 100644 --- a/addon/index.ts +++ b/addon/index.ts @@ -8,7 +8,9 @@ import { task as createTaskProperty, taskGroup as createTaskGroupProperty, TaskProperty, - TaskGroupProperty + TaskGroupProperty, + Task, + GeneratorFn } from 'ember-concurrency'; export { default as lastValue } from './last-value'; @@ -86,7 +88,7 @@ function createTaskFromDescriptor(desc: DecoratorDescriptor) { (typeof value === 'object' && typeof value.perform === 'function') ); - return createTaskProperty(value); + return (createTaskProperty(value) as unknown) as TaskProperty; } /** @@ -110,14 +112,15 @@ function createTaskGroupFromDescriptor(_desc: DecoratorDescriptor) { */ function applyOptions( options: TaskGroupOptions, - task: TaskGroupProperty + taskProperty: TaskGroupProperty ): TaskGroupProperty & Decorator; function applyOptions( options: TaskOptions, - task: TaskProperty + taskProperty: TaskProperty ): TaskProperty & Decorator { return Object.entries(options).reduce( ( + // eslint-disable-next-line no-shadow taskProperty, [key, value]: [ keyof typeof options, @@ -135,7 +138,7 @@ function applyOptions( value ); }, - task + taskProperty // The CP decorator gets executed in `createDecorator` ) as TaskProperty & Decorator; } @@ -192,7 +195,38 @@ const createDecorator = ( * @param {object?} [options={}] * @return {TaskProperty} */ -export const task = createDecorator(createTaskFromDescriptor); +const taskDecorator = createDecorator(createTaskFromDescriptor); + +export function task( + taskFn: GeneratorFn +): Task>>; +export function task(encapsulatedTask: { + perform: GeneratorFn; +}): Task>>; +export function task(options: TaskOptions): PropertyDecorator; +export function task( + target: Record, + propertyKey: string | symbol +): void; +export function task( + ...args: + | [GeneratorFn] + | [{ perform: GeneratorFn }] + | [TaskOptions] + | [Record, string | symbol] +): Task>> | PropertyDecorator | void { + const [firstParam] = args; + if ( + typeof firstParam === 'function' || + (typeof firstParam === 'object' && + // @ts-ignore + typeof firstParam.perform === 'function') + ) + // @ts-ignore + return firstParam; + // @ts-ignore + return taskDecorator(...args); +} /** * 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 c5e16b3..01749f4 100644 --- a/tests/unit/decorators-test.ts +++ b/tests/unit/decorators-test.ts @@ -19,60 +19,80 @@ module('Unit | decorators', function() { class TestSubject extends EmberObject { @task - doStuff = function*() { + doStuff = task(function*() { yield; return 123; - }; + }); @restartableTask - a = function*() { + a = task(function*() { yield; return 456; - }; + }); @keepLatestTask - b = function*() { + b = task(function*() { yield; return 789; - }; + }); @dropTask - c = function*() { + c = task(function*() { yield; return 12; - }; + }); @enqueueTask - d = function*() { + d = task(function*() { yield; return 34; - }; + }); } let subject!: TestSubject; run(() => { subject = TestSubject.create(); - // @ts-ignore subject.get('doStuff').perform(); - // @ts-ignore subject.get('a').perform(); - // @ts-ignore subject.get('b').perform(); - // @ts-ignore subject.get('c').perform(); - // @ts-ignore subject.get('d').perform(); }); - // @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); + assert.equal( + subject + .get('doStuff') + .get('last')! + .get('value'), + 123 + ); + assert.equal( + subject + .get('a') + .get('last')! + .get('value'), + 456 + ); + assert.equal( + subject + .get('b') + .get('last')! + .get('value'), + 789 + ); + assert.equal( + subject + .get('c') + .get('last')! + .get('value'), + 12 + ); + assert.equal( + subject + .get('d') + .get('last')! + .get('value'), + 34 + ); }); // This has actually never worked. @@ -81,13 +101,13 @@ module('Unit | decorators', function() { class TestSubject extends EmberObject { @task - encapsulated = { + encapsulated = task({ privateState: 56, *perform() { yield; return this.privateState; } - }; + }); } let subject!: TestSubject; @@ -95,7 +115,12 @@ module('Unit | decorators', function() { subject = TestSubject.create(); subject.get('encapsulated').perform(); }); - // @ts-ignore - assert.equal(subject.get('encapsulated.last.value'), 56); + assert.equal( + subject + .get('encapsulated') + .get('last') + .get('value'), + 56 + ); }); }); diff --git a/tests/unit/last-value-test.ts b/tests/unit/last-value-test.ts index 3ae6a9f..c39e59c 100644 --- a/tests/unit/last-value-test.ts +++ b/tests/unit/last-value-test.ts @@ -14,9 +14,9 @@ module('Unit | last-value', function(hooks) { test('without a default value', function(assert) { class ObjectWithTask extends EmberObject { @task - task = function*() { + task = task(function*() { return yield 'foo'; - }; + }); @lastValue('task') value?: 'foo'; @@ -29,7 +29,6 @@ module('Unit | last-value', function(hooks) { 'it returns nothing if the task has not been performed' ); - // @ts-ignore instance.get('task').perform(); nextLoop(); @@ -43,9 +42,9 @@ module('Unit | last-value', function(hooks) { test('with a default value', function(assert) { class ObjectWithTaskDefaultValue extends EmberObject { @task - task = function*() { + task = task(function*() { return yield 'foo'; - }; + }); @lastValue('task') value = 'default value'; @@ -59,7 +58,6 @@ module('Unit | last-value', function(hooks) { 'it returns the default value if the task has not been performed' ); - // @ts-ignore instance.get('task').perform(); nextLoop(); diff --git a/types/ember-concurrency.d.ts b/types/ember-concurrency.d.ts deleted file mode 100644 index 88dad0a..0000000 --- a/types/ember-concurrency.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -export function task(fn: () => IterableIterator): TaskProperty; - -export function taskGroup(): TaskGroupProperty; - -interface CommonTaskProperty { - restartable: () => TaskProperty; - drop: () => TaskProperty; - keepLatest: () => TaskProperty; - enqueue: () => TaskProperty; - maxConcurrency: (n: number) => TaskProperty; - cancelOn: (eventName: string) => TaskProperty; - group: (groupName: string) => TaskProperty; -} - -export interface TaskProperty extends CommonTaskProperty { - evented: () => TaskProperty; - debug: () => TaskProperty; - on: (eventName: string) => TaskProperty; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface TaskGroupProperty extends CommonTaskProperty {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface TaskInstance {}