design-token-library 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/dist/lib/css-reflector.d.ts +32 -24
- package/dist/lib/css-reflector.js +10 -26
- package/dist/lib/css-reflector.js.map +1 -1
- package/dist/lib/equality.d.ts +16 -0
- package/dist/lib/equality.js +90 -0
- package/dist/lib/equality.js.map +1 -0
- package/dist/lib/graph.d.ts +58 -0
- package/dist/lib/graph.js +104 -0
- package/dist/lib/graph.js.map +1 -0
- package/dist/lib/index.d.ts +4 -2
- package/dist/lib/index.js +2 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/library.d.ts +221 -10
- package/dist/lib/library.js +1256 -258
- package/dist/lib/library.js.map +1 -1
- package/dist/lib/notifier.d.ts +0 -15
- package/dist/lib/notifier.js +0 -23
- package/dist/lib/notifier.js.map +1 -1
- package/dist/lib/parse-document.d.ts +21 -0
- package/dist/lib/parse-document.js +31 -0
- package/dist/lib/parse-document.js.map +1 -0
- package/dist/lib/parse-expression.d.ts +45 -0
- package/dist/lib/parse-expression.js +148 -0
- package/dist/lib/parse-expression.js.map +1 -0
- package/dist/lib/parse-extension.d.ts +24 -0
- package/dist/lib/parse-extension.js +68 -0
- package/dist/lib/parse-extension.js.map +1 -0
- package/dist/lib/parse-resolve.d.ts +16 -0
- package/dist/lib/parse-resolve.js +281 -0
- package/dist/lib/parse-resolve.js.map +1 -0
- package/dist/lib/parse-value.d.ts +35 -0
- package/dist/lib/parse-value.js +330 -0
- package/dist/lib/parse-value.js.map +1 -0
- package/dist/lib/parse.d.ts +103 -0
- package/dist/lib/parse.js +562 -0
- package/dist/lib/parse.js.map +1 -0
- package/dist/lib/queue.d.ts +13 -2
- package/dist/lib/queue.js +18 -2
- package/dist/lib/queue.js.map +1 -1
- package/dist/lib/ref.d.ts +18 -1
- package/dist/lib/ref.js +22 -4
- package/dist/lib/ref.js.map +1 -1
- package/dist/lib/standard-values.d.ts +50 -0
- package/dist/lib/standard-values.js +73 -0
- package/dist/lib/standard-values.js.map +1 -0
- package/dist/lib/tsconfig.tsbuildinfo +1 -1
- package/dist/lib/utilities.d.ts +6 -0
- package/dist/lib/utilities.js +7 -1
- package/dist/lib/utilities.js.map +1 -1
- package/package.json +9 -5
- package/dist/lib/public.d.ts +0 -1405
- package/dist/lib/tsdoc-metadata.json +0 -11
- package/dist/lib/watcher.d.ts +0 -12
- package/dist/lib/watcher.js +0 -25
- package/dist/lib/watcher.js.map +0 -1
package/dist/lib/library.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { COMPUTED, RECIPE, isComputed } from "./recipe.js";
|
|
2
2
|
import { REF, isRef } from "./ref.js";
|
|
3
|
-
import {
|
|
3
|
+
import { isStandardType, standardEquality } from "./equality.js";
|
|
4
|
+
import { Graph } from "./graph.js";
|
|
4
5
|
import { Queue } from "./queue.js";
|
|
5
|
-
import { empty, isToken } from "./utilities.js";
|
|
6
|
-
import { Watcher } from "./watcher.js";
|
|
6
|
+
import { empty, isObject, isToken } from "./utilities.js";
|
|
7
7
|
/**
|
|
8
8
|
* @public
|
|
9
9
|
*/
|
|
@@ -12,11 +12,206 @@ export var Library;
|
|
|
12
12
|
/**
|
|
13
13
|
* @public
|
|
14
14
|
*/
|
|
15
|
-
Library.create = (config) => {
|
|
15
|
+
Library.create = (config, ...options) => {
|
|
16
16
|
// The runtime library is untyped; its shape is guaranteed by `config`.
|
|
17
|
-
return LibraryImpl.create(config);
|
|
17
|
+
return LibraryImpl.create(config, options[0]);
|
|
18
18
|
};
|
|
19
19
|
})(Library || (Library = {}));
|
|
20
|
+
/**
|
|
21
|
+
* Removes the scope of a collected extension from its source's.
|
|
22
|
+
*/
|
|
23
|
+
const extensionsOf = new FinalizationRegistry(({ source, extension }) => source.extensions.delete(extension));
|
|
24
|
+
const scopeOf = (recipes, types, source, overridden = new Set()) => {
|
|
25
|
+
const own = new Set();
|
|
26
|
+
const scope = {
|
|
27
|
+
tokens: {},
|
|
28
|
+
source,
|
|
29
|
+
recipes,
|
|
30
|
+
types,
|
|
31
|
+
queue: new Queue(LibraryToken.reported),
|
|
32
|
+
subscribers: new Map(),
|
|
33
|
+
errors: null,
|
|
34
|
+
ancestral: 0,
|
|
35
|
+
callers: new Set(),
|
|
36
|
+
discovered: false,
|
|
37
|
+
made: new Map(),
|
|
38
|
+
own,
|
|
39
|
+
extensions: new Set(),
|
|
40
|
+
graph: source
|
|
41
|
+
? new Graph(source.graph, (name) => own.has(name))
|
|
42
|
+
: new Graph(null, () => false),
|
|
43
|
+
complete: false,
|
|
44
|
+
found: new Map(),
|
|
45
|
+
cached: 0,
|
|
46
|
+
overridden,
|
|
47
|
+
origin: null,
|
|
48
|
+
};
|
|
49
|
+
if (source) {
|
|
50
|
+
const extension = new WeakRef(scope);
|
|
51
|
+
source.extensions.add(extension);
|
|
52
|
+
extensionsOf.register(scope, { source, extension });
|
|
53
|
+
}
|
|
54
|
+
return scope;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Calls `visit` with the path of each ref in a value, searched as
|
|
58
|
+
* {@link resolve} resolves them: in plain data and in the arguments of
|
|
59
|
+
* computed values. `resolve` walks values on its own, as it copies them, so
|
|
60
|
+
* the two must search the same values.
|
|
61
|
+
*/
|
|
62
|
+
const forEachRef = (value, visit) => {
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const walk = (value) => {
|
|
65
|
+
if (!isObject(value) || seen.has(value)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
seen.add(value);
|
|
69
|
+
if (isRef(value)) {
|
|
70
|
+
visit(value[REF]);
|
|
71
|
+
}
|
|
72
|
+
else if (isComputed(value)) {
|
|
73
|
+
value[COMPUTED].args.forEach(walk);
|
|
74
|
+
}
|
|
75
|
+
else if (isPlain(value)) {
|
|
76
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
77
|
+
walk(Reflect.get(value, key));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
walk(value);
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* The names of the tokens that the refs in a value are to.
|
|
85
|
+
*/
|
|
86
|
+
const refsOf = (value) => {
|
|
87
|
+
const names = new Set();
|
|
88
|
+
forEachRef(value, (path) => names.add(path.join(".")));
|
|
89
|
+
return names;
|
|
90
|
+
};
|
|
91
|
+
const none = new Set();
|
|
92
|
+
const noInputs = Object.freeze([]);
|
|
93
|
+
/**
|
|
94
|
+
* What a token that isn't queued for a batch has in place of the value to
|
|
95
|
+
* compare with.
|
|
96
|
+
*/
|
|
97
|
+
const unqueued = Symbol();
|
|
98
|
+
/**
|
|
99
|
+
* The custom types of a library: those it inherits, and those it registers,
|
|
100
|
+
* which can be neither a DTCG type nor one it inherits.
|
|
101
|
+
*/
|
|
102
|
+
const typesOf = (types = {}, inherited = new Map()) => {
|
|
103
|
+
const registered = new Map(inherited);
|
|
104
|
+
for (const [name, options] of Object.entries(types)) {
|
|
105
|
+
if (isStandardType(name)) {
|
|
106
|
+
throw new Error(`"${name}" is a DTCG type, which has its own equality, so it can't be registered in 'types'.`);
|
|
107
|
+
}
|
|
108
|
+
if (inherited.has(name)) {
|
|
109
|
+
throw new Error(`The custom type "${name}" is registered by a library this one extends, so it can't be registered again.`);
|
|
110
|
+
}
|
|
111
|
+
if (typeof options?.isEqual !== "function") {
|
|
112
|
+
throw new Error(`The custom type "${name}" has no isEqual function.`);
|
|
113
|
+
}
|
|
114
|
+
registered.set(name, (a, b) => options.isEqual(a, b));
|
|
115
|
+
}
|
|
116
|
+
return registered;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* The equality of a type in a library.
|
|
120
|
+
*/
|
|
121
|
+
const equalityOf = (scope, type) => isStandardType(type) ? standardEquality[type] : scope.types.get(type);
|
|
122
|
+
/**
|
|
123
|
+
* The dependents of a token found last, with the version of its library's
|
|
124
|
+
* graph they were found in.
|
|
125
|
+
*/
|
|
126
|
+
const dependentsOfToken = new WeakMap();
|
|
127
|
+
/**
|
|
128
|
+
* The scope that has the configured value of a token of a library: the
|
|
129
|
+
* library's, or that of the library it extends that the token inherits it
|
|
130
|
+
* from.
|
|
131
|
+
*/
|
|
132
|
+
const ownerOf = (scope, name) => {
|
|
133
|
+
let owner = scope;
|
|
134
|
+
while (owner.source !== undefined && !owner.own.has(name)) {
|
|
135
|
+
owner = owner.source;
|
|
136
|
+
}
|
|
137
|
+
return owner;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* The graph of a library with the edges of every token: of each token of a
|
|
141
|
+
* library made by `create()`, and of each own token of an extension, on the
|
|
142
|
+
* graph of its source.
|
|
143
|
+
*/
|
|
144
|
+
const completeGraphOf = (scope) => {
|
|
145
|
+
if (!scope.complete) {
|
|
146
|
+
const { source, own, made } = scope;
|
|
147
|
+
const add = (token) => {
|
|
148
|
+
if (!scope.graph.has(token.name)) {
|
|
149
|
+
scope.graph.add(token.name, refsOf(LibraryToken.configuredValue(token)));
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
if (source) {
|
|
153
|
+
completeGraphOf(source);
|
|
154
|
+
for (const name of own) {
|
|
155
|
+
add(made.get(name));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Every token of a library made by `create()` is made, so its
|
|
160
|
+
// getter makes nothing.
|
|
161
|
+
forEachToken(scope.tokens, (_, descriptor) => add(descriptor.get()));
|
|
162
|
+
}
|
|
163
|
+
scope.complete = true;
|
|
164
|
+
}
|
|
165
|
+
return scope.graph;
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* The token of a library with a name, if it is made: an extension's token
|
|
169
|
+
* that hasn't been read isn't.
|
|
170
|
+
*/
|
|
171
|
+
const madeToken = (scope, name) => scope.source ? scope.made.get(name) : tokenNamed(scope, name);
|
|
172
|
+
/**
|
|
173
|
+
* The token of a library with a name, made if it is an extension's token
|
|
174
|
+
* that hasn't been read.
|
|
175
|
+
*/
|
|
176
|
+
const tokenNamed = (scope, name) => tokenAt(scope.tokens, name.split("."));
|
|
177
|
+
/**
|
|
178
|
+
* The place of each token in the order of its library, by name. The
|
|
179
|
+
* library's shape doesn't change, so it is found once.
|
|
180
|
+
*/
|
|
181
|
+
const orders = new WeakMap();
|
|
182
|
+
const orderOf = (scope) => {
|
|
183
|
+
let order = orders.get(scope);
|
|
184
|
+
if (order === undefined) {
|
|
185
|
+
const found = new Map();
|
|
186
|
+
forEachToken(scope.tokens, (name) => found.set(name, found.size));
|
|
187
|
+
order = found;
|
|
188
|
+
orders.set(scope, order);
|
|
189
|
+
}
|
|
190
|
+
return order;
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Calls `visit` with the name and the property of each token of a library,
|
|
194
|
+
* in its order. A group is a data property and a token an accessor, so this
|
|
195
|
+
* makes no token of an extension.
|
|
196
|
+
*/
|
|
197
|
+
const forEachToken = (tokens, visit) => {
|
|
198
|
+
const walk = (node, name) => {
|
|
199
|
+
for (const key of Object.keys(node)) {
|
|
200
|
+
if (!isChildKey(key)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const path = name.length === 0 ? key : `${name}.${key}`;
|
|
204
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(node, key);
|
|
205
|
+
if ("value" in descriptor) {
|
|
206
|
+
walk(descriptor.value, path);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
visit(path, descriptor);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
walk(tokens, "");
|
|
214
|
+
};
|
|
20
215
|
/**
|
|
21
216
|
* Thrown when reading a token whose value refers back to itself through
|
|
22
217
|
* one or more refs or computed values.
|
|
@@ -36,28 +231,96 @@ export class CircularReferenceError extends Error {
|
|
|
36
231
|
}
|
|
37
232
|
}
|
|
38
233
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
234
|
+
* Given to a subscriber's `onError` when a token throws as a subscribed
|
|
235
|
+
* library resolves it on its own. The error thrown is its `cause`.
|
|
236
|
+
*
|
|
237
|
+
* @public
|
|
238
|
+
*/
|
|
239
|
+
export class TokenResolutionError extends Error {
|
|
240
|
+
name = "TokenResolutionError";
|
|
241
|
+
/**
|
|
242
|
+
* The name of the token that threw.
|
|
243
|
+
*/
|
|
244
|
+
tokenName;
|
|
245
|
+
/**
|
|
246
|
+
* What threw: its type's `isEqual`, comparing the value it resolved with
|
|
247
|
+
* the one it had, or resolving its value.
|
|
248
|
+
*/
|
|
249
|
+
phase;
|
|
250
|
+
constructor(tokenName, phase, cause) {
|
|
251
|
+
super(phase === "isEqual"
|
|
252
|
+
? `The isEqual of "${tokenName}" threw.`
|
|
253
|
+
: `"${tokenName}" threw as it resolved.`, { cause });
|
|
254
|
+
this.tokenName = tokenName;
|
|
255
|
+
this.phase = phase;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The token that first threw the error being thrown, and what threw it,
|
|
260
|
+
* found as a read that throws unwinds. The outermost read clears it.
|
|
42
261
|
*/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
262
|
+
let thrown = null;
|
|
263
|
+
const recordThrow = (error, name, phase) => {
|
|
264
|
+
if (thrown === null || thrown.error !== error) {
|
|
265
|
+
thrown = { error, name, phase };
|
|
47
266
|
}
|
|
48
|
-
return version;
|
|
49
267
|
};
|
|
50
268
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
269
|
+
* Delivers the errors a subscribed library found in a microtask to its
|
|
270
|
+
* subscribers with `onError` that are told of the token's changes, by its
|
|
271
|
+
* origin: one of an extension's own tokens to those with `origin` `"own"` or
|
|
272
|
+
* `"all"`, and one that is its ancestor's to those with `"ancestor"` or
|
|
273
|
+
* `"all"`. One that no subscriber is given is thrown, as is one that
|
|
274
|
+
* `onError` throws.
|
|
53
275
|
*/
|
|
54
|
-
const
|
|
276
|
+
const deliverErrors = (scope, errors) => {
|
|
277
|
+
for (const { error, ancestors } of errors) {
|
|
278
|
+
let delivered = false;
|
|
279
|
+
for (const [subscriber, { delivered: to, ancestral },] of scope.subscribers) {
|
|
280
|
+
// Only a subscriber told of the ancestor's tokens alone is
|
|
281
|
+
// ancestral with a filter.
|
|
282
|
+
const told = ancestors
|
|
283
|
+
? ancestral
|
|
284
|
+
: !ancestral || to === subscriber;
|
|
285
|
+
if (typeof subscriber.onError !== "function" || !told) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
delivered = true;
|
|
289
|
+
try {
|
|
290
|
+
subscriber.onError(error);
|
|
291
|
+
}
|
|
292
|
+
catch (failure) {
|
|
293
|
+
queueMicrotask(() => {
|
|
294
|
+
throw failure;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!delivered) {
|
|
299
|
+
queueMicrotask(() => {
|
|
300
|
+
throw error;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
55
305
|
/**
|
|
56
306
|
* The tokens currently resolving their values, outermost first.
|
|
57
307
|
*/
|
|
58
308
|
const resolving = [];
|
|
59
|
-
|
|
60
|
-
|
|
309
|
+
/**
|
|
310
|
+
* The code of a value that is running as a token resolves, a recipe's
|
|
311
|
+
* function or a getter, if any. A token it reads other than by a ref isn't
|
|
312
|
+
* followed, so reading one there throws.
|
|
313
|
+
*/
|
|
314
|
+
let reading = null;
|
|
315
|
+
const guarded = (token, within, read) => {
|
|
316
|
+
const previous = reading;
|
|
317
|
+
reading = { token, within };
|
|
318
|
+
try {
|
|
319
|
+
return read();
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
reading = previous;
|
|
323
|
+
}
|
|
61
324
|
};
|
|
62
325
|
const isGroup = (value) => {
|
|
63
326
|
return isObject(value) && !Array.isArray(value) && !isToken(value);
|
|
@@ -93,18 +356,9 @@ const defineGroupMetadata = (group, ...sources) => {
|
|
|
93
356
|
}
|
|
94
357
|
};
|
|
95
358
|
const defineToken = (library, key, get) => {
|
|
96
|
-
Reflect.defineProperty(library, key, {
|
|
97
|
-
get() {
|
|
98
|
-
const token = get();
|
|
99
|
-
// Token access is tracked because a ref reads its token from
|
|
100
|
-
// the library
|
|
101
|
-
Watcher.track(token);
|
|
102
|
-
return token;
|
|
103
|
-
},
|
|
104
|
-
enumerable: true,
|
|
105
|
-
});
|
|
359
|
+
Reflect.defineProperty(library, key, { get, enumerable: true });
|
|
106
360
|
};
|
|
107
|
-
const createToken = (key, name, config, scope, typeContext
|
|
361
|
+
const createToken = (key, name, config, scope, typeContext) => {
|
|
108
362
|
for (const property of Object.keys(config)) {
|
|
109
363
|
if (!property.startsWith("$")) {
|
|
110
364
|
throw new Error(`"${name}" has a $value, so it is a token and cannot have a child "${property}".`);
|
|
@@ -115,9 +369,18 @@ const createToken = (key, name, config, scope, typeContext, queue) => {
|
|
|
115
369
|
if (!resolvedType) {
|
|
116
370
|
throw new Error(`No '$type' found for token '${key}'. Types cannot be inferred, please add a $type to the token or to a group ancestor.`);
|
|
117
371
|
}
|
|
118
|
-
|
|
372
|
+
if (!isStandardType(resolvedType) && !scope.types.has(resolvedType)) {
|
|
373
|
+
throw new Error(`"${name}" has the custom type "${resolvedType}", which isn't registered. Register it, with its isEqual, in the 'types' option of create() or extend().`);
|
|
374
|
+
}
|
|
375
|
+
const token = new LibraryToken(name, $value, resolvedType, scope, $description || "", $extensions || {}, $deprecated ?? false);
|
|
376
|
+
// A token that is new in an extension has a value of its own.
|
|
377
|
+
if (scope.source) {
|
|
378
|
+
scope.made.set(name, token);
|
|
379
|
+
scope.own.add(name);
|
|
380
|
+
}
|
|
381
|
+
return token;
|
|
119
382
|
};
|
|
120
|
-
const recurseCreate = (name, library, config, scope, typeContext
|
|
383
|
+
const recurseCreate = (name, library, config, scope, typeContext) => {
|
|
121
384
|
defineGroupMetadata(library, config);
|
|
122
385
|
typeContext = config.$type || typeContext;
|
|
123
386
|
for (const key in config) {
|
|
@@ -131,16 +394,16 @@ const recurseCreate = (name, library, config, scope, typeContext, queue) => {
|
|
|
131
394
|
value: group,
|
|
132
395
|
enumerable: true,
|
|
133
396
|
});
|
|
134
|
-
recurseCreate(_name, group, config[key], scope, typeContext
|
|
397
|
+
recurseCreate(_name, group, config[key], scope, typeContext);
|
|
135
398
|
Object.freeze(group);
|
|
136
399
|
}
|
|
137
400
|
else if (isToken(config[key])) {
|
|
138
|
-
const token = createToken(key, _name, config[key], scope, typeContext
|
|
401
|
+
const token = createToken(key, _name, config[key], scope, typeContext);
|
|
139
402
|
defineToken(library, key, () => token);
|
|
140
403
|
}
|
|
141
404
|
}
|
|
142
405
|
};
|
|
143
|
-
const recurseExtend = (name, sourceTokens, extendedTokens, config, scope, typeContext
|
|
406
|
+
const recurseExtend = (name, sourceTokens, extendedTokens, config, scope, typeContext) => {
|
|
144
407
|
// An extension can restate a group's type, or give one to a group that
|
|
145
408
|
// has none, but can't change it, as for a token.
|
|
146
409
|
const sourceType = Reflect.get(sourceTokens, "$type");
|
|
@@ -170,128 +433,67 @@ const recurseExtend = (name, sourceTokens, extendedTokens, config, scope, typeCo
|
|
|
170
433
|
enumerable: true,
|
|
171
434
|
});
|
|
172
435
|
if (source) {
|
|
173
|
-
recurseExtend(_name, source.value, group, configValue || {}, scope, typeContext
|
|
436
|
+
recurseExtend(_name, source.value, group, configValue || {}, scope, typeContext);
|
|
174
437
|
}
|
|
175
438
|
else {
|
|
176
|
-
recurseCreate(_name, group, configValue, scope, typeContext
|
|
439
|
+
recurseCreate(_name, group, configValue, scope, typeContext);
|
|
177
440
|
}
|
|
178
441
|
Object.freeze(group);
|
|
179
442
|
}
|
|
180
443
|
else if (source) {
|
|
181
|
-
const token = new InheritedToken(() =>
|
|
444
|
+
const token = new InheritedToken(() => sourceTokens[key], scope, configValue);
|
|
445
|
+
// An override has a value of its own.
|
|
446
|
+
if (configValue !== undefined && "$value" in configValue) {
|
|
447
|
+
scope.own.add(_name);
|
|
448
|
+
}
|
|
182
449
|
// A token the extension configures is made now, so its config
|
|
183
450
|
// is checked when the library is.
|
|
184
451
|
if (configValue !== undefined) {
|
|
185
452
|
token.get();
|
|
186
453
|
}
|
|
187
|
-
if (configValue === undefined || !("$value" in configValue)) {
|
|
188
|
-
inherited.push(token);
|
|
189
|
-
}
|
|
190
454
|
defineToken(extendedTokens, key, () => token.get());
|
|
191
455
|
}
|
|
192
456
|
else if (isToken(configValue)) {
|
|
193
|
-
const token = createToken(key, _name, configValue, scope, typeContext
|
|
457
|
+
const token = createToken(key, _name, configValue, scope, typeContext);
|
|
194
458
|
defineToken(extendedTokens, key, () => token);
|
|
195
459
|
}
|
|
196
460
|
}
|
|
197
461
|
};
|
|
198
|
-
/**
|
|
199
|
-
* Reads without the read being tracked by the token that is resolving.
|
|
200
|
-
*/
|
|
201
|
-
const untracked = (read) => {
|
|
202
|
-
const stopWatching = Watcher.use({ watch() { } });
|
|
203
|
-
try {
|
|
204
|
-
return read();
|
|
205
|
-
}
|
|
206
|
-
finally {
|
|
207
|
-
stopWatching();
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
/**
|
|
211
|
-
* Unsubscribes the link of a collected {@link InheritedToken} from its
|
|
212
|
-
* source token.
|
|
213
|
-
*/
|
|
214
|
-
const links = new FinalizationRegistry((unlink) => unlink());
|
|
215
462
|
/**
|
|
216
463
|
* The token of an extension for a token of the library it extends. It is
|
|
217
464
|
* made when it is first read, so an extension costs what it reads (#113).
|
|
218
465
|
*
|
|
219
|
-
* Until it is set, it has the value of its source token
|
|
220
|
-
*
|
|
221
|
-
* library
|
|
222
|
-
*
|
|
223
|
-
* subscriber, which is told of a change to a token it hasn't read.
|
|
466
|
+
* Until it is set, it has the configured value of its source token, through
|
|
467
|
+
* its prototype. A change to the source's token reaches it through its
|
|
468
|
+
* library, which its source holds weakly, so the library it extends doesn't
|
|
469
|
+
* keep an extension alive that nothing else refers to.
|
|
224
470
|
*/
|
|
225
471
|
class InheritedToken {
|
|
226
472
|
source;
|
|
227
473
|
scope;
|
|
228
|
-
queue;
|
|
229
474
|
node;
|
|
230
475
|
token = null;
|
|
231
|
-
|
|
232
|
-
detached;
|
|
233
|
-
constructor(source, scope, queue, node) {
|
|
476
|
+
constructor(source, scope, node) {
|
|
234
477
|
this.source = source;
|
|
235
478
|
this.scope = scope;
|
|
236
|
-
this.queue = queue;
|
|
237
479
|
this.node = node;
|
|
238
|
-
// An override has a value of its own.
|
|
239
|
-
this.detached = node !== undefined && "$value" in node;
|
|
240
480
|
}
|
|
241
481
|
get() {
|
|
242
482
|
if (this.token !== null) {
|
|
243
483
|
return this.token;
|
|
244
484
|
}
|
|
245
|
-
const token = extendToken(this.source(), this.scope, this.
|
|
485
|
+
const token = extendToken(this.source(), this.scope, this.node);
|
|
246
486
|
this.token = token;
|
|
247
|
-
this.
|
|
487
|
+
this.scope.made.set(token.name, token);
|
|
248
488
|
return token;
|
|
249
489
|
}
|
|
250
|
-
follow() {
|
|
251
|
-
if (this.unlink !== null || this.detached) {
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
// The link holds only a weak reference, so that the registry can
|
|
255
|
-
// collect this token, and nothing that refers to it.
|
|
256
|
-
const target = new WeakRef(this);
|
|
257
|
-
const notifier = getNotifier(this.source());
|
|
258
|
-
const link = {
|
|
259
|
-
onChange() {
|
|
260
|
-
target.deref()?.get().onChange();
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
|
-
const unlink = () => notifier.unsubscribe(link);
|
|
264
|
-
notifier.subscribe(link);
|
|
265
|
-
links.register(this, unlink, link);
|
|
266
|
-
this.unlink = () => {
|
|
267
|
-
unlink();
|
|
268
|
-
links.unregister(link);
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Stops following the source token, when the token is set.
|
|
273
|
-
*/
|
|
274
|
-
detach() {
|
|
275
|
-
this.unlink?.();
|
|
276
|
-
this.unlink = null;
|
|
277
|
-
this.detached = true;
|
|
278
|
-
}
|
|
279
490
|
}
|
|
280
|
-
function extendToken(token, scope,
|
|
491
|
+
function extendToken(token, scope, node) {
|
|
281
492
|
if (node?.$type !== undefined && node.$type !== token.$type) {
|
|
282
493
|
throw new Error(`An override cannot change the '$type' of token '${token.name}' from '${token.$type}' to '${node.$type}'.`);
|
|
283
494
|
}
|
|
284
495
|
const extendingToken = Object.create(token);
|
|
285
|
-
extendingToken.
|
|
286
|
-
extendingToken.cached = empty;
|
|
287
|
-
extendingToken.resolving = false;
|
|
288
|
-
extendingToken.notifying = false;
|
|
289
|
-
// Its own dependencies; sharing the source's set would let either token
|
|
290
|
-
// drop the other's subscriptions.
|
|
291
|
-
extendingToken.subscriptions = new Set();
|
|
292
|
-
extendingToken.previousSubscriptions = null;
|
|
293
|
-
extendingToken.watchContext = extendingToken;
|
|
294
|
-
extendingToken.queue = queue;
|
|
496
|
+
extendingToken.state = new TokenState(scope);
|
|
295
497
|
// An extension can restate the token's metadata along with its value.
|
|
296
498
|
if (node?.$description !== undefined) {
|
|
297
499
|
extendingToken._description = node.$description;
|
|
@@ -310,10 +512,9 @@ function extendToken(token, scope, queue, node, inherited) {
|
|
|
310
512
|
// which sets the extended token.
|
|
311
513
|
const set = LibraryToken.prototype.set;
|
|
312
514
|
extendingToken.set = (value) => {
|
|
313
|
-
inherited
|
|
314
|
-
// Own the inherited value, so set() detaches even when the
|
|
515
|
+
// Own the inherited value first, so set() detaches even when the
|
|
315
516
|
// new value is identical and doesn't notify.
|
|
316
|
-
|
|
517
|
+
LibraryToken.detach(extendingToken);
|
|
317
518
|
set.call(extendingToken, value);
|
|
318
519
|
extendingToken.set = set;
|
|
319
520
|
};
|
|
@@ -366,21 +567,46 @@ const tokenAt = (tokens, path) => {
|
|
|
366
567
|
* is frozen, so the config isn't. A getter is read once, into a data
|
|
367
568
|
* property. An object that is in the value twice, or in itself, has one copy.
|
|
368
569
|
* Anything else, such as a class instance or a function, is returned as it
|
|
369
|
-
* is: it can't be rebuilt, so it isn't copied, searched or frozen.
|
|
570
|
+
* is: it can't be rebuilt, so it isn't copied, searched or frozen. A token
|
|
571
|
+
* read by a recipe's function or a getter, rather than by a ref, throws.
|
|
370
572
|
*/
|
|
371
|
-
const resolve = (value, scope) => {
|
|
573
|
+
const resolve = (value, token, scope, found) => {
|
|
372
574
|
const copies = new Map();
|
|
575
|
+
// Within the result of a recipe, whose refs the configured value doesn't
|
|
576
|
+
// show.
|
|
577
|
+
let inResult = 0;
|
|
373
578
|
const walk = (value) => {
|
|
374
579
|
if (!isObject(value)) {
|
|
375
580
|
return value;
|
|
376
581
|
}
|
|
377
582
|
if (isRef(value)) {
|
|
378
|
-
|
|
583
|
+
const name = value[REF].join(".");
|
|
584
|
+
if (inResult > 0) {
|
|
585
|
+
(found.results ??= new Set()).add(name);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
(found.refs ??= new Set()).add(name);
|
|
589
|
+
}
|
|
590
|
+
const token = tokenAt(scope.tokens, value[REF]);
|
|
591
|
+
const resolved = token.$value;
|
|
592
|
+
found.inputs?.push(token, resolved);
|
|
593
|
+
return resolved;
|
|
379
594
|
}
|
|
380
595
|
if (isComputed(value)) {
|
|
381
596
|
const { recipe, args } = value[COMPUTED];
|
|
597
|
+
if (inResult > 0) {
|
|
598
|
+
(found.calls ??= new Set()).add(recipe);
|
|
599
|
+
}
|
|
382
600
|
const fn = scope.recipes.get(recipe) ?? recipe[RECIPE].fn;
|
|
383
|
-
|
|
601
|
+
const resolved = args.map(walk);
|
|
602
|
+
const result = guarded(token, `the recipe "${recipe[RECIPE].name}"`, () => fn(...resolved));
|
|
603
|
+
inResult++;
|
|
604
|
+
try {
|
|
605
|
+
return walk(result);
|
|
606
|
+
}
|
|
607
|
+
finally {
|
|
608
|
+
inResult--;
|
|
609
|
+
}
|
|
384
610
|
}
|
|
385
611
|
if (copies.has(value)) {
|
|
386
612
|
return copies.get(value);
|
|
@@ -403,7 +629,7 @@ const resolve = (value, scope) => {
|
|
|
403
629
|
Reflect.defineProperty(copy, key, "value" in descriptor
|
|
404
630
|
? { ...descriptor, value: walk(descriptor.value) }
|
|
405
631
|
: {
|
|
406
|
-
value: walk(Reflect.get(value, key)),
|
|
632
|
+
value: walk(guarded(token, "a getter of its value", () => Reflect.get(value, key))),
|
|
407
633
|
enumerable: descriptor.enumerable,
|
|
408
634
|
writable: true,
|
|
409
635
|
configurable: true,
|
|
@@ -424,94 +650,163 @@ const assertNoRoot = (config) => {
|
|
|
424
650
|
*/
|
|
425
651
|
class LibraryImpl {
|
|
426
652
|
scope;
|
|
427
|
-
queue;
|
|
428
653
|
tokens;
|
|
429
|
-
|
|
430
|
-
* The inherited tokens of an extension. Each follows its source token
|
|
431
|
-
* once it is read, and all of them once the library has a subscriber,
|
|
432
|
-
* after which this is null.
|
|
433
|
-
*/
|
|
434
|
-
inherited;
|
|
435
|
-
constructor(scope, queue, inherited = []) {
|
|
654
|
+
constructor(scope) {
|
|
436
655
|
this.scope = scope;
|
|
437
|
-
this.queue = queue;
|
|
438
656
|
this.tokens = scope.tokens;
|
|
439
|
-
this.inherited = inherited;
|
|
440
657
|
// Groups are frozen as they are built; the root is frozen here.
|
|
441
658
|
Object.freeze(this.tokens);
|
|
442
659
|
}
|
|
443
|
-
subscribe(subscriber) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
660
|
+
subscribe(subscriber, options = {}) {
|
|
661
|
+
const { scope } = this;
|
|
662
|
+
this.unsubscribe(subscriber);
|
|
663
|
+
const { origin = "own" } = options;
|
|
664
|
+
// A library made by `create()` has only its own tokens.
|
|
665
|
+
const ancestral = scope.source !== undefined && origin !== "own";
|
|
666
|
+
const delivered = origin === "all" || (origin === "own" && scope.source === undefined)
|
|
667
|
+
? subscriber
|
|
668
|
+
: changesFrom(origin, subscriber);
|
|
669
|
+
const subscribed = scope.queue.subscribed;
|
|
670
|
+
scope.subscribers.set(subscriber, { delivered, ancestral });
|
|
671
|
+
scope.queue.subscribe(delivered);
|
|
672
|
+
if (ancestral) {
|
|
673
|
+
scope.ancestral++;
|
|
674
|
+
}
|
|
675
|
+
if (!subscribed) {
|
|
676
|
+
resolveOwn(scope);
|
|
677
|
+
}
|
|
678
|
+
if (ancestral && scope.ancestral === 1) {
|
|
679
|
+
resolveAncestors(scope);
|
|
451
680
|
}
|
|
452
|
-
this.queue.subscribe(subscriber);
|
|
453
681
|
}
|
|
454
682
|
unsubscribe(subscriber) {
|
|
455
|
-
this
|
|
683
|
+
const { scope } = this;
|
|
684
|
+
const subscribed = scope.subscribers.get(subscriber);
|
|
685
|
+
if (subscribed === undefined) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
scope.subscribers.delete(subscriber);
|
|
689
|
+
scope.queue.unsubscribe(subscribed.delivered);
|
|
690
|
+
if (subscribed.ancestral) {
|
|
691
|
+
scope.ancestral--;
|
|
692
|
+
}
|
|
693
|
+
if (!scope.queue.subscribed) {
|
|
694
|
+
LibraryToken.dropPrevious(scope);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* The tokens of a library that are its own, in the order of the library.
|
|
699
|
+
*/
|
|
700
|
+
static ownTokens(library) {
|
|
701
|
+
if (!(library instanceof LibraryImpl)) {
|
|
702
|
+
throw new Error("Not a library.");
|
|
703
|
+
}
|
|
704
|
+
const { scope } = library;
|
|
705
|
+
const origin = scope.source === undefined ? null : originOf(scope);
|
|
706
|
+
const tokens = [];
|
|
707
|
+
forEachToken(scope.tokens, (name, descriptor) => {
|
|
708
|
+
if (origin === null || origin.has(name)) {
|
|
709
|
+
tokens.push(descriptor.get());
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
return tokens;
|
|
456
713
|
}
|
|
457
714
|
extend(config, options = {}) {
|
|
458
715
|
assertNoRoot(config);
|
|
459
|
-
const queue = new Queue();
|
|
460
716
|
const recipes = new Map(this.scope.recipes);
|
|
717
|
+
const overridden = new Set();
|
|
461
718
|
for (const { recipe, fn } of options.recipes ?? []) {
|
|
462
719
|
recipes.set(recipe, fn);
|
|
720
|
+
overridden.add(recipe);
|
|
463
721
|
}
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
source: this.scope,
|
|
469
|
-
};
|
|
470
|
-
const inherited = [];
|
|
471
|
-
recurseExtend("", this.tokens, scope.tokens, config, scope, null, queue, inherited);
|
|
472
|
-
return new LibraryImpl(scope, queue, inherited);
|
|
722
|
+
const types = typesOf(options.types, this.scope.types);
|
|
723
|
+
const scope = scopeOf(recipes, types, this.scope, overridden);
|
|
724
|
+
recurseExtend("", this.tokens, scope.tokens, config, scope, null);
|
|
725
|
+
return new LibraryImpl(scope);
|
|
473
726
|
}
|
|
474
|
-
static create(config) {
|
|
727
|
+
static create(config, options = {}) {
|
|
475
728
|
assertNoRoot(config);
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
recipes: new Map(),
|
|
480
|
-
configurationVersion: 0,
|
|
481
|
-
};
|
|
482
|
-
recurseCreate("", scope.tokens, config, scope, null, queue);
|
|
483
|
-
return new LibraryImpl(scope, queue);
|
|
729
|
+
const scope = scopeOf(new Map(), typesOf(options.types));
|
|
730
|
+
recurseCreate("", scope.tokens, config, scope, null);
|
|
731
|
+
return new LibraryImpl(scope);
|
|
484
732
|
}
|
|
485
733
|
}
|
|
734
|
+
/**
|
|
735
|
+
* The state of a token of a library.
|
|
736
|
+
*/
|
|
737
|
+
class TokenState {
|
|
738
|
+
scope;
|
|
739
|
+
constructor(scope) {
|
|
740
|
+
this.scope = scope;
|
|
741
|
+
}
|
|
742
|
+
cached = empty;
|
|
743
|
+
/**
|
|
744
|
+
* The value that was dropped, kept by a library with a subscriber until
|
|
745
|
+
* the token resolves again: a value equal to it is replaced by it, so
|
|
746
|
+
* it's the same object.
|
|
747
|
+
*/
|
|
748
|
+
previous = empty;
|
|
749
|
+
resolving = false;
|
|
750
|
+
/**
|
|
751
|
+
* The tokens that refs in the results of recipes were to, the last time
|
|
752
|
+
* the value was resolved.
|
|
753
|
+
*/
|
|
754
|
+
found = null;
|
|
755
|
+
/**
|
|
756
|
+
* The recipes that the results of recipes called, the last time the
|
|
757
|
+
* value was resolved, which the configured value doesn't show.
|
|
758
|
+
*/
|
|
759
|
+
called = null;
|
|
760
|
+
/**
|
|
761
|
+
* Each token whose value the value was resolved from, followed by that
|
|
762
|
+
* value, the last time it was resolved; `null` when its library had no
|
|
763
|
+
* subscriber then, which doesn't check them.
|
|
764
|
+
*/
|
|
765
|
+
inputs = null;
|
|
766
|
+
/**
|
|
767
|
+
* The cached value, unless a token it depends on may have changed since
|
|
768
|
+
* it was resolved: then it's checked against its inputs when it's read.
|
|
769
|
+
* It's the only field the getter reads for a cached value.
|
|
770
|
+
*/
|
|
771
|
+
current = empty;
|
|
772
|
+
/**
|
|
773
|
+
* The value it had when a subscribed library queued it, which the batch
|
|
774
|
+
* compares with the one it has then.
|
|
775
|
+
*/
|
|
776
|
+
before = unqueued;
|
|
777
|
+
/**
|
|
778
|
+
* For a token whose value is its ancestor's, the value it last had for a
|
|
779
|
+
* subscriber told of those.
|
|
780
|
+
*/
|
|
781
|
+
seen = empty;
|
|
782
|
+
/**
|
|
783
|
+
* Whether it became its extension's own since its library's last batch.
|
|
784
|
+
*/
|
|
785
|
+
originChanged = false;
|
|
786
|
+
}
|
|
486
787
|
/**
|
|
487
788
|
* An individual token value in a library
|
|
488
789
|
*/
|
|
489
790
|
class LibraryToken {
|
|
490
791
|
name;
|
|
491
792
|
_type;
|
|
492
|
-
scope;
|
|
493
793
|
_description;
|
|
494
794
|
_deprecated;
|
|
495
|
-
queue;
|
|
496
795
|
raw;
|
|
497
|
-
cached = empty;
|
|
498
|
-
resolving = false;
|
|
499
|
-
notifying = false;
|
|
500
|
-
subscriptions = new Set();
|
|
501
796
|
/**
|
|
502
|
-
*
|
|
503
|
-
*
|
|
797
|
+
* What changes as the token resolves, in an object of one shape. A token
|
|
798
|
+
* of an extension has a shape of its own, since its prototype is its
|
|
799
|
+
* source's token, so its own fields are slow to read and to add.
|
|
504
800
|
*/
|
|
505
|
-
|
|
801
|
+
state;
|
|
506
802
|
_extensions;
|
|
507
|
-
constructor(name, value, _type, scope, _description, extensions, _deprecated
|
|
803
|
+
constructor(name, value, _type, scope, _description, extensions, _deprecated) {
|
|
508
804
|
this.name = name;
|
|
509
805
|
this._type = _type;
|
|
510
|
-
this.scope = scope;
|
|
511
806
|
this._description = _description;
|
|
512
807
|
this._deprecated = _deprecated;
|
|
513
|
-
this.queue = queue;
|
|
514
808
|
this.raw = value;
|
|
809
|
+
this.state = new TokenState(scope);
|
|
515
810
|
// A copy, so changing the config's top-level keys can't change the token.
|
|
516
811
|
this._extensions = { ...extensions };
|
|
517
812
|
}
|
|
@@ -531,38 +826,104 @@ class LibraryToken {
|
|
|
531
826
|
* Gets the token value
|
|
532
827
|
*/
|
|
533
828
|
get $value() {
|
|
534
|
-
|
|
535
|
-
|
|
829
|
+
// Before the cache, so a token that was read is caught too.
|
|
830
|
+
if (reading !== null) {
|
|
831
|
+
throw new Error(`"${reading.token}" read the token "${this.name}" in ${reading.within}. A token read there isn't followed, so the value would not change with it. Pass the token to it as a ref instead.`);
|
|
832
|
+
}
|
|
833
|
+
// A token whose value is its ancestor's holds none.
|
|
834
|
+
const { current } = this.state;
|
|
835
|
+
return current !== empty ? current : this.value();
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Whether the value is its ancestor's: the token is a subscribed
|
|
839
|
+
* extension's and isn't its own.
|
|
840
|
+
*/
|
|
841
|
+
isAncestors() {
|
|
842
|
+
const { origin, queue } = this.state.scope;
|
|
843
|
+
return origin !== null && queue.subscribed && !origin.has(this.name);
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* The value, resolved if it isn't cached or may have changed.
|
|
847
|
+
*/
|
|
848
|
+
value() {
|
|
849
|
+
const { scope } = this.state;
|
|
850
|
+
if (resolving.length === 0) {
|
|
851
|
+
thrown = null;
|
|
852
|
+
}
|
|
853
|
+
if (this.isAncestors()) {
|
|
854
|
+
// The ancestor token has the same type.
|
|
855
|
+
const value = LibraryToken.ancestorValue(this);
|
|
856
|
+
if (scope.ancestral > 0) {
|
|
857
|
+
this.state.seen = value;
|
|
858
|
+
}
|
|
859
|
+
return value;
|
|
536
860
|
}
|
|
537
|
-
if (this.
|
|
861
|
+
if (this.state.cached !== empty) {
|
|
862
|
+
if (this.inputsAreCurrent()) {
|
|
863
|
+
return (this.state.current = this.state.cached);
|
|
864
|
+
}
|
|
865
|
+
this.uncache();
|
|
866
|
+
}
|
|
867
|
+
if (this.state.resolving) {
|
|
538
868
|
const chain = resolving
|
|
539
869
|
.slice(resolving.indexOf(this))
|
|
540
870
|
.map((token) => token.name);
|
|
541
|
-
|
|
871
|
+
const error = new CircularReferenceError([...chain, this.name]);
|
|
872
|
+
recordThrow(error, this.name, "resolve");
|
|
873
|
+
throw error;
|
|
542
874
|
}
|
|
543
|
-
|
|
544
|
-
this.previousSubscriptions = previous;
|
|
545
|
-
this.subscriptions = new Set();
|
|
546
|
-
this.resolving = true;
|
|
875
|
+
this.state.resolving = true;
|
|
547
876
|
resolving.push(this);
|
|
548
|
-
const stopWatching = Watcher.use(this);
|
|
549
877
|
try {
|
|
878
|
+
const found = {
|
|
879
|
+
refs: null,
|
|
880
|
+
results: null,
|
|
881
|
+
calls: null,
|
|
882
|
+
// Only a library with a subscriber checks them.
|
|
883
|
+
inputs: scope.queue.subscribed ? [] : null,
|
|
884
|
+
};
|
|
550
885
|
// The config's types guarantee what the raw value resolves to.
|
|
551
|
-
|
|
552
|
-
|
|
886
|
+
let value = resolve(this.raw, this.name, this.state.scope, found);
|
|
887
|
+
// Resolving found every ref of the configured value, so the
|
|
888
|
+
// graph has the token's edges before any token depends on its
|
|
889
|
+
// value.
|
|
890
|
+
ownerOf(this.state.scope, this.name).graph.add(this.name, found.refs ?? none);
|
|
891
|
+
this.setFound(found.results);
|
|
892
|
+
this.state.called = found.calls;
|
|
893
|
+
this.state.inputs =
|
|
894
|
+
found.inputs?.length === 0 ? noInputs : found.inputs;
|
|
895
|
+
const { previous } = this.state;
|
|
896
|
+
if (previous !== empty) {
|
|
897
|
+
this.state.previous = empty;
|
|
898
|
+
if (scope.queue.subscribed && this.equals(previous, value)) {
|
|
899
|
+
value = previous;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
this.state.cached = value;
|
|
903
|
+
this.state.current = value;
|
|
904
|
+
this.state.scope.cached++;
|
|
553
905
|
return value;
|
|
554
906
|
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
recordThrow(error, this.name, "resolve");
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
555
911
|
finally {
|
|
556
|
-
this.resolving = false;
|
|
912
|
+
this.state.resolving = false;
|
|
557
913
|
resolving.pop();
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Compares two values by the token's type, recording an error that its
|
|
918
|
+
* equality throws.
|
|
919
|
+
*/
|
|
920
|
+
equals(a, b) {
|
|
921
|
+
try {
|
|
922
|
+
return equalityOf(this.state.scope, this._type)(a, b);
|
|
923
|
+
}
|
|
924
|
+
catch (error) {
|
|
925
|
+
recordThrow(error, this.name, "isEqual");
|
|
926
|
+
throw error;
|
|
566
927
|
}
|
|
567
928
|
}
|
|
568
929
|
toString() {
|
|
@@ -580,33 +941,481 @@ class LibraryToken {
|
|
|
580
941
|
return;
|
|
581
942
|
}
|
|
582
943
|
this.raw = value;
|
|
583
|
-
|
|
584
|
-
|
|
944
|
+
// A token that is set has a value of its own, so its edges are its
|
|
945
|
+
// library's. Until the graph is complete, they are added again when
|
|
946
|
+
// the token is resolved.
|
|
947
|
+
const { graph, complete } = this.state.scope;
|
|
948
|
+
if (graph.remove(this.name) && complete) {
|
|
949
|
+
graph.add(this.name, refsOf(value));
|
|
950
|
+
}
|
|
951
|
+
LibraryToken.changed(this.state.scope, this.name, this);
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Drops the resolved value, and says whether there was one.
|
|
955
|
+
*/
|
|
956
|
+
uncache() {
|
|
957
|
+
if (this.state.cached === empty) {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
if (this.state.scope.queue.subscribed) {
|
|
961
|
+
this.state.previous = this.state.cached;
|
|
962
|
+
}
|
|
963
|
+
this.state.cached = empty;
|
|
964
|
+
this.state.current = empty;
|
|
965
|
+
this.state.scope.cached--;
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Drops the value of each token of a subscribed extension that is its
|
|
970
|
+
* ancestor's, which it reads from its ancestor from then on.
|
|
971
|
+
*/
|
|
972
|
+
static dropAncestors(scope) {
|
|
973
|
+
const origin = scope.origin;
|
|
974
|
+
for (const [name, token] of scope.made) {
|
|
975
|
+
if (!origin.has(name) && token.state.cached !== empty) {
|
|
976
|
+
token.state.cached = empty;
|
|
977
|
+
token.state.current = empty;
|
|
978
|
+
token.state.previous = empty;
|
|
979
|
+
scope.cached--;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Whether each token the value was resolved from has the value it had,
|
|
985
|
+
* as far as is known.
|
|
986
|
+
*/
|
|
987
|
+
inputsAreCurrent() {
|
|
988
|
+
const { inputs } = this.state;
|
|
989
|
+
if (inputs === null) {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
for (let i = 0; i < inputs.length; i += 2) {
|
|
993
|
+
try {
|
|
994
|
+
if (!Object.is(inputs[i].$value, inputs[i + 1])) {
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
catch {
|
|
999
|
+
return false;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return true;
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Queues the token for its library's next batch, with the value it has
|
|
1006
|
+
* now to compare with.
|
|
1007
|
+
*/
|
|
1008
|
+
queue() {
|
|
1009
|
+
const { state } = this;
|
|
1010
|
+
if (state.before === unqueued) {
|
|
1011
|
+
// A token that was dropped and not resolved since had the value
|
|
1012
|
+
// it keeps to compare with, and one that is its ancestor's the
|
|
1013
|
+
// value its subscribers were last told of.
|
|
1014
|
+
// Only a subscriber told of its ancestor's tokens queues those.
|
|
1015
|
+
state.before =
|
|
1016
|
+
state.scope.ancestral > 0 && this.isAncestors()
|
|
1017
|
+
? state.seen
|
|
1018
|
+
: state.cached !== empty
|
|
1019
|
+
? state.cached
|
|
1020
|
+
: state.previous;
|
|
1021
|
+
}
|
|
1022
|
+
this.state.scope.queue.add(this);
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* The value of a token of a subscribed extension that is its ancestor's:
|
|
1026
|
+
* its ancestor's, the same object. Which tokens are its own is found when
|
|
1027
|
+
* it's first subscribed, and again as changes reach it, so none that
|
|
1028
|
+
* should be is read here.
|
|
1029
|
+
*/
|
|
1030
|
+
static ancestorValue(token) {
|
|
1031
|
+
const source = Object.getPrototypeOf(token);
|
|
1032
|
+
return source.$value;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Whether a token of a subscribed extension that isn't its own yet, and
|
|
1036
|
+
* whose configured value calls a recipe, must be: it calls a recipe the
|
|
1037
|
+
* extension overrides, or a recipe's result does, or refers to a token
|
|
1038
|
+
* that is the extension's own. It's resolved in the source to find out
|
|
1039
|
+
* what its results do.
|
|
1040
|
+
*/
|
|
1041
|
+
static discover(scope, name) {
|
|
1042
|
+
const { overridden } = scope;
|
|
1043
|
+
if (overridden.size > 0 &&
|
|
1044
|
+
callsAny(LibraryToken.configuredValue(tokenNamed(scope.source, name)), overridden)) {
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
const resolver = resolverOf(scope, name);
|
|
1048
|
+
try {
|
|
1049
|
+
resolver.$value;
|
|
1050
|
+
}
|
|
1051
|
+
catch {
|
|
1052
|
+
// An error of the source's token is reported by what follows it:
|
|
1053
|
+
// the source's subscribers, and the extension's subscribers told
|
|
1054
|
+
// of its ancestor's tokens, as it resolves them. Otherwise it
|
|
1055
|
+
// throws when it's read.
|
|
1056
|
+
}
|
|
1057
|
+
const { called, found } = resolver.state;
|
|
1058
|
+
const origin = scope.origin;
|
|
1059
|
+
for (const recipe of called ?? []) {
|
|
1060
|
+
if (overridden.has(recipe)) {
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
for (const name of found ?? none) {
|
|
1065
|
+
if (origin.has(name)) {
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return false;
|
|
585
1070
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
1071
|
+
/**
|
|
1072
|
+
* Finds the tokens of a subscribed extension that a change to the token
|
|
1073
|
+
* named `name` makes its own: a recipe's result that the change reaches
|
|
1074
|
+
* may now call a recipe the extension overrides, or refer to one of its
|
|
1075
|
+
* own tokens.
|
|
1076
|
+
*/
|
|
1077
|
+
static rediscover(scope, name) {
|
|
1078
|
+
const origin = scope.origin;
|
|
1079
|
+
const { callers } = scope;
|
|
1080
|
+
for (const caller of callers) {
|
|
1081
|
+
if (origin.has(caller)) {
|
|
1082
|
+
callers.delete(caller);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (callers.size === 0) {
|
|
590
1086
|
return;
|
|
591
1087
|
}
|
|
592
|
-
|
|
1088
|
+
const visited = new Set([name]);
|
|
1089
|
+
const pending = [name];
|
|
1090
|
+
const visit = (dependent) => {
|
|
1091
|
+
if (!visited.has(dependent)) {
|
|
1092
|
+
visited.add(dependent);
|
|
1093
|
+
pending.push(dependent);
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
for (let i = 0; i < pending.length; i++) {
|
|
1097
|
+
scope.graph.dependentsOf(pending[i], visit);
|
|
1098
|
+
foundDependentsOf(scope, pending[i], visit);
|
|
1099
|
+
}
|
|
1100
|
+
for (const reached of pending) {
|
|
1101
|
+
if (callers.has(reached) &&
|
|
1102
|
+
!origin.has(reached) &&
|
|
1103
|
+
LibraryToken.discover(scope, reached)) {
|
|
1104
|
+
addOrigin(scope, reached);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Queues a token that becomes a subscribed extension's own, as its
|
|
1110
|
+
* origin changed. It's compared with the value it had as its ancestor's,
|
|
1111
|
+
* which it keeps when its own is equal.
|
|
1112
|
+
*/
|
|
1113
|
+
static inherit(scope, name) {
|
|
1114
|
+
if (scope.source === undefined || !scope.queue.subscribed) {
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const token = tokenNamed(scope, name);
|
|
1118
|
+
if (token.state.cached === empty) {
|
|
1119
|
+
try {
|
|
1120
|
+
token.state.previous = tokenNamed(scope.source, name).$value;
|
|
1121
|
+
}
|
|
1122
|
+
catch {
|
|
1123
|
+
// It throws as its ancestor's, so any value of its own is new.
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
token.state.originChanged = true;
|
|
1127
|
+
token.queue();
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Delivers what a batch of a subscribed library reports: each token it
|
|
1131
|
+
* queued whose value isn't the one it had, which reading it brings up to
|
|
1132
|
+
* date, or whose origin changed. A token that starts or stops throwing
|
|
1133
|
+
* has changed. A token that is its ancestor's is compared by its type's
|
|
1134
|
+
* equality, since its ancestor may not keep equal values' objects.
|
|
1135
|
+
*/
|
|
1136
|
+
static reported(tokens) {
|
|
1137
|
+
const reported = [];
|
|
1138
|
+
for (const token of tokens) {
|
|
1139
|
+
const { state } = token;
|
|
1140
|
+
const { scope } = state;
|
|
1141
|
+
const { before, originChanged } = state;
|
|
1142
|
+
state.originChanged = false;
|
|
1143
|
+
if (before === unqueued) {
|
|
1144
|
+
reported.push(changeOf(token, true, "own", originChanged));
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
state.before = unqueued;
|
|
1148
|
+
if (!scope.queue.subscribed) {
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
let after;
|
|
1152
|
+
try {
|
|
1153
|
+
after = token.$value;
|
|
1154
|
+
}
|
|
1155
|
+
catch (error) {
|
|
1156
|
+
after = empty;
|
|
1157
|
+
LibraryToken.report(token, error);
|
|
1158
|
+
}
|
|
1159
|
+
// Only a subscriber told of its ancestor's tokens queues those.
|
|
1160
|
+
const ancestors = scope.ancestral > 0 && token.isAncestors();
|
|
1161
|
+
let valueChanged = !Object.is(before, after);
|
|
1162
|
+
if (valueChanged &&
|
|
1163
|
+
ancestors &&
|
|
1164
|
+
before !== empty &&
|
|
1165
|
+
after !== empty) {
|
|
1166
|
+
try {
|
|
1167
|
+
valueChanged = !token.equals(before, after);
|
|
1168
|
+
}
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
LibraryToken.report(token, error);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (valueChanged || originChanged) {
|
|
1174
|
+
reported.push(changeOf(token, valueChanged, ancestors ? "ancestor" : "own", originChanged));
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return reported;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Resolves a token of a subscribed library, reporting an error it throws
|
|
1181
|
+
* to the library's subscribers.
|
|
1182
|
+
*/
|
|
1183
|
+
static tryResolve(token) {
|
|
593
1184
|
try {
|
|
594
|
-
|
|
595
|
-
this.cached = empty;
|
|
596
|
-
getNotifier(this).notify();
|
|
1185
|
+
token.$value;
|
|
597
1186
|
}
|
|
598
|
-
|
|
599
|
-
|
|
1187
|
+
catch (error) {
|
|
1188
|
+
LibraryToken.report(token, error);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Queues an error a token of a subscribed library threw as the library
|
|
1193
|
+
* resolved it, for its subscribers, as a {@link TokenResolutionError} of
|
|
1194
|
+
* the token that threw it first. An error is reported once in a
|
|
1195
|
+
* microtask, as the tokens that depend on a token that throws rethrow
|
|
1196
|
+
* its error.
|
|
1197
|
+
*/
|
|
1198
|
+
static report(token, error) {
|
|
1199
|
+
const { name, phase } = thrown !== null && thrown.error === error
|
|
1200
|
+
? thrown
|
|
1201
|
+
: { name: token.name, phase: "resolve" };
|
|
1202
|
+
thrown = null;
|
|
1203
|
+
const { scope } = token.state;
|
|
1204
|
+
let errors = scope.errors;
|
|
1205
|
+
if (errors === null) {
|
|
1206
|
+
const pending = (errors = scope.errors = []);
|
|
1207
|
+
queueMicrotask(() => {
|
|
1208
|
+
scope.errors = null;
|
|
1209
|
+
deliverErrors(scope, pending);
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
if (errors.some((pending) => pending.error.cause === error)) {
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
errors.push({
|
|
1216
|
+
error: new TokenResolutionError(name, phase, error),
|
|
1217
|
+
ancestors: token.isAncestors(),
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Resolves a token of a library that is first subscribed, if it doesn't
|
|
1222
|
+
* throw. A value it resolved before has no inputs, which are the values
|
|
1223
|
+
* the tokens it depends on have now, as its library dropped it when one
|
|
1224
|
+
* of them changed.
|
|
1225
|
+
*/
|
|
1226
|
+
static resolveForSubscriber(token) {
|
|
1227
|
+
LibraryToken.tryResolve(token);
|
|
1228
|
+
const { state } = token;
|
|
1229
|
+
if (state.inputs !== null || state.cached === empty) {
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
const { scope } = state;
|
|
1233
|
+
const inputs = [];
|
|
1234
|
+
const add = (name) => {
|
|
1235
|
+
const dependency = tokenNamed(scope, name);
|
|
1236
|
+
try {
|
|
1237
|
+
inputs.push(dependency, dependency.$value);
|
|
1238
|
+
}
|
|
1239
|
+
catch {
|
|
1240
|
+
inputs.push(dependency, empty);
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
ownerOf(scope, token.name)
|
|
1244
|
+
.graph.dependenciesOf(token.name)
|
|
1245
|
+
?.forEach(add);
|
|
1246
|
+
state.found?.forEach(add);
|
|
1247
|
+
state.inputs = inputs.length === 0 ? noInputs : inputs;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Drops the value each token of a library kept to compare with, once the
|
|
1251
|
+
* library has no subscriber, which compares values.
|
|
1252
|
+
*/
|
|
1253
|
+
static dropPrevious(scope) {
|
|
1254
|
+
const drop = (token) => {
|
|
1255
|
+
token.state.previous = empty;
|
|
1256
|
+
};
|
|
1257
|
+
if (scope.source) {
|
|
1258
|
+
// The tokens an extension hasn't made have no value.
|
|
1259
|
+
scope.made.forEach(drop);
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
forEachToken(scope.tokens, (_, descriptor) => drop(descriptor.get()));
|
|
600
1263
|
}
|
|
601
1264
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (this.subscriptions.has(notifier)) {
|
|
1265
|
+
setFound(names) {
|
|
1266
|
+
if (this.state.found === null && names === null) {
|
|
605
1267
|
return;
|
|
606
1268
|
}
|
|
607
|
-
this.
|
|
608
|
-
|
|
609
|
-
|
|
1269
|
+
const { found } = this.state.scope;
|
|
1270
|
+
for (const name of this.state.found ?? []) {
|
|
1271
|
+
found.get(name)?.delete(this.name);
|
|
1272
|
+
}
|
|
1273
|
+
for (const name of names ?? []) {
|
|
1274
|
+
let dependents = found.get(name);
|
|
1275
|
+
if (dependents === undefined) {
|
|
1276
|
+
dependents = new Set();
|
|
1277
|
+
found.set(name, dependents);
|
|
1278
|
+
}
|
|
1279
|
+
dependents.add(this.name);
|
|
1280
|
+
}
|
|
1281
|
+
this.state.found = names;
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Makes an extension's token have a value of its own: the one it
|
|
1285
|
+
* inherits, until it is set.
|
|
1286
|
+
*/
|
|
1287
|
+
static detach(token) {
|
|
1288
|
+
token.raw = token.raw;
|
|
1289
|
+
token.state.scope.own.add(token.name);
|
|
1290
|
+
token.state.scope.graph.add(token.name, refsOf(token.raw));
|
|
1291
|
+
if (token.state.scope.origin !== null) {
|
|
1292
|
+
addOrigin(token.state.scope, token.name);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Invalidates what a change to the token named `name` in a library
|
|
1297
|
+
* changes: its value, the values that depend on it, and the same in the
|
|
1298
|
+
* libraries that extend it and inherit the token. Each token whose value
|
|
1299
|
+
* is dropped is queued for its library's subscribers, and so is the token
|
|
1300
|
+
* itself, which a library with a subscriber makes if it hasn't.
|
|
1301
|
+
*
|
|
1302
|
+
* A token without a resolved value is not followed: a token that has one
|
|
1303
|
+
* read the tokens it depends on as it resolved, and they have one too.
|
|
1304
|
+
*/
|
|
1305
|
+
static changed(scope, name, token) {
|
|
1306
|
+
if (scope.queue.subscribed) {
|
|
1307
|
+
LibraryToken.reach(scope, name, token);
|
|
1308
|
+
}
|
|
1309
|
+
else {
|
|
1310
|
+
LibraryToken.invalidate(scope, name, token);
|
|
1311
|
+
}
|
|
1312
|
+
for (const reference of scope.extensions) {
|
|
1313
|
+
const extension = reference.deref();
|
|
1314
|
+
if (extension !== undefined && !extension.own.has(name)) {
|
|
1315
|
+
LibraryToken.changed(extension, name);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Marks what a change to the token named `name` reaches in a subscribed
|
|
1321
|
+
* library, and queues it: the token, and each token that depends on it,
|
|
1322
|
+
* however far, whether it was read or not. A token is resolved again
|
|
1323
|
+
* when the batch is delivered, or when it's read first, and it's checked
|
|
1324
|
+
* against the values it was resolved from first. An extension queues
|
|
1325
|
+
* only its own tokens: its ancestor's are its ancestor's values, which it
|
|
1326
|
+
* doesn't hold.
|
|
1327
|
+
*/
|
|
1328
|
+
static reach(scope, name, token) {
|
|
1329
|
+
const { origin } = scope;
|
|
1330
|
+
if (origin !== null) {
|
|
1331
|
+
LibraryToken.rediscover(scope, name);
|
|
1332
|
+
}
|
|
1333
|
+
const ancestral = scope.ancestral > 0;
|
|
1334
|
+
if (!ancestral && origin !== null && origin.size === 0) {
|
|
1335
|
+
// Its values are all its ancestor's, and it holds none of them.
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const isOwn = (name) => origin === null || origin.has(name);
|
|
1339
|
+
// The tokens queued: its own, and with a subscriber told of its
|
|
1340
|
+
// ancestor's, those too.
|
|
1341
|
+
const isQueued = (name) => ancestral || isOwn(name);
|
|
1342
|
+
const changed = token ??
|
|
1343
|
+
madeToken(scope, name) ??
|
|
1344
|
+
(isQueued(name) ? tokenNamed(scope, name) : undefined);
|
|
1345
|
+
if (changed !== undefined) {
|
|
1346
|
+
if (isQueued(name)) {
|
|
1347
|
+
changed.queue();
|
|
1348
|
+
}
|
|
1349
|
+
changed.uncache();
|
|
1350
|
+
}
|
|
1351
|
+
const visited = new Set([name]);
|
|
1352
|
+
const pending = [name];
|
|
1353
|
+
const visit = (dependent) => {
|
|
1354
|
+
if (visited.has(dependent)) {
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
visited.add(dependent);
|
|
1358
|
+
pending.push(dependent);
|
|
1359
|
+
if (isOwn(dependent)) {
|
|
1360
|
+
const token = tokenNamed(scope, dependent);
|
|
1361
|
+
token.queue();
|
|
1362
|
+
token.state.current = empty;
|
|
1363
|
+
}
|
|
1364
|
+
else if (ancestral) {
|
|
1365
|
+
tokenNamed(scope, dependent).queue();
|
|
1366
|
+
}
|
|
1367
|
+
else {
|
|
1368
|
+
madeToken(scope, dependent)?.uncache();
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
// In the order they're reached, so that each is resolved after the
|
|
1372
|
+
// tokens it depends on, and not through them.
|
|
1373
|
+
for (let i = 0; i < pending.length; i++) {
|
|
1374
|
+
scope.graph.dependentsOf(pending[i], visit);
|
|
1375
|
+
foundDependentsOf(scope, pending[i], visit);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Drops what a change to the token named `name` changes in a library
|
|
1380
|
+
* with no subscriber: its value, and the values that depend on it.
|
|
1381
|
+
*
|
|
1382
|
+
* A token without a resolved value is not followed: a token that has one
|
|
1383
|
+
* read the tokens it depends on as it resolved, and they have one too.
|
|
1384
|
+
* An extension's ancestor's tokens are followed whatever they hold,
|
|
1385
|
+
* since its own tokens may have read their ancestor's values while it was
|
|
1386
|
+
* subscribed.
|
|
1387
|
+
*/
|
|
1388
|
+
static invalidate(scope, name, token) {
|
|
1389
|
+
const changed = token ??
|
|
1390
|
+
madeToken(scope, name) ??
|
|
1391
|
+
(scope.queue.subscribed ? tokenNamed(scope, name) : undefined);
|
|
1392
|
+
if (changed !== undefined) {
|
|
1393
|
+
changed.uncache();
|
|
1394
|
+
scope.queue.add(changed);
|
|
1395
|
+
}
|
|
1396
|
+
if (scope.cached > 0) {
|
|
1397
|
+
const { graph, origin } = scope;
|
|
1398
|
+
const visited = new Set([name]);
|
|
1399
|
+
const pending = [name];
|
|
1400
|
+
const visit = (dependent) => {
|
|
1401
|
+
if (visited.has(dependent)) {
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
visited.add(dependent);
|
|
1405
|
+
const token = madeToken(scope, dependent);
|
|
1406
|
+
if (token?.uncache()) {
|
|
1407
|
+
scope.queue.add(token);
|
|
1408
|
+
pending.push(dependent);
|
|
1409
|
+
}
|
|
1410
|
+
else if (origin !== null && !origin.has(dependent)) {
|
|
1411
|
+
pending.push(dependent);
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
while (pending.length > 0) {
|
|
1415
|
+
const next = pending.pop();
|
|
1416
|
+
graph.dependentsOf(next, visit);
|
|
1417
|
+
scope.found.get(next)?.forEach(visit);
|
|
1418
|
+
}
|
|
610
1419
|
}
|
|
611
1420
|
}
|
|
612
1421
|
/**
|
|
@@ -618,62 +1427,226 @@ class LibraryToken {
|
|
|
618
1427
|
}
|
|
619
1428
|
/**
|
|
620
1429
|
* The tokens that the refs in a token's configured value are to, in the
|
|
621
|
-
* token's library
|
|
622
|
-
* data and in the arguments of computed values.
|
|
1430
|
+
* token's library.
|
|
623
1431
|
*/
|
|
624
1432
|
static dependencies(token) {
|
|
625
1433
|
const dependencies = new Set();
|
|
626
|
-
|
|
627
|
-
const walk = (value) => {
|
|
628
|
-
if (!isObject(value) || seen.has(value)) {
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
seen.add(value);
|
|
632
|
-
if (isRef(value)) {
|
|
633
|
-
dependencies.add(tokenAt(token.scope.tokens, value[REF]));
|
|
634
|
-
}
|
|
635
|
-
else if (isComputed(value)) {
|
|
636
|
-
value[COMPUTED].args.forEach(walk);
|
|
637
|
-
}
|
|
638
|
-
else if (isPlain(value)) {
|
|
639
|
-
for (const key of Reflect.ownKeys(value)) {
|
|
640
|
-
walk(Reflect.get(value, key));
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
};
|
|
644
|
-
walk(token.raw);
|
|
1434
|
+
forEachRef(token.raw, (path) => dependencies.add(tokenAt(token.state.scope.tokens, path)));
|
|
645
1435
|
return dependencies;
|
|
646
1436
|
}
|
|
647
1437
|
/**
|
|
648
|
-
* The tokens of the token's library whose configured values refer to it
|
|
649
|
-
*
|
|
650
|
-
* token is set to a different value.
|
|
1438
|
+
* The tokens of the token's library whose configured values refer to it,
|
|
1439
|
+
* in the order of the library.
|
|
651
1440
|
*/
|
|
652
1441
|
static dependents(token) {
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
1442
|
+
const { scope } = token.state;
|
|
1443
|
+
const version = completeGraphOf(scope).version;
|
|
1444
|
+
const kept = dependentsOfToken.get(token);
|
|
1445
|
+
if (kept?.version === version) {
|
|
1446
|
+
return kept.dependents;
|
|
1447
|
+
}
|
|
1448
|
+
const names = [];
|
|
1449
|
+
completeGraphOf(scope).dependentsOf(token.name, (name) => names.push(name));
|
|
1450
|
+
if (names.length > 1) {
|
|
1451
|
+
const order = orderOf(scope);
|
|
1452
|
+
names.sort((a, b) => order.get(a) - order.get(b));
|
|
1453
|
+
}
|
|
1454
|
+
const dependents = names.map((name) => tokenNamed(scope, name));
|
|
1455
|
+
dependentsOfToken.set(token, { version, dependents });
|
|
1456
|
+
return dependents;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Calls `visit` with each token of a library whose value resolved a ref to
|
|
1461
|
+
* the token named `name` in a recipe's result. An extension's are its own
|
|
1462
|
+
* and those of its ancestors' tokens it doesn't give a value.
|
|
1463
|
+
*/
|
|
1464
|
+
function foundDependentsOf(scope, name, visit) {
|
|
1465
|
+
if (scope.source !== undefined) {
|
|
1466
|
+
foundDependentsOf(scope.source, name, (dependent) => {
|
|
1467
|
+
if (!scope.own.has(dependent)) {
|
|
1468
|
+
visit(dependent);
|
|
670
1469
|
}
|
|
671
|
-
|
|
672
|
-
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
scope.found.get(name)?.forEach(visit);
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Makes a token of a subscribed extension its own, and each token that
|
|
1476
|
+
* depends on it. With `inherit`, each is compared next with the value it had
|
|
1477
|
+
* as its ancestor's.
|
|
1478
|
+
*/
|
|
1479
|
+
function addOrigin(scope, name, inherit = true) {
|
|
1480
|
+
const origin = scope.origin;
|
|
1481
|
+
if (origin.has(name)) {
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
// Completed when an extension first has a token of its own.
|
|
1485
|
+
completeGraphOf(scope);
|
|
1486
|
+
const add = (name) => {
|
|
1487
|
+
origin.add(name);
|
|
1488
|
+
pending.push(name);
|
|
1489
|
+
if (inherit) {
|
|
1490
|
+
LibraryToken.inherit(scope, name);
|
|
1491
|
+
}
|
|
1492
|
+
};
|
|
1493
|
+
const pending = [];
|
|
1494
|
+
add(name);
|
|
1495
|
+
const visit = (dependent) => {
|
|
1496
|
+
if (!origin.has(dependent)) {
|
|
1497
|
+
add(dependent);
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
for (let i = 0; i < pending.length; i++) {
|
|
1501
|
+
scope.graph.dependentsOf(pending[i], visit);
|
|
1502
|
+
foundDependentsOf(scope, pending[i], visit);
|
|
1503
|
+
}
|
|
1504
|
+
if (!scope.discovered && scope.queue.subscribed) {
|
|
1505
|
+
discoverAll(scope, inherit);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Finds the tokens of an extension that recipes' results make its own.
|
|
1510
|
+
* What a result calls and refers to is known only once it has resolved, so
|
|
1511
|
+
* each token of the source that calls a recipe is resolved: the
|
|
1512
|
+
* extension's own tokens are then the same whatever the source has read.
|
|
1513
|
+
*/
|
|
1514
|
+
function discoverAll(scope, inherit) {
|
|
1515
|
+
scope.discovered = true;
|
|
1516
|
+
const origin = scope.origin;
|
|
1517
|
+
for (const name of callersOf(scope.source)) {
|
|
1518
|
+
if (!origin.has(name)) {
|
|
1519
|
+
scope.callers.add(name);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
for (const name of scope.callers) {
|
|
1523
|
+
if (!origin.has(name) && LibraryToken.discover(scope, name)) {
|
|
1524
|
+
addOrigin(scope, name, inherit);
|
|
673
1525
|
}
|
|
674
|
-
return index.dependents.get(token) ?? [];
|
|
675
1526
|
}
|
|
676
1527
|
}
|
|
1528
|
+
/**
|
|
1529
|
+
* The token of an extension's source that resolves the value the
|
|
1530
|
+
* extension's token named `name` has as its ancestor's: the source's, or,
|
|
1531
|
+
* when that is its own ancestor's too, the nearest that isn't.
|
|
1532
|
+
*/
|
|
1533
|
+
const resolverOf = (scope, name) => {
|
|
1534
|
+
let source = scope.source;
|
|
1535
|
+
while (source.source !== undefined &&
|
|
1536
|
+
source.origin !== null &&
|
|
1537
|
+
source.queue.subscribed &&
|
|
1538
|
+
!source.origin.has(name)) {
|
|
1539
|
+
source = source.source;
|
|
1540
|
+
}
|
|
1541
|
+
return tokenNamed(source, name);
|
|
1542
|
+
};
|
|
1543
|
+
/**
|
|
1544
|
+
* Whether a value calls one of `recipes`, or with `null` any recipe, in plain
|
|
1545
|
+
* data or in the arguments of a computed value.
|
|
1546
|
+
*/
|
|
1547
|
+
const callsAny = (value, recipes, seen = new Set()) => {
|
|
1548
|
+
if (!isObject(value) || seen.has(value)) {
|
|
1549
|
+
return false;
|
|
1550
|
+
}
|
|
1551
|
+
seen.add(value);
|
|
1552
|
+
if (isComputed(value)) {
|
|
1553
|
+
const { recipe, args } = value[COMPUTED];
|
|
1554
|
+
return (recipes === null ||
|
|
1555
|
+
recipes.has(recipe) ||
|
|
1556
|
+
args.some((arg) => callsAny(arg, recipes, seen)));
|
|
1557
|
+
}
|
|
1558
|
+
return (isPlain(value) &&
|
|
1559
|
+
Reflect.ownKeys(value).some((key) => callsAny(Reflect.get(value, key), recipes, seen)));
|
|
1560
|
+
};
|
|
1561
|
+
/**
|
|
1562
|
+
* Resolves the tokens of a library that is first subscribed, so a change
|
|
1563
|
+
* is compared with the values they have: every token of a library made by
|
|
1564
|
+
* `create()`, and an extension's own tokens, which it finds then.
|
|
1565
|
+
*/
|
|
1566
|
+
function resolveOwn(scope) {
|
|
1567
|
+
if (scope.source === undefined) {
|
|
1568
|
+
forEachToken(scope.tokens, (_, descriptor) => LibraryToken.resolveForSubscriber(descriptor.get()));
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
const origin = originOf(scope);
|
|
1572
|
+
LibraryToken.dropAncestors(scope);
|
|
1573
|
+
for (const name of origin) {
|
|
1574
|
+
LibraryToken.resolveForSubscriber(tokenNamed(scope, name));
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Resolves the tokens of a subscribed extension that are its ancestor's,
|
|
1579
|
+
* for a subscriber told of them, which is told of a change to each from the
|
|
1580
|
+
* value it has then.
|
|
1581
|
+
*/
|
|
1582
|
+
function resolveAncestors(scope) {
|
|
1583
|
+
const origin = originOf(scope);
|
|
1584
|
+
completeGraphOf(scope);
|
|
1585
|
+
forEachToken(scope.tokens, (name, descriptor) => {
|
|
1586
|
+
if (!origin.has(name)) {
|
|
1587
|
+
LibraryToken.tryResolve(descriptor.get());
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* The names of the tokens of a library whose configured values call a
|
|
1593
|
+
* recipe.
|
|
1594
|
+
*/
|
|
1595
|
+
const callersOf = (scope) => {
|
|
1596
|
+
const { source, own, made } = scope;
|
|
1597
|
+
if (source === undefined) {
|
|
1598
|
+
const names = [];
|
|
1599
|
+
forEachToken(scope.tokens, (name, descriptor) => {
|
|
1600
|
+
if (callsAny(LibraryToken.configuredValue(descriptor.get()), null)) {
|
|
1601
|
+
names.push(name);
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
return names;
|
|
1605
|
+
}
|
|
1606
|
+
const names = callersOf(source).filter((name) => !own.has(name));
|
|
1607
|
+
for (const name of own) {
|
|
1608
|
+
if (callsAny(LibraryToken.configuredValue(made.get(name)), null)) {
|
|
1609
|
+
names.push(name);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return names;
|
|
1613
|
+
};
|
|
1614
|
+
/**
|
|
1615
|
+
* The tokens of an extension that are its own, found the first time they're
|
|
1616
|
+
* needed: those it gives a value, and those that recipes' results make its
|
|
1617
|
+
* own, which {@link discoverAll} finds once it can have any.
|
|
1618
|
+
*/
|
|
1619
|
+
function originOf(scope) {
|
|
1620
|
+
let { origin } = scope;
|
|
1621
|
+
if (origin === null) {
|
|
1622
|
+
origin = scope.origin = new Set();
|
|
1623
|
+
for (const name of scope.own) {
|
|
1624
|
+
addOrigin(scope, name, false);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
// With no token of its own and no recipe overridden, none can be, until
|
|
1628
|
+
// it has one.
|
|
1629
|
+
if (!scope.discovered && (origin.size > 0 || scope.overridden.size > 0)) {
|
|
1630
|
+
discoverAll(scope, false);
|
|
1631
|
+
}
|
|
1632
|
+
return origin;
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* A change of a batch.
|
|
1636
|
+
*/
|
|
1637
|
+
const changeOf = (token, valueChanged, origin, originChanged) => Object.freeze({ token, valueChanged, origin, originChanged });
|
|
1638
|
+
/**
|
|
1639
|
+
* A subscriber of an extension's batches that's told only of the changes to
|
|
1640
|
+
* its own tokens.
|
|
1641
|
+
*/
|
|
1642
|
+
const changesFrom = (origin, subscriber) => ({
|
|
1643
|
+
onChange(changes) {
|
|
1644
|
+
const told = changes.filter((change) => change.origin === origin);
|
|
1645
|
+
if (told.length > 0) {
|
|
1646
|
+
subscriber.onChange(told.length === changes.length ? changes : Object.freeze(told));
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1649
|
+
});
|
|
677
1650
|
const libraryTokenOf = (token) => {
|
|
678
1651
|
if (!(token instanceof LibraryToken)) {
|
|
679
1652
|
throw new Error(`"${token.name}" is not a token of a library.`);
|
|
@@ -784,14 +1757,12 @@ function* walkEntries(node, path, options) {
|
|
|
784
1757
|
*
|
|
785
1758
|
* This reads the configured values and not the values, so it resolves no
|
|
786
1759
|
* token, and so it is known of a library whose tokens have not been read. It
|
|
787
|
-
* is found for the whole library once
|
|
788
|
-
*
|
|
1760
|
+
* is found for the whole library once, kept as tokens are set, and shared
|
|
1761
|
+
* with the libraries that extend it, so each call after the first is a
|
|
1762
|
+
* lookup, and an extension makes only the tokens it lists. A value a recipe
|
|
789
1763
|
* reads other than through its arguments is not seen, as for
|
|
790
1764
|
* {@link dependenciesOf}.
|
|
791
1765
|
*
|
|
792
|
-
* It throws when a ref in a token of the library isn't a token of it, as
|
|
793
|
-
* reading the token's `$value` does.
|
|
794
|
-
*
|
|
795
1766
|
* @example
|
|
796
1767
|
* ```ts
|
|
797
1768
|
* // hover: { $value: Recipes.darken(theme.colors.primary, 0.2) }
|
|
@@ -819,4 +1790,31 @@ export const dependentsOf = (token) =>
|
|
|
819
1790
|
Object.freeze([
|
|
820
1791
|
...LibraryToken.dependents(libraryTokenOf(token)),
|
|
821
1792
|
]);
|
|
1793
|
+
/**
|
|
1794
|
+
* The tokens of a library whose values are its own, in the order of the
|
|
1795
|
+
* library: every token of a library made by `create()`, and an extension's
|
|
1796
|
+
* tokens that don't come from its ancestors.
|
|
1797
|
+
*
|
|
1798
|
+
* @remarks
|
|
1799
|
+
* An extension's token is its own when it has a value of its own (one its
|
|
1800
|
+
* config gives, one that is set, or a token new in it), when it calls a
|
|
1801
|
+
* recipe the extension overrides, or when a token it depends on is its own.
|
|
1802
|
+
* A token depends on the tokens its configured value refers to, and on
|
|
1803
|
+
* those a ref in a recipe's result is to, so tokens of its ancestors that
|
|
1804
|
+
* call recipes may be resolved to find them.
|
|
1805
|
+
*
|
|
1806
|
+
* It's what a first render of an extension writes, such as with
|
|
1807
|
+
* {@link toCSS}'s `filter`.
|
|
1808
|
+
*
|
|
1809
|
+
* @example
|
|
1810
|
+
* ```ts
|
|
1811
|
+
* const own = new Set(ownTokensOf(extension));
|
|
1812
|
+
* toCSS(extension, { filter: (token) => own.has(token) });
|
|
1813
|
+
* ```
|
|
1814
|
+
*
|
|
1815
|
+
* @public
|
|
1816
|
+
*/
|
|
1817
|
+
export const ownTokensOf = (library) =>
|
|
1818
|
+
// The tokens of a library of shape `R`.
|
|
1819
|
+
Object.freeze(LibraryImpl.ownTokens(library));
|
|
822
1820
|
//# sourceMappingURL=library.js.map
|