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.
Files changed (56) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/lib/css-reflector.d.ts +32 -24
  3. package/dist/lib/css-reflector.js +10 -26
  4. package/dist/lib/css-reflector.js.map +1 -1
  5. package/dist/lib/equality.d.ts +16 -0
  6. package/dist/lib/equality.js +90 -0
  7. package/dist/lib/equality.js.map +1 -0
  8. package/dist/lib/graph.d.ts +58 -0
  9. package/dist/lib/graph.js +104 -0
  10. package/dist/lib/graph.js.map +1 -0
  11. package/dist/lib/index.d.ts +4 -2
  12. package/dist/lib/index.js +2 -1
  13. package/dist/lib/index.js.map +1 -1
  14. package/dist/lib/library.d.ts +221 -10
  15. package/dist/lib/library.js +1256 -258
  16. package/dist/lib/library.js.map +1 -1
  17. package/dist/lib/notifier.d.ts +0 -15
  18. package/dist/lib/notifier.js +0 -23
  19. package/dist/lib/notifier.js.map +1 -1
  20. package/dist/lib/parse-document.d.ts +21 -0
  21. package/dist/lib/parse-document.js +31 -0
  22. package/dist/lib/parse-document.js.map +1 -0
  23. package/dist/lib/parse-expression.d.ts +45 -0
  24. package/dist/lib/parse-expression.js +148 -0
  25. package/dist/lib/parse-expression.js.map +1 -0
  26. package/dist/lib/parse-extension.d.ts +24 -0
  27. package/dist/lib/parse-extension.js +68 -0
  28. package/dist/lib/parse-extension.js.map +1 -0
  29. package/dist/lib/parse-resolve.d.ts +16 -0
  30. package/dist/lib/parse-resolve.js +281 -0
  31. package/dist/lib/parse-resolve.js.map +1 -0
  32. package/dist/lib/parse-value.d.ts +35 -0
  33. package/dist/lib/parse-value.js +330 -0
  34. package/dist/lib/parse-value.js.map +1 -0
  35. package/dist/lib/parse.d.ts +103 -0
  36. package/dist/lib/parse.js +562 -0
  37. package/dist/lib/parse.js.map +1 -0
  38. package/dist/lib/queue.d.ts +13 -2
  39. package/dist/lib/queue.js +18 -2
  40. package/dist/lib/queue.js.map +1 -1
  41. package/dist/lib/ref.d.ts +18 -1
  42. package/dist/lib/ref.js +22 -4
  43. package/dist/lib/ref.js.map +1 -1
  44. package/dist/lib/standard-values.d.ts +50 -0
  45. package/dist/lib/standard-values.js +73 -0
  46. package/dist/lib/standard-values.js.map +1 -0
  47. package/dist/lib/tsconfig.tsbuildinfo +1 -1
  48. package/dist/lib/utilities.d.ts +6 -0
  49. package/dist/lib/utilities.js +7 -1
  50. package/dist/lib/utilities.js.map +1 -1
  51. package/package.json +9 -5
  52. package/dist/lib/public.d.ts +0 -1405
  53. package/dist/lib/tsdoc-metadata.json +0 -11
  54. package/dist/lib/watcher.d.ts +0 -12
  55. package/dist/lib/watcher.js +0 -25
  56. package/dist/lib/watcher.js.map +0 -1
@@ -1,9 +1,9 @@
1
1
  import { COMPUTED, RECIPE, isComputed } from "./recipe.js";
2
2
  import { REF, isRef } from "./ref.js";
3
- import { getNotifier } from "./notifier.js";
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
- * The changes to the configured values in a library's tokens: its own, and
40
- * those of the libraries it extends, whose values it takes until its tokens
41
- * are set.
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
- const configurationVersionOf = (scope) => {
44
- let version = 0;
45
- for (let s = scope; s; s = s.source) {
46
- version += s.configurationVersion;
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
- * For a library's tokens, the tokens whose configured values refer to each,
52
- * and the configuration it was made from.
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 dependentsIndexes = new WeakMap();
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
- const isObject = (value) => {
60
- return typeof value === "object" && value !== null;
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, queue) => {
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
- return new LibraryToken(name, $value, resolvedType, scope, $description || "", $extensions || {}, $deprecated ?? false, queue);
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, queue) => {
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, queue);
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, queue);
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, queue, inherited) => {
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, queue, inherited);
436
+ recurseExtend(_name, source.value, group, configValue || {}, scope, typeContext);
174
437
  }
