Skip to content

Commit

Permalink
wip: back to how to safely invoke functions
Browse files Browse the repository at this point in the history
  • Loading branch information
NullVoxPopuli committed Sep 5, 2021
1 parent e881af5 commit b201ddd
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 11 deletions.
11 changes: 10 additions & 1 deletion addon/-private/resources/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends LooseArgs = ArgsWrapper> {
args: T;
Expand All @@ -19,6 +19,15 @@ export declare interface LifecycleResource<T extends LooseArgs = ArgsWrapper> {
}

export class LifecycleResource<T extends LooseArgs = ArgsWrapper> {
static with<Args extends ArgsWrapper, SubClass extends LifecycleResource<Args>>(
/* 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);
}
Expand Down
7 changes: 4 additions & 3 deletions addon/-private/resources/simple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends LooseArgs = ArgsWrapper> {
args: T;
Expand All @@ -23,9 +23,10 @@ export class Resource<T extends LooseArgs = ArgsWrapper> {

static with<Args extends ArgsWrapper, SubClass extends Resource<Args>>(
/* 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;
}

Expand Down
114 changes: 112 additions & 2 deletions addon/-private/use.ts
Original file line number Diff line number Diff line change
@@ -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<T = unknown> {
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<object, { resource: unknown; type: 'class' | 'function' }>();
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<unknown, unknown[]> {
[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));
}
6 changes: 5 additions & 1 deletion addon/-private/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
86 changes: 82 additions & 4 deletions tests/unit/use-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<Args extends Positional<[number]>> extends LifecycleResource<Args> {
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);
});
});
});

0 comments on commit b201ddd

Please sign in to comment.