j-templates 8.0.13 → 8.0.15

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 (49) hide show
  1. package/DOM/createAssignment.js +2 -6
  2. package/DOM/createAttributeAssignment.js +2 -6
  3. package/DOM/createEventAssignment.js +2 -6
  4. package/DOM/createPropertyAssignment.js +10 -14
  5. package/DOM/domNodeConfig.js +27 -30
  6. package/DOM/elements.js +102 -108
  7. package/DOM/index.js +3 -19
  8. package/Node/component.js +12 -16
  9. package/Node/component.types.js +1 -2
  10. package/Node/nodeConfig.js +2 -5
  11. package/Node/vNode.js +64 -67
  12. package/Node/vNode.types.js +5 -9
  13. package/Store/Diff/defaultDiffTreeWorker.d.ts +1 -0
  14. package/Store/Diff/defaultDiffTreeWorker.js +4 -0
  15. package/Store/Diff/diffAsync.d.ts +2 -4
  16. package/Store/Diff/diffAsync.js +4 -33
  17. package/Store/Diff/diffSync.js +3 -10
  18. package/Store/Diff/diffTree.d.ts +66 -41
  19. package/Store/Diff/diffTree.js +374 -426
  20. package/Store/Diff/diffTreeWorker.d.ts +8 -0
  21. package/Store/Diff/diffTreeWorker.js +27 -0
  22. package/Store/Diff/workerQueue.js +6 -10
  23. package/Store/Store/store.js +10 -14
  24. package/Store/Store/storeAsync.d.ts +1 -2
  25. package/Store/Store/storeAsync.js +10 -14
  26. package/Store/Store/storeSync.js +10 -14
  27. package/Store/Tree/observableNode.js +35 -38
  28. package/Store/Tree/observableScope.js +35 -42
  29. package/Store/index.js +5 -13
  30. package/Utils/animation.js +7 -11
  31. package/Utils/array.js +2 -6
  32. package/Utils/asyncQueue.js +7 -11
  33. package/Utils/decorators.js +44 -53
  34. package/Utils/emitter.js +6 -9
  35. package/Utils/functions.js +1 -4
  36. package/Utils/index.js +3 -21
  37. package/Utils/injector.js +2 -6
  38. package/Utils/json.d.ts +32 -10
  39. package/Utils/json.js +195 -208
  40. package/Utils/list.js +2 -5
  41. package/Utils/scheduling.js +4 -7
  42. package/Utils/thread.js +18 -26
  43. package/Utils/utils.types.js +1 -2
  44. package/_not_used/array.js +4 -10
  45. package/_not_used/avlTree.js +2 -5
  46. package/index.js +2 -10
  47. package/package.json +1 -1
  48. package/Store/Diff/diffWorker.d.ts +0 -3
  49. package/Store/Diff/diffWorker.js +0 -21
@@ -1,458 +1,406 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PROJECTION_PREFIX = void 0;
4
- exports.DiffTreeFactory = DiffTreeFactory;
1
+ import { ArraysEqual } from "../../Utils/array";
2
+ import { JsonDeepClone, JsonDiff, JsonType } from "../../Utils/json";
5
3
  /** Reserved key prefix a projection's result is stored under: `$projection_<id>`. */
6
- exports.PROJECTION_PREFIX = "$projection_";
4
+ export const PROJECTION_PREFIX = "$projection_";
7
5
  /**
8
- * Factory function that creates a DiffTree class.
9
- * Can operate in worker mode for async operations.
10
- * @param jsonDiffFactory - Optional factory for JSON diff utilities
11
- * @param worker - If true, sets up worker message handling
12
- * @returns DiffTree constructor
6
+ * Flattens nested objects/arrays, extracting keyed objects to root.
7
+ * @param root - Root object to store flattened values
8
+ * @param value - Value to flatten
9
+ * @param keyFunc - Function to extract key from objects
10
+ * @returns The root object with flattened values
13
11
  */
