j-templates 8.0.10 → 8.0.13

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.
@@ -1,4 +1,4 @@
1
- import { DiffSpliceResult, IDiffTree } from "./diffTree";
1
+ import { DiffSpliceResult, DiffTreeProjectionMap, IDiffTree } from "./diffTree";
2
2
  import { JsonDiffResult } from "../../Utils/json";
3
3
  /**
4
4
  * Async version of IDiffTree interface with all methods returning promises.
@@ -18,10 +18,13 @@ export declare class DiffAsync implements IDiffTreeAsync {
18
18
  /**
19
19
  * Creates a DiffAsync instance and initializes the worker.
20
20
  * @param keyFunc - Optional function to extract a key from objects
21
+ * @param projections - Optional map of derived-value projections, keyed by id. Each
22
+ * projection's function is serialized with `.toString()` and `eval`'d back into a
23
+ * function in the worker, the same mechanic used for `keyFunc`.
21
24
  */
22
25
  constructor(keyFunc?: {
23
26
  (val: any): string;
24
- });
27
+ }, projections?: DiffTreeProjectionMap);
25
28
  /**
26
29
  * Computes the diff between a new value and the current value at a path asynchronously.
27
30
  * @param path - Dot-separated path to the value
@@ -3,6 +3,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DiffAsync = void 0;
4
4
  const workerQueue_1 = require("./workerQueue");
5
5
  const diffWorker_1 = require("./diffWorker");
6
+ /**
7
+ * Serializes a `DiffTreeProjectionMap` for the worker boundary: `reads` survives structured
8
+ * clone as-is, and each `projection` function is serialized with `.toString()`, the same
9
+ * mechanic used below for `keyFunc`.
10
+ */
11
+ function SerializeProjections(projections) {
12
+ const serialized = {};
13
+ const ids = Object.keys(projections);
14
+ for (let x = 0; x < ids.length; x++) {
15
+ const id = ids[x];
16
+ serialized[id] = {
17
+ reads: projections[id].reads,
18
+ projection: projections[id].projection.toString(),
19
+ };
20
+ }
21
+ return serialized;
22
+ }
6
23
  /**
7
24
  * Asynchronous diff implementation using web workers.
8
25
  * Offloads diff computation to a worker to prevent blocking the main thread.
@@ -14,12 +31,18 @@ class DiffAsync {
14
31
  /**
15
32
  * Creates a DiffAsync instance and initializes the worker.
16
33
  * @param keyFunc - Optional function to extract a key from objects
34
+ * @param projections - Optional map of derived-value projections, keyed by id. Each
35
+ * projection's function is serialized with `.toString()` and `eval`'d back into a
36
+ * function in the worker, the same mechanic used for `keyFunc`.
17
37
  */
18
- constructor(keyFunc) {
38
+ constructor(keyFunc, projections) {
19
39
  this.workerQueue = new workerQueue_1.WorkerQueue(diffWorker_1.DiffWorker.Create());
20
40
  this.workerQueue.Push({
21
41
  method: "create",
22
- arguments: keyFunc ? [keyFunc.toString()] : [],
42
+ arguments: [
43
+ keyFunc ? keyFunc.toString() : undefined,
44
+ projections ? SerializeProjections(projections) : undefined,
45
+ ],
23
46
  });
24
47
  }
25
48
  /**
@@ -1,4 +1,4 @@
1
- import { DiffSpliceResult, IDiffTree } from "./diffTree";
1
+ import { DiffSpliceResult, DiffTreeProjectionMap, IDiffTree } from "./diffTree";
2
2
  /**
3
3
  * Synchronous diff implementation.
4
4
  * Computes diffs immediately without queuing or workers.
@@ -11,10 +11,11 @@ export declare class DiffSync implements IDiffTree {
11
11
  /**
12
12
  * Creates a DiffSync instance.
13
13
  * @param keyFunc - Optional function to extract a key from objects
14
+ * @param projections - Optional map of derived-value projections, keyed by id
14
15
  */
15
16
  constructor(keyFunc?: {
16
17
  (val: any): string;
17
- });
18
+ }, projections?: DiffTreeProjectionMap);
18
19
  /**
19
20
  * Computes the diff between a new value and the current value at a path.
20
21
  * @param path - Dot-separated path to the value
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DiffSync = void 0;
4
+ const array_1 = require("../../Utils/array");
4
5
  const json_1 = require("../../Utils/json");
5
6
  const diffTree_1 = require("./diffTree");
6
- const diffCnstr = (0, diffTree_1.DiffTreeFactory)(json_1.JsonDiffFactory);
7
+ const diffCnstr = (0, diffTree_1.DiffTreeFactory)(json_1.JsonDiffFactory, array_1.ArraysEqual);
7
8
  /**
8
9
  * Synchronous diff implementation.
9
10
  * Computes diffs immediately without queuing or workers.
@@ -15,9 +16,10 @@ class DiffSync {
15
16
  /**
16
17
  * Creates a DiffSync instance.
17
18
  * @param keyFunc - Optional function to extract a key from objects
19
+ * @param projections - Optional map of derived-value projections, keyed by id
18
20
  */
