Skip to content

Commit 74a9a5c

Browse files
Merge pull request #1166 from NullVoxPopuli/make-the-return-the-value-public-api
de-mystify resource internals
2 parents 378c6fd + 876abec commit 74a9a5c

File tree

12 files changed

+429
-249
lines changed

12 files changed

+429
-249
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// @ts-ignore
2+
import { createCache, getValue } from '@glimmer/tracking/primitives/cache';
3+
import { assert } from '@ember/debug';
4+
import { associateDestroyableChild, destroy, registerDestructor } from '@ember/destroyable';
5+
// @ts-ignore
6+
import { invokeHelper, setHelperManager } from '@ember/helper';
7+
8+
import { ReadonlyCell } from './cell.ts';
9+
import { ResourceManagerFactory } from './resource-manager.ts';
10+
import { INTERNAL } from './types.ts';
11+
import { registerUsable, TYPE_KEY } from './use.ts';
12+
import { shallowFlat } from './utils.ts';
13+
14+
import type { Destructor, Reactive, ResourceFunction } from './types.ts';
15+
import type Owner from '@ember/owner';
16+
17+
export const CREATE_KEY = Symbol.for('__configured-resource-key__');
18+
export const DEBUG_NAME = Symbol.for('DEBUG_NAME');
19+
export const RESOURCE_CACHE = Symbol.for('__resource_cache__');
20+
21+
import { compatOwner } from './ember-compat.ts';
22+
23+
const getOwner = compatOwner.getOwner;
24+
const setOwner = compatOwner.setOwner;
25+
26+
/**
27+
* The return value from resource()
28+
*
29+
* This is semi-public API, and is meant to de-magic the intermediary
30+
* value returned from resource(), allowing us to both document how to
31+
* - manually create a resource (instance)
32+
* - explain how the helper manager interacts with the methods folks
33+
* can use to manually create a resource
34+
*
35+
*
36+
* With an owner, you can manually create a resource this way:
37+
* ```js
38+
* import { destroy } from '@ember/destroyable';
39+
* import { resource } from 'ember-resources';
40+
*
41+
* const builder = resource(() => {}); // builder can be invoked multiple times
42+
* const owner = {};
43+
* const state = builder.create(owner); // state can be created any number of times
44+
*
45+
* state.current // the current value
46+
* destroy(state); // some time later, calls cleanup
47+
* ```
48+
*/
49+
export class Builder<Value> {
50+
#fn: ResourceFunction<Value>;
51+
52+
[TYPE_KEY] = TYPE;
53+
54+
constructor(fn: ResourceFunction<Value>, key: Symbol) {
55+
assert(
56+
`Cannot instantiate ConfiguredResource without using the resource() function.`,
57+
key === CREATE_KEY,
58+
);
59+
60+
this.#fn = fn;
61+
}
62+
63+
create() {
64+
return new Resource(this.#fn);
65+
}
66+
}
67+
68+
const TYPE = 'function-based';
69+
70+
registerUsable(TYPE, (context: object, config: Builder<unknown>) => {
71+
let instance = config.create();
72+
73+
instance.link(context);
74+
75+
return instance[RESOURCE_CACHE];
76+
});
77+
78+
/**
79+
* TODO:
80+
*/
81+
export class Resource<Value> {
82+
#originalFn: ResourceFunction<Value>;
83+
#owner: Owner | undefined;
84+
#previousFn: object | undefined;
85+
#usableCache = new WeakMap<object, ReturnType<typeof invokeHelper>>();
86+
#cache: ReturnType<typeof invokeHelper>;
87+
88+
constructor(fn: ResourceFunction<Value>) {
89+
/**
90+
* We have to copy the `fn` in case there are multiple
91+
* usages or invocations of the function.
92+
*
93+
* This copy is what we'll ultimately work with and eventually
94+
* destroy.
95+
*/
96+
this.#originalFn = fn.bind(null);
97+
98+
this.#cache = createCache(() => {
99+
if (this.#previousFn) {
100+
destroy(this.#previousFn);
101+
}
102+
103+
let currentFn = this.#originalFn.bind(null);
104+
105+
associateDestroyableChild(this.#originalFn, currentFn);
106+
this.#previousFn = currentFn;
107+
108+
assert(
109+
`Cannot create a resource without an owner. Must have previously called .link()`,
110+
this.#owner,
111+
);
112+
113+
let maybeValue = currentFn({
114+
on: {
115+
cleanup: (destroyer: Destructor) => {
116+
registerDestructor(currentFn, destroyer);
117+
},
118+
},
119+
use: (usable) => {
120+
assert(
121+
`Expected the resource's \`use(...)\` utility to have been passed an object, but a \`${typeof usable}\` was passed.`,
122+
typeof usable === 'object',
123+
);
124+
assert(
125+
`Expected the resource's \`use(...)\` utility to have been passed a truthy value, instead was passed: ${usable}.`,
126+
usable,
127+
);
128+
assert(
129+
`Expected the resource's \`use(...)\` utility to have been passed another resource, but something else was passed.`,
130+
INTERNAL in usable || usable instanceof Builder,
131+
);
132+
133+
let previousCache = this.#usableCache.get(usable);
134+
135+
if (previousCache) {
136+
destroy(previousCache);
137+
}
138+
139+
let nestedCache = invokeHelper(this.#cache, usable);
140+
141+
associateDestroyableChild(currentFn, nestedCache as object);
142+
143+
this.#usableCache.set(usable, nestedCache);
144+
145+
return new ReadonlyCell<any>(() => {
146+
let cache = this.#usableCache.get(usable);
147+
148+
assert(`Cache went missing while evaluating the result of a resource.`, cache);
149+
150+
return getValue(cache);
151+
});
152+
},
153+
owner: this.#owner,
154+
});
155+
156+
return maybeValue;
157+
});
158+
}
159+
link(context: object) {
160+
let owner = getOwner(context);
161+
162+
assert(`Cannot link without an owner`, owner);
163+
164+
this.#owner = owner;
165+
166+
associateDestroyableChild(context, this.#cache);
167+
associateDestroyableChild(context, this.#originalFn);
168+
169+
setOwner(this.#cache, this.#owner);
170+
}
171+
172+
get [RESOURCE_CACHE](): unknown {
173+
return this.#cache;
174+
}
175+
176+
get fn() {
177+
return this.#originalFn;
178+
}
179+
180+
get current() {
181+
return shallowFlat(this.#cache);
182+
}
183+
184+
[DEBUG_NAME]() {
185+
return `Resource Function`;
186+
}
187+
}
188+
189+
setHelperManager(ResourceManagerFactory, Builder.prototype);

ember-resources/src/resource-manager.ts

Lines changed: 13 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,10 @@
1-
// @ts-ignore
2-
import { createCache, getValue } from '@glimmer/tracking/primitives/cache';
31
import { assert } from '@ember/debug';
4-
import { associateDestroyableChild, destroy, registerDestructor } from '@ember/destroyable';
5-
// @ts-ignore
6-
import { invokeHelper } from '@ember/helper';
72
// @ts-ignore
83
import { capabilities as helperCapabilities } from '@ember/helper';
94

10-
import { ReadonlyCell } from './cell.ts';
115
import { compatOwner } from './ember-compat.ts';
12-
import { CURRENT, INTERNAL } from './types.ts';
136

14-
import type {
15-
Destructor,
16-
InternalFunctionResourceConfig,
17-
Reactive,
18-
ResourceFunction,
19-
} from './types.ts';
7+
import type { Builder, Resource } from './intermediate-representation.ts';
208
import type Owner from '@ember/owner';
219

2210
const setOwner = compatOwner.setOwner;
@@ -25,119 +13,37 @@ const setOwner = compatOwner.setOwner;
2513
* Note, a function-resource receives on object, hooks.
2614
* We have to build that manually in this helper manager
2715
*/
28-
class FunctionResourceManager {
16+
class FunctionResourceManager<Value> {
2917
capabilities: ReturnType<typeof helperCapabilities> = helperCapabilities('3.23', {
3018
hasValue: true,
3119
hasDestroyable: true,
3220
});
3321

34-
constructor(protected owner: Owner) {}
22+
constructor(protected owner: Owner) {
23+
setOwner(this, owner);
24+
}
3525

3626
/**
3727
* Resources do not take args.
3828
* However, they can access tracked data
3929
*/
40-
createHelper(config: InternalFunctionResourceConfig): {
41-
fn: InternalFunctionResourceConfig['definition'];
42-
cache: ReturnType<typeof invokeHelper>;
43-
} {
44-
let { definition: fn } = config;
45-
/**
46-
* We have to copy the `fn` in case there are multiple
47-
* usages or invocations of the function.
48-
*
49-
* This copy is what we'll ultimately work with and eventually
50-
* destroy.
51-
*/
52-
let thisFn = fn.bind(null);
53-
let previousFn: object;
54-
let usableCache = new WeakMap<object, ReturnType<typeof invokeHelper>>();
55-
let owner = this.owner;
56-
57-
let cache = createCache(() => {
58-
if (previousFn) {
59-
destroy(previousFn);
60-
}
61-
62-
let currentFn = thisFn.bind(null);
63-
64-
associateDestroyableChild(thisFn, currentFn);
65-
previousFn = currentFn;
66-
67-
let maybeValue = currentFn({
68-
on: {
69-
cleanup: (destroyer: Destructor) => {
70-
registerDestructor(currentFn, destroyer);
71-
},
72-
},
73-
use: (usable) => {
74-
assert(
75-
`Expected the resource's \`use(...)\` utility to have been passed an object, but a \`${typeof usable}\` was passed.`,
76-
typeof usable === 'object',
77-
);
78-
assert(
79-
`Expected the resource's \`use(...)\` utility to have been passed a truthy value, instead was passed: ${usable}.`,
80-
usable,
81-
);
82-
assert(
83-
`Expected the resource's \`use(...)\` utility to have been passed another resource, but something else was passed.`,
84-
INTERNAL in usable,
85-
);
86-
87-
let previousCache = usableCache.get(usable);
88-
89-
if (previousCache) {
90-
destroy(previousCache);
91-
}
92-
93-
let nestedCache = invokeHelper(cache, usable);
94-
95-
associateDestroyableChild(currentFn, nestedCache as object);
30+
createHelper(builder: Builder<Value>): Resource<Value> {
31+
let instance = builder.create();
9632

97-
usableCache.set(usable, nestedCache);
33+
instance.link(this);
9834

99-
return new ReadonlyCell<any>(() => {
100-
let cache = usableCache.get(usable);
101-
102-
assert(`Cache went missing while evaluating the result of a resource.`, cache);
103-
104-
return getValue(cache);
105-
});
106-
},
107-
owner: this.owner,
108-
});
109-
110-
return maybeValue;
111-
});
112-
113-
setOwner(cache, owner);
114-
115-
return { fn: thisFn, cache };
35+
return instance;
11636
}
11737

118-
getValue({ cache }: { fn: ResourceFunction; cache: ReturnType<typeof invokeHelper> }) {
119-
let maybeValue = getValue(cache);
120-
121-
if (typeof maybeValue === 'function') {
122-
return maybeValue();
123-
}
124-
125-
if (isReactive(maybeValue)) {
126-
return maybeValue[CURRENT];
127-
}
128-
129-
return maybeValue;
38+
getValue(state: Resource<Value>) {
39+
return state.current;
13040
}
13141

132-
getDestroyable({ fn }: { fn: ResourceFunction }) {
133-
return fn;
42+
getDestroyable(state: Resource<Value>) {
43+
return state.fn;
13444
}
13545
}
13646

137-
function isReactive<Value>(maybe: unknown): maybe is Reactive<Value> {
138-
return typeof maybe === 'object' && maybe !== null && CURRENT in maybe;
139-
}
140-
14147
export const ResourceManagerFactory = (owner: Owner | undefined) => {
14248
assert(`Cannot create resource without an owner`, owner);
14349

0 commit comments

Comments
 (0)