Skip to content

Commit 06c1c53

Browse files
Merge pull request #66 from NullVoxPopuli/function-initial-value
feat(useFunction): now supports an initialValue param
2 parents 4377e4d + 6ba9e26 commit 06c1c53

File tree

5 files changed

+223
-66
lines changed

5 files changed

+223
-66
lines changed

addon/-private/resources/function-runner.ts

+9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { LifecycleResource } from './lifecycle';
88
import type { ArgsWrapper } from '../types';
99

1010
export const FUNCTION_TO_RUN = Symbol('FUNCTION TO RUN');
11+
export const INITIAL_VALUE = Symbol('INITIAL VALUE');
12+
const HAS_RUN = Symbol('HAS RUN');
1113

1214
const SECRET_VALUE = '___ Secret Value ___';
1315

@@ -29,12 +31,18 @@ export class FunctionRunner<
2931
> extends LifecycleResource<BaseArgs<Args>> {
3032
// Set when using useResource
3133
protected declare [FUNCTION_TO_RUN]: Fn;
34+
protected declare [INITIAL_VALUE]: Return | undefined;
3235
private declare [SECRET_VALUE]: Return | undefined;
36+
private [HAS_RUN] = false;
3337

3438
get value(): Return | undefined {
3539
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3640
consume(this, SECRET_VALUE as any);
3741

42+
if (!this[HAS_RUN] && this[INITIAL_VALUE]) {
43+
return this[INITIAL_VALUE];
44+
}
45+
3846
return this[SECRET_VALUE];
3947
}
4048

@@ -80,6 +88,7 @@ export class FunctionRunner<
8088
}
8189

8290
this[SECRET_VALUE] = value;
91+
this[HAS_RUN] = true;
8392
dirty(this, SECRET_VALUE);
8493
};
8594

