/
chain-tags.ts
223 lines (187 loc) · 7.04 KB
/
chain-tags.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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
import { Meta, meta as metaFor, peekMeta } from '@ember/-internals/meta';
import { isObject } from '@ember/-internals/utils';
import { assert, deprecate } from '@ember/debug';
import { _WeakSet } from '@glimmer/util';
import {
combine,
createUpdatableTag,
Tag,
TagMeta,
tagMetaFor,
updateTag,
validateTag,
} from '@glimmer/validator';
import { objectAt } from './array';
import { tagForProperty } from './tags';
export const CHAIN_PASS_THROUGH = new _WeakSet();
export function finishLazyChains(meta: Meta, key: string, value: any) {
let lazyTags = meta.readableLazyChainsFor(key);
if (lazyTags === undefined) {
return;
}
if (isObject(value)) {
for (let i = 0; i < lazyTags.length; i++) {
let [tag, deps] = lazyTags[i];
updateTag(tag, getChainTagsForKey(value, deps as string, tagMetaFor(value), peekMeta(value)));
}
}
lazyTags.length = 0;
}
export function getChainTagsForKeys(
obj: object,
keys: string[],
tagMeta: TagMeta,
meta: Meta | null
): Tag {
let tags: Tag[] = [];
for (let i = 0; i < keys.length; i++) {
getChainTags(tags, obj, keys[i], tagMeta, meta);
}
return combine(tags);
}
export function getChainTagsForKey(
obj: object,
key: string,
tagMeta: TagMeta,
meta: Meta | null
): Tag {
return combine(getChainTags([], obj, key, tagMeta, meta));
}
function getChainTags(
chainTags: Tag[],
obj: object,
path: string,
tagMeta: TagMeta,
meta: Meta | null
) {
let current: any = obj;
let currentTagMeta = tagMeta;
let currentMeta = meta;
let pathLength = path.length;
let segmentEnd = -1;
// prevent closures
let segment: string, descriptor: any;
// eslint-disable-next-line no-constant-condition
while (true) {
let lastSegmentEnd = segmentEnd + 1;
segmentEnd = path.indexOf('.', lastSegmentEnd);
if (segmentEnd === -1) {
segmentEnd = pathLength;
}
segment = path.slice(lastSegmentEnd, segmentEnd);
// If the segment is an @each, we can process it and then break
if (segment === '@each' && segmentEnd !== pathLength) {
lastSegmentEnd = segmentEnd + 1;
segmentEnd = path.indexOf('.', lastSegmentEnd);
// There should be exactly one segment after an `@each` (i.e. `@each.foo`, not `@each.foo.bar`)
deprecate(
`When using @each in a dependent-key or an observer, ` +
`you can only chain one property level deep after ` +
`the @each. That is, \`${path.slice(0, segmentEnd)}\` ` +
`is allowed but \`${path}\` (which is what you passed) ` +
`is not.\n\n` +
`This was never supported. Currently, the extra segments ` +
`are silently ignored, i.e. \`${path}\` behaves exactly ` +
`the same as \`${path.slice(0, segmentEnd)}\`. ` +
`In the future, this will throw an error.\n\n` +
`If the current behavior is acceptable for your use case, ` +
`please remove the extraneous segments by changing your ` +
`key to \`${path.slice(0, segmentEnd)}\`. ` +
`Otherwise, please create an intermediary computed property ` +
`or switch to using tracked properties.`,
segmentEnd === -1,
{
until: '3.17.0',
id: 'ember-metal.computed-deep-each',
}
);
let arrLength = current.length;
if (
typeof arrLength !== 'number' ||
// TODO: should the second test be `isEmberArray` instead?
!(Array.isArray(current) || 'objectAt' in current)
) {
// If the current object isn't an array, there's nothing else to do,
// we don't watch individual properties. Break out of the loop.
break;
} else if (arrLength === 0) {
// Fast path for empty arrays
chainTags.push(tagForProperty(current, '[]'));
break;
}
if (segmentEnd === -1) {
segment = path.slice(lastSegmentEnd);
} else {
// Deprecated, remove once we turn the deprecation into an assertion
segment = path.slice(lastSegmentEnd, segmentEnd);
}
// Push the tags for each item's property
for (let i = 0; i < arrLength; i++) {
let item = objectAt(current as Array<any>, i);
if (item) {
assert(
`When using @each to observe the array \`${current.toString()}\`, the items in the array must be objects`,
typeof item === 'object'
);
chainTags.push(tagForProperty(item, segment, true));
}
}
// Push the tag for the array length itself
chainTags.push(tagForProperty(current, '[]', true, currentTagMeta));
break;
}
let propertyTag = tagForProperty(current, segment, true, currentTagMeta);
descriptor = currentMeta !== null ? currentMeta.peekDescriptors(segment) : undefined;
chainTags.push(propertyTag);
// If we're at the end of the path, processing the last segment, and it's
// not an alias, we should _not_ get the last value, since we already have
// its tag. There's no reason to access it and do more work.
if (segmentEnd === pathLength) {
// If the key was an alias, we should always get the next value in order to
// bootstrap the alias. This is because aliases, unlike other CPs, should
// always be in sync with the aliased value.
if (CHAIN_PASS_THROUGH.has(descriptor)) {
// tslint:disable-next-line: no-unused-expression
current[segment];
}
break;
}
if (descriptor === undefined) {
// If the descriptor is undefined, then its a normal property, so we should
// lookup the value to chain off of like normal.
if (!(segment in current) && typeof current.unknownProperty === 'function') {
current = current.unknownProperty(segment);
} else {
current = current[segment];
}
} else if (CHAIN_PASS_THROUGH.has(descriptor)) {
current = current[segment];
} else {
// If the descriptor is defined, then its a normal CP (not an alias, which
// would have been handled earlier). We get the last revision to check if
// the CP is still valid, and if so we use the cached value. If not, then
// we create a lazy chain lookup, and the next time the CP is calculated,
// it will update that lazy chain.
let instanceMeta = currentMeta!.source === current ? currentMeta! : metaFor(current);
let lastRevision = instanceMeta.revisionFor(segment);
if (lastRevision !== undefined && validateTag(propertyTag, lastRevision)) {
current = instanceMeta.valueFor(segment);
} else {
// use metaFor here to ensure we have the meta for the instance
let lazyChains = instanceMeta.writableLazyChainsFor(segment);
let rest = path.substr(segmentEnd + 1);
let placeholderTag = createUpdatableTag();
lazyChains.push([placeholderTag, rest]);
chainTags.push(placeholderTag);
break;
}
}
if (!isObject(current)) {
// we've hit the end of the chain for now, break out
break;
}
currentTagMeta = tagMetaFor(current);
currentMeta = peekMeta(current);
}
return chainTags;
}