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

Exploring options for replacing resourceFactory with real functions #994

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions ember-resources/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"./core": "./dist/core/index.js",
"./core/class-based": "./dist/core/class-based/index.js",
"./core/function-based": "./dist/core/function-based/index.js",
"./override-default-managers": "./dist/override-default-managers.js",
"./link": "./dist/link.js",
"./service": "./dist/service.js",
"./modifier": "./dist/modifier/index.js",
Expand All @@ -38,6 +39,9 @@
"link": [
"dist/link.d.ts"
],
"override-default-managers": [
"dist/override-default-managers.d.ts"
],
"service": [
"dist/service.d.ts"
],
Expand Down
15 changes: 12 additions & 3 deletions ember-resources/src/core/function-based/immediate-invocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,19 @@ class ResourceInvokerManager {
/**
* This cache is for args passed to the ResourceInvoker/Factory
*
* We want to cache the helper result, and only re-inoke when the args
* We want to cache the helper result, and only re-invoke when the args
* change.
*/
let cache = createCache(() => {
let resource = fn(...args.positional) as object;
let argsForFn = [];

if (Object.keys(args.named).length > 0 ) {
argsForFn = [...args.positional, args.named];
} else {
argsForFn = [...args.positional];
}

let resource = fn(...argsForFn) as object;

setOwner(resource, this.owner);

Expand Down Expand Up @@ -178,4 +186,5 @@ type ResourceBlueprint<Value, Args> =
// semicolon

// Provide a singleton manager.
const ResourceInvokerFactory = (owner: Owner) => new ResourceInvokerManager(owner);
export const ResourceInvokerFactory = (owner: Owner) => new ResourceInvokerManager(owner);

5 changes: 1 addition & 4 deletions ember-resources/src/modifier/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@ import { assert } from '@ember/debug';
import { setModifierManager } from '@ember/modifier';

import { resourceFactory } from '../index';
import FunctionBasedModifierManager from './manager';
import { MANAGER } from './manager';

import type { resource } from '../index';
import type { ArgsFor, ElementFor, EmptyObject } from '[core-types]';
import type { ModifierLike } from '@glint/template';

// Provide a singleton manager.
const MANAGER = new FunctionBasedModifierManager();

type PositionalArgs<S> = S extends { Args?: object } ? ArgsFor<S['Args']>['Positional'] : [];
type NamedArgs<S> = S extends { Args?: object }
? ArgsFor<S['Args']>['Named'] extends object
Expand Down
4 changes: 4 additions & 0 deletions ember-resources/src/modifier/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,7 @@ export default class FunctionBasedModifierManager<S> {
}
}
}

// Provide a singleton manager.
export const MANAGER = new FunctionBasedModifierManager();

12 changes: 12 additions & 0 deletions ember-resources/src/override-default-managers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @ts-expect-error
import { setHelperManager } from '@ember/helper';
// @ts-expect-error
import { setModifierManager } from '@ember/modifier';

import { ResourceInvokerFactory } from './core/function-based/immediate-invocation';
import { MANAGER } from './modifier/manager';

export function overrideDefaultManagers() {
setModifierManager(() => MANAGER, Function.prototype);
setHelperManager(ResourceInvokerFactory, Function.prototype);
}
59 changes: 59 additions & 0 deletions test-app/tests/function-wrappers/factory-test.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@

import { render} from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';

import { resource, cell } from 'ember-resources';

// Will need to be a class for .current flattening / auto-rendering
interface Reactive<Value> {
current: Value;
}

module('function-wrappers | Core | (function) resource | use | rendering', function (hooks) {
setupRenderingTest(hooks);

const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

let formatter = new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: false,
});

test('it works', async function (assert) {
let nowDate = Date.now();
let format = (time: Reactive<number>) => formatter.format(time.current);

function Now(ms = 1000) {
return resource(({ on }) => {
let now = cell(nowDate);
let timer = setInterval(() => now.set(Date.now()), ms);

on.cleanup(() => clearInterval(timer));

return () => now.current;
});
}

function Stopwatch(ms = 500) {
return resource(({ use }) => {
let time = use(Now(ms));

return () => format(time);
});
}

await render(<template><time>{{Stopwatch 250}}</time></template>);

let first = formatter.format(Date.now());
assert.dom('time').hasText(first);

await wait(1010);

let second = formatter.format(Date.now());
assert.dom('time').hasText(second);
assert.notEqual(first, second);
});
});
77 changes: 77 additions & 0 deletions test-app/tests/function-wrappers/modifier-test.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { render, settled } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';

import { resource, cell } from 'ember-resources';
import { overrideDefaultManagers } from 'ember-resources/override-default-managers';

overrideDefaultManagers();

module('function wrappers | modifier | rendering', function (hooks) {
setupRenderingTest(hooks);

test('no arguments', async function (assert) {
function capture(...args) {
assert.step(`${args.length} args`);

let [element] = args;

return resource(() => {
assert.step(`received element ${element.tagName}`);
});
}

await render(<template><div {{capture}}>content</div></template>);

assert.verifySteps(['1 args', 'received element DIV']);
});

test('mixed arguments', async function (assert) {
const named = cell(0);
const positional = cell(0);
const visible = cell(true);

function capture(...args: [Element, number, { value: number }]) {
assert.step(`${args.length} args`);

return resource(({ on }) => {
let positionalValue = args[1];
let { value } = args[2];

assert.step(`received element ${args[0].tagName} for value ${positionalValue},${value}`);

on.cleanup(() => assert.step(`cleanup ${positionalValue},${value}`));
});
}

await render(<template>
{{#if visible.current}}
<div {{capture positional.current value=named.current}}>content</div>
{{/if}}
</template>);

assert.verifySteps(['3 args', 'received element DIV for value 0,0']);

named.current++;
await settled();
assert.verifySteps(['3 args', 'received element DIV for value 0,1', 'cleanup 0,0']);

visible.current = false;
await settled();
assert.verifySteps(['cleanup 0,1']);

positional.current++;
visible.current = true;
await settled();

assert.verifySteps(['3 args', 'received element DIV for value 1,1']);

positional.current++;
await settled();
assert.verifySteps(['3 args', 'received element DIV for value 2,1', 'cleanup 1,1']);

visible.current = false;
await settled();
assert.verifySteps(['cleanup 2,1']);
});
});