addon/-private/use-function.ts

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/* eslint-disable @typescript-eslint/ban-types */
2+
// typed-ember has not publihsed types for this yet
3+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4+
// @ts-ignore
5+
import { getValue } from '@glimmer/tracking/primitives/cache';
6+
import { assert } from '@ember/debug';
7+
// typed-ember has not publihsed types for this yet
8+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
9+
// @ts-ignore
10+
import { invokeHelper } from '@ember/helper';
11+
12+
import { FUNCTION_TO_RUN, FunctionRunner, INITIAL_VALUE } from './resources/function-runner';
13+
import { DEFAULT_THUNK, normalizeThunk, proxyClass } from './utils';
14+
15+
import type { ResourceFn } from './resources/function-runner';
16+
import type { Cache, Constructable } from './types';
17+
18+
type NonReactiveVanilla<Return, Args extends unknown[]> = [object, ResourceFn<Return, Args>];
19+
type VanillaArgs<Return, Args extends unknown[]> = [object, ResourceFn<Return, Args>, () => Args];
20+
type NonReactiveWithInitialValue<Return, Args extends unknown[]> = [
21+
object,
22+
NotFunction<Return>,
23+
ResourceFn<Return, Args>
24+
];
25+
type WithInitialValueArgs<Return, Args extends unknown[]> = [
26+
object,
27+
NotFunction<Return>,
28+
ResourceFn<Return, Args>,
29+
() => Args
30+
];
31+
32+
type NotFunction<T> = T extends Function ? never : T;
33+
34+
type UseFunctionArgs<Return, Args extends unknown[]> =
35+
| NonReactiveVanilla<Return, Args>
36+
| NonReactiveWithInitialValue<Return, Args>
37+
| VanillaArgs<Return, Args>
38+
| WithInitialValueArgs<Return, Args>;
39+
40+
/**
41+
* For use in the body of a class.
42+
* Constructs a cached Resource that will reactively respond to tracked data changes
43+
*
44+
* @param {Object} destroyable context, e.g.: component instance aka "this"
45+
* @param {Function} theFunction the function to run with the return value available on .value
46+
*/
47+
export function useFunction<Return, Args extends unknown[]>(
48+
...passed: NonReactiveVanilla<Return, Args>
49+
): { value: Return };
50+
/**
51+
* For use in the body of a class.
52+
* Constructs a cached Resource that will reactively respond to tracked data changes
53+
*
54+
* @param {Object} destroyable context, e.g.: component instance aka "this"
55+
* @param {Function} theFunction the function to run with the return value available on .value
56+
* @param {Function} thunk to generate / bind tracked data to the function so that the function can re-run when the tracked data updates
57+
*/
58+
export function useFunction<Return, Args extends unknown[]>(
59+
...passed: VanillaArgs<Return, Args>
60+
): { value: Return };
61+
/**
62+
* For use in the body of a class.
63+
* Constructs a cached Resource that will reactively respond to tracked data changes
64+
*
65+
* @param {Object} destroyable context, e.g.: component instance aka "this"
66+
* @param {Object} initialValue - a non-function that matches the shape of the eventual return value of theFunction
67+
* @param {Function} theFunction the function to run with the return value available on .value
68+
* @param {Function} thunk to generate / bind tracked data to the function so that the function can re-run when the tracked data updates
69+
*/
70+
export function useFunction<Return, Args extends unknown[]>(
71+
...passed: WithInitialValueArgs<Return, Args>
72+
): { value: Return };
73+
/**
74+
* For use in the body of a class.
75+
* Constructs a cached Resource that will reactively respond to tracked data changes
76+
*
77+
* @param {Object} destroyable context, e.g.: component instance aka "this"
78+
* @param {Object} initialValue - a non-function that matches the shape of the eventual return value of theFunction
79+
* @param {Function} theFunction the function to run with the return value available on .value
80+
*/
81+
export function useFunction<Return, Args extends unknown[]>(
82+
...passed: NonReactiveWithInitialValue<Return, Args>
83+
): { value: Return };
84+
85+
export function useFunction<Return, Args extends unknown[]>(
86+
...passedArgs: UseFunctionArgs<Return, Args>
87+
): { value: Return } {
88+
let [context] = passedArgs;
89+
let initialValue: Return | undefined;
90+
let fn: ResourceFn<Return, Args>;
91+
let thunk: (() => Args) | undefined;
92+
93+
assert(
94+
`Expected second argument to useFunction to either be an initialValue or the function to run`,
95+
passedArgs[1] !== undefined
96+
);
97+
98+
if (isVanillaArgs(passedArgs)) {
99+
fn = passedArgs[1];
100+
thunk = passedArgs[2];
101+
} else {
102+
initialValue = passedArgs[1];
103+
fn = passedArgs[2];
104+
thunk = passedArgs[3];
105+
}
106+
107+
let target = buildUnproxiedFunctionResource<Return, Args>(
108+
context,
109+
initialValue,
110+
fn,
111+
(thunk || DEFAULT_THUNK) as () => Args
112+
);
113+
114+
// :(
115+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
116+
return proxyClass<any>(target) as { value: Return };
117+
}
118+
119+
function isVanillaArgs<R, A extends unknown[]>(
120+
args: UseFunctionArgs<R, A>
121+
): args is VanillaArgs<R, A> | NonReactiveVanilla<R, A> {
122+
return typeof args[1] === 'function';
123+
}
124+
125+
const FUNCTION_CACHE = new WeakMap<ResourceFn<unknown, unknown[]>, Constructable<FunctionRunner>>();
126+
127+
/**
128+
* The function is wrapped in a bespoke resource per-function definition
129+
* because passing a vanilla function to invokeHelper would trigger a
130+
* different HelperManager, which we want to work a bit differently.
131+
* See:
132+
* - function HelperManager in ember-could-get-used-to-this
133+
* - Default Managers RFC
134+
*
135+
*/
136+
function buildUnproxiedFunctionResource<Return, ArgsList extends unknown[]>(
137+
context: object,
138+
initial: Return | undefined,
139+
fn: ResourceFn<Return, ArgsList>,
140+
thunk: () => ArgsList
141+
): { value: Return } {
142+
let resource: Cache<Return>;
143+
144+
let klass: Constructable<FunctionRunner>;
145+
146+
let existing = FUNCTION_CACHE.get(fn);
147+
148+
if (existing) {
149+
klass = existing;
150+
} else {
151+
klass = class AnonymousFunctionRunner extends FunctionRunner<Return, ArgsList> {
152+
[INITIAL_VALUE] = initial;
153+
[FUNCTION_TO_RUN] = fn;
154+
};
155+
156+
FUNCTION_CACHE.set(fn, klass);
157+
}
158+
159+
return {
160+
get value(): Return {
161+
if (!resource) {
162+
resource = invokeHelper(context, klass, () => {
163+
return normalizeThunk(thunk);
164+
}) as Cache<Return>;
165+
}
166+
167+
return getValue<Return>(resource);
168+
},
169+
};
170+
}

addon/-private/use-resource.ts

+8-64
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
55
// @ts-ignore
66
import { getValue } from '@glimmer/tracking/primitives/cache';
7+
import { assert } from '@ember/debug';
78
// typed-ember has not publihsed types for this yet
89
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
910
// @ts-ignore
1011
import { invokeHelper } from '@ember/helper';
1112

12-
import { FUNCTION_TO_RUN, FunctionRunner } from './resources/function-runner';
1313
import { LifecycleResource } from './resources/lifecycle';
1414
import { DEFAULT_THUNK, normalizeThunk, proxyClass } from './utils';
1515

@@ -37,61 +37,11 @@ function useUnproxiedResource<Instance = unknown>(
3737
};
3838
}
3939