14
- function DiffTreeFactory(jsonDiffFactory, arraysEqual, worker) {
15
- const { JsonDiff, JsonType, JsonDeepClone } = jsonDiffFactory();
16
- /**
17
- * Reconstructs a `DiffTreeProjectionMap` from its wire form: each projection's function
18
- * source, `eval`'d back into a function, the same mechanic used to deserialize `keyFunc`.
19
- */
20
- function DeserializeProjections(serialized) {
21
- const projections = {};
22
- const ids = Object.keys(serialized);
23
- for (let x = 0; x < ids.length; x++) {
24
- const id = ids[x];
25
- projections[id] = {
26
- reads: serialized[id].reads,
27
- projection: eval(serialized[id].projection),
28
- };
29
- }
30
- return projections;
31
- }
32
- const ctx = this;
33
- if (worker && ctx) {
34
- let diffTree = null;
35
- ctx.onmessage = function (event) {
36
- const data = event.data;
37
- switch (data.method) {
38
- case "create": {
39
- const keyFunc = data.arguments[0]
40
- ? eval(data.arguments[0])
41
- : undefined;
42
- const projections = data.arguments[1]
43
- ? DeserializeProjections(data.arguments[1])
44
- : undefined;
45
- diffTree = new DiffTree(keyFunc, projections);
46
- ctx.postMessage(null);
47
- break;
48
- }
49
- case "diffpath": {
50
- const diff = diffTree.DiffPath(data.arguments[0], data.arguments[1], data.arguments[2]);
51
- ctx.postMessage(diff);
52
- break;
53
- }
54
- case "splicepath": {
55
- const diff = diffTree.SplicePath(data.arguments[0], data.arguments[1], data.arguments[2], data.arguments[3], data.arguments[4]);
56
- ctx.postMessage(diff);
57
- break;
58
- }
59
- case "diffbatch": {
60
- const diff = diffTree.DiffBatch(data.arguments[0]);
61
- ctx.postMessage(diff);
62
- break;
63
- }
64
- case "getpath": {
65
- const ret = diffTree.GetPath(data.arguments[0]);
66
- ctx.postMessage(ret);
67
- break;
68
- }
69
- }
70
- };
12
+ function FlattenValue(root, value, keyFunc) {
13
+ const type = JsonType(value);
14
+ switch (type) {
15
+ case "array":
16
+ const typedArray = value;
17
+ for (let x = 0; x < typedArray.length; x++)
18
+ FlattenValue(root, typedArray[x], keyFunc);
19
+ break;
20
+ case "object":
21
+ const typedObject = value;
22
+ const key = keyFunc(typedObject);
23
+ if (key)
24
+ root[key] = typedObject;
25
+ const keys = Object.keys(typedObject);
26
+ for (let x = 0; x < keys.length; x++)
27
+ FlattenValue(root, typedObject[keys[x]], keyFunc);
71
28
  }
72
- /**
73
- * Flattens nested objects/arrays, extracting keyed objects to root.
74
- * @param root - Root object to store flattened values
75
- * @param value - Value to flatten
76
- * @param keyFunc - Function to extract key from objects
77
- * @returns The root object with flattened values
78
- */
79
- function FlattenValue(root, value, keyFunc) {
80
- const type = JsonType(value);
81
- switch (type) {
82
- case "array":
83
- const typedArray = value;
84
- for (let x = 0; x < typedArray.length; x++)
85
- FlattenValue(root, typedArray[x], keyFunc);
86
- break;
87
- case "object":
88
- const typedObject = value;
89
- const key = keyFunc(typedObject);
90
- if (key)
91
- root[key] = typedObject;
92
- const keys = Object.keys(typedObject);
93
- for (let x = 0; x < keys.length; x++)
94
- FlattenValue(root, typedObject[keys[x]], keyFunc);
95
- }
96
- return root;
29
+ return root;
30
+ }
31
+ /**
32
+ * Retrieves a value from a source object using a dot-separated path.
33
+ * @param source - The source object
34
+ * @param path - Dot-separated path to the value (empty string returns source)
35
+ * @returns The value at the specified path
36
+ */
37
+ function GetPathValue(source, path) {
38
+ if (path === "")
39
+ return [source, true];
40
+ const parts = path.split(".");
41
+ let curr = source;
42
+ let x = 0;
43
+ for (; curr && x < parts.length - 1; x++)
44
+ curr = curr[parts[x]];
45
+ if (curr && Object.hasOwn(curr, parts[x])) {
46
+ curr = curr[parts[x]];
47
+ x++;
97
48
  }
98
- /**
99
- * Retrieves a value from a source object using a dot-separated path.
100
- * @param source - The source object
101
- * @param path - Dot-separated path to the value (empty string returns source)
102
- * @returns The value at the specified path
103
- */
104
- function GetPathValue(source, path) {
105
- if (path === "")
106
- return [source, true];
107
- const parts = path.split(".");
108
- let curr = source;
109
- let x = 0;
110
- for (; curr && x < parts.length - 1; x++)
111
- curr = curr[parts[x]];
112
- if (curr && Object.hasOwn(curr, parts[x])) {
113
- curr = curr[parts[x]];
114
- x++;
115
- }
116
- else
117
- curr = undefined;
118
- return [curr, x === parts.length];
49
+ else
50
+ curr = undefined;
51
+ return [curr, x === parts.length];
52
+ }
53
+ /**
54
+ * Sets a value in a source object at the specified path.
55
+ * @param source - The source object
56
+ * @param path - Array of path segments (strings for keys, numbers for indices)
57
+ * @param value - The value to set
58
+ */
59
+ function SetPathValue(source, path, value) {
60
+ if (path.length === 0)
61
+ return;
62
+ let curr = source;
63
+ let x = 0;
64
+ for (; x < path.length - 1; x++)
65
+ curr = curr[path[x]];
66
+ curr[path[x]] = value;
67
+ InvalidateSnapshotForValue(curr);
68
+ }
69
+ const valueSnapshotCache = new WeakMap();
70
+ const valueSnapshotHierarchy = new WeakMap();
71
+ function CreateSnapshotLink(child, parent) {
72
+ let parents = valueSnapshotHierarchy.get(child);
73
+ if (!parents)
74
+ valueSnapshotHierarchy.set(child, (parents = new Set()));
75
+ parents.add(parent);
76
+ }
77
+ /**
78
+ * Clears the cached snapshot for a raw value that was just written to, and climbs to
79
+ * every parent whose own cached snapshot embedded it, so a write to a deeply nested value
80
+ * invalidates every cached ancestor that still holds a snapshot. Mirrors ObservableNode's
81
+ * `InvalidateSnapshotForValue`.
82
+ */
83
+ function InvalidateSnapshotForValue(value) {
84
+ valueSnapshotCache.delete(value);
85
+ const parents = valueSnapshotHierarchy.get(value);
86
+ if (parents === undefined)
87
+ return;
88
+ valueSnapshotHierarchy.delete(value);
89
+ for (const parent of parents)
90
+ if (valueSnapshotCache.has(parent))
91
+ InvalidateSnapshotForValue(parent);
92
+ }
93
+ /**
94
+ * An immutable snapshot of a raw tree value (arrays and plain objects; other values are
95
+ * returned as-is), cached per raw value and reused until a write invalidates it. Mirrors
96
+ * ObservableNode's `CreateSnapshot`: a rebuild makes new containers but reuses the cached
97
+ * snapshots of unchanged children. A keyed child is read through the root entity `keyFunc`
98
+ * resolves it to, the parallel of ObservableNode's `alias`, so every reference to the same
99
+ * entity shares one snapshot and a write to the entity invalidates every holder.
100
+ */
101
+ function CreateSnapshot(source, keyFunc, value) {
102
+ if (JsonType(value) === "value")
103
+ return value;
104
+ const cached = valueSnapshotCache.get(value);
105
+ if (cached !== undefined)
106
+ return cached;
107
+ let snapshot;
108
+ if (Array.isArray(value)) {
109
+ snapshot = new Array(value.length);
110
+ for (let x = 0; x < value.length; x++)
111
+ snapshot[x] = ChildSnapshot(source, keyFunc, value, value[x]);
119
112
  }
120
- /**
121
- * Sets a value in a source object at the specified path.
122
- * @param source - The source object
123
- * @param path - Array of path segments (strings for keys, numbers for indices)
124
- * @param value - The value to set
125
- */
126
- function SetPathValue(source, path, value) {
127
- if (path.length === 0)
128
- return;
129
- let curr = source;
130
- let x = 0;
131
- for (; x < path.length - 1; x++)
132
- curr = curr[path[x]];
133
- curr[path[x]] = value;
134
- InvalidateSnapshotForValue(curr);
113
+ else {
114
+ snapshot = {};
115
+ const keys = Object.keys(value);
116
+ for (let x = 0; x < keys.length; x++)
117
+ snapshot[keys[x]] = ChildSnapshot(source, keyFunc, value, value[keys[x]]);
135
118
  }
136
- const valueSnapshotCache = new WeakMap();
137
- const valueSnapshotHierarchy = new WeakMap();
138
- function CreateSnapshotLink(child, parent) {
139
- let parents = valueSnapshotHierarchy.get(child);
140
- if (!parents)
141
- valueSnapshotHierarchy.set(child, (parents = new Set()));
142
- parents.add(parent);
119
+ valueSnapshotCache.set(value, snapshot);
120
+ return snapshot;
121
+ }
122
+ /**
123
+ * A child's snapshot, linking the child to its parent so a write to the child invalidates
124
+ * the parent. A keyed child's snapshot comes from its root entity (`source[key]`), so the
125
+ * root is linked too; the parent's own embedded copy stays linked because a positional
126
+ * write (one that changes which entity a slot holds) mutates that copy in place.
127
+ */
128
+ function ChildSnapshot(source, keyFunc, parent, child) {
129
+ if (JsonType(child) === "value")
130
+ return child;
131
+ CreateSnapshotLink(child, parent);
132
+ if (keyFunc) {
133
+ const key = keyFunc(child);
134
+ if (key) {
135
+ const root = source[key];
136
+ if (root !== undefined && root !== child) {
137
+ CreateSnapshotLink(root, parent);
138
+ return CreateSnapshot(source, keyFunc, root);
139
+ }
140
+ }
143
141
  }
144
- /**
145
- * Clears the cached snapshot for a raw value that was just written to, and climbs to
146
- * every parent whose own cached snapshot embedded it, so a write to a deeply nested value
147
- * invalidates every cached ancestor that still holds a snapshot. Mirrors ObservableNode's
148
- * `InvalidateSnapshotForValue`.
149
- */
150
- function InvalidateSnapshotForValue(value) {
151
- valueSnapshotCache.delete(value);
152
- const parents = valueSnapshotHierarchy.get(value);
153
- if (parents === undefined)
142
+ return CreateSnapshot(source, keyFunc, child);
143
+ }
144
+ /**
145
+ * Topologically sorts projections by their declared `reads` into a single array, run
146
+ * sequentially: a read of another projection's `$projection_<id>` output is a dependency
147
+ * edge, so that projection is sorted earlier. Runs once, at construction, so a cycle (or a
148
+ * read of an id that isn't registered) is rejected immediately instead of on first write.
149
+ */
150
+ function BuildProjectionOrder(projections) {
151
+ const order = [];
152
+ const state = new Map();
153
+ function Visit(id, stack) {
154
+ if (state.get(id) === "done")
154
155
  return;
155
- valueSnapshotHierarchy.delete(value);
156
- for (const parent of parents)
157
- if (valueSnapshotCache.has(parent))
158
- InvalidateSnapshotForValue(parent);
156
+ if (state.get(id) === "visiting")
157
+ throw new Error(`DiffTree projection cycle detected: ${stack.concat(id).join(" -> ")}`);
158
+ const def = projections[id];
159
+ if (!def)
160
+ throw new Error(`DiffTree projection "${id}" is not defined`);
161
+ state.set(id, "visiting");
162
+ for (let x = 0; x < def.reads.length; x++) {
163
+ const read = def.reads[x];
164
+ const root = read.split(".", 1)[0];
165
+ if (root.endsWith("*") && x !== def.reads.length - 1)
166
+ throw `Wildcard reads only supported as the final read: ${root}`;
167
+ if (root.endsWith("*") && root.includes("."))
168
+ throw `Wildcard reads do not support dot nesting: ${root}`;
169
+ if (root.startsWith(PROJECTION_PREFIX) && read.endsWith("*"))
170
+ throw `Projection keys do not support wildcard reads: ${root}`;
171
+ if (root.startsWith(PROJECTION_PREFIX))
172
+ Visit(root.slice(PROJECTION_PREFIX.length), stack.concat(id));
173
+ }
174
+ state.set(id, "done");
175
+ order.push({ id, ...def });
159
176
  }
160
- /**
161
- * An immutable snapshot of a raw tree value (arrays and plain objects; other values are
162
- * returned as-is), cached per raw value and reused until a write invalidates it. Mirrors
163
- * ObservableNode's `CreateSnapshot`: a rebuild makes new containers but reuses the cached
164
- * snapshots of unchanged children. A keyed child is read through the root entity `keyFunc`
165
- * resolves it to, the parallel of ObservableNode's `alias`, so every reference to the same
166
- * entity shares one snapshot and a write to the entity invalidates every holder.
167
- */
168
- function CreateSnapshot(source, keyFunc, value) {
169
- if (JsonType(value) === "value")
170
- return value;
171
- const cached = valueSnapshotCache.get(value);
172
- if (cached !== undefined)
173
- return cached;
174
- let snapshot;
175
- if (Array.isArray(value)) {
176
- snapshot = new Array(value.length);
177
- for (let x = 0; x < value.length; x++)
178
- snapshot[x] = ChildSnapshot(source, keyFunc, value, value[x]);
177
+ const ids = Object.keys(projections);
178
+ for (let x = 0; x < ids.length; x++)
179
+ Visit(ids[x], []);
180
+ return order;
181
+ }
182
+ /**
183
+ * Runs every registered projection, sequentially in the dependency order
184
+ * `BuildProjectionOrder` computed, skipping ones whose declared `reads` all still resolve
185
+ * to the same cached snapshot as last time (so nothing they read has changed), and returns
186
+ * the diff of every projection whose result changed. A projection's result is stored at
187
+ * `$projection_<id>` the same way a `DiffPath` write is: diffed against what was there
188
+ * before and applied path by path, so a change to one field of a projection's result
189
+ * produces one small diff entry rather than replacing the whole thing.
190
+ *
191
+ * Because `order` runs dependencies before dependents, a projection that reads another's
192
+ * `$projection_<id>` output (chaining) always sees that projection's up-to-date result
193
+ * within the same pass.
194
+ *
195
+ * A projection whose `reads` names a root key that doesn't exist yet (a plain path never
196
+ * written, an entity never seen, or another projection that hasn't produced a result) is
197
+ * not run at all, so a projection's author never has to guard every parameter against
198
+ * missing data. It's tried again on every later write until every read exists. This also
199
+ * makes a blocked dependency propagate for free: a projection that never runs never writes
200
+ * `$projection_<id>`, so anything chaining off it stays blocked in turn.
201
+ *
202
+ * @param source - The root tree object
203
+ * @param keyFunc - Optional function to extract a key from objects, for reading through roots
204
+ * @param projections - Dependency-sorted projection entries, from `BuildProjectionOrder`
205
+ * @param memo - Per-tree, cross-call memo of each projection's last-read values, by id,
206
+ * positional with its `reads`
207
+ * @returns Diff entries for every projection whose result changed
208
+ */
209
+ function RunProjections(source, keyFunc, projections, memo) {
210
+ const diffResult = [];
211
+ for (let x = 0; x < projections.length; x++) {
212
+ const { id, reads, projection } = projections[x];
213
+ const values = [];
214
+ let allPathsExist = true;
215
+ let y = 0;
216
+ for (; allPathsExist && y < reads.length && !reads[y].endsWith("*"); y++) {
217
+ const read = reads[y];
218
+ const [value, found] = GetPathValue(source, read);
219
+ allPathsExist = found;
220
+ values[y] = (found && CreateSnapshot(source, keyFunc, value)) || value;
179
221
  }
180
- else {
181
- snapshot = {};
182
- const keys = Object.keys(value);
183
- for (let x = 0; x < keys.length; x++)
184
- snapshot[keys[x]] = ChildSnapshot(source, keyFunc, value, value[keys[x]]);
222
+ if (y < reads.length && reads[y].endsWith("*")) {
223
+ const prefix = reads[y].slice(0, reads[y].length - 1);
224
+ const keys = Object.keys(source).filter(key => key.startsWith(prefix));
225
+ for (let z = 0; z < keys.length; z++) {
226
+ const [value, found] = GetPathValue(source, keys[z]);
227
+ allPathsExist = allPathsExist && found;
228
+ values.push((found && CreateSnapshot(source, keyFunc, value)) || value);
229
+ }
185
230
  }
186
- valueSnapshotCache.set(value, snapshot);
187
- return snapshot;
188
- }
189
- /**
190
- * A child's snapshot, linking the child to its parent so a write to the child invalidates
191
- * the parent. A keyed child's snapshot comes from its root entity (`source[key]`), so the
192
- * root is linked too; the parent's own embedded copy stays linked because a positional
193
- * write (one that changes which entity a slot holds) mutates that copy in place.
194
- */
195
- function ChildSnapshot(source, keyFunc, parent, child) {
196
- if (JsonType(child) === "value")
197
- return child;
198
- CreateSnapshotLink(child, parent);
199
- if (keyFunc) {
200
- const key = keyFunc(child);
201
- if (key) {
202
- const root = source[key];
203
- if (root !== undefined && root !== child) {
204
- CreateSnapshotLink(root, parent);
205
- return CreateSnapshot(source, keyFunc, root);
206
- }
231
+ if (allPathsExist) {
232
+ const priorValues = memo.get(id);
233
+ if (!ArraysEqual(priorValues, values)) {
234
+ const result = projection(...values);
235
+ memo.set(id, values);
236
+ // The same diff-and-apply UpdateSource uses for any other write, so a keyed entity
237
+ // embedded in a projection's result is flattened to its own root like any other write,
238
+ // and the diff reported here is unfiltered, the same as DiffPath/DiffBatch return.
239
+ diffResult.push(...UpdateSource(source, PROJECTION_PREFIX + id, result, true, keyFunc));
207
240
  }
208
241
  }
209
- return CreateSnapshot(source, keyFunc, child);
210
242
  }
211
- /**
212
- * Topologically sorts projections by their declared `reads` into a single array, run
213
- * sequentially: a read of another projection's `$projection_<id>` output is a dependency
214
- * edge, so that projection is sorted earlier. Runs once, at construction, so a cycle (or a
215
- * read of an id that isn't registered) is rejected immediately instead of on first write.
216
- */
217
- function BuildProjectionOrder(projections) {
218
- const order = [];
219
- const state = new Map();
220
- function Visit(id, stack) {
221
- if (state.get(id) === "done")
222
- return;
223
- if (state.get(id) === "visiting")
224
- throw new Error(`DiffTree projection cycle detected: ${stack.concat(id).join(" -> ")}`);
225
- const def = projections[id];
226
- if (!def)
227
- throw new Error(`DiffTree projection "${id}" is not defined`);
228
- state.set(id, "visiting");
229
- for (let x = 0; x < def.reads.length; x++) {
230
- const root = def.reads[x].split(".", 1)[0];
231
- if (root.startsWith(exports.PROJECTION_PREFIX))
232
- Visit(root.slice(exports.PROJECTION_PREFIX.length), stack.concat(id));
233
- }
234
- state.set(id, "done");
235
- order.push({ id, ...def });
243
+ return diffResult;
244
+ }
245
+ /**
246
+ * Resolves a path to its keyed root object if applicable.
247
+ * Searches up the path to find an object with a key from keyFunc.
248
+ * @param source - The source object
249
+ * @param path - Dot-separated path
250
+ * @param keyFunc - Function to extract key from objects
251
+ * @returns The resolved path (possibly shortened to root key)
252
+ */
253
+ function ResolveKeyPath(source, path, keyFunc) {
254
+ const parts = path.split(".");
255
+ const pathValues = new Array(parts.length - 1);
256
+ let curr = source;
257
+ for (let x = 0; x < parts.length - 1; x++) {
258
+ curr = curr[parts[x]];
259
+ pathValues[x] = curr;
260
+ }
261
+ let y = pathValues.length - 1;
262
+ for (; y >= 0 && !(JsonType(pathValues[y]) === "object" && keyFunc(pathValues[y])); y--) { }
263
+ if (y >= 0) {
264
+ const key = keyFunc(pathValues[y]);
265
+ parts.splice(0, y + 1, key);
266
+ return parts.join(".");
267
+ }
268
+ return path;
269
+ }
270
+ /**
271
+ * Updates the source with a new value at the specified path and computes diffs.
272
+ * Also updates any keyed objects that were nested in the value.
273
+ * @param source - The source object to update
274
+ * @param path - Dot-separated path to update
275
+ * @param value - The new value
276
+ * @param keyFunc - Optional function to extract keys from objects
277
+ * @returns Diff results showing all changes made
278
+ */
279
+ function UpdateSource(source, path, value, flatten, keyFunc) {
280
+ const diffResult = [];
281
+ if (keyFunc) {
282
+ const keyPath = ResolveKeyPath(source, path, keyFunc);
283
+ if (keyPath !== path) {
284
+ const keyDiffResult = UpdateSource(source, keyPath, value, flatten, keyFunc);
285
+ diffResult.push(...keyDiffResult);
236
286
  }
237
- const ids = Object.keys(projections);
238
- for (let x = 0; x < ids.length; x++)
239
- Visit(ids[x], []);
240
- return order;
241
287
  }
288
+ const [sourceValue] = GetPathValue(source, path);
289
+ JsonDiff(value, sourceValue, path, diffResult);
290
+ if (flatten && keyFunc) {
291
+ let flattened = {};
292
+ flattened = FlattenValue(flattened, value, keyFunc);
293
+ flattened = JsonDeepClone(flattened);
294
+ const keys = Object.keys(flattened);
295
+ for (let x = 0; x < keys.length; x++)
296
+ JsonDiff(flattened[keys[x]], source[keys[x]], keys[x], diffResult);
297
+ }
298
+ const filteredDiffResult = diffResult.filter((diff) => diff.value !== undefined);
299
+ for (let x = 0; x < filteredDiffResult.length; x++) {
300
+ SetPathValue(source, filteredDiffResult[x].path, filteredDiffResult[x].value);
301
+ }
302
+ return diffResult;
303
+ }
304
+ function SpliceSource(source, path, start, deleteCount, items, flatten, keyFunc) {
305
+ const [sourceValue, found] = GetPathValue(source, path);
306
+ if (!found)
307
+ throw `Value not found at path ${path}`;
308
+ if (!Array.isArray(sourceValue))
309
+ throw `Value found at path ${path} is not an array`;
310
+ deleteCount ??= sourceValue.length - start;
311
+ const spliceResult = {
312
+ path: path.split("."),
313
+ start,
314
+ deleteCount,
315
+ spliceResult: null,
316
+ diffResult: [],
317
+ };
318
+ spliceResult.spliceResult = sourceValue.splice(start, deleteCount, ...items);
319
+ InvalidateSnapshotForValue(sourceValue);
320
+ if (flatten && keyFunc) {
321
+ // Register the keyed entities the new items carry, diffed against the roots
322
+ // already held: a new entity is added whole, one re-inserted unchanged (an undo)
323
+ // produces no diff, so its readers aren't woken.
324
+ const flattened = JsonDeepClone(FlattenValue({}, items, keyFunc));
325
+ const keys = Object.keys(flattened);
326
+ for (let x = 0; x < keys.length; x++)
327
+ JsonDiff(flattened[keys[x]], source[keys[x]], keys[x], spliceResult.diffResult);
328
+ }
329
+ const filteredDiffResult = spliceResult.diffResult.filter((diff) => diff.value !== undefined);
330
+ for (let x = 0; x < filteredDiffResult.length; x++) {
331
+ SetPathValue(source, filteredDiffResult[x].path, filteredDiffResult[x].value);
332
+ }
333
+ return spliceResult;
334
+ }
335
+ /**
336
+ * Internal diff tree implementation.
337
+ * Maintains root state and computes diffs for path/value updates.
338
+ * @private
339
+ */
340
+ export class DiffTree {
242
341
  /**
243
- * Runs every registered projection, sequentially in the dependency order
244
- * `BuildProjectionOrder` computed, skipping ones whose declared `reads` all still resolve
245
- * to the same cached snapshot as last time (so nothing they read has changed), and returns
246
- * the diff of every projection whose result changed. A projection's result is stored at
247
- * `$projection_<id>` the same way a `DiffPath` write is: diffed against what was there
248
- * before and applied path by path, so a change to one field of a projection's result
249
- * produces one small diff entry rather than replacing the whole thing.
250
- *
251
- * Because `order` runs dependencies before dependents, a projection that reads another's
252
- * `$projection_<id>` output (chaining) always sees that projection's up-to-date result
253
- * within the same pass.
254
- *
255
- * A projection whose `reads` names a root key that doesn't exist yet (a plain path never
256
- * written, an entity never seen, or another projection that hasn't produced a result) is
257
- * not run at all, so a projection's author never has to guard every parameter against
258
- * missing data. It's tried again on every later write until every read exists. This also
259
- * makes a blocked dependency propagate for free: a projection that never runs never writes
260
- * `$projection_<id>`, so anything chaining off it stays blocked in turn.
261
- *
262
- * @param source - The root tree object
263
- * @param keyFunc - Optional function to extract a key from objects, for reading through roots
264
- * @param projections - Dependency-sorted projection entries, from `BuildProjectionOrder`
265
- * @param memo - Per-tree, cross-call memo of each projection's last-read values, by id,
266
- * positional with its `reads`
267
- * @returns Diff entries for every projection whose result changed
342
+ * Creates a DiffTree instance.
343
+ * @param keyFunc - Optional function to extract a key from objects
344
+ * @param projections - Optional map of derived-value projections, keyed by id. Cycles
345
+ * and reads of an unregistered projection id are rejected here, at construction.
268
346
  */
269
- function RunProjections(source, keyFunc, projections, memo) {
270
- const diffResult = [];
271
- for (let x = 0; x < projections.length; x++) {
272
- const { id, reads, projection } = projections[x];
273
- const values = new Array(reads.length);
274
- let allPathsExist = true;
275
- for (let y = 0; allPathsExist && y < reads.length; y++) {
276
- const [value, found] = GetPathValue(source, reads[y]);
277
- allPathsExist = found;
278
- values[y] = found && CreateSnapshot(source, keyFunc, value) || value;
279
- }
280
- if (allPathsExist) {
281
- const priorValues = memo.get(id);
282
- if (!arraysEqual(priorValues, values)) {
283
- const result = projection(...values);
284
- memo.set(id, values);
285
- // The same diff-and-apply UpdateSource uses for any other write, so a keyed entity
286
- // embedded in a projection's result is flattened to its own root like any other write,
287
- // and the diff reported here is unfiltered, the same as DiffPath/DiffBatch return.
288
- diffResult.push(...UpdateSource(source, exports.PROJECTION_PREFIX + id, result, true, keyFunc));
289
- }
290
- }
291
- }
292
- return diffResult;
347
+ constructor(keyFunc, projections) {
348
+ this.keyFunc = keyFunc;
349
+ this.rootState = {};
350
+ this.projectionMemo = new Map();
351
+ this.projectionOrder = projections ? BuildProjectionOrder(projections) : [];
352
+ }
353
+ RunProjections() {
354
+ if (this.projectionOrder.length === 0)
355
+ return [];
356
+ return RunProjections(this.rootState, this.keyFunc, this.projectionOrder, this.projectionMemo);
293
357
  }
294
358
  /**
295
- * Resolves a path to its keyed root object if applicable.
296
- * Searches up the path to find an object with a key from keyFunc.
297
- * @param source - The source object
298
- * @param path - Dot-separated path
299
- * @param keyFunc - Function to extract key from objects
300
- * @returns The resolved path (possibly shortened to root key)
359
+ * Computes diffs for a batch of path/value pairs.
360
+ * @param data - Array of objects with path and value properties
361
+ * @returns Combined diff results
301
362
  */
302
- function ResolveKeyPath(source, path, keyFunc) {
303
- const parts = path.split(".");
304
- const pathValues = new Array(parts.length - 1);
305
- let curr = source;
306
- for (let x = 0; x < parts.length - 1; x++) {
307
- curr = curr[parts[x]];
308
- pathValues[x] = curr;
309
- }
310
- let y = pathValues.length - 1;
311
- for (; y >= 0 &&
312
- !(JsonType(pathValues[y]) === "object" && keyFunc(pathValues[y])); y--) { }
313
- if (y >= 0) {
314
- const key = keyFunc(pathValues[y]);
315
- parts.splice(0, y + 1, key);
316
- return parts.join(".");
317
- }
318
- return path;
363
+ DiffBatch(data) {
364
+ const results = data
365
+ .map(({ path, value }) => UpdateSource(this.rootState, path, value, true, this.keyFunc))
366
+ .flat(1);
367
+ results.push(...this.RunProjections());
368
+ return results;
319
369
  }
320
370
  /**
321
- * Updates the source with a new value at the specified path and computes diffs.
322
- * Also updates any keyed objects that were nested in the value.
323
- * @param source - The source object to update
324
- * @param path - Dot-separated path to update
325
- * @param value - The new value
326
- * @param keyFunc - Optional function to extract keys from objects
327
- * @returns Diff results showing all changes made
371
+ * Computes the diff between a new value and the current value at a path.
372
+ * @param path - Dot-separated path to the value
373
+ * @param value - The new value to compare
374
+ * @returns Diff results showing changes
328
375
  */
329
- function UpdateSource(source, path, value, flatten, keyFunc) {
330
- const diffResult = [];
331
- if (keyFunc) {
332
- const keyPath = ResolveKeyPath(source, path, keyFunc);
333
- if (keyPath !== path) {
334
- const keyDiffResult = UpdateSource(source, keyPath, value, flatten, keyFunc);
335
- diffResult.push(...keyDiffResult);
336
- }
337
- }
338
- const [sourceValue] = GetPathValue(source, path);
339
- JsonDiff(value, sourceValue, path, diffResult);
340
- if (flatten && keyFunc) {
341
- let flattened = {};
342
- flattened = FlattenValue(flattened, value, keyFunc);
343
- flattened = JsonDeepClone(flattened);
344
- const keys = Object.keys(flattened);
345
- for (let x = 0; x < keys.length; x++)
346
- JsonDiff(flattened[keys[x]], source[keys[x]], keys[x], diffResult);
347
- }
348
- const filteredDiffResult = diffResult.filter((diff) => diff.value !== undefined);
349
- for (let x = 0; x < filteredDiffResult.length; x++) {
350
- SetPathValue(source, filteredDiffResult[x].path, filteredDiffResult[x].value);
351
- }
352
- return diffResult;
376
+ DiffPath(path, value, flatten = true) {
377
+ const results = UpdateSource(this.rootState, path, value, flatten, this.keyFunc);
378
+ results.push(...this.RunProjections());
379
+ return results;
353
380
  }
354
- function SpliceSource(source, path, start, deleteCount, items, flatten, keyFunc) {
355
- const [sourceValue, found] = GetPathValue(source, path);
356
- if (!found)
357
- throw `Value not found at path ${path}`;
358
- if (!Array.isArray(sourceValue))
359
- throw `Value found at path ${path} is not an array`;
360
- deleteCount ??= sourceValue.length - start;
361
- const spliceResult = {
362
- path: path.split("."),
363
- start,
364
- deleteCount,
365
- spliceResult: null,
366
- diffResult: []
367
- };
368
- spliceResult.spliceResult = sourceValue.splice(start, deleteCount, ...items);
369
- InvalidateSnapshotForValue(sourceValue);
370
- if (flatten && keyFunc) {
371
- // Register the keyed entities the new items carry, diffed against the roots
372
- // already held: a new entity is added whole, one re-inserted unchanged (an undo)
373
- // produces no diff, so its readers aren't woken.
374
- const flattened = JsonDeepClone(FlattenValue({}, items, keyFunc));
375
- const keys = Object.keys(flattened);
376
- for (let x = 0; x < keys.length; x++)
377
- JsonDiff(flattened[keys[x]], source[keys[x]], keys[x], spliceResult.diffResult);
378
- }
379
- const filteredDiffResult = spliceResult.diffResult.filter((diff) => diff.value !== undefined);
380
- for (let x = 0; x < filteredDiffResult.length; x++) {
381
- SetPathValue(source, filteredDiffResult[x].path, filteredDiffResult[x].value);
382
- }
381
+ SplicePath(path, start, deleteCount, items, flatten = true) {
382
+ const spliceResult = SpliceSource(this.rootState, path, start, deleteCount, items, flatten, this.keyFunc);
383
+ spliceResult.diffResult.push(...this.RunProjections());
383
384
  return spliceResult;
384
385
  }
385
386
  /**
386
- * Internal diff tree implementation.
387
- * Maintains root state and computes diffs for path/value updates.
388
- * @private
387
+ * Retrieves the current value at a path.
388
+ * @param path - Dot-separated path to the value
389
+ * @returns The value at the specified path
389
390
  */
390
- class DiffTree {
391
- /**
392
- * Creates a DiffTree instance.
393
- * @param keyFunc - Optional function to extract a key from objects
394
- * @param projections - Optional map of derived-value projections, keyed by id. Cycles
395
- * and reads of an unregistered projection id are rejected here, at construction.
396
- */
397
- constructor(keyFunc, projections) {
398
- this.keyFunc = keyFunc;
399
- this.rootState = {};
400
- this.projectionMemo = new Map();
401
- this.projectionOrder = projections ? BuildProjectionOrder(projections) : [];
402
- }
403
- RunProjections() {
404
- if (this.projectionOrder.length === 0)
405
- return [];
406
- return RunProjections(this.rootState, this.keyFunc, this.projectionOrder, this.projectionMemo);
407
- }
408
- /**
409
- * Computes diffs for a batch of path/value pairs.
410
- * @param data - Array of objects with path and value properties
411
- * @returns Combined diff results
412
- */
413
- DiffBatch(data) {
414
- const results = data
415
- .map(({ path, value }) => UpdateSource(this.rootState, path, value, true, this.keyFunc))
416
- .flat(1);
417
- results.push(...this.RunProjections());
418
- return results;
419
- }
420
- /**
421
- * Computes the diff between a new value and the current value at a path.
422
- * @param path - Dot-separated path to the value
423
- * @param value - The new value to compare
424
- * @returns Diff results showing changes
425
- */
426
- DiffPath(path, value, flatten = true) {
427
- const results = UpdateSource(this.rootState, path, value, flatten, this.keyFunc);
428
- results.push(...this.RunProjections());
429
- return results;
430
- }
431
- SplicePath(path, start, deleteCount, items, flatten = true) {
432
- const spliceResult = SpliceSource(this.rootState, path, start, deleteCount, items, flatten, this.keyFunc);
433
- spliceResult.diffResult.push(...this.RunProjections());
434
- return spliceResult;
435
- }
436
- /**
437
- * Retrieves the current value at a path.
438
- * @param path - Dot-separated path to the value
439
- * @returns The value at the specified path
440
- */
441
- GetPath(path) {
442
- const [value] = GetPathValue(this.rootState, path);
443
- return value;
444
- }
445
- /**
446
- * Returns an immutable, cached snapshot of the value at a path (empty string for the
447
- * whole tree). Not part of `IDiffTree`; internal, for a future feature.
448
- * @param path - Dot-separated path to the value
449
- */
450
- Snapshot(path) {
451
- const [value, found] = GetPathValue(this.rootState, path);
452
- if (!found)
453
- throw `Unable to get snapshot for path ${path}`;
454
- return CreateSnapshot(this.rootState, this.keyFunc, value);
455
- }
391
+ GetPath(path) {
392
+ const [value] = GetPathValue(this.rootState, path);
393
+ return value;
394
+ }
395
+ /**
396
+ * Returns an immutable, cached snapshot of the value at a path (empty string for the
397
+ * whole tree). Not part of `IDiffTree`; internal, for a future feature.
398
+ * @param path - Dot-separated path to the value
399
+ */
400
+ Snapshot(path) {
401
+ const [value, found] = GetPathValue(this.rootState, path);
402
+ if (!found)
403
+ throw `Unable to get snapshot for path ${path}`;
404
+ return CreateSnapshot(this.rootState, this.keyFunc, value);
456
405
  }
457
- return DiffTree;
458
406
  }