j-templates 8.1.1 → 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.
@@ -0,0 +1,141 @@
1
+ import { JsonType } from "../../Utils/json";
2
+ function CreateSnapshotLink(state, child, parent) {
3
+ let parents = state.hierarchy.get(child);
4
+ if (!parents)
5
+ state.hierarchy.set(child, (parents = new Set()));
6
+ parents.add(parent);
7
+ }
8
+ /**
9
+ * Clears the cached snapshot for a raw value that was just written to, and climbs to
10
+ * every parent whose own cached snapshot embedded it, so a write to a deeply nested value
11
+ * invalidates every cached ancestor that still holds a snapshot.
12
+ */
13
+ function InvalidateValue(state, value) {
14
+ state.rawToSnapshotCache.delete(value);
15
+ const parents = state.hierarchy.get(value);
16
+ if (parents === undefined)
17
+ return;
18
+ state.hierarchy.delete(value);
19
+ for (const parent of parents)
20
+ if (state.rawToSnapshotCache.has(parent))
21
+ InvalidateValue(state, parent);
22
+ }
23
+ /**
24
+ * An immutable snapshot of a raw value (arrays and plain objects; other values are
25
+ * returned as they are). A snapshot is cached per raw value and returned as-is until a
26
+ * write invalidates it. A rebuild makes new containers but reuses the cached snapshots of
27
+ * unchanged children, so a snapshot handed out never changes and unchanged parts are
28
+ * shared between snapshots. Keyed children are read through their root.
29
+ */
30
+ function CreateSnapshot(state, resolve, value, path) {
31
+ if (JsonType(value) === "value")
32
+ return value;
33
+ const cached = state.rawToSnapshotCache.get(value);
34
+ if (cached !== undefined)
35
+ return cached;
36
+ let snapshot;
37
+ if (Array.isArray(value)) {
38
+ snapshot = new Array(value.length);
39
+ for (let x = 0; x < value.length; x++)
40
+ snapshot[x] = ChildSnapshot(state, resolve, value, x, path);
41
+ }
42
+ else {
43
+ snapshot = {};
44
+ const keys = Object.keys(value);
45
+ for (let x = 0; x < keys.length; x++)
46
+ snapshot[keys[x]] = ChildSnapshot(state, resolve, value, keys[x], path);
47
+ }
48
+ state.rawToSnapshotCache.set(value, snapshot);
49
+ return snapshot;
50
+ }
51
+ /**
52
+ * A child's snapshot, linking the child to its parent so a write to the child
53
+ * invalidates the parent. A keyed child's snapshot comes from its root, so the root is
54
+ * linked too; the parent's own embedded copy stays linked because a positional write
55
+ * (one that changes which entity a slot holds) mutates that copy in place.
56
+ */
57
+ function ChildSnapshot(state, resolve, parent, key, path) {
58
+ const child = parent[key];
59
+ if (JsonType(child) === "value")
60
+ return child;
61
+ CreateSnapshotLink(state, child, parent);
62
+ path.push(key);
63
+ try {
64
+ const resolved = resolve?.(child, path);
65
+ if (resolved !== undefined && resolved.value !== child) {
66
+ CreateSnapshotLink(state, resolved.value, parent);
67
+ return CreateSnapshot(state, resolve, resolved.value, resolved.path);
68
+ }
69
+ return CreateSnapshot(state, resolve, child, path);
70
+ }
71
+ finally {
72
+ path.pop();
73
+ }
74
+ }
75
+ function RegisterSnapshot(state, snapshot) {
76
+ if (JsonType(snapshot) !== 'value')
77
+ state.rawToSnapshotCache.set(snapshot, snapshot);
78
+ }
79
+ export var Snapshot;
80
+ (function (Snapshot) {
81
+ /**
82
+ * Creates a new, empty snapshot cache.
83
+ * @param resolve Optional default resolver for keyed children.
84
+ * @returns A new snapshot state.
85
+ */
86
+ function Create(resolve) {
87
+ return {
88
+ rawToSnapshotCache: new WeakMap(),
89
+ hierarchy: new WeakMap(),
90
+ resolve,
91
+ };
92
+ }
93
+ Snapshot.Create = Create;
94
+ /**
95
+ * Returns an immutable, cached snapshot of a raw value. The same snapshot is returned
96
+ * until a write invalidates it, and unchanged nested parts are shared between snapshots,
97
+ * so `===` holds for any part that hasn't changed.
98
+ * @param state The snapshot cache to read from and populate.
99
+ * @param value The raw value to snapshot.
100
+ * @param resolve Resolver for keyed children; defaults to the state's resolver.
101
+ * @param path The value's own path. Snapshots are cached per raw value, so a value must
102
+ * always be seeded with the same path or a resolver may see a different one.
103
+ * @returns The snapshot.
104
+ */
105
+ function Get(state, value, resolve = state.resolve, path = []) {
106
+ return CreateSnapshot(state, resolve, value, path.slice());
107
+ }
108
+ Snapshot.Get = Get;
109
+ /**
110
+ * Invalidates the cached snapshot of a raw value that was just written to, along with
111
+ * every cached ancestor that embedded it.
112
+ * @param state The snapshot cache to invalidate.
113
+ * @param value The raw value that was written to.
114
+ */
115
+ function Invalidate(state, value) {
116
+ InvalidateValue(state, value);
117
+ }
118
+ Snapshot.Invalidate = Invalidate;
119
+ /**
120
+ * The cached snapshot of a raw value, without building one. A write to the value, or to
121
+ * anything its snapshot embeds, clears the cache, so a reader that kept the snapshot it got
122
+ * can tell nothing under the value changed when this returns that same snapshot.
123
+ * @param state The snapshot cache.
124
+ * @param value The raw value.
125
+ * @returns The cached snapshot, or undefined when there's none.
126
+ */
127
+ function Peek(state, value) {
128
+ return state.rawToSnapshotCache.get(value);
129
+ }
130
+ Snapshot.Peek = Peek;
131
+ /**
132
+ * Registers a value as its own snapshot. Use in a case where an immutable value can
133
+ * represent itself as a valid snapshot without cloning.
134
+ * @param state The snapshot cache.
135
+ * @param value The raw value that is its own snapshot.
136
+ */
137
+ function Register(state, value) {
138
+ RegisterSnapshot(state, value);
139
+ }
140
+ Snapshot.Register = Register;
141
+ })(Snapshot || (Snapshot = {}));
package/Utils/json.d.ts CHANGED
@@ -1,13 +1,19 @@
1
+ export type JsonWrite = {
2
+ type: "write";
3
+ path: (string | number)[];
4
+ value: unknown;
5
+ };
6
+ export type JsonMove = {
7
+ type: "move";
8
+ path: (string | number)[];
9
+ sourceProp: string | number;
10
+ targetProp: string | number;
11
+ };
1
12
  /**
2
13
  * Result of a JSON diff operation, containing paths and values that changed.
3
14
  * @template T - The type of the JSON value being diffed
4
15
  */
