j-templates 8.1.2 → 8.2.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.
@@ -26,7 +26,7 @@ import { Store } from "./store";
26
26
  * await store.Push("users", { id: 1 }, { id: 2 });
27
27
  *
28
28
  * // Splicing an array asynchronously
29
- * const deletedItems = await store.Splice("users", 0, 1, { id: 3 });
29
+ * await store.Splice("users", 0, 1, { id: 3 });
30
30
  *
31
31
  * // Cleaning up resources
32
32
  * store.Destroy();
@@ -38,17 +38,17 @@ export class StoreAsync extends Store {
38
38
  /**
39
39
  * Creates an instance of StoreAsync.
40
40
  * @param diffWorker A `Worker` running a `DiffTree` connected via `ConnectWorkerToDiffTree`
41
- * (e.g. a bundled `defaultDiffTreeWorker` entry). Any `keyFunc`/projections the diff needs
41
+ * (e.g. a bundled `defaultDiffTreeWorker` entry). Any `keyFunc`/routes the diff needs
42
42
  * must be baked into that worker's own entry file.
43
43
  * @param keyFunc Optional function to generate a key for a given data value, used locally by
44
44
  * this store to resolve root objects. Pass the same key logic the worker's `DiffTree` was
45
45
  * constructed with, if any.
46
46
  */
47
47
  constructor(diffWorker, keyFunc) {
48
- super(keyFunc, false);
48
+ super(keyFunc);
49
49
  this.writeKeySet = new Set();
50
50
  this.currentBatch = [];
51
- this.currentBatchPromise = Promise.resolve([]);
51
+ this.currentBatchPromise = Promise.resolve();
52
52
  this.workerQueue = new WorkerQueue(diffWorker);
53
53
  this.asyncQueue = new AsyncQueue(true);
54
54
  }
@@ -59,29 +59,13 @@ export class StoreAsync extends Store {
59
59
  await Promise.resolve();
60
60
  if (batch === this.currentBatch)
61
61
  this.currentBatch = [];
62
- const results = await this.workerQueue.Push(batch);
63
- for (let x = 0; x < results.length; x++) {
64
- const result = results[x];
65
- switch (result.type) {
66
- case "diff": {
67
- this.UpdateRootMap(result.result);
68
- break;
69
- }
70
- case "splice": {
71
- const spliceResult = result.result;
72
- this.UpdateRootMap(spliceResult.diffResult);
73
- this.SpliceRootObject(spliceResult.path[0], spliceResult.start, spliceResult.deleteCount, spliceResult.items);
74
- break;
75
- }
76
- }
77
- }
78
- return results;
62
+ // The worker's Commit() diff covers every change in the batch
63
+ const diff = await this.workerQueue.Push(batch);
64
+ this.UpdateRootMap(diff);
79
65
  });
80
66
  }
81
- const batchIndex = this.currentBatch.length;
82
67
  this.currentBatch.push(method);
83
- const batchResult = await this.currentBatchPromise;
84
- return batchResult[batchIndex].result;
68
+ await this.currentBatchPromise;
85
69
  }
86
70
  /**
87
71
  * Writes data to the store asynchronously.
@@ -100,7 +84,7 @@ export class StoreAsync extends Store {
100
84
  throw new Error("No key provided for data");
101
85
  this.writeKeySet.add(key);
102
86
  await this.Schedule({
103
- method: "diffpath",
87
+ method: "writepath",
104
88
  arguments: [key, data]
105
89
  });
106
90
  this.writeKeySet.delete(key);
@@ -146,19 +130,17 @@ export class StoreAsync extends Store {
146
130
  * @param start The position at which to start changing the array.
147
131
  * @param deleteCount Optional number of elements to delete. If not provided, all elements from start to end will be deleted.
148
132
  * @param items Optional elements to insert into the array.
149
- * @returns A promise that resolves to the array of deleted elements once the splice has been
150
- * applied to the store.
133
+ * @returns A promise that resolves once the splice has been applied to the store.
151
134
  * @throws Will throw if the key isn't in the store and has no pending Write, or reject with
152
135
  * the original error if an earlier change failed.
153
136
  */
