diff --git a/addon/-private/resources/lifecycle.ts b/addon/-private/resources/lifecycle.ts index 8e8151e10..1c86f1519 100644 --- a/addon/-private/resources/lifecycle.ts +++ b/addon/-private/resources/lifecycle.ts @@ -9,7 +9,7 @@ import { associateDestroyableChild, registerDestructor } from '@ember/destroyabl // @ts-ignore import { capabilities as helperCapabilities, setHelperManager } from '@ember/helper'; -import type { ArgsWrapper, Cache, LooseArgs } from '../types'; +import type { ArgsWrapper, Cache, LooseArgs, Thunk } from '../types'; export declare interface LifecycleResource { args: T; @@ -19,6 +19,15 @@ export declare interface LifecycleResource { } export class LifecycleResource { + static with>( + /* hack to get inheritence in static methods */ + this: { new (owner: unknown, args: Args, previous?: SubClass): SubClass }, + thunk: Thunk + ): SubClass { + // Lie about the type because `with` must be used with the `@use` decorator + return [this, thunk] as unknown as SubClass; + } + constructor(owner: unknown, public args: T) { setOwner(this, owner); } diff --git a/addon/-private/resources/simple.ts b/addon/-private/resources/simple.ts index 2452a2f9e..30c256e20 100644 --- a/addon/-private/resources/simple.ts +++ b/addon/-private/resources/simple.ts @@ -9,7 +9,7 @@ import { associateDestroyableChild, destroy } from '@ember/destroyable'; // @ts-ignore import { capabilities as helperCapabilities, setHelperManager } from '@ember/helper'; -import type { ArgsWrapper, Cache, LooseArgs } from '../types'; +import type { ArgsWrapper, Cache, LooseArgs, Thunk } from '../types'; export declare interface Resource { args: T; @@ -23,9 +23,10 @@ export class Resource { static with>( /* hack to get inheritence in static methods */ - this: { new (thunk: () => ArgsWrapper): SubClass }, - thunk: () => ArgsWrapper + this: { new (owner: unknown, args: Args, previous?: SubClass): SubClass }, + thunk: Thunk ): SubClass { + // Lie about the type because `with` must be used with the `@use` decorator return [this, thunk] as unknown as SubClass; } diff --git a/addon/-private/use.ts b/addon/-private/use.ts index a64ee79dd..0f2ee2b19 100644 --- a/addon/-private/use.ts +++ b/addon/-private/use.ts @@ -1,3 +1,113 @@ -export function use(target: object, key: string, descriptor: PropertyDescriptor): any { - return; +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/ban-types */ + +// typed-ember has not publihsed types for this yet +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { getValue } from '@glimmer/tracking/primitives/cache'; +import { assert } from '@ember/debug'; +// typed-ember has not publihsed types for this yet +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { invokeHelper } from '@ember/helper'; + +import { FUNCTION_TO_RUN, FunctionRunner, INITIAL_VALUE } from './resources/function-runner'; +import { normalizeThunk } from './utils'; + +import type { Resource } from './resources/simple'; +import type { Thunk } from './types'; + +interface Class { + new (...args: unknown[]): T; +} + +interface Descriptor { + initializer: () => [Class, Thunk]; +} + +/** + * works with + * - resources (both Resource and LifecycleResource) + * - functions + */ +export function use(prototype: object, key: string, descriptor?: Descriptor): void { + if (!descriptor) return; + + assert(`@use can only be used with string-keys`, typeof key === 'string'); + + let resources = new WeakMap(); + let { initializer } = descriptor; + + // https://github.com/pzuraq/ember-could-get-used-to-this/blob/master/addon/index.js + return { + get() { + let wrapper = resources.get(this as object); + + if (!wrapper) { + let initialized = initializer.call(this); + + if (Array.isArray(initialized)) { + assert( + `@use ${key} was given unexpected value. Make sure usage is '@use ${key} = MyResource.with(() => ...)'`, + + initialized.length === 2 && typeof initialized[1] === 'function' + ); + + let [Klass, thunk] = initialized; + + let resource = invokeHelper(this, Klass, () => { + return normalizeThunk(thunk); + }); + + wrapper = { resource, type: 'class' }; + resources.set(this as object, wrapper); + + } else if (typeof initialized === 'function') { + let klass = class AnonymousFunctionRunner extends FunctionRunner { + [INITIAL_VALUE] = undefined; + [FUNCTION_TO_RUN] = initialized; + }; + + let resource = invokeHelper(this, klass, () => { + return normalizeThunk(); + }); + + wrapper = { resource, type: 'function' }; + resources.set(this as object, wrapper); + } + } + + assert(`Resource could not be created`, wrapper); + + switch (wrapper.type) { + case 'function': + return getValue(wrapper.resource).value; + case 'class': + return getValue(wrapper.resource); + + default: + assert('Resource value could not be extracted', false); + } + }, + } as unknown as void /* Thanks TS. */; +} + +/** + * Class: + * typeof klass.prototype === 'object' + * typeof klass === 'function' + * klass instanceof Object === true + * Symbol.hasInstance in klass === true + * Function: + * typeof fun.prototype === 'object'; + * typeof fun === 'function'; + * fun instanceof Object === true + * Symbol.hasInstance in fun === true + * Object: + * typeof obj.prototype === 'undefined' + * typeof obj === 'object' + * + */ +function isClass(klass?: any) { + return typeof klass === 'function' && /^class\s/.test(Function.prototype.toString.call(klass)); } diff --git a/addon/-private/utils.ts b/addon/-private/utils.ts index 0b5962088..b9fcdc7c6 100644 --- a/addon/-private/utils.ts +++ b/addon/-private/utils.ts @@ -3,7 +3,11 @@ import type { ArgsWrapper, Thunk } from './types'; export const DEFAULT_THUNK = () => []; -export function normalizeThunk(thunk: Thunk): ArgsWrapper { +export function normalizeThunk(thunk?: Thunk): ArgsWrapper { + if (!thunk) { + return { named: {}, positional: [] }; + } + let args = thunk(); if (Array.isArray(args)) { diff --git a/tests/unit/use-test.ts b/tests/unit/use-test.ts index f1e0830c8..201175aaf 100644 --- a/tests/unit/use-test.ts +++ b/tests/unit/use-test.ts @@ -3,9 +3,10 @@ import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; -import { Resource, use } from 'ember-resources'; +import { timeout } from 'ember-concurrency'; +import { LifecycleResource, Resource, use } from 'ember-resources'; -import type { ArgsWrapper, Positional } from 'ember-resources'; +import type { Positional } from 'ember-resources'; module('@use', function (hooks) { setupTest(hooks); @@ -43,7 +44,84 @@ module('@use', function (hooks) { assert.equal(instance.data.doubled, 6); }); }); - module('LifecycleResource', function () {}); + module('LifecycleResource', function () { + test('it works', async function (assert) { + class MyResource> extends LifecycleResource { + doubled = 0; + + setup() { + this.update(); + } + + update() { + this.doubled = this.args.positional[0] * 2; + } + } + + class Test { + @tracked num = 1; + + @use data = MyResource.with(() => [this.num]); + } + + let instance = new Test(); + + assert.equal(instance.data.doubled, 2); + + instance.num = 3; + await settled(); + + assert.equal(instance.data.doubled, 6); + }); + }); module('Task', function () {}); - module('Function', function () {}); + module('Function', function () { + test('it works with sync functions', async function (assert) { + class Test { + @tracked num = 1; + + // How to make TS happy about this? + @use data = () => { + return this.num * 2; + }; + } + + let instance = new Test(); + + assert.equal(instance.data, undefined); + await settled(); + assert.equal(instance.data, 2); + + instance.num = 3; + await settled(); + + assert.equal(instance.data, 6); + }); + + test('it works with async functions', async function (assert) { + class Test { + @tracked num = 1; + + // How to make TS happy about this? + @use data = async () => { + await timeout(100); + + return this.num * 2; + }; + } + + let instance = new Test(); + + assert.equal(instance.data, undefined); + await timeout(100); + await settled(); + + assert.equal(instance.data, 2); + + instance.num = 3; + await settled(); + + assert.equal(instance.data, 6); + }); + }); });