175
438
  else {
176
- recurseCreate(_name, group, configValue, scope, typeContext, queue);
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(() => untracked(() => sourceTokens[key]), scope, queue, configValue);
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, queue);
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 and follows its
220
- * changes, through a link on the source token that holds it weakly: the
221
- * library it extends doesn't keep an extension alive that nothing else
222
- * refers to. It follows once it is made, or once its extension has a
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
- unlink = null;
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.queue, this.node, this);
485
+ const token = extendToken(this.source(), this.scope, this.node);
246
486
  this.token = token;
247
- this.follow();
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, queue, node, inherited) {
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.scope = scope;
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.detach();
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
- extendingToken.raw = extendingToken.raw;
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
- return tokenAt(scope.tokens, value[REF]).$value;
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
- return walk(fn(...args.map(walk)));
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
- // A subscriber is told of a change to a token of the source that the
445
- // extension hasn't read.
446
- if (this.inherited !== null) {
447
- for (const token of this.inherited) {
448
- token.follow();
449
- }
450
- this.inherited = null;
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.queue.unsubscribe(subscriber);
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 scope = {
465
- tokens: {},
466
- recipes,
467
- configurationVersion: 0,
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 queue = new Queue();
477
- const scope = {
478
- tokens: {},
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
- * While resolving, the subscriptions from the previous resolution. Sources
503
- * watched again keep their subscription instead of re-subscribing.
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
- previousSubscriptions = null;
801
+ state;
506
802
  _extensions;
507
- constructor(name, value, _type, scope, _description, extensions, _deprecated, queue) {
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
- if (this.cached !== empty) {
535
- return this.cached;
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.resolving) {
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
- throw new CircularReferenceError([...chain, this.name]);
871
+ const error = new CircularReferenceError([...chain, this.name]);
872
+ recordThrow(error, this.name, "resolve");
873
+ throw error;
542
874
  }
543
- const previous = this.subscriptions;
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
- const value = resolve(this.raw, this.scope);
552
- this.cached = value;
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
- stopWatching();
559
- this.previousSubscriptions = null;
560
- // Unsubscribe from sources that were not watched this time.
561
- for (const notifier of previous) {
562
- if (!this.subscriptions.has(notifier)) {
563
- notifier.unsubscribe(this);
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
- this.scope.configurationVersion++;
584
- this.onChange();
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
- onChange() {
587
- // A circular reference that threw while resolving still leaves the
588
- // tokens subscribed to each other, so don't notify re-entrantly.
589
- if (this.notifying) {
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
- this.notifying = true;
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
- this.queue.add(this);
595
- this.cached = empty;
596
- getNotifier(this).notify();
1185
+ token.$value;
597
1186
  }
598
- finally {
599
- this.notifying = false;
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
- watch(source) {
603
- const notifier = getNotifier(source);
604
- if (this.subscriptions.has(notifier)) {
1265
+ setFound(names) {
1266
+ if (this.state.found === null && names === null) {
605
1267
  return;
606
1268
  }
607
- this.subscriptions.add(notifier);
608
- if (!this.previousSubscriptions?.has(notifier)) {
609
- notifier.subscribe(this);
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, searched as {@link resolve} resolves them: in plain
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
- const seen = new Set();
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
- * The index is made from the library's tokens once, and again after a
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 version = configurationVersionOf(token.scope);
654
- let index = dependentsIndexes.get(token.scope);
655
- if (index?.version !== version) {
656
- const dependents = new Map();
657
- for (const entry of walkEntries(token.scope.tokens, [], {
658
- whatToShow: "token",
659
- })) {
660
- const dependent = libraryTokenOf(entry.token);
661
- for (const dependency of LibraryToken.dependencies(dependent)) {
662
- const list = dependents.get(dependency);
663
- if (list) {
664
- list.push(dependent);
665
- }
666
- else {
667
- dependents.set(dependency, [dependent]);
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
- index = { version, dependents };
672
- dependentsIndexes.set(token.scope, index);
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 and kept until a token is set to a
788
- * different value, so each call after the first is a lookup. A value a recipe
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