@yahoo/uds-create-config 4.34.3 → 4.34.4

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.
@@ -2,6 +2,7 @@ import { isRef, kindOf, pathOf, ref, refSchema, sourceOf, splitRef } from "../..
2
2
  import { defineEntity } from "../../framework/defineEntity.js";
3
3
  import { cssValueIssue, isCssProperty } from "../../css/values.js";
4
4
  import { camelCase } from "./style-bag.js";
5
+ import { createSliceMemo } from "../../framework/memo.js";
5
6
  import { isIdentifier } from "../../framework/utils/identifier.js";
6
7
  import { z } from "zod";
7
8
  //#region src/entities/system/StyleProperty.ts
@@ -289,11 +290,9 @@ function tokenRefOf(entry) {
289
290
  * `spacing/2` — since that is one value with two spellings, not two values.
290
291
  */
291
292
  const copiesNoToken = (config) => (ctx) => {
292
- const tokens = /* @__PURE__ */ new Map();
293
- for (const token of config.list("token")) {
294
- const value = comparable(token.body.value);
295
- if (value !== void 0 && !tokens.has(value)) tokens.set(value, token.path);
296
- }
293
+ const literal = (entry) => typeof entry === "object" && entry !== null && "value" in entry;
294
+ if (!ctx.value.some(literal)) return;
295
+ const tokens = tokenPathsByValue(config);
297
296
  ctx.value.forEach((entry, index) => {
298
297
  if (typeof entry !== "object" || entry === null || !("value" in entry)) return;
299
298
  const value = comparable(entry.value);
@@ -308,6 +307,19 @@ const copiesNoToken = (config) => (ctx) => {
308
307
  });
309
308
  });
310
309
  };
310
+ /** Each token value, as `comparable` spells it, and the first token path that holds it. Kept per token
311
+ * slice, since a batch of style-property writes would otherwise list every token once per write. */
312
+ const tokenPathsByValue = createSliceMemo({
313
+ kinds: ["token"],
314
+ compute: (config) => {
315
+ const tokens = /* @__PURE__ */ new Map();
316
+ for (const token of config.list("token")) {
317
+ const value = comparable(token.body.value);
318
+ if (value !== void 0 && !tokens.has(value)) tokens.set(value, token.path);
319
+ }
320
+ return tokens;
321
+ }
322
+ });
311
323
  /** A check refusing a ref to a token or group nothing declares: it offers no values, and the prop
312
324
  * paints nothing. */
313
325
  const namesDeclaredTokens = (config) => (ctx) => {
@@ -1301,9 +1301,9 @@ var Config = class Config {
1301
1301
  }
1302
1302
  }
1303
1303
  const land = (checked) => {
1304
- let config = this;
1304
+ let config = checked ? this : this.derive({ snapshot: this.state.snapshot.drafting() });
1305
1305
  for (const patch of patches) config = config.derive({ snapshot: config.computeApply(config.state.snapshot, patch, "write", checked) });
1306
- return config;
1306
+ return checked ? config : config.derive({ snapshot: config.state.snapshot.sealed() });
1307
1307
  };