40-
const FUNCTION_CACHE = new WeakMap<ResourceFn<unknown, unknown[]>, Constructable<FunctionRunner>>();
41-
42-
/**
43-
* The function is wrapped in a bespoke resource per-function definition
44-
* because passing a vanilla function to invokeHelper would trigger a
45-
* different HelperManager, which we want to work a bit differently.
46-
* See:
47-
* - function HelperManager in ember-could-get-used-to-this
48-
* - Default Managers RFC
49-
*
50-
*/
51-
function buildUnproxiedFunctionResource<Return, ArgsList extends unknown[]>(
52-
context: object,
53-
fn: ResourceFn<Return, ArgsList>,
54-
thunk: () => ArgsList
55-
): { value: Return } {
56-
let resource: Cache<Return>;
57-
58-
let klass: Constructable<FunctionRunner>;
59-
60-
let existing = FUNCTION_CACHE.get(fn);
61-
62-
if (existing) {
63-
klass = existing;
64-
} else {
65-
klass = class AnonymousFunctionRunner extends FunctionRunner<Return, ArgsList> {
66-
[FUNCTION_TO_RUN] = fn;
67-
};
68-
69-
FUNCTION_CACHE.set(fn, klass);
70-
}
71-
72-
return {
73-
get value(): Return {
74-
if (!resource) {
75-
resource = invokeHelper(context, klass, () => {
76-
return normalizeThunk(thunk);
77-
}) as Cache<Return>;
78-
}
79-
80-
return getValue<Return>(resource);
81-
},
82-
};
83-
}
84-
8540
/**
8641
* For use in the body of a class.
8742
* Constructs a cached Resource that will reactively respond to tracked data changes
8843
*
8944
*/
90-
export function useResource<Return, Args extends unknown[]>(
91-
context: object,
92-
fn: ResourceFn<Return, Args>,
93-
thunk?: () => Args
94-
): { value: Return };
9545
export function useResource<Instance extends LifecycleResource<any>>(
9646
context: object,
9747
klass: Constructable<Instance>,
@@ -100,22 +50,16 @@ export function useResource<Instance extends LifecycleResource<any>>(
10050

10151
export function useResource<Instance extends object, Args extends unknown[]>(
10252
context: object,
103-
klass: Constructable<Instance> | ResourceFn<Instance, Args>,
53+
klass: Constructable<Instance>,
10454
thunk?: Thunk | (() => Args)
10555
): Instance {
106-
let target: { value: Instance };
107-
108-
if (isLifecycleResource(klass)) {
109-
target = useUnproxiedResource<Instance>(context, klass, thunk || DEFAULT_THUNK);
110-
111-
return proxyClass(target);
112-
}
113-
114-
target = buildUnproxiedFunctionResource<Instance, Args>(
115-
context,
116-
klass,
117-
(thunk || DEFAULT_THUNK) as () => Args
56+
assert(
57+
`Expected second argument, klass, to be a Resource. ` +
58+
`This is different from the v1 series where useResource could be used for both functions and class-based Resources. ` +
59+
`If you intended to pass a function, you'll now (since v2) want to use useFunction instead`,
60+
isLifecycleResource(klass)
11861
);
62+
let target = useUnproxiedResource<Instance>(context, klass, thunk || DEFAULT_THUNK);
11963

12064
return proxyClass(target);
12165
}

addon/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// Public API
22
export { useTask } from './-private/ember-concurrency';
33
export { LifecycleResource } from './-private/resources/lifecycle';
4+
export { useFunction } from './-private/use-function';
45
export { useResource } from './-private/use-resource';
5-
export { useResource as useFunction } from './-private/use-resource';
66

77
// Public Type Utilities
88
export type { ArgsWrapper, Named, Positional } from './-private/types';

tests/unit/use-function-test.ts

+35-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { setupRenderingTest, setupTest } from 'ember-qunit';
1010
import { timeout } from 'ember-concurrency';
1111
import { useFunction } from 'ember-resources';
1212

13-
module('useFunction (aliased from useResource)', function () {
13+
module('useFunction', function () {
1414
module('in js', function (hooks) {
1515
setupTest(hooks);
1616

@@ -112,6 +112,40 @@ module('useFunction (aliased from useResource)', function () {
112112

113113
assert.equal(foo.data.value, 12);
114114
});
115+
116+
test('async functions can have a fallback/initial value', async function (assert) {
117+
let initialValue = -Infinity;
118+
119+
class Test {
120+
@tracked count = 1;
121+
122+
data = useFunction(
123+
this,
124+
initialValue,
125+
async (previous: undefined | number, count: number) => {
126+
// Pretend we're doing async work
127+
await Promise.resolve();
128+
129+
return count * (previous || 1);
130+
},
131+
() => [this.count]
132+
);
133+
}
134+
135+
let foo = new Test();
136+
137+
assert.equal(foo.data.value, initialValue);
138+
139+
foo.data.value;
140+
await settled();
141+
assert.equal(foo.data.value, 1);
142+
143+
foo.count = 2;
144+
foo.data.value;
145+
await settled();
146+
147+
assert.equal(foo.data.value, 2);
148+
});
115149
});
116150

117151
module('in templates', function (hooks) {

0 commit comments

Comments
 (0)