154
137
  async Splice(key, start, deleteCount, ...items) {
155
138
  if (!this.Has(key) && !this.writeKeySet.has(key))
156
139
  throw "Key not found in store";
157
- const spliceResult = await this.Schedule({
140
+ await this.Schedule({
158
141
  method: "splicepath",
159
- arguments: [key, start, deleteCount, items]
142
+ arguments: [key, start, deleteCount ?? Infinity, items]
160
143
  });
161
- return spliceResult.spliceResult;
162
144
  }
163
145
  /**
164
146
  * Returns a promise that resolves once every change made before this call has been applied
@@ -1,4 +1,4 @@
1
- import { DiffTreeProjectionMap } from "../Diff/diffTree.types";
1
+ import { IRouteDefinitions } from "../Diff/utils/diffTree.types";
2
2
  import { Store } from "./store";
3
3
  /**
4
4
  * StoreSync class extends the base Store class to provide synchronous data management operations.
@@ -22,25 +22,26 @@ import { Store } from "./store";
22
22
  * store.Push("user.array", { id: 1 }, { id: 2 });
23
23
  *
24
24
  * // Splicing an array
25
- * const deletedItems = store.Splice("user.array", 0, 1, { id: 3 });
25
+ * store.Splice("user.array", 0, 1, { id: 3 });
26
26
  *
27
27
  * @see Store
28
28
  * @see StoreAsync
29
- * @see DiffSync
29
+ * @see DiffTree
30
30
  */
31
31
  export declare class StoreSync extends Store {
32
32
  /**
33
- * The diff instance used to compute differences between current and new data states.
33
+ * The tree each change is applied to. Its `Commit()` diff keeps the store in sync.
34
34
  * @private
35
35
  */
36
36
  private diff;
37
37
  /**
38
38
  * Creates an instance of StoreSync.
39
39
  * @param keyFunc Optional function to generate a key for a given data value.
40
- * @param projections Optional map of derived-value projections, keyed by id. A
41
- * projection's result is readable through `Get` at `$projection_<id>`.
40
+ * @param routes Optional route definitions. A projection in the root route (`"/"`) is
41
+ * readable through `Get` at `$projection_<key>`.
42
+ * @throws When projections depend on each other in a cycle.
42
43
  */
43
- constructor(keyFunc?: (value: any) => string | undefined, projections?: DiffTreeProjectionMap);
44
+ constructor(keyFunc?: (value: any) => string | undefined, routes?: IRouteDefinitions);
44
45
  /**
45
46
  * Writes data to the store synchronously.
46
47
  * @param data The data to be written. Can be of any type.
@@ -68,7 +69,11 @@ export declare class StoreSync extends Store {
68
69
  * @param start The position at which to start changing the array.
69
70
  * @param deleteCount Optional number of elements to delete. If not provided, all elements from start to end will be deleted.
70
71
  * @param items Optional elements to insert into the array.
71
- * @returns The array of deleted elements.
72
72
  */
73
- Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): unknown[];
73
+ Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): void;
74
+ /**
75
+ * Runs projections and applies everything changed since the last sync to the store.
76
+ * @private
77
+ */
78
+ private Sync;
74
79
  }
@@ -1,5 +1,4 @@
1
- import { JsonDeepClone } from "../../Utils/json";
2
- import { DiffSync } from "../Diff/diffSync";
1
+ import { DiffTree } from "../Diff/diffTree";
3
2
  import { Store } from "./store";
4
3
  /**
5
4
  * StoreSync class extends the base Store class to provide synchronous data management operations.
@@ -23,22 +22,23 @@ import { Store } from "./store";
23
22
  * store.Push("user.array", { id: 1 }, { id: 2 });
24
23
  *
25
24
  * // Splicing an array
26
- * const deletedItems = store.Splice("user.array", 0, 1, { id: 3 });
25
+ * store.Splice("user.array", 0, 1, { id: 3 });
27
26
  *
28
27
  * @see Store
29
28
  * @see StoreAsync
30
- * @see DiffSync
29
+ * @see DiffTree
31
30
  */