1308
1308
  const settle = () => {
1309
1309
  try {
@@ -56,16 +56,22 @@ interface RefIndexChange {
56
56
  }
57
57
  /** Index `entries` in both directions. Pure — callers memoize (see `Snapshot.refs`). */
58
58
  declare function buildRefIndex(entries: readonly RefGraphEntry[], linkKinds?: readonly string[], singletonKinds?: readonly string[]): RefIndex;
59
+ /** The maps and sets a batch of snapshot writes has copied, which its later writes change in place. */
60
+ interface SnapshotDraft {
61
+ readonly owned: WeakSet<object>;
62
+ }
59
63
  /**
60
64
  * Re-index only the entities in `changes`, returning a new index that shares nothing mutable with
61
- * the old one. The top-level maps are copied per CALL (a batch), and only the edge sets actually
62
- * touched are copied — so the deep `collectRefs` walk, which is the expensive part, runs for the
63
- * changed entities alone rather than for the whole config.
65
+ * the old one. The top-level maps and each edge set a change touches are copied once, so the deep
66
+ * `collectRefs` walk runs for the changed entities alone rather than for the whole config. Once means
67
+ * once per call, or once per batch inside a `draft`, which then writes its own copies in place.
68
+ * Changes whose entities kept exactly the refs they had hand back the same index, since copying the
69
+ * top-level maps costs a walk of every node in the config.
64
70
  */
65
- declare function updateRefIndex(index: RefIndex, changes: readonly RefIndexChange[], linkKinds?: readonly string[], singletonKinds?: readonly string[]): RefIndex;
71
+ declare function updateRefIndex(index: RefIndex, changes: readonly RefIndexChange[], linkKinds?: readonly string[], singletonKinds?: readonly string[], draft?: SnapshotDraft): RefIndex;
66
72
  /** The query facade over a built index — the shape every consumer asks questions through. */
67
73
  declare function refGraphOf(index: RefIndex): RefGraph;
68
74
  /** Build and query in one step — for a caller with no snapshot to carry the index on. */
69
75
  declare function buildRefGraph(entries: readonly RefGraphEntry[]): RefGraph;
70
76
  //#endregion
71
- export { RefGraph, RefGraphEntry, RefIndex, RefIndexChange, RefTarget, TraverseOptions, buildRefGraph, buildRefIndex, refGraphOf, updateRefIndex };
77
+ export { RefGraph, RefGraphEntry, RefIndex, RefIndexChange, RefTarget, SnapshotDraft, TraverseOptions, buildRefGraph, buildRefIndex, refGraphOf, updateRefIndex };
@@ -59,32 +59,47 @@ function buildRefIndex(entries, linkKinds = [], singletonKinds = []) {
59
59
  }
60
60
  /**
61
61
  * Re-index only the entities in `changes`, returning a new index that shares nothing mutable with
62
- * the old one. The top-level maps are copied per CALL (a batch), and only the edge sets actually
63
- * touched are copied — so the deep `collectRefs` walk, which is the expensive part, runs for the
64
- * changed entities alone rather than for the whole config.
62
+ * the old one. The top-level maps and each edge set a change touches are copied once, so the deep
63
+ * `collectRefs` walk runs for the changed entities alone rather than for the whole config. Once means
64
+ * once per call, or once per batch inside a `draft`, which then writes its own copies in place.
65
+ * Changes whose entities kept exactly the refs they had hand back the same index, since copying the
66
+ * top-level maps costs a walk of every node in the config.
65
67
  */
66
- function updateRefIndex(index, changes, linkKinds = [], singletonKinds = []) {
68
+ function updateRefIndex(index, changes, linkKinds = [], singletonKinds = [], draft) {
67
69
  if (changes.length === 0) return index;
68
- const out = /* @__PURE__ */ new Map();
69
- for (const [node, set] of index.out) out.set(node, set);
70
- const incoming = /* @__PURE__ */ new Map();
71
- for (const [node, set] of index.incoming) incoming.set(node, set);
72
- /** Copy-on-write a single edge set, so sets shared with the previous index are never mutated. */
70
+ const next = changes.map(({ ref, data, extra }) => ({
71
+ ref,
72
+ targets: data === void 0 ? void 0 : edgesOf(ref, data, extra, linkKinds, singletonKinds)
73
+ }));
74
+ if (next.every(({ ref, targets }) => sameEdges(index.out.get(ref), targets))) return index;
75
+ const owned = draft?.owned ?? /* @__PURE__ */ new WeakSet();
76
+ const writable = (map) => {
77
+ if (owned.has(map)) return map;
78
+ const copy = /* @__PURE__ */ new Map();
79
+ for (const [node, set] of map) copy.set(node, set);
80
+ owned.add(copy);
81
+ return copy;
82
+ };
83
+ const out = writable(index.out);
84
+ const incoming = writable(index.incoming);
85
+ /** Copy-on-write a single edge set, so sets shared with an earlier index are never mutated. A target
86
+ * many changes touch would otherwise be copied once per change. */
73
87
  const mutable = (map, node) => {
74
- const next = new Set(map.get(node));
88
+ const current = map.get(node);
89
+ if (current && owned.has(current)) return current;
90
+ const next = new Set(current);
91
+ owned.add(next);
75
92
  map.set(node, next);
76
93
  return next;
77
94
  };
78
- for (const { ref, data, extra } of changes) {
79
- for (const target of index.out.get(ref) ?? []) {
95
+ for (const { ref, targets } of next) {
96
+ for (const target of out.get(ref) ?? []) {
80
97
  const back = mutable(incoming, target);
81
98
  back.delete(ref);
82
99
  if (back.size === 0) incoming.delete(target);
83
100
  }
84
101
  out.delete(ref);
85
- if (data === void 0) continue;
86
- const targets = edgesOf(ref, data, extra, linkKinds, singletonKinds);
87
- if (targets.size === 0) continue;
102
+ if (!targets || targets.size === 0) continue;
88
103
  out.set(ref, targets);
89
104
  for (const target of targets) mutable(incoming, target).add(ref);
90
105
  }
@@ -93,6 +108,14 @@ function updateRefIndex(index, changes, linkKinds = [], singletonKinds = []) {
93
108
  incoming
94
109
  };
95
110
  }
111
+ /** Whether an entity's edges are the ones it had, where no entry and an empty set both mean none. */
112
+ function sameEdges(before, after) {
113
+ const size = before?.size ?? 0;
114
+ if (size !== (after?.size ?? 0)) return false;
115
+ if (size === 0) return true;
116
+ for (const target of before ?? []) if (!after?.has(target)) return false;
117
+ return true;
118
+ }
96
119
  /** The query facade over a built index — the shape every consumer asks questions through. */
97
120
  function refGraphOf(index) {
98
121
  const { out, incoming } = index;
@@ -1,5 +1,5 @@
1
1
  import { Entity } from "./Entity.js";
2
- import { RefGraph } from "./ref-graph.js";
2
+ import { RefGraph, SnapshotDraft } from "./ref-graph.js";
3
3
  import { EntityKind } from "./registry.js";
4
4
 
5
5
  //#region src/framework/snapshot.d.ts
@@ -44,6 +44,18 @@ type EntityChange = {
44
44
  declare class Snapshot {
45
45
  #private;
46
46
  private constructor();
47
+ /**
48
+ * The same snapshot, starting a batch of writes. A store or index map is copied the first time the
49
+ * batch writes it and written in place after that, so a batch of N writes copies each once instead
50
+ * of N times. Every write still returns a new snapshot, with a new slice for each kind it wrote.
51
+ *
52
+ * The batch owns its copies, so the snapshots it produced share them: reading one written earlier
53
+ * in the batch shows the batch's later writes. The caller keeps only the last and {@link sealed}s it.
54
+ * Nothing from before the batch is ever written.
55
+ */
56
+ drafting(): Snapshot;
57
+ /** The same snapshot with its batch closed, so a later write copies again. */
58
+ sealed(): Snapshot;
47
59
  /**
48
60
  * The same snapshot, reading name-borne edges through `readers` from here on.
49
61
  *
@@ -17,14 +17,36 @@ const emptySlice = () => Object.freeze({
17
17
  groups: /* @__PURE__ */ new Map(),
18
18
  overrides: /* @__PURE__ */ new Map()
19
19
  });
20
- /** Apply one change to a mutable draft of a slice. */
21
- function applyToSlice(draft, change) {
22
- let store;
23
- if (change.target === "item") store = draft.items;
24
- else if (change.target === "group") store = draft.groups;
25
- else store = draft.overrides;
26
- if (change.value === void 0) store.delete(change.path);
27
- else store.set(change.path, change.value);
20
+ const STORE_OF = {
21
+ item: "items",
22
+ group: "groups",
23
+ override: "overrides"
24
+ };
25
+ /**
26
+ * One kind's slice with `changes` applied. Only the stores a change touches are replaced. Outside a
27
+ * draft each is copied once per call; inside one, a store the draft already copied is written in place,
28
+ * so a batch copies each store once rather than once per patch.
29
+ */
30
+ function changedSlice({ current, changes, draft }) {
31
+ const stores = {
32
+ items: current.items,
33
+ groups: current.groups,
34
+ overrides: current.overrides
35
+ };
36
+ const written = /* @__PURE__ */ new Set();
37
+ for (const change of changes) {
38
+ const name = STORE_OF[change.target];
39
+ if (!written.has(name) && !draft?.owned.has(stores[name])) {
40
+ const copy = new Map(stores[name]);
41
+ draft?.owned.add(copy);
42
+ stores[name] = copy;
43
+ }
44
+ written.add(name);
45
+ const store = stores[name];
46
+ if (change.value === void 0) store.delete(change.path);
47
+ else store.set(change.path, change.value);
48
+ }
49
+ return Object.freeze({ ...stores });
28
50
  }
29
51
  var Snapshot = class Snapshot {
30
52
  #nameEdges;
@@ -41,14 +63,36 @@ var Snapshot = class Snapshot {
41
63
  * independent configs — a global singleton would make two unrelated configs that both lack a kind
42
64
  * collide on one memo key, and each would serve the other's cached result. */
43
65
  #empties;
66
+ /** The batch this snapshot was written in, when it was — see {@link drafting}. */
67
+ #draft;
44
68
  #graph;
45
- constructor(slices, refIndex, empties, nameEdges = /* @__PURE__ */ new Map(), linkKinds = [], singletonKinds = []) {
69
+ constructor(slices, refIndex, empties, nameEdges = /* @__PURE__ */ new Map(), linkKinds = [], singletonKinds = [], draft = void 0) {
46
70
  this.#slices = slices;
47
71
  this.#refIndex = refIndex;
48
72
  this.#empties = empties;
49
73
  this.#nameEdges = nameEdges;
50
74
  this.#linkKinds = linkKinds;
51
75
  this.#singletonKinds = singletonKinds;
76
+ this.#draft = draft;
77
+ }
78
+ /**
79
+ * The same snapshot, starting a batch of writes. A store or index map is copied the first time the
80
+ * batch writes it and written in place after that, so a batch of N writes copies each once instead
81
+ * of N times. Every write still returns a new snapshot, with a new slice for each kind it wrote.
82
+ *
83
+ * The batch owns its copies, so the snapshots it produced share them: reading one written earlier
84
+ * in the batch shows the batch's later writes. The caller keeps only the last and {@link sealed}s it.
85
+ * Nothing from before the batch is ever written.
86
+ */
87
+ drafting() {
88
+ return this.#with({ draft: { owned: /* @__PURE__ */ new WeakSet() } });
89
+ }
90
+ /** The same snapshot with its batch closed, so a later write copies again. */
91
+ sealed() {
92
+ return this.#draft ? this.#with({ draft: void 0 }) : this;
93
+ }
94
+ #with({ draft }) {
95
+ return new Snapshot(this.#slices, this.#refIndex, this.#empties, this.#nameEdges, this.#linkKinds, this.#singletonKinds, draft);
52
96
  }
53
97
  /**
54
98
  * The same snapshot, reading name-borne edges through `readers` from here on.
@@ -145,17 +189,12 @@ var Snapshot = class Snapshot {
145
189
  else byKind.set(change.kind, [change]);
146
190
  }
147
191
  const slices = new Map(this.#slices);
148
- for (const [kind, kindChanges] of byKind) {
149
- const current = this.slice(kind);
150
- const draft = {
151
- items: new Map(current.items),
152
- groups: new Map(current.groups),
153
- overrides: new Map(current.overrides)
154
- };
155
- for (const change of kindChanges) applyToSlice(draft, change);
156
- slices.set(kind, Object.freeze(draft));
157
- }
158
- return new Snapshot(slices, updateRefIndex(this.#refIndex, refChanges(changes, this.#nameEdges), this.#linkKinds, this.#singletonKinds), this.#empties, this.#nameEdges, this.#linkKinds, this.#singletonKinds);
192
+ for (const [kind, kindChanges] of byKind) slices.set(kind, changedSlice({
193
+ current: this.slice(kind),
194
+ changes: kindChanges,
195
+ draft: this.#draft
196
+ }));
197
+ return new Snapshot(slices, updateRefIndex(this.#refIndex, refChanges(changes, this.#nameEdges), this.#linkKinds, this.#singletonKinds, this.#draft), this.#empties, this.#nameEdges, this.#linkKinds, this.#singletonKinds, this.#draft);
159
198
  }
160
199
  /**
161
200
  * Move an entity to a new path, KEEPING ITS POSITION in the record.
@@ -176,6 +215,7 @@ var Snapshot = class Snapshot {
176
215
  const rebuilt = /* @__PURE__ */ new Map();
177
216
  for (const [path, entity] of store) if (path === from) rebuilt.set(to, value);
178
217
  else if (path !== to) rebuilt.set(path, entity);
218
+ this.#draft?.owned.add(rebuilt);
179
219
  const slices = new Map(this.#slices);
180
220
  slices.set(kind, Object.freeze({
181
221
  ...current,
@@ -188,7 +228,7 @@ var Snapshot = class Snapshot {
188
228
  ref: `${kind}:${to}`,
189
229
  data: value.toJSON(),
190
230
  extra: this.#nameEdges.get(kind)?.(value.toJSON())
191
- }], this.#linkKinds, this.#singletonKinds), this.#empties, this.#nameEdges, this.#linkKinds, this.#singletonKinds);
231
+ }], this.#linkKinds, this.#singletonKinds, this.#draft), this.#empties, this.#nameEdges, this.#linkKinds, this.#singletonKinds, this.#draft);
192
232
  }
193
233
  /**
194
234
  * Rearrange a record's keys — the operation that MOVES something, since order is key order.