j-templates 8.1.2 → 9.0.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.
- package/SYNTAX_PRIMER.md +3 -4
- package/Store/Diff/diffTree.d.ts +39 -33
- package/Store/Diff/diffTree.js +147 -435
- package/Store/Diff/diffTree.types.d.ts +5 -79
- package/Store/Diff/diffTree.types.js +1 -3
- package/Store/Diff/diffTreeWorker.d.ts +4 -0
- package/Store/Diff/diffTreeWorker.js +17 -39
- package/Store/Diff/diffTreeWriter.d.ts +27 -38
- package/Store/Diff/diffTreeWriter.js +44 -95
- package/Store/Diff/index.d.ts +2 -0
- package/Store/Diff/index.js +2 -0
- package/Store/Diff/utils/diffTree.types.d.ts +113 -0
- package/Store/Diff/utils/diffTree.types.js +1 -0
- package/Store/Diff/utils/homes.d.ts +46 -0
- package/Store/Diff/utils/homes.js +213 -0
- package/Store/Diff/utils/paths.d.ts +42 -0
- package/Store/Diff/utils/paths.js +87 -0
- package/Store/Diff/utils/projections.d.ts +55 -0
- package/Store/Diff/utils/projections.js +290 -0
- package/Store/Diff/utils/routes.d.ts +27 -0
- package/Store/Diff/utils/routes.js +168 -0
- package/Store/Diff/utils/state.d.ts +56 -0
- package/Store/Diff/utils/state.js +155 -0
- package/Store/Store/pushStore.d.ts +1 -5
- package/Store/Store/pushStore.js +3 -22
- package/Store/Store/store.d.ts +10 -4
- package/Store/Store/store.js +25 -11
- package/Store/Store/storeAsync.d.ts +4 -5
- package/Store/Store/storeAsync.js +19 -36
- package/Store/Store/storeSync.d.ts +14 -9
- package/Store/Store/storeSync.js +27 -22
- package/Store/Tree/observableNode.d.ts +2 -23
- package/Store/Tree/observableNode.js +46 -183
- package/Store/Tree/snapshot.d.ts +68 -0
- package/Store/Tree/snapshot.js +147 -0
- package/Utils/json.d.ts +180 -10
- package/Utils/json.js +532 -67
- package/package.json +1 -1
- package/Store/Diff/diffSync.d.ts +0 -36
- package/Store/Diff/diffSync.js +0 -43
|
@@ -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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
@@ -123,64 +109,45 @@ function GetPropertyScope(object, prop, create = true) {
|
|
|
123
109
|
return object[prop];
|
|
124
110
|
}
|
|
125
111
|
function SetObjectValue(object, prop, value) {
|
|
126
|
-
|
|
127
|
-
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]);
|
|
112
|
+
AssignNodeValue(object[NODE_VALUE], prop, value, object);
|
|
131
113
|
}
|
|
132
114
|
function SetArrayValue(object, prop, value) {
|
|
133
115
|
object[NODE_VALUE][prop] = value;
|
|
134
|
-
|
|
135
|
-
|
|
116
|
+
NodeValueSet(object[NODE_VALUE], prop, false, object);
|
|
117
|
+
}
|
|
118
|
+
function NodeValueSet(obj, prop, added, wrapper = wrapperCache.get(obj)) {
|
|
119
|
+
InvalidateSnapshotForValue(obj);
|
|
120
|
+
if (!wrapper)
|
|
121
|
+
return;
|
|
122
|
+
// An array's wrapper is an array with no property scopes: its own `length` isn't one, and a
|
|
123
|
+
// diff that only shrinks an array writes nothing but `length`
|
|
124
|
+
const propScopeExists = !!prop && !wrapper[ARRAY_SCOPE] && 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
|
+
/** A deleted property: the object's keys changed, so its scope updates, and so does the property's. */
|
|
131
|
+
function RemoveNodeValue(obj, prop) {
|
|
132
|
+
delete obj[prop];
|
|
133
|
+
NodeValueSet(obj, prop, true);
|
|
134
|
+
}
|
|
135
|
+
/** An array spliced in place: its elements changed, so its scope updates. Moved elements keep their nodes. */
|
|
136
|
+
function SplicedNodeValue(array) {
|
|
137
|
+
NodeValueSet(array, "", false);
|
|
138
|
+
}
|
|
139
|
+
/** A property assigned: the object's scope updates when it's new, and the property's does */
|
|
140
|
+
function AssignNodeValue(obj, prop, value, wrapper) {
|
|
141
|
+
const added = !Object.hasOwn(obj, prop);
|
|
142
|
+
obj[prop] = value;
|
|
143
|
+
NodeValueSet(obj, prop, added, wrapper);
|
|
136
144
|
}
|
|
137
145
|
function CreateProxyFactory(alias) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
*/
|
|
146
|
+
// Keyed children are read through their root. Every factory shares the module-level
|
|
147
|
+
// snapshotState, matching wrapperCache; only the resolver differs per factory.
|
|
148
|
+
const resolveSnapshot = alias && (child => alias(child, false));
|
|
147
149
|
function CreateSnapshot(value) {
|
|
148
|
-
|
|
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);
|
|
150
|
+
return Snapshot.Get(snapshotState, value, resolveSnapshot);
|
|
184
151
|
}
|
|
185
152
|
const readOnly = alias !== undefined;
|
|
186
153
|
function CreateArrayProxy(value) {
|
|
@@ -225,6 +192,8 @@ function CreateProxyFactory(alias) {
|
|
|
225
192
|
return function () {
|
|
226
193
|
return CreateSnapshot(object[NODE_VALUE]);
|
|
227
194
|
};
|
|
195
|
+
case NODE_WRAPPER:
|
|
196
|
+
return object;
|
|
228
197
|
case NODE_VALUE:
|
|
229
198
|
return object[NODE_VALUE];
|
|
230
199
|
case NODE_SNAPSHOT:
|
|
@@ -290,6 +259,8 @@ function CreateProxyFactory(alias) {
|
|
|
290
259
|
switch (prop) {
|
|
291
260
|
case IS_NODE:
|
|
292
261
|
return true;
|
|
262
|
+
case NODE_WRAPPER:
|
|
263
|
+
return object;
|
|
293
264
|
case NODE_SNAPSHOT:
|
|
294
265
|
return CreateSnapshot(object[NODE_VALUE]);
|
|
295
266
|
case toJSON:
|
|
@@ -404,36 +375,6 @@ export var ObservableNode;
|
|
|
404
375
|
return undefined;
|
|
405
376
|
}
|
|
406
377
|
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
378
|
function Touch(value, prop) {
|
|
438
379
|
const wrapper = wrapperCache.get(value);
|
|
439
380
|
if (wrapper) {
|
|
@@ -442,14 +383,6 @@ export var ObservableNode;
|
|
|
442
383
|
}
|
|
443
384
|
}
|
|
444
385
|
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
386
|
/**
|
|
454
387
|
* Merges a new value into an observable node in-place, preserving the node's object identity.
|
|
455
388
|
* Computes the diff between the node's current value and the provided value, then applies only
|
|
@@ -474,86 +407,16 @@ export var ObservableNode;
|
|
|
474
407
|
ApplyDiff(rootNode, diff);
|
|
475
408
|
}
|
|
476
409
|
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
410
|
/**
|
|
496
411
|
* Applies a JSON diff result to an observable node, efficiently updating only changed properties.
|
|
497
412
|
* Optimizes nested object updates by computing paths incrementally and touching modified properties.
|
|
498
413
|
* @param rootNode The observable node to apply the diff to.
|
|
499
414
|
* @param diffResult The diff result from JsonDiff containing path-value pairs of changes.
|
|
500
|
-
*
|
|
501
|
-
* default), so the store isn't left holding a reference the caller can still mutate.
|
|
415
|
+
* Each value is deep-cloned as it's applied (see JsonApplyDiff).
|
|
502
416
|
*/
|
|
503
|
-
function ApplyDiff(rootNode, diffResult
|
|
417
|
+
function ApplyDiff(rootNode, diffResult) {
|
|
504
418
|
const root = rootNode[NODE_VALUE];
|
|
505
|
-
|
|
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
|
-
}
|
|
419
|
+
JsonApplyDiff(root, diffResult, AssignNodeValue, RemoveNodeValue, SplicedNodeValue);
|
|
557
420
|
}
|
|
558
421
|
ObservableNode.ApplyDiff = ApplyDiff;
|
|
559
422
|
/**
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a keyed child to the root entity it should be read through. Returns the entity, or
|
|
3
|
+
* undefined when the child isn't keyed (or has no root), in which case the child itself is used.
|
|
4
|
+
*/
|
|
5
|
+
export type SnapshotResolver = (child: any) => object | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Snapshot cache state. Each owner (a `DiffTree` instance, or `ObservableNode` at module
|
|
8
|
+
* level) creates its own state, so cached snapshots are never shared between owners.
|
|
9
|
+
*/
|
|
10
|
+
export interface ISnapshotState {
|
|
11
|
+
/** Raw value -> its cached immutable snapshot */
|
|
12
|
+
rawToSnapshotCache: WeakMap<object, any>;
|
|
13
|
+
/** Raw value -> every parent whose cached snapshot embedded it */
|
|
14
|
+
hierarchy: WeakMap<object, Set<object>>;
|
|
15
|
+
}
|
|
16
|
+
export declare namespace Snapshot {
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new, empty snapshot cache.
|
|
19
|
+
* @returns A new snapshot state.
|
|
20
|
+
*/
|
|
21
|
+
function Create(): ISnapshotState;
|
|
22
|
+
/**
|
|
23
|
+
* Returns an immutable, cached snapshot of a raw value. The same snapshot is returned
|
|
24
|
+
* until a write invalidates it, and unchanged nested parts are shared between snapshots,
|
|
25
|
+
* so `===` holds for any part that hasn't changed.
|
|
26
|
+
* @param state The snapshot cache to read from and populate.
|
|
27
|
+
* @param value The raw value to snapshot.
|
|
28
|
+
* @param resolve Resolver for keyed children. Snapshots are cached per raw value, so a value
|
|
29
|
+
* must always be read with the same resolver.
|
|
30
|
+
* @returns The snapshot.
|
|
31
|
+
*/
|
|
32
|
+
function Get<T>(state: ISnapshotState, value: T, resolve?: SnapshotResolver): T;
|
|
33
|
+
/**
|
|
34
|
+
* The snapshot of one property of a raw value, as the value's own snapshot would hold it:
|
|
35
|
+
* cached, keyed children read through their root, and linked to the value so a write to the
|
|
36
|
+
* property invalidates the value's snapshot. Reads one property without building a snapshot of
|
|
37
|
+
* the whole value.
|
|
38
|
+
* @param state The snapshot cache to read from and populate.
|
|
39
|
+
* @param parent The raw value the property is on.
|
|
40
|
+
* @param key The property.
|
|
41
|
+
* @param resolve Resolver for keyed children.
|
|
42
|
+
* @returns The property's snapshot.
|
|
43
|
+
*/
|
|
44
|
+
function Child(state: ISnapshotState, parent: any, key: string | number, resolve?: SnapshotResolver): any;
|
|
45
|
+
/**
|
|
46
|
+
* Invalidates the cached snapshot of a raw value that was just written to, along with
|
|
47
|
+
* every cached ancestor that embedded it.
|
|
48
|
+
* @param state The snapshot cache to invalidate.
|
|
49
|
+
* @param value The raw value that was written to.
|
|
50
|
+
*/
|
|
51
|
+
function Invalidate(state: ISnapshotState, value: object): void;
|
|
52
|
+
/**
|
|
53
|
+
* The cached snapshot of a raw value, without building one. A write to the value, or to
|
|
54
|
+
* anything its snapshot embeds, clears the cache, so a reader that kept the snapshot it got
|
|
55
|
+
* can tell nothing under the value changed when this returns that same snapshot.
|
|
56
|
+
* @param state The snapshot cache.
|
|
57
|
+
* @param value The raw value.
|
|
58
|
+
* @returns The cached snapshot, or undefined when there's none.
|
|
59
|
+
*/
|
|
60
|
+
function Peek<T>(state: ISnapshotState, value: T): T | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Registers a value as its own snapshot. Use in a case where an immutable value can
|
|
63
|
+
* represent itself as a valid snapshot without cloning.
|
|
64
|
+
* @param state The snapshot cache.
|
|
65
|
+
* @param value The raw value that is its own snapshot.
|
|
66
|
+
*/
|
|
67
|
+
function Register(state: ISnapshotState, value: object): void;
|
|
68
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
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) {
|
|
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);
|
|
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]);
|
|
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) {
|
|
58
|
+
const child = parent[key];
|
|
59
|
+
if (JsonType(child) === "value")
|
|
60
|
+
return child;
|
|
61
|
+
CreateSnapshotLink(state, child, parent);
|
|
62
|
+
const resolved = resolve?.(child);
|
|
63
|
+
if (resolved !== undefined && resolved !== child) {
|
|
64
|
+
CreateSnapshotLink(state, resolved, parent);
|
|
65
|
+
return CreateSnapshot(state, resolve, resolved);
|
|
66
|
+
}
|
|
67
|
+
return CreateSnapshot(state, resolve, child);
|
|
68
|
+
}
|
|
69
|
+
function RegisterSnapshot(state, snapshot) {
|
|
70
|
+
if (JsonType(snapshot) !== 'value')
|
|
71
|
+
state.rawToSnapshotCache.set(snapshot, snapshot);
|
|
72
|
+
}
|
|
73
|
+
export var Snapshot;
|
|
74
|
+
(function (Snapshot) {
|
|
75
|
+
/**
|
|
76
|
+
* Creates a new, empty snapshot cache.
|
|
77
|
+
* @returns A new snapshot state.
|
|
78
|
+
*/
|
|
79
|
+
function Create() {
|
|
80
|
+
return {
|
|
81
|
+
rawToSnapshotCache: new WeakMap(),
|
|
82
|
+
hierarchy: new WeakMap(),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
Snapshot.Create = Create;
|
|
86
|
+
/**
|
|
87
|
+
* Returns an immutable, cached snapshot of a raw value. The same snapshot is returned
|
|
88
|
+
* until a write invalidates it, and unchanged nested parts are shared between snapshots,
|
|
89
|
+
* so `===` holds for any part that hasn't changed.
|
|
90
|
+
* @param state The snapshot cache to read from and populate.
|
|
91
|
+
* @param value The raw value to snapshot.
|
|
92
|
+
* @param resolve Resolver for keyed children. Snapshots are cached per raw value, so a value
|
|
93
|
+
* must always be read with the same resolver.
|
|
94
|
+
* @returns The snapshot.
|
|
95
|
+
*/
|
|
96
|
+
function Get(state, value, resolve) {
|
|
97
|
+
return CreateSnapshot(state, resolve, value);
|
|
98
|
+
}
|
|
99
|
+
Snapshot.Get = Get;
|
|
100
|
+
/**
|
|
101
|
+
* The snapshot of one property of a raw value, as the value's own snapshot would hold it:
|
|
102
|
+
* cached, keyed children read through their root, and linked to the value so a write to the
|
|
103
|
+
* property invalidates the value's snapshot. Reads one property without building a snapshot of
|
|
104
|
+
* the whole value.
|
|
105
|
+
* @param state The snapshot cache to read from and populate.
|
|
106
|
+
* @param parent The raw value the property is on.
|
|
107
|
+
* @param key The property.
|
|
108
|
+
* @param resolve Resolver for keyed children.
|
|
109
|
+
* @returns The property's snapshot.
|
|
110
|
+
*/
|
|
111
|
+
function Child(state, parent, key, resolve) {
|
|
112
|
+
return ChildSnapshot(state, resolve, parent, key);
|
|
113
|
+
}
|
|
114
|
+
Snapshot.Child = Child;
|
|
115
|
+
/**
|
|
116
|
+
* Invalidates the cached snapshot of a raw value that was just written to, along with
|
|
117
|
+
* every cached ancestor that embedded it.
|
|
118
|
+
* @param state The snapshot cache to invalidate.
|
|
119
|
+
* @param value The raw value that was written to.
|
|
120
|
+
*/
|
|
121
|
+
function Invalidate(state, value) {
|
|
122
|
+
InvalidateValue(state, value);
|
|
123
|
+
}
|
|
124
|
+
Snapshot.Invalidate = Invalidate;
|
|
125
|
+
/**
|
|
126
|
+
* The cached snapshot of a raw value, without building one. A write to the value, or to
|
|
127
|
+
* anything its snapshot embeds, clears the cache, so a reader that kept the snapshot it got
|
|
128
|
+
* can tell nothing under the value changed when this returns that same snapshot.
|
|
129
|
+
* @param state The snapshot cache.
|
|
130
|
+
* @param value The raw value.
|
|
131
|
+
* @returns The cached snapshot, or undefined when there's none.
|
|
132
|
+
*/
|
|
133
|
+
function Peek(state, value) {
|
|
134
|
+
return state.rawToSnapshotCache.get(value);
|
|
135
|
+
}
|
|
136
|
+
Snapshot.Peek = Peek;
|
|
137
|
+
/**
|
|
138
|
+
* Registers a value as its own snapshot. Use in a case where an immutable value can
|
|
139
|
+
* represent itself as a valid snapshot without cloning.
|
|
140
|
+
* @param state The snapshot cache.
|
|
141
|
+
* @param value The raw value that is its own snapshot.
|
|
142
|
+
*/
|
|
143
|
+
function Register(state, value) {
|
|
144
|
+
RegisterSnapshot(state, value);
|
|
145
|
+
}
|
|
146
|
+
Snapshot.Register = Register;
|
|
147
|
+
})(Snapshot || (Snapshot = {}));
|