32
31
  export class StoreSync extends Store {
33
32
  /**
34
33
  * Creates an instance of StoreSync.
35
34
  * @param keyFunc Optional function to generate a key for a given data value.
36
- * @param projections Optional map of derived-value projections, keyed by id. A
37
- * projection's result is readable through `Get` at `$projection_<id>`.
35
+ * @param routes Optional route definitions. A projection in the root route (`"/"`) is
36
+ * readable through `Get` at `$projection_<key>`.
37
+ * @throws When projections depend on each other in a cycle.
38
38
  */
39
- constructor(keyFunc, projections) {
39
+ constructor(keyFunc, routes) {
40
40
  super(keyFunc);
41
- this.diff = new DiffSync(keyFunc, projections);
41
+ this.diff = new DiffTree(keyFunc, routes);
42
42
  }
43
43
  /**
44
44
  * Writes data to the store synchronously.
@@ -50,8 +50,8 @@ export class StoreSync extends Store {
50
50
  key = key || this.keyFunc?.(data);
51
51
  if (!key)
52
52
  throw "No key provided for data";
53
- const diffResult = this.diff.DiffPath(key, data);
54
- this.UpdateRootMap(diffResult);
53
+ this.diff.WritePath(key, data);
54
+ this.Sync();
55
55
  }
56
56
  /**
57
57
  * Patches an existing value in the store with new data synchronously.
@@ -62,8 +62,8 @@ export class StoreSync extends Store {
62
62
  Patch(key, patch) {
63
63
  if (!this.Has(key))
64
64
  throw "Key not found in store";
65
- const diffResult = this.diff.PatchPath(key, patch);
66
- this.UpdateRootMap(diffResult);
65
+ this.diff.PatchPath(key, patch);
66
+ this.Sync();
67
67
  }
68
68
  /**
69
69
  * Pushes data into an array stored at the specified key synchronously.
@@ -73,8 +73,8 @@ export class StoreSync extends Store {
73
73
  Push(key, ...data) {
74
74
  if (!this.Has(key))
75
75
  throw "Key not found in store";
76
- const diffResult = this.diff.PushPath(key, data);
77
- this.UpdateRootMap(diffResult);
76
+ this.diff.PushPath(key, data);
77
+ this.Sync();
78
78
  }
79
79
  /**
80
80
  * Splices an array stored at the specified key with new data synchronously.
@@ -83,14 +83,19 @@ export class StoreSync extends Store {
83
83
  * @param start The position at which to start changing the array.
84
84
  * @param deleteCount Optional number of elements to delete. If not provided, all elements from start to end will be deleted.
85
85
  * @param items Optional elements to insert into the array.
86
- * @returns The array of deleted elements.
87
86
  */
88
87
  Splice(key, start, deleteCount, ...items) {
89
88
  if (!this.Has(key))
90
89
  throw "Key not found in store";
91
- const spliceResult = this.diff.SplicePath(key, start, deleteCount, items);
92
- this.UpdateRootMap(spliceResult.diffResult);
93
- this.SpliceRootObject(spliceResult.path[0], spliceResult.start, spliceResult.deleteCount, items);
94
- return JsonDeepClone(spliceResult.spliceResult);
90
+ this.diff.SplicePath(key, start, deleteCount ?? Infinity, items);
91
+ this.Sync();
92
+ }
93
+ /**
94
+ * Runs projections and applies everything changed since the last sync to the store.
95
+ * @private
96
+ */
97
+ Sync() {
98
+ this.diff.RunProjections();
99
+ this.UpdateRootMap(this.diff.Commit());
95
100
  }
96
101
  }
@@ -48,16 +48,7 @@ export declare namespace ObservableNode {
48
48
  * @returns The snapshot, or `undefined` if `proxy` is not an observable node.
49
49
  */
50
50
  function Snapshot<T>(proxy: T): T | undefined;
51
- /**
52
- * Marks an observable node or its property as changed, triggering reactive updates.
53
- * Used internally to notify dependencies that a value has been modified.
54
- * @param value The observable node to touch.
55
- * @param prop Optional property name or index to touch a specific nested property.
56
- */
57
- function Update<T>(value: T, prop?: keyof T): void;
58
- function Assign<T>(value: T, prop: keyof T, propValue: any): void;
59
51
  function Touch<T>(value: T, prop?: keyof T): void;
60
- function Read<T>(value: T, prop: keyof T): void;
61
52
  /**
62
53
  * Merges a new value into an observable node in-place, preserving the node's object identity.
63
54
  * Computes the diff between the node's current value and the provided value, then applies only
@@ -77,26 +68,14 @@ export declare namespace ObservableNode {
77
68
  * @remarks No-op when `value` deep-equals the node's current value (empty diff).
78
69
  */
79
70
  function Apply(rootNode: any, value: any): void;
80
- /**
81
- * Splices an observable node's underlying array in-place, invalidating its snapshot and
82
- * touching its scope so reactive readers update.
83
- * @param rootNode The observable array node to splice.
84
- * @param start Index at which to start changing the array.
85
- * @param deleteCount Number of elements to remove starting at `start`.
86
- * @param items Elements to insert at `start`.
87
- * @param cloneData Whether to deep-clone `items` before inserting them (true by default,
88
- * so the store isn't left holding a reference the caller can still mutate).
89
- */
90
- function ApplySplice(rootNode: any, start: number, deleteCount: number, items: any[], cloneData?: boolean): void;
91
71
  /**
92
72
  * Applies a JSON diff result to an observable node, efficiently updating only changed properties.
93
73
  * Optimizes nested object updates by computing paths incrementally and touching modified properties.
94
74
  * @param rootNode The observable node to apply the diff to.
95
75
  * @param diffResult The diff result from JsonDiff containing path-value pairs of changes.
96
- * @param cloneData Whether to deep-clone each diff value before writing it in (true by
97
- * default), so the store isn't left holding a reference the caller can still mutate.
76
+ * Each value is deep-cloned as it's applied (see JsonApplyDiff).
98
77
  */
99
- function ApplyDiff(rootNode: any, diffResult: JsonDiffResult, cloneData?: boolean): void;
78
+ function ApplyDiff(rootNode: any, diffResult: JsonDiffResult): void;
100
79
  /**
101
80
  * Creates a factory function for making values observable with optional aliasing.
102
81
  * The alias function transforms values before creating the observable proxy,
@@ -1,9 +1,11 @@
1
- import { JsonDeepClone, JsonDiff } from "../../Utils/json";
1
+ import { JsonApplyDiff, JsonDeepClone, JsonDiff } from "../../Utils/json";
2
2
  import { JsonType } from "../../Utils/json";
3
3
  import { ObservableScope } from "./observableScope";
4
+ import { Snapshot } from "./snapshot";
4
5
  const NODE_VALUE = Symbol("NODE_VALUE");
5
6
  const NODE_PROXY = Symbol("NODE_PROXY");
6
7
  const NODE_SNAPSHOT = Symbol("NODE_SNAPSHOT");
8
+ const NODE_WRAPPER = Symbol("NODE_WRAPPER");
7
9
  const toJSON = "toJSON";
8
10
  const IS_NODE = Symbol("IS_NODE");
9
11
  const ARRAY_SCOPE = Symbol("ARRAY_SCOPE");
@@ -15,23 +17,9 @@ function Property(value, prop) {
15
17
  return value[prop];
16
18
  }
17
19
  const wrapperCache = new WeakMap();
18
- const valueSnapshotCache = new WeakMap();
19
- const valueSnapshotHierarchy = new WeakMap();
20
- function CreateSnapshotLink(child, parent) {
21
- let parents = valueSnapshotHierarchy.get(child);
22
- if (!parents)
23
- valueSnapshotHierarchy.set(child, (parents = new Set()));
24
- parents.add(parent);
25
- }
20
+ const snapshotState = Snapshot.Create();
26
21
  function InvalidateSnapshotForValue(value) {
27
- valueSnapshotCache.delete(value);
28
- const parents = valueSnapshotHierarchy.get(value);
29
- if (parents === undefined)
30
- return;
31
- valueSnapshotHierarchy.delete(value);
32
- for (const parent of parents)
33
- if (valueSnapshotCache.has(parent))
34
- InvalidateSnapshotForValue(parent);
22
+ Snapshot.Invalidate(snapshotState, value);
35
23
  }
36
24
  function getOwnPropertyDescriptor(target, prop) {
37
25
  ObservableScope.Touch(target[OBJECT_SCOPE]);
@@ -109,10 +97,8 @@ function CloneProxy(value, type = JsonType(value)) {
109
97
  }
110
98
  return value;
111
99
  }
112
- function GetPropertyScope(object, prop, create = true) {
100
+ function GetPropertyScope(object, prop) {
113
101
  if (!Object.hasOwn(object, prop)) {
114
- if (!create)
115
- return undefined;
116
102
  Object.defineProperty(object, prop, {
117
103
  value: ObservableScope.Basic(Property.bind(null, object[NODE_VALUE], prop)),
118
104
  writable: true,
@@ -125,62 +111,37 @@ function GetPropertyScope(object, prop, create = true) {
125
111
  function SetObjectValue(object, prop, value) {
126
112
  const propExists = Object.hasOwn(object[NODE_VALUE], prop);
127
113
  object[NODE_VALUE][prop] = value;
128
- InvalidateSnapshotForValue(object[NODE_VALUE]);
129
- Object.hasOwn(object, prop) && ObservableScope.Update(object[prop]);
130
- !propExists && ObservableScope.Update(object[OBJECT_SCOPE]);
114
+ NodeValueSet(object[NODE_VALUE], prop, !propExists, object);
131
115
  }
132
116
  function SetArrayValue(object, prop, value) {
133
117
  object[NODE_VALUE][prop] = value;
134
- InvalidateSnapshotForValue(object[NODE_VALUE]);
135
- ObservableScope.Update(object[ARRAY_SCOPE]);
118
+ NodeValueSet(object[NODE_VALUE], prop, false, object);
119
+ }
120
+ function NodeValueSet(obj, prop, added, wrapper = wrapperCache.get(obj)) {
121
+ InvalidateSnapshotForValue(obj);
122
+ if (!wrapper)
123
+ return;
124
+ const propScopeExists = !!prop && Object.hasOwn(wrapper, prop);
125
+ if (!prop || added || !propScopeExists)
126
+ ObservableScope.Update(wrapper[ARRAY_SCOPE] ?? wrapper[OBJECT_SCOPE]);
127
+ if (propScopeExists)
128
+ ObservableScope.Update(wrapper[prop]);
129
+ }
130
+ function AssignNodeValue(obj, prop, value) {
131
+ const added = !Object.hasOwn(obj, prop);
132
+ obj[prop] = value;
133
+ NodeValueSet(obj, prop, added);
136
134
  }
137
135
  function CreateProxyFactory(alias) {
138
- /**
139
- * An immutable snapshot of a raw node value (arrays and plain objects; other values are
140
- * returned as they are). A snapshot is cached per raw value in valueSnapshotCache and
141
- * returned as-is until a write invalidates it: every write path calls Invalidate, which
142
- * clears the written value and every cached ancestor, found through the parent links
143
- * recorded here. A rebuild makes new containers but reuses the cached snapshots of
144
- * unchanged children, so a snapshot handed out never changes and unchanged parts are
145
- * shared between snapshots. Keyed children are read through their root.
146
- */
136
+ // Keyed children are read through their root. Every factory shares the module-level
137
+ // snapshotState, matching wrapperCache; only the resolver differs per factory.
138
+ // The alias ignores paths, so the root is snapshotted at the child's own path
139
+ const resolveSnapshot = alias && function (child, path) {
140
+ const value = alias(child, false);
141
+ return value === undefined ? undefined : { value, path: path.slice() };
142
+ };
147
143
  function CreateSnapshot(value) {
148
- if (JsonType(value) === "value")
149
- return value;
150
- const cached = valueSnapshotCache.get(value);
151
- if (cached !== undefined)
152
- return cached;
153
- let snapshot;
154
- if (Array.isArray(value)) {
155
- snapshot = new Array(value.length);
156
- for (let x = 0; x < value.length; x++)
157
- snapshot[x] = ChildSnapshot(value, value[x]);
158
- }
159
- else {
160
- snapshot = {};
161
- const keys = Object.keys(value);
162
- for (let x = 0; x < keys.length; x++)
163
- snapshot[keys[x]] = ChildSnapshot(value, value[keys[x]]);
164
- }
165
- valueSnapshotCache.set(value, snapshot);
166
- return snapshot;
167
- }
168
- /**
169
- * A child's snapshot, linking the child to its parent so a write to the child
170
- * invalidates the parent. A keyed child's snapshot comes from its root, so the root is
171
- * linked too; the parent's own embedded copy stays linked because a positional write
172
- * (one that changes which entity a slot holds) mutates that copy in place.
173
- */
174
- function ChildSnapshot(parent, child) {
175
- if (JsonType(child) === "value")
176
- return child;
177
- CreateSnapshotLink(child, parent);
178
- const resolved = alias?.(child, false);
179
- if (resolved !== undefined && resolved !== child) {
180
- CreateSnapshotLink(resolved, parent);
181
- return CreateSnapshot(resolved);
182
- }
183
- return CreateSnapshot(child);
144
+ return Snapshot.Get(snapshotState, value, resolveSnapshot);
184
145
  }
185
146
  const readOnly = alias !== undefined;
186
147
  function CreateArrayProxy(value) {
@@ -225,6 +186,8 @@ function CreateProxyFactory(alias) {
225
186
  return function () {
226
187
  return CreateSnapshot(object[NODE_VALUE]);
227
188
  };
189
+ case NODE_WRAPPER:
190
+ return object;
228
191
  case NODE_VALUE:
229
192
  return object[NODE_VALUE];
230
193
  case NODE_SNAPSHOT:
@@ -290,6 +253,8 @@ function CreateProxyFactory(alias) {
290
253
  switch (prop) {
291
254
  case IS_NODE:
292
255
  return true;
256
+ case NODE_WRAPPER:
257
+ return object;
293
258
  case NODE_SNAPSHOT:
294
259
  return CreateSnapshot(object[NODE_VALUE]);
295
260
  case toJSON:
@@ -404,36 +369,6 @@ export var ObservableNode;
404
369
  return undefined;
405
370
  }
406
371
  ObservableNode.Snapshot = Snapshot;
407
- /**
408
- * Marks an observable node or its property as changed, triggering reactive updates.
409
- * Used internally to notify dependencies that a value has been modified.
410
- * @param value The observable node to touch.
411
- * @param prop Optional property name or index to touch a specific nested property.
412
- */
413
- function Update(value, prop) {
414
- const wrapper = wrapperCache.get(value);
415
- if (wrapper) {
416
- const scope = Array.isArray(wrapper) ? wrapper[ARRAY_SCOPE] : (prop && GetPropertyScope(wrapper, prop)) ?? wrapper[OBJECT_SCOPE];
417
- ObservableScope.Update(scope);
418
- }
419
- }
420
- ObservableNode.Update = Update;
421
- function Assign(value, prop, propValue) {
422
- // const propExists = Object.hasOwn(value as Object, prop);
423
- // value[prop] = propValue;
424
- const wrapper = wrapperCache.get(value);
425
- if (wrapper) {
426
- if (Array.isArray(wrapper))
427
- SetArrayValue(wrapper, prop, propValue);
428
- else
429
- SetObjectValue(wrapper, prop, propValue);
430
- }
431
- else {
432
- InvalidateSnapshotForValue(value);
433
- value[prop] = propValue;
434
- }
435
- }
436
- ObservableNode.Assign = Assign;
437
372
  function Touch(value, prop) {
438
373
  const wrapper = wrapperCache.get(value);
439
374
  if (wrapper) {
@@ -442,14 +377,6 @@ export var ObservableNode;
442
377
  }
443
378
  }
444
379
  ObservableNode.Touch = Touch;
445
- function Read(value, prop) {
446
- const wrapper = wrapperCache.get(value);
447
- if (wrapper) {
448
- const scope = Array.isArray(wrapper) ? wrapper[ARRAY_SCOPE] : (prop && wrapper[prop]) ?? wrapper[OBJECT_SCOPE];
449
- ObservableScope.Update(scope);
450
- }
451
- }
452
- ObservableNode.Read = Read;
453
380
  /**
454
381
  * Merges a new value into an observable node in-place, preserving the node's object identity.
455
382
  * Computes the diff between the node's current value and the provided value, then applies only
@@ -474,86 +401,19 @@ export var ObservableNode;
474
401
  ApplyDiff(rootNode, diff);
475
402
  }
476
403
  ObservableNode.Apply = Apply;
477
- /**
478
- * Splices an observable node's underlying array in-place, invalidating its snapshot and
479
- * touching its scope so reactive readers update.
480
- * @param rootNode The observable array node to splice.
481
- * @param start Index at which to start changing the array.
482
- * @param deleteCount Number of elements to remove starting at `start`.
483
- * @param items Elements to insert at `start`.
484
- * @param cloneData Whether to deep-clone `items` before inserting them (true by default,
485
- * so the store isn't left holding a reference the caller can still mutate).
486
- */
487
- function ApplySplice(rootNode, start, deleteCount, items, cloneData = true) {
488
- const root = rootNode[NODE_VALUE];
489
- InvalidateSnapshotForValue(rootNode[NODE_VALUE]);
490
- const addItems = cloneData ? JsonDeepClone(items) : items;
491
- root.splice(start, deleteCount, ...addItems);
492
- ObservableNode.Update(root);
493
- }
494
- ObservableNode.ApplySplice = ApplySplice;
495
404
  /**
496
405
  * Applies a JSON diff result to an observable node, efficiently updating only changed properties.
497
406
  * Optimizes nested object updates by computing paths incrementally and touching modified properties.
498
407
  * @param rootNode The observable node to apply the diff to.
499
408
  * @param diffResult The diff result from JsonDiff containing path-value pairs of changes.
500
- * @param cloneData Whether to deep-clone each diff value before writing it in (true by
501
- * default), so the store isn't left holding a reference the caller can still mutate.
409
+ * Each value is deep-cloned as it's applied (see JsonApplyDiff).
502
410
  */
503
- function ApplyDiff(rootNode, diffResult, cloneData = true) {
411
+ function ApplyDiff(rootNode, diffResult) {
504
412
  const root = rootNode[NODE_VALUE];
505
- if (diffResult.length === 1 && diffResult[0].path.length === 0) {
506
- // Replacing rootNode
507
- InvalidateSnapshotForValue(root);
508
- const rootPatch = cloneData ? JsonDeepClone(diffResult[0].value) : diffResult[0].value;
509
- const rootType = JsonType(root);
510
- const rootPatchType = JsonType(rootPatch);
511
- if (rootType !== rootPatchType)
512
- throw new Error("Unable to change type of Root ObservableNode: " + rootType);
513
- switch (rootType) {
514
- case "array": {
515
- root.splice(0, root.length, ...rootPatch);
516
- ObservableNode.Update(root);
517
- break;
518
- }
519
- case "object": {
520
- const keys = Object.keys(root);
521
- const patchKeys = Object.keys(rootPatch);
522
- for (let x = 0; x < keys.length; x++)
523
- if (!patchKeys.includes(keys[x]))
524
- delete root[keys[x]];
525
- Object.assign(root, rootPatch);
526
- for (let x = 0; x < keys.length; x++)
527
- ObservableNode.Update(root, keys[x]);
528
- break;
529
- }
530
- case "value":
531
- throw new Error("Unable to replace value type: " + root);
532
- }
533
- return;
534
- }
535
- const pathTuples = [["", root]];
536
- for (let x = 0; x < diffResult.length; x++) {
537
- const { path, value } = diffResult[x];
538
- let y = 0;
539
- for (; y < path.length - 1; y++) {
540
- const property = path[y];
541
- const value = pathTuples[y][1];
542
- const tupleIndex = y + 1;
543
- if (pathTuples.length <= tupleIndex)
544
- pathTuples.push([property, value[property]]);
545
- else if (pathTuples[tupleIndex][0] !== property) {
546
- pathTuples[tupleIndex][0] = property;
547
- pathTuples[tupleIndex][1] = value[property];
548
- const next = tupleIndex + 1;
549
- if (next < pathTuples.length)
550
- pathTuples[next][0] = null;
551
- }
552
- }
553
- const assignValue = pathTuples[y][1];
554
- const assignData = cloneData ? JsonDeepClone(value) : value;
555
- ObservableNode.Assign(assignValue, path[y], assignData);
556
- }
413
+ JsonApplyDiff(root, diffResult, AssignNodeValue);
414
+ // A root replacement is applied in place without calling AssignNodeValue
415
+ if (diffResult.length === 1 && diffResult[0].path.length === 0)
416
+ NodeValueSet(root, "", false, rootNode[NODE_WRAPPER]);
557
417
  }
558
418
  ObservableNode.ApplyDiff = ApplyDiff;
559
419
  /**
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The keys walked from the snapshot's seed to a value: property names, array indices and
3
+ * hidden (symbol) props. A resolved child continues from the path its resolver returns.
4
+ */
5
+ export type SnapshotPath = (string | number | symbol)[];
6
+ /**
7
+ * Resolves a keyed child to the root entity it should be read through, given the child's
8
+ * path. Returns the entity and its path, or undefined when the child isn't keyed (or has no
9
+ * root), in which case the child itself is used. `path` is the walk's own stack, so a
10
+ * resolver must not keep it.
11
+ */
12
+ export type SnapshotResolver = (child: any, path: Readonly<SnapshotPath>) => {
13
+ value: object;
14
+ path: SnapshotPath;
15
+ } | undefined;
16
+ /**
17
+ * Snapshot cache state. Each owner (a `DiffTree` instance, or `ObservableNode` at module
18
+ * level) creates its own state, so cached snapshots are never shared between owners.
19
+ */
20
+ export interface ISnapshotState {
21
+ /** Raw value -> its cached immutable snapshot */
22
+ rawToSnapshotCache: WeakMap<object, any>;
23
+ /** Raw value -> every parent whose cached snapshot embedded it */
24
+ hierarchy: WeakMap<object, Set<object>>;
25
+ /** Default resolver for keyed children; a call to `Snapshot.Get` may override it */
26
+ resolve: SnapshotResolver | undefined;
27
+ }
28
+ export declare namespace Snapshot {
29
+ /**
30
+ * Creates a new, empty snapshot cache.
31
+ * @param resolve Optional default resolver for keyed children.
32
+ * @returns A new snapshot state.
33
+ */
34
+ function Create(resolve?: SnapshotResolver): ISnapshotState;
35
+ /**
36
+ * Returns an immutable, cached snapshot of a raw value. The same snapshot is returned
37
+ * until a write invalidates it, and unchanged nested parts are shared between snapshots,
38
+ * so `===` holds for any part that hasn't changed.
39
+ * @param state The snapshot cache to read from and populate.
40
+ * @param value The raw value to snapshot.
41
+ * @param resolve Resolver for keyed children; defaults to the state's resolver.
42
+ * @param path The value's own path. Snapshots are cached per raw value, so a value must
43
+ * always be seeded with the same path or a resolver may see a different one.
44
+ * @returns The snapshot.
45
+ */
46
+ function Get<T>(state: ISnapshotState, value: T, resolve?: SnapshotResolver | undefined, path?: SnapshotPath): T;
47
+ /**
48
+ * Invalidates the cached snapshot of a raw value that was just written to, along with
49
+ * every cached ancestor that embedded it.
50
+ * @param state The snapshot cache to invalidate.
51
+ * @param value The raw value that was written to.
52
+ */
53
+ function Invalidate(state: ISnapshotState, value: object): void;
54
+ /**
55
+ * The cached snapshot of a raw value, without building one. A write to the value, or to
56
+ * anything its snapshot embeds, clears the cache, so a reader that kept the snapshot it got
57
+ * can tell nothing under the value changed when this returns that same snapshot.
58
+ * @param state The snapshot cache.
59
+ * @param value The raw value.
60
+ * @returns The cached snapshot, or undefined when there's none.
61
+ */
62
+ function Peek<T>(state: ISnapshotState, value: T): T | undefined;
63
+ /**
64
+ * Registers a value as its own snapshot. Use in a case where an immutable value can
65
+ * represent itself as a valid snapshot without cloning.
66
+ * @param state The snapshot cache.
67
+ * @param value The raw value that is its own snapshot.
68
+ */
69
+ function Register(state: ISnapshotState, value: object): void;
70
+ }