19
- constructor(keyFunc) {
20
- this.diffTree = new diffCnstr(keyFunc);
21
+ constructor(keyFunc, projections) {
22
+ this.diffTree = new diffCnstr(keyFunc, projections);
21
23
  }
22
24
  /**
23
25
  * Computes the diff between a new value and the current value at a path.
@@ -8,6 +8,40 @@ export interface IDiffMethod {
8
8
  /** Arguments for the method call */
9
9
  arguments: Array<any>;
10
10
  }
11
+ /**
12
+ * A pure function computing a derived value from the current values at `reads`, passed
13
+ * positionally in the same order, so each parameter can be typed at the call site instead of
14
+ * cast out of a generic read.
15
+ */
16
+ export type DiffTreeProjectionFunc = (...values: any[]) => any;
17
+ /**
18
+ * A projection's declaration: the dot-separated paths it reads (a plain tree path, an
19
+ * entity's key, or another projection's `$projection_<id>` key for chaining) and the
20
+ * function computing its result from their current values, positionally. Declaring `reads`
21
+ * up front lets the tree topologically sort projections and reject a dependency cycle at
22
+ * construction instead of on first write.
23
+ */
24
+ export type DiffTreeProjectionDefinition = {
25
+ reads: string[];
26
+ projection: DiffTreeProjectionFunc;
27
+ };
28
+ /** A map of projection id to its declaration, keyed by the id used in `$projection_<id>`. */
29
+ export type DiffTreeProjectionMap = {
30
+ [id: string]: DiffTreeProjectionDefinition;
31
+ };
32
+ /**
33
+ * The wire form of a `DiffTreeProjectionMap` sent to the diff worker: `reads` survives
34
+ * structured clone as-is, and `projection` is serialized with `.toString()`, the same
35
+ * mechanic `DiffAsync` already uses to send `keyFunc` across the worker boundary.
36
+ */
37
+ export type DiffTreeSerializedProjectionMap = {
38
+ [id: string]: {
39
+ reads: string[];
40
+ projection: string;
41
+ };
42
+ };
43
+ /** Reserved key prefix a projection's result is stored under: `$projection_<id>`. */
44
+ export declare const PROJECTION_PREFIX = "$projection_";
11
45
  export type DiffSpliceResult<T = unknown> = {
12
46
  /** Array path segments (strings for object keys, numbers for array indices) */
13
47
  path: (string | number)[];
@@ -46,10 +80,11 @@ export interface IDiffTreeConstructor {
46
80
  /**
47
81
  * Creates a new IDiffTree instance.
48
82
  * @param keyFunc - Optional function to extract a key from a value
83
+ * @param projections - Optional map of derived-value projections, keyed by id
49
84
  */
50
85
  new (keyFunc?: {
51
86
  (val: any): string;
52
- }): IDiffTree;
87
+ }, projections?: DiffTreeProjectionMap): IDiffTree;
53
88
  }
54
89
  /**
55
90
  * Factory function that creates a DiffTree class.
@@ -58,4 +93,4 @@ export interface IDiffTreeConstructor {
58
93
  * @param worker - If true, sets up worker message handling
59
94
  * @returns DiffTree constructor
60
95
  */
61
- export declare function DiffTreeFactory(jsonDiffFactory?: () => JsonDiffFactoryResult, worker?: boolean): IDiffTreeConstructor;
96
+ export declare function DiffTreeFactory(jsonDiffFactory: () => JsonDiffFactoryResult, arraysEqual: (left: any[], right: any[]) => boolean, worker?: boolean): IDiffTreeConstructor;
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PROJECTION_PREFIX = void 0;
3
4
  exports.DiffTreeFactory = DiffTreeFactory;