5
- export type JsonDiffResult<T = unknown> = {
6
- /** Array path segments (strings for object keys, numbers for array indices) */
7
- path: (string | number)[];
8
- /** The new value at this path */
9
- value: unknown;
10
- }[];
16
+ export type JsonDiffResult<T = unknown> = (JsonMove | JsonWrite)[];
11
17
  /**
12
18
  * Determines the JSON type of a value.
13
19
  * @param value - The value to check
@@ -31,9 +37,33 @@ export declare function JsonMerge(source: unknown, patch: unknown): unknown;
31
37
  * @returns A deep clone of the value
32
38
  */
33
39
  export declare function JsonDeepClone<T>(value: T, type?: string): T;
40
+ /**
41
+ * Applies a diff result to a root value in place: moves first, then writes.
42
+ * @param resolve - Returns the object to walk into for `parent[prop]`; the walk stores it back at
43
+ * `parent[prop]` when it's a different object
44
+ * @param assign - Assigns a written value (already cloned) to its parent
45
+ * @param move - Assigns a moved value to its new slot. A move relocates a value that's already in
46
+ * the root, so it's kept separate from `assign`, which handles new values.
47
+ */
48
+ export declare function JsonApplyDiff(root: any, diff: JsonDiffResult, assign?: (parent: any, prop: string | number, value: any) => void): void;
49
+ /**
50
+ * Applies a diff result to a root value in place. A diff doesn't own its values (see JsonDiff),
51
+ * so each value is deep-cloned before it's written: the root never shares an object with the
52
+ * diff, the value it was computed from, or another record.
53
+ * - A single record with an empty path replaces the root's contents, keeping its identity
54
+ * - Otherwise each record's value is assigned at its path
55
+ * @param root - The value to update; must be an object or array
56
+ * @param diff - The diff result to apply
57
+ * @param callback - Called with the object written to, the property written ("" when an array
58
+ * root is replaced), and whether that property is new to the object
59
+ * @throws If a root replacement would change the root's JSON type, or the root is a primitive
60
+ */
61
+ export declare function JsonApplyWrites(root: any, diff: JsonWrite[], assign?: (parent: any, prop: string | number, value: any) => void): void;
34
62
  /**
35
63
  * Computes the differences between two JSON values.
36
- * Returns an array of paths and values that changed from oldValue to newValue.
64
+ * Returns an array of paths and values that changed from oldValue to newValue. A record doesn't
65
+ * own its value: it references `newValue`'s objects, so two records can share one, and
66
+ * JsonApplyDiff clones each value as it's applied.
37
67
  * @template T - The type of the JSON values being compared
38
68
  * @param newValue - The new value
39
69
  * @param oldValue - The old value to compare against
package/Utils/json.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ArraysEqual } from "./array";
1
2
  const jsonProto = Object.getPrototypeOf({});
2
3
  const arrayProto = Object.getPrototypeOf([]);
3
4
  const strProto = Object.getPrototypeOf("");
@@ -94,9 +95,124 @@ export function JsonDeepClone(value, type = JsonType(value)) {
94
95
  return value;
95
96
  }
96
97
  }
98
+ /**
99
+ * Applies a diff result to a root value in place: moves first, then writes.
100
+ * @param resolve - Returns the object to walk into for `parent[prop]`; the walk stores it back at
101
+ * `parent[prop]` when it's a different object
102
+ * @param assign - Assigns a written value (already cloned) to its parent
103
+ * @param move - Assigns a moved value to its new slot. A move relocates a value that's already in
104
+ * the root, so it's kept separate from `assign`, which handles new values.
105
+ */
106
+ export function JsonApplyDiff(root, diff,
107
+ /* resolve?: (parent: any, prop: string | number) => any, */
108
+ assign) {
109
+ const moves = diff.filter(function (diff) {
110
+ return diff.type === "move";
111
+ });
112
+ JsonApplyMoves(root, moves, assign);
113
+ const writes = diff.filter(function (diff) {
114
+ return diff.type === "write";
115
+ });
116
+ JsonApplyWrites(root, writes, assign);
117
+ }
118
+ function DefaultAssign(parent, prop, value) {
119
+ parent[prop] = value;
120
+ }
121
+ /**
122
+ * Applies a diff result to a root value in place. A diff doesn't own its values (see JsonDiff),
123
+ * so each value is deep-cloned before it's written: the root never shares an object with the
124
+ * diff, the value it was computed from, or another record.
125
+ * - A single record with an empty path replaces the root's contents, keeping its identity
126
+ * - Otherwise each record's value is assigned at its path
127
+ * @param root - The value to update; must be an object or array
128
+ * @param diff - The diff result to apply
129
+ * @param callback - Called with the object written to, the property written ("" when an array
130
+ * root is replaced), and whether that property is new to the object
131
+ * @throws If a root replacement would change the root's JSON type, or the root is a primitive
132
+ */
133
+ export function JsonApplyWrites(root, diff, assign = DefaultAssign) {
134
+ const pathTuples = [["", root]];
135
+ if (diff.length === 1 && diff[0].path.length === 0) {
136
+ const diffRow = diff[0];
137
+ const diffPatch = JsonDeepClone(diffRow.value);
138
+ const type = JsonType(root);
139
+ const patchType = JsonType(diffPatch);
140
+ if (type !== patchType)
141
+ throw new Error("Unable to change type of root value");
142
+ switch (type) {
143
+ case "array": {
144
+ root.splice(0, root.length, ...diffPatch);
145
+ break;
146
+ }
147
+ case "object": {
148
+ const keys = Object.keys(root);
149
+ const patchKeys = Object.keys(diffPatch);
150
+ for (let x = 0; x < keys.length; x++)
151
+ if (!patchKeys.includes(keys[x]))
152
+ delete root[keys[x]];
153
+ Object.assign(root, diffPatch);
154
+ break;
155
+ }
156
+ case "value":
157
+ throw new Error("Unable to replace root value type: " + root);
158
+ }
159
+ return;
160
+ }
161
+ for (let x = 0; x < diff.length; x++) {
162
+ const { path, value } = diff[x];
163
+ let y = 0;
164
+ for (; y < path.length - 1; y++) {
165
+ const property = path[y];
166
+ const tupleIndex = y + 1;
167
+ if (tupleIndex >= pathTuples.length || pathTuples[tupleIndex][0] !== property) {
168
+ const parent = pathTuples[y][1];
169
+ const child = parent[property];
170
+ if (tupleIndex < pathTuples.length) {
171
+ pathTuples[tupleIndex] = [property, child];
172
+ const next = tupleIndex + 1;
173
+ if (next < pathTuples.length)
174
+ pathTuples[next][0] = null;
175
+ }
176
+ else
177
+ pathTuples.push([property, child]);
178
+ }
179
+ }
180
+ if (y >= path.length - 1) {
181
+ const assignValue = pathTuples[y][1];
182
+ const assignData = JsonDeepClone(value);
183
+ assign(assignValue, path[y], assignData);
184
+ }
185
+ }
186
+ }
187
+ function ReadPath(value, path) {
188
+ let curr = value;
189
+ let x = 0;
190
+ for (; curr && x < path.length; x++)
191
+ curr = curr[path[x]];
192
+ return curr;
193
+ }
194
+ function JsonApplyMoves(root, diff, move = DefaultAssign) {
195
+ if (diff.length === 0)
196
+ return;
197
+ let moveRoot = null;
198
+ let basePath = null;
199
+ let copy = null;
200
+ for (let x = diff.length - 1; x >= 0; x--) {
201
+ if (!ArraysEqual(diff[x].path, basePath)) {
202
+ basePath = diff[x].path;
203
+ moveRoot = ReadPath(root, diff[x].path); //, resolve);
204
+ const type = JsonType(moveRoot);
205
+ copy = type === "array" ? moveRoot.slice() : type === "object" ? Object.assign({}, moveRoot) : root;
206
+ }
207
+ const sourceValue = copy[diff[x].sourceProp];
208
+ move(moveRoot, diff[x].targetProp, sourceValue);
209
+ }
210
+ }
97
211
  /**
98
212
  * Computes the differences between two JSON values.
99
- * Returns an array of paths and values that changed from oldValue to newValue.
213
+ * Returns an array of paths and values that changed from oldValue to newValue. A record doesn't
214
+ * own its value: it references `newValue`'s objects, so two records can share one, and
215
+ * JsonApplyDiff clones each value as it's applied.
100
216
  * @template T - The type of the JSON values being compared
101
217
  * @param newValue - The new value
102
218
  * @param oldValue - The old value to compare against
@@ -108,6 +224,11 @@ export function JsonDiff(newValue, oldValue, rootPath, initResult) {
108
224
  const result = initResult ?? [];
109
225
  const startPath = rootPath ? rootPath.split(".") : [];
110
226
  JsonDiffRecursive(startPath, newValue, oldValue, result);
227
+ result.sort(function (a, b) {
228
+ const aNum = a.type === "write" ? 1 : 0;
229
+ const bNum = b.type === "write" ? 1 : 0;
230
+ return aNum - bNum;
231
+ });
111
232
  return result;
112
233
  }
113
234
  /**
@@ -140,6 +261,7 @@ function JsonDiffRecursive(path, newValue, oldValue, resp) {
140
261
  if (allChildrenChanged) {
141
262
  resp.length > changedPathLength && resp.splice(changedPathLength);
142
263
  resp.push({
264
+ type: "write",
143
265
  path,
144
266
  value: newValue,
145
267
  });
@@ -147,6 +269,7 @@ function JsonDiffRecursive(path, newValue, oldValue, resp) {
147
269
  }
148
270
  return false;
149
271
  }
272
+ const MOVE_SYMBOL = Symbol("MOVE_SYMBOL");
150
273
  /**
151
274
  * Compares two arrays and records differences.
152
275
  * @internal
@@ -160,24 +283,38 @@ function JsonDiffArrays(path, newValue, oldValue, resp) {
160
283
  if (oldValue.length === 0 || newValue.length === 0) {
161
284
  return oldValue.length !== newValue.length;
162
285
  }
163
- let allChildrenChanged = true;
164
- if (newValue.length > 0 || oldValue.length > 0) {
165
- for (let y = 0; y < newValue.length; y++) {
166
- const arrayPath = path.concat(y);
167
- const oldValueElem = oldValue[y];
168
- allChildrenChanged =
169
- JsonDiffRecursive(arrayPath, newValue[y], oldValueElem, resp) &&
170
- allChildrenChanged;
286
+ let startIndex = 0;
287
+ for (; startIndex < newValue.length && startIndex < oldValue.length && newValue[startIndex] === oldValue[startIndex]; startIndex++) { }
288
+ // Arrays are equal, no diffing required
289
+ if (startIndex === newValue.length && startIndex === oldValue.length)
290
+ return false;
291
+ const newArraySlice = newValue.slice();
292
+ const oldArraySlice = oldValue.slice();
293
+ for (let x = startIndex; x < oldValue.length; x++) {
294
+ const newIndex = newValue.indexOf(oldValue[x], startIndex);
295
+ if (newIndex >= 0 && newIndex !== x) {
296
+ oldArraySlice[x] = MOVE_SYMBOL;
297
+ newArraySlice[newIndex] = MOVE_SYMBOL;
298
+ resp.push({
299
+ type: "move",
300
+ path: path,
301
+ sourceProp: x,
302
+ targetProp: newIndex
303
+ });
171
304
  }
172
305
  }
173
- else
174
- allChildrenChanged = false;
175
- if (newValue.length !== oldValue.length)
306
+ for (let x = startIndex; x < newValue.length; x++) {
307
+ const arrayPath = path.concat(x);
308
+ if (newArraySlice[x] !== MOVE_SYMBOL)
309
+ JsonDiffRecursive(arrayPath, newArraySlice[x], oldArraySlice[x], resp);
310
+ }
311
+ if (oldValue.length !== newValue.length)
176
312
  resp.push({
313
+ type: "write",
177
314
  path: path.concat("length"),
178
315
  value: newValue.length,
179
316
  });
180
- return allChildrenChanged;
317
+ return false;
181
318
  }
182
319
  /**
183
320
  * Compares two objects and records differences.
@@ -207,6 +344,7 @@ function JsonDiffObjects(path, newValue, oldValue, resp) {
207
344
  }
208
345
  else if (newValue[newKeys[newKeyIndex]] !== undefined) {
209
346
  resp.push({
347
+ type: "write",
210
348
  path: childPath,
211
349
  value: newValue[newKeys[newKeyIndex]],
212
350
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "j-templates",
3
- "version": "8.1.1",
3
+ "version": "8.2.0",
4
4
  "description": "j-templates",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/TypesInCode/jTemplates",
@@ -1,36 +0,0 @@
1
- import { JsonDiffResult } from "../../Utils/json";
2
- import { DiffSpliceResult, DiffTreeProjectionMap, IDiffTree } from "./diffTree.types";
3
- /**
4
- * Synchronous diff implementation.
5
- * Computes diffs immediately without queuing or workers.
6
- *
7
- * @see StoreSync
8
- * @see StoreAsync
9
- */
10
- export declare class DiffSync implements IDiffTree {
11
- private diffTree;
12
- /**
13
- * Creates a DiffSync instance.
14
- * @param keyFunc - Optional function to extract a key from objects
15
- * @param projections - Optional map of derived-value projections, keyed by id
16
- */
17
- constructor(keyFunc?: {
18
- (val: any): string;
19
- }, projections?: DiffTreeProjectionMap);
20
- /**
21
- * Computes the diff between a new value and the current value at a path.
22
- * @param path - Dot-separated path to the value
23
- * @param value - The new value to compare
24
- * @returns Diff results showing changes
25
- */
26
- DiffPath(path: string, value: any): {
27
- path: (string | number)[];
28
- value: unknown;
29
- }[];
30
- PatchPath(path: string, patch: any): {
31
- path: (string | number)[];
32
- value: unknown;
33
- }[];
34
- PushPath(path: string, items: any[]): JsonDiffResult;
35
- SplicePath(path: string, start: number, deleteCount: number | undefined, items: any[]): DiffSpliceResult;
36
- }
@@ -1,41 +0,0 @@
1
- import { DiffTree } from "./diffTree";
2
- /**
3
- * Synchronous diff implementation.
4
- * Computes diffs immediately without queuing or workers.
5
- *
6
- * @see StoreSync
7
- * @see StoreAsync
8
- */
9
- export class DiffSync {
10
- /**
11
- * Creates a DiffSync instance.
12
- * @param keyFunc - Optional function to extract a key from objects
13
- * @param projections - Optional map of derived-value projections, keyed by id
14
- */
15
- constructor(keyFunc, projections) {
16
- this.diffTree = new DiffTree(keyFunc, projections);
17
- }
18
- /**
19
- * Computes the diff between a new value and the current value at a path.
20
- * @param path - Dot-separated path to the value
21
- * @param value - The new value to compare
22
- * @returns Diff results showing changes
23
- */
24
- DiffPath(path, value) {
25
- const diff = this.diffTree.DiffPath(path, value);
26
- return diff.concat(this.diffTree.RunProjections());
27
- }
28
- PatchPath(path, patch) {
29
- const diff = this.diffTree.PatchPath(path, patch);
30
- return diff.concat(this.diffTree.RunProjections());
31
- }
32
- PushPath(path, items) {
33
- const diff = this.diffTree.PushPath(path, items);
34
- return diff.concat(this.diffTree.RunProjections());
35
- }
36
- SplicePath(path, start, deleteCount, items) {
37
- const spliceResult = this.diffTree.SplicePath(path, start, deleteCount, items);
38
- spliceResult.diffResult.push(...this.diffTree.RunProjections());
39
- return spliceResult;
40
- }
41
- }