-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
/
custom.ts
181 lines (147 loc) · 5.41 KB
/
custom.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import { getOwner } from '@ember/-internals/owner';
import { getDebugName } from '@ember/-internals/utils';
import { assert } from '@ember/debug';
import { DEBUG } from '@glimmer/env';
import { Arguments, Helper as GlimmerHelper } from '@glimmer/interfaces';
import { createComputeRef, UNDEFINED_REFERENCE } from '@glimmer/reference';
import {
associateDestroyableChild,
EMPTY_ARGS,
EMPTY_NAMED,
EMPTY_POSITIONAL,
isDestroyed,
isDestroying,
} from '@glimmer/runtime';
import { Cache, createCache, getValue } from '@glimmer/validator';
import { argsProxyFor } from '../utils/args-proxy';
import { buildCapabilities, getHelperManager, InternalCapabilities } from '../utils/managers';
export type HelperDefinition = object;
export interface HelperCapabilities extends InternalCapabilities {
hasValue: boolean;
hasDestroyable: boolean;
hasScheduledEffect: boolean;
}
export function helperCapabilities(
managerAPI: string,
options: Partial<HelperCapabilities> = {}
): HelperCapabilities {
assert('Invalid helper manager compatibility specified', managerAPI === '3.23');
assert(
'You must pass either the `hasValue` OR the `hasScheduledEffect` capability when defining a helper manager. Passing neither, or both, is not permitted.',
(options.hasValue || options.hasScheduledEffect) &&
!(options.hasValue && options.hasScheduledEffect)
);
assert(
'The `hasScheduledEffect` capability has not yet been implemented for helper managers. Please pass `hasValue` instead',
!options.hasScheduledEffect
);
return buildCapabilities({
hasValue: Boolean(options.hasValue),
hasDestroyable: Boolean(options.hasDestroyable),
hasScheduledEffect: Boolean(options.hasScheduledEffect),
});
}
export interface HelperManager<HelperStateBucket = unknown> {
capabilities: HelperCapabilities;
createHelper(definition: HelperDefinition, args: Arguments): HelperStateBucket;
getDebugName?(definition: HelperDefinition): string;
}
export interface HelperManagerWithValue<HelperStateBucket = unknown>
extends HelperManager<HelperStateBucket> {
getValue(bucket: HelperStateBucket): unknown;
}
function hasValue(manager: HelperManager): manager is HelperManagerWithValue {
return manager.capabilities.hasValue;
}
export interface HelperManagerWithDestroyable<HelperStateBucket = unknown>
extends HelperManager<HelperStateBucket> {
getDestroyable(bucket: HelperStateBucket): object;
}
function hasDestroyable(manager: HelperManager): manager is HelperManagerWithDestroyable {
return manager.capabilities.hasDestroyable;
}
let ARGS_CACHES = DEBUG ? new WeakMap<SimpleArgsProxy, Cache<Partial<Arguments>>>() : undefined;
function getArgs(proxy: SimpleArgsProxy): Partial<Arguments> {
return getValue(DEBUG ? ARGS_CACHES!.get(proxy)! : proxy.argsCache!)!;
}
class SimpleArgsProxy {
argsCache?: Cache<Partial<Arguments>>;
constructor(
context: object,
computeArgs: (context: object) => Partial<Arguments> = () => EMPTY_ARGS
) {
let argsCache = createCache(() => computeArgs(context));
if (DEBUG) {
ARGS_CACHES!.set(this, argsCache);
Object.freeze(this);
} else {
this.argsCache = argsCache;
}
}
get named() {
return getArgs(this).named || EMPTY_NAMED;
}
get positional() {
return getArgs(this).positional || EMPTY_POSITIONAL;
}
}
export function invokeHelper(
context: object,
definition: HelperDefinition,
computeArgs: (context: object) => Partial<Arguments>
): Cache<unknown> {
assert(
`Expected a context object to be passed as the first parameter to invokeHelper, got ${context}`,
context !== null && typeof context === 'object'
);
const owner = getOwner(context);
const manager = getHelperManager(owner, definition)!;
// TODO: figure out why assert isn't using the TS assert thing
assert(
`Expected a helper definition to be passed as the second parameter to invokeHelper, but no helper manager was found. The definition value that was passed was \`${getDebugName!(
definition
)}\`. Did you use setHelperManager to associate a helper manager with this value?`,
manager
);
let args = new SimpleArgsProxy(context, computeArgs);
let bucket = manager.createHelper(definition, args);
let cache: Cache<unknown>;
if (hasValue(manager)) {
cache = createCache(() => {
assert(
`You attempted to get the value of a helper after the helper was destroyed, which is not allowed`,
!isDestroying(cache) && !isDestroyed(cache)
);
return manager.getValue(bucket);
});
associateDestroyableChild(context, cache);
} else {
throw new Error('TODO: unreachable, to be implemented with hasScheduledEffect');
}
if (hasDestroyable(manager)) {
let destroyable = manager.getDestroyable(bucket);
associateDestroyableChild(cache, destroyable);
}
return cache;
}
export default function customHelper(
manager: HelperManager<unknown>,
definition: HelperDefinition
): GlimmerHelper {
return (vmArgs, vm) => {
const args = argsProxyFor(vmArgs.capture(), 'helper');
const bucket = manager.createHelper(definition, args);
if (hasDestroyable(manager)) {
vm.associateDestroyable(manager.getDestroyable(bucket));
}
if (hasValue(manager)) {
return createComputeRef(
() => manager.getValue(bucket),
null,
DEBUG && manager.getDebugName && manager.getDebugName(definition)
);
} else {
return UNDEFINED_REFERENCE;
}
};
}