5
+ /** Reserved key prefix a projection's result is stored under: `$projection_<id>`. */
6
+ exports.PROJECTION_PREFIX = "$projection_";
4
7
  /**
5
8
  * Factory function that creates a DiffTree class.
6
9
  * Can operate in worker mode for async operations.
@@ -8,8 +11,24 @@ exports.DiffTreeFactory = DiffTreeFactory;
8
11
  * @param worker - If true, sets up worker message handling
9
12
  * @returns DiffTree constructor
10
13
  */
11
- function DiffTreeFactory(jsonDiffFactory, worker) {
14
+ function DiffTreeFactory(jsonDiffFactory, arraysEqual, worker) {
12
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
+ }
13
32
  const ctx = this;
14
33
  if (worker && ctx) {
15
34
  let diffTree = null;
@@ -20,7 +39,10 @@ function DiffTreeFactory(jsonDiffFactory, worker) {
20
39
  const keyFunc = data.arguments[0]
21
40
  ? eval(data.arguments[0])
22
41
  : undefined;
23
- diffTree = new DiffTree(keyFunc);
42
+ const projections = data.arguments[1]
43
+ ? DeserializeProjections(data.arguments[1])
44
+ : undefined;
45
+ diffTree = new DiffTree(keyFunc, projections);
24
46
  ctx.postMessage(null);
25
47
  break;
26
48
  }
@@ -81,12 +103,19 @@ function DiffTreeFactory(jsonDiffFactory, worker) {
81
103
  */
82
104
  function GetPathValue(source, path) {
83
105
  if (path === "")
84
- return source;
106
+ return [source, true];
85
107
  const parts = path.split(".");
86
108
  let curr = source;
87
- for (let x = 0; x < parts.length; x++)
109
+ let x = 0;
110
+ for (; curr && x < parts.length - 1; x++)
88
111
  curr = curr[parts[x]];
89
- return curr;
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];
90
119
  }
91
120
  /**
92
121
  * Sets a value in a source object at the specified path.
@@ -102,6 +131,165 @@ function DiffTreeFactory(jsonDiffFactory, worker) {
102
131
  for (; x < path.length - 1; x++)
103
132
  curr = curr[path[x]];
104
133
  curr[path[x]] = value;
134
+ InvalidateSnapshotForValue(curr);
135
+ }
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);
143
+ }
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)
154
+ return;
155
+ valueSnapshotHierarchy.delete(value);
156
+ for (const parent of parents)
157
+ if (valueSnapshotCache.has(parent))
158
+ InvalidateSnapshotForValue(parent);
159
+ }
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]);
179
+ }
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]]);
185
+ }
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
+ }
207
+ }
208
+ }
209
+ return CreateSnapshot(source, keyFunc, child);
210
+ }
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 });
236
+ }
237
+ const ids = Object.keys(projections);
238
+ for (let x = 0; x < ids.length; x++)
239
+ Visit(ids[x], []);
240
+ return order;
241
+ }
242
+ /**
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
268
+ */
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;
105
293
  }
106
294
  /**
107
295
  * Resolves a path to its keyed root object if applicable.
@@ -147,7 +335,7 @@ function DiffTreeFactory(jsonDiffFactory, worker) {
147
335
  diffResult.push(...keyDiffResult);
148
336
  }
149
337
  }
150
- const sourceValue = GetPathValue(source, path);
338
+ const [sourceValue] = GetPathValue(source, path);
151
339
  JsonDiff(value, sourceValue, path, diffResult);
152
340
  if (flatten && keyFunc) {
153
341
  let flattened = {};
@@ -164,7 +352,9 @@ function DiffTreeFactory(jsonDiffFactory, worker) {
164
352
  return diffResult;
165
353
  }
166
354
  function SpliceSource(source, path, start, deleteCount, items, flatten, keyFunc) {
167
- const sourceValue = GetPathValue(source, path);
355
+ const [sourceValue, found] = GetPathValue(source, path);
356
+ if (!found)
357
+ throw `Value not found at path ${path}`;
168
358
  if (!Array.isArray(sourceValue))
169
359
  throw `Value found at path ${path} is not an array`;
170
360
  deleteCount ??= sourceValue.length - start;
@@ -176,6 +366,7 @@ function DiffTreeFactory(jsonDiffFactory, worker) {
176
366
  diffResult: []
177
367
  };
178
368
  spliceResult.spliceResult = sourceValue.splice(start, deleteCount, ...items);
369
+ InvalidateSnapshotForValue(sourceValue);
179
370
  if (flatten && keyFunc) {
180
371
  // Register the keyed entities the new items carry, diffed against the roots
181
372
  // already held: a new entity is added whole, one re-inserted unchanged (an undo)
@@ -200,10 +391,19 @@ function DiffTreeFactory(jsonDiffFactory, worker) {
200
391
  /**
201
392
  * Creates a DiffTree instance.
202
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.
203
396
  */
204
- constructor(keyFunc) {
397
+ constructor(keyFunc, projections) {
205
398
  this.keyFunc = keyFunc;
206
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);
207
407
  }
208
408
  /**
209
409
  * Computes diffs for a batch of path/value pairs.
@@ -212,8 +412,9 @@ function DiffTreeFactory(jsonDiffFactory, worker) {
212
412
  */
213
413
  DiffBatch(data) {
214
414
  const results = data
215
- .map(({ path, value }) => this.DiffPath(path, value))
415
+ .map(({ path, value }) => UpdateSource(this.rootState, path, value, true, this.keyFunc))
216
416
  .flat(1);
417
+ results.push(...this.RunProjections());
217
418
  return results;
218
419
  }
219
420
  /**
@@ -223,10 +424,14 @@ function DiffTreeFactory(jsonDiffFactory, worker) {
223
424
  * @returns Diff results showing changes
224
425
  */
225
426
  DiffPath(path, value, flatten = true) {
226
- return UpdateSource(this.rootState, path, value, flatten, this.keyFunc);
427
+ const results = UpdateSource(this.rootState, path, value, flatten, this.keyFunc);
428
+ results.push(...this.RunProjections());
429
+ return results;
227
430
  }
228
431
  SplicePath(path, start, deleteCount, items, flatten = true) {
229
- return SpliceSource(this.rootState, path, start, deleteCount, items, flatten, this.keyFunc);
432
+ const spliceResult = SpliceSource(this.rootState, path, start, deleteCount, items, flatten, this.keyFunc);
433
+ spliceResult.diffResult.push(...this.RunProjections());
434
+ return spliceResult;
230
435
  }
231
436
  /**
232
437
  * Retrieves the current value at a path.
@@ -234,7 +439,19 @@ function DiffTreeFactory(jsonDiffFactory, worker) {
234
439
  * @returns The value at the specified path
235
440
  */
236
441
  GetPath(path) {
237
- return GetPathValue(this.rootState, 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);
238
455
  }
239
456
  }
240
457
  return DiffTree;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DiffWorker = void 0;
4
+ const array_1 = require("../../Utils/array");
4
5
  const json_1 = require("../../Utils/json");
5
6
  const diffTree_1 = require("./diffTree");
6
7
  var DiffWorker;
@@ -9,7 +10,7 @@ var DiffWorker;
9
10
  let workerParameter = null;
10
11
  if (typeof Worker !== "undefined") {
11
12
  workerConstructor = Worker;
12
- workerParameter = URL.createObjectURL(new Blob([`(${diffTree_1.DiffTreeFactory}).call(this, (${json_1.JsonDiffFactory}), true)`]));
13
+ workerParameter = URL.createObjectURL(new Blob([`(${diffTree_1.DiffTreeFactory}).call(this, (${json_1.JsonDiffFactory}), (${array_1.ArraysEqual}), true)`]));
13
14
  }
14
15
  function Create() {
15
16
  if (!workerConstructor)
@@ -1,3 +1,4 @@
1
+ import { DiffTreeProjectionMap } from "../Diff/diffTree";
1
2
  import { Store } from "./store";
2
3
  /**
3
4
  * StoreAsync class extends the base Store class to provide asynchronous data management operations.
@@ -45,8 +46,13 @@ export declare class StoreAsync extends Store {
45
46
  /**
46
47
  * Creates an instance of StoreAsync.
47
48
  * @param keyFunc Optional function to generate a key for a given data value.
49
+ * @param projections Optional map of derived-value projections, keyed by id. A
50
+ * projection's result is readable through `Get` at `$projection_<id>`. Each projection's
51
+ * function is serialized with `.toString()` and `eval`'d back into a function in the
52
+ * worker, the same mechanic used for `keyFunc`, so it must not close over state that
53
+ * doesn't survive that round trip.
48
54
  */
49
- constructor(keyFunc?: (value: any) => string | undefined);
55
+ constructor(keyFunc?: (value: any) => string | undefined, projections?: DiffTreeProjectionMap);
50
56
  /**
51
57
  * Returns a promise that resolve once all current queued writes have completed
52
58
  */
@@ -41,15 +41,20 @@ class StoreAsync extends store_1.Store {
41
41
  /**
42
42
  * Creates an instance of StoreAsync.
43
43
  * @param keyFunc Optional function to generate a key for a given data value.
44
+ * @param projections Optional map of derived-value projections, keyed by id. A
45
+ * projection's result is readable through `Get` at `$projection_<id>`. Each projection's
46
+ * function is serialized with `.toString()` and `eval`'d back into a function in the
47
+ * worker, the same mechanic used for `keyFunc`, so it must not close over state that
48
+ * doesn't survive that round trip.
44
49
  */
45
- constructor(keyFunc) {
50
+ constructor(keyFunc, projections) {
46
51
  super(keyFunc, false);
47
52
  /**
48
53
  * The async queue instance used to manage asynchronous operations in a non-blocking manner.
49
54
  * @private
50
55
  */
51
56
  this.queue = new asyncQueue_1.AsyncQueue();
52
- this.diff = new diffAsync_1.DiffAsync(keyFunc);
57
+ this.diff = new diffAsync_1.DiffAsync(keyFunc, projections);
53
58
  }
54
59
  /**
55
60
  * Returns a promise that resolve once all current queued writes have completed
@@ -1,3 +1,4 @@
1
+ import { DiffTreeProjectionMap } from "../Diff/diffTree";
1
2
  import { Store } from "./store";
2
3
  /**
3
4
  * StoreSync class extends the base Store class to provide synchronous data management operations.
@@ -36,8 +37,10 @@ export declare class StoreSync extends Store {
36
37
  /**
37
38
  * Creates an instance of StoreSync.
38
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>`.
39
42
  */
40
- constructor(keyFunc?: (value: any) => string | undefined);
43
+ constructor(keyFunc?: (value: any) => string | undefined, projections?: DiffTreeProjectionMap);
41
44
  /**
42
45
  * Writes data to the store synchronously.
43
46
  * @param data The data to be written. Can be of any type.
@@ -37,10 +37,12 @@ class StoreSync extends store_1.Store {
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
42
  */
41
- constructor(keyFunc) {
43
+ constructor(keyFunc, projections) {
42
44
  super(keyFunc);
43
- this.diff = new diffSync_1.DiffSync(keyFunc);
45
+ this.diff = new diffSync_1.DiffSync(keyFunc, projections);
44
46
  }
45
47
  /**
46
48
  * Writes data to the store synchronously.
@@ -39,6 +39,7 @@ export declare namespace ObservableNode {
39
39
  * @returns A proxied version of the value that emits change events.
40
40
  */
41
41
  function Create<T>(value: T): T;
42
+ function Snapshot<T>(proxy: T): T | undefined;
42
43
  /**
43
44
  * Marks an observable node or its property as changed, triggering reactive updates.
44
45
  * Used internally to notify dependencies that a value has been modified.
@@ -6,6 +6,7 @@ const json_2 = require("../../Utils/json");
6
6
  const observableScope_1 = require("./observableScope");
7
7
  const NODE_VALUE = Symbol("NODE_VALUE");
8
8
  const NODE_PROXY = Symbol("NODE_PROXY");
9
+ const NODE_SNAPSHOT = Symbol("NODE_SNAPSHOT");
9
10
  const toJSON = "toJSON";
10
11
  const IS_NODE = Symbol("IS_NODE");
11
12
  const OBJECT_SCOPE = Symbol("OBJECT_SCOPE");
@@ -17,6 +18,24 @@ function Property(value, prop) {
17
18
  return value[prop];
18
19
  }
19
20
  const wrapperCache = new WeakMap();
21
+ const valueSnapshotCache = new WeakMap();
22
+ const valueSnapshotHierarchy = new WeakMap();
23
+ function CreateSnapshotLink(child, parent) {
24
+ let parents = valueSnapshotHierarchy.get(child);
25
+ if (!parents)
26
+ valueSnapshotHierarchy.set(child, (parents = new Set()));
27
+ parents.add(parent);
28
+ }
29
+ function InvalidateSnapshotForValue(value) {
30
+ valueSnapshotCache.delete(value);
31
+ const parents = valueSnapshotHierarchy.get(value);
32
+ if (parents === undefined)
33
+ return;
34
+ valueSnapshotHierarchy.delete(value);
35
+ for (const parent of parents)
36
+ if (valueSnapshotCache.has(parent))
37
+ InvalidateSnapshotForValue(parent);
38
+ }
20
39
  function getOwnPropertyDescriptor(target, prop) {
21
40
  const descriptor = Object.getOwnPropertyDescriptor(target[NODE_VALUE], prop);
22
41
  return {
@@ -93,45 +112,67 @@ function CloneProxy(value, type = (0, json_2.JsonType)(value)) {
93
112
  function GetPropertyScope(object, prop) {
94
113
  return object[prop] ??= observableScope_1.ObservableScope.Basic(Property.bind(null, object[NODE_VALUE], prop));
95
114
  }
115
+ function SetObjectValue(object, prop, value) {
116
+ const propExists = Object.hasOwn(object[NODE_VALUE], prop);
117
+ object[NODE_VALUE][prop] = value;
118
+ InvalidateSnapshotForValue(object[NODE_VALUE]);
119
+ observableScope_1.ObservableScope.Update(object[prop]);
120
+ !propExists && observableScope_1.ObservableScope.Update(object[ITERATOR_SCOPE]);
121
+ }
122
+ function SetArrayValue(object, prop, value) {
123
+ object[NODE_VALUE][prop] = value;
124
+ InvalidateSnapshotForValue(object[NODE_VALUE]);
125
+ observableScope_1.ObservableScope.Update(object[OBJECT_SCOPE]);
126
+ }
96
127
  function CreateProxyFactory(alias) {
97
- function ToJsonCopy(value) {
98
- const type = (0, json_2.JsonType)(value);
99
- switch (type) {
100
- case "array": {
101
- const typedValue = value;
102
- const arrayValue = alias(typedValue, false) ?? typedValue;
103
- return arrayValue.map(ToJsonCopy);
104
- }
105
- case "object": {
106
- const typedValue = alias(value) ?? value;
107
- const objectValue = alias(typedValue, false) ?? typedValue;
108
- const keys = Object.keys(objectValue);
109
- const copy = {};
110
- for (let x = 0; x < keys.length; x++)
111
- copy[keys[x]] = ToJsonCopy(typedValue[keys[x]]);
112
- return copy;
113
- }
114
- default:
115
- return value;
128
+ /**
129
+ * An immutable snapshot of a raw node value (arrays and plain objects; other values are
130
+ * returned as they are). A snapshot is cached per raw value in valueSnapshotCache and
131
+ * returned as-is until a write invalidates it: every write path calls Invalidate, which
132
+ * clears the written value and every cached ancestor, found through the parent links
133
+ * recorded here. A rebuild makes new containers but reuses the cached snapshots of
134
+ * unchanged children, so a snapshot handed out never changes and unchanged parts are
135
+ * shared between snapshots. Keyed children are read through their root.
136
+ */
137
+ function CreateSnapshot(value) {
138
+ if ((0, json_2.JsonType)(value) === "value")
139
+ return value;
140
+ const cached = valueSnapshotCache.get(value);
141
+ if (cached !== undefined)
142
+ return cached;
143
+ let snapshot;
144
+ if (Array.isArray(value)) {
145
+ snapshot = new Array(value.length);
146
+ for (let x = 0; x < value.length; x++)
147
+ snapshot[x] = ChildSnapshot(value, value[x]);
116
148
  }
149
+ else {
150
+ snapshot = {};
151
+ const keys = Object.keys(value);
152
+ for (let x = 0; x < keys.length; x++)
153
+ snapshot[keys[x]] = ChildSnapshot(value, value[keys[x]]);
154
+ }
155
+ valueSnapshotCache.set(value, snapshot);
156
+ return snapshot;
117
157
  }
118
- function ToJsonDefault(value) {
119
- return (0, json_1.JsonDeepClone)(value);
158
+ /**
159
+ * A child's snapshot, linking the child to its parent so a write to the child
160
+ * invalidates the parent. A keyed child's snapshot comes from its root, so the root is
161
+ * linked too; the parent's own embedded copy stays linked because a positional write
162
+ * (one that changes which entity a slot holds) mutates that copy in place.
163
+ */
164
+ function ChildSnapshot(parent, child) {
165
+ if ((0, json_2.JsonType)(child) === "value")
166
+ return child;
167
+ CreateSnapshotLink(child, parent);
168
+ const resolved = alias?.(child, false);
169
+ if (resolved !== undefined && resolved !== child) {
170
+ CreateSnapshotLink(resolved, parent);
171
+ return CreateSnapshot(resolved);
172
+ }
173
+ return CreateSnapshot(child);
120
174
  }
121
- const ToJson = alias !== undefined ? ToJsonCopy : ToJsonDefault;
122
175
  const readOnly = alias !== undefined;
123
- function SetObjectValue(object, prop, value) {
124
- object[NODE_VALUE][prop] = value;
125
- observableScope_1.ObservableScope.Update(object[prop]);
126
- }
127
- function SetArrayValue(object, prop, value) {
128
- object[NODE_VALUE][prop] = value;
129
- observableScope_1.ObservableScope.Update(object[OBJECT_SCOPE]);
130
- }
131
- function CreateProxy(value) {
132
- value = UnwrapProxy(value);
133
- return CreateProxyFromValue(value);
134
- }
135
176
  function CreateArrayProxy(value) {
136
177
  const wrapper = Object.assign([], {
137
178
  [NODE_VALUE]: value,
@@ -172,10 +213,12 @@ function CreateProxyFactory(alias) {
172
213
  return true;
173
214
  case toJSON:
174
215
  return function () {
175
- return ToJson(object[NODE_VALUE]);
216
+ return CreateSnapshot(object[NODE_VALUE]);
176
217
  };
177
218
  case NODE_VALUE:
178
219
  return object[NODE_VALUE];
220
+ case NODE_SNAPSHOT:
221
+ return CreateSnapshot(object[NODE_VALUE]);
179
222
  default: {
180
223
  const scope = object[OBJECT_SCOPE];
181
224
  const array = observableScope_1.ObservableScope.Value(scope);
@@ -199,6 +242,7 @@ function CreateProxyFactory(alias) {
199
242
  array.length = proxyArray.length;
200
243
  for (let x = 0; x < proxyArray.length; x++)
201
244
  array[x] = UnwrapProxy(proxyArray[x]);
245
+ InvalidateSnapshotForValue(object[NODE_VALUE]);
202
246
  observableScope_1.ObservableScope.Update(scope);
203
247
  break;
204
248
  }
@@ -237,9 +281,11 @@ function CreateProxyFactory(alias) {
237
281
  switch (prop) {
238
282
  case IS_NODE:
239
283
  return true;
284
+ case NODE_SNAPSHOT:
285
+ return CreateSnapshot(object[NODE_VALUE]);
240
286
  case toJSON:
241
287
  return function () {
242
- return ToJson(object[NODE_VALUE]);
288
+ return CreateSnapshot(object[NODE_VALUE]);
243
289
  };
244
290
  case NODE_VALUE:
245
291
  return object[NODE_VALUE];
@@ -334,6 +380,12 @@ var ObservableNode;
334
380
  return DefaultCreateProxy(value);
335
381
  }
336
382
  ObservableNode.Create = Create;
383
+ function Snapshot(proxy) {
384
+ if (proxy[IS_NODE])
385
+ return proxy[NODE_SNAPSHOT];
386
+ return undefined;
387
+ }
388
+ ObservableNode.Snapshot = Snapshot;
337
389
  /**
338
390
  * Marks an observable node or its property as changed, triggering reactive updates.
339
391
  * Used internally to notify dependencies that a value has been modified.
@@ -349,23 +401,18 @@ var ObservableNode;
349
401
  }
350
402
  ObservableNode.Update = Update;
351
403
  function Assign(value, prop, propValue) {
352
- const propExists = Object.hasOwn(value, prop);
353
- value[prop] = propValue;
404
+ // const propExists = Object.hasOwn(value as Object, prop);
405
+ // value[prop] = propValue;
354
406
  const wrapper = wrapperCache.get(value);
355
407
  if (wrapper) {
356
408
  if (Array.isArray(wrapper))
357
- observableScope_1.ObservableScope.Update(wrapper[OBJECT_SCOPE]);
358
- else {
359
- const scope = wrapper[prop];
360
- if (scope) {
361
- // A prop was just added to the object, fire the iterator scope
362
- if (!propExists)
363
- observableScope_1.ObservableScope.Update(wrapper[ITERATOR_SCOPE]);
364
- observableScope_1.ObservableScope.Update(scope);
365
- }
366
- else
367
- observableScope_1.ObservableScope.Update(wrapper[ITERATOR_SCOPE]);
368
- }
409
+ SetArrayValue(wrapper, prop, propValue);
410
+ else
411
+ SetObjectValue(wrapper, prop, propValue);
412
+ }
413
+ else {
414
+ InvalidateSnapshotForValue(value);
415
+ value[prop] = propValue;
369
416
  }
370
417
  }
371
418
  ObservableNode.Assign = Assign;
@@ -411,6 +458,7 @@ var ObservableNode;
411
458
  ObservableNode.Apply = Apply;
412
459
  function ApplySplice(rootNode, start, deleteCount, items, cloneData = true) {
413
460
  const root = rootNode[NODE_VALUE];
461
+ InvalidateSnapshotForValue(rootNode[NODE_VALUE]);
414
462
  const addItems = cloneData ? (0, json_1.JsonDeepClone)(items) : items;
415
463
  root.splice(start, deleteCount, ...addItems);
416
464
  ObservableNode.Update(root);
@@ -426,6 +474,7 @@ var ObservableNode;
426
474
  const root = rootNode[NODE_VALUE];
427
475
  if (diffResult.length === 1 && diffResult[0].path.length === 0) {
428
476
  // Replacing rootNode
477
+ InvalidateSnapshotForValue(root);
429
478
  const rootPatch = cloneData ? (0, json_1.JsonDeepClone)(diffResult[0].value) : diffResult[0].value;
430
479
  const rootType = (0, json_2.JsonType)(root);
431
480
  const rootPatchType = (0, json_2.JsonType)(rootPatch);
@@ -191,7 +191,7 @@ function RegisterScope(scope) {
191
191
  */
192
192
  function GetScopeValue(scope) {
193
193
  if (scope.type === "basic")
194
- return scope.getFunction();
194
+ return ExecuteBasicScope(scope);
195
195
  if (scope.type === "static" || !scope.dirty || scope.destroyed)
196
196
  return scope.value;
197
197
  ExecuteScope(scope);
@@ -274,6 +274,13 @@ function ExecuteScope(scope) {
274
274
  scope.onUpdated && emitter_1.Emitter.Emit(scope.onUpdated, lastValue, scope);
275
275
  }
276
276
  }
277
+ function ExecuteBasicScope(scope) {
278
+ const parent = watchState;
279
+ watchState = null;
280
+ const result = scope.getFunction();
281
+ watchState = parent;
282
+ return result;
283
+ }
277
284
  /**
278
285
  * Creates a scope from a function, choosing between static and dynamic based on dependencies.
279
286
  * @template T The type of value returned by the callback.
package/Utils/array.d.ts CHANGED
@@ -16,3 +16,4 @@
16
16
  * ```
17
17
  */
18
18
  export declare function RemoveNulls(array: (unknown | null)[], startIndex?: number): void;
19
+ export declare function ArraysEqual(left: any[], right: any[]): boolean;
package/Utils/array.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RemoveNulls = RemoveNulls;
4
+ exports.ArraysEqual = ArraysEqual;
4
5
  /**
5
6
  * Removes all null values from an array starting from a specified index.
6
7
  * This function modifies the array in-place by shifting non-null elements
@@ -32,3 +33,9 @@ function RemoveNulls(array, startIndex = 0) {
32
33
  if (nullIndex < array.length)
33
34
  array.splice(nullIndex);
34
35
  }
36
+ function ArraysEqual(left, right) {
37
+ let equal = left === right || left?.length === right?.length;
38
+ for (let x = 0; equal && x < left.length; x++)
39
+ equal = left[x] === right[x];
40
+ return equal;
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "j-templates",
3
- "version": "8.0.10",
3
+ "version": "8.0.13",
4
4
  "description": "j-templates",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/TypesInCode/jTemplates",