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.
- package/SYNTAX_PRIMER.md +3 -4
- package/Store/Diff/diffTree.d.ts +65 -34
- package/Store/Diff/diffTree.js +528 -417
- 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 +1 -0
- package/Store/Diff/index.js +1 -0
- package/Store/Diff/utils/diffTree.types.d.ts +108 -0
- package/Store/Diff/utils/diffTree.types.js +1 -0
- package/Store/Diff/utils/diffTree.utils.d.ts +79 -0
- package/Store/Diff/utils/diffTree.utils.js +398 -0
- package/Store/Store/pushStore.d.ts +1 -5
- package/Store/Store/pushStore.js +3 -22
- package/Store/Store/store.d.ts +2 -4
- package/Store/Store/store.js +12 -11
- package/Store/Store/storeAsync.d.ts +4 -5
- package/Store/Store/storeAsync.js +12 -30
- package/Store/Store/storeSync.d.ts +14 -9
- package/Store/Store/storeSync.js +24 -19
- package/Store/Tree/observableNode.d.ts +2 -23
- package/Store/Tree/observableNode.js +41 -181
- package/Store/Tree/snapshot.d.ts +70 -0
- package/Store/Tree/snapshot.js +141 -0
- package/Utils/json.d.ts +37 -7
- package/Utils/json.js +151 -13
- package/package.json +1 -1
- package/Store/Diff/diffSync.d.ts +0 -36
- package/Store/Diff/diffSync.js +0 -43
|
@@ -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
|
-
*
|
|
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`/
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
*
|
|
25
|
+
* store.Splice("user.array", 0, 1, { id: 3 });
|
|
26
26
|
*
|
|
27
27
|
* @see Store
|
|
28
28
|
* @see StoreAsync
|
|
29
|
-
* @see
|
|
29
|
+
* @see DiffTree
|
|
30
30
|
*/
|
|
31
31
|
export declare class StoreSync extends Store {
|
|
32
32
|
/**
|
|
33
|
-
* The
|
|
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
|
|
41
|
-
*
|
|
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,
|
|
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[]):
|
|
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
|
}
|
package/Store/Store/storeSync.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
*
|
|
25
|
+
* store.Splice("user.array", 0, 1, { id: 3 });
|
|
27
26
|
*
|
|
28
27
|
* @see Store
|
|
29
28
|
* @see StoreAsync
|
|
30
|
-
* @see
|
|
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
|
|
37
|
-
*
|
|
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,
|
|
39
|
+
constructor(keyFunc, routes) {
|
|
40
40
|
super(keyFunc);
|
|
41
|
-
this.diff = new
|
|
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
|
-
|
|
54
|
-
this.
|
|
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
|
-
|
|
66
|
-
this.
|
|
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
|
-
|
|
77
|
-
this.
|
|
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
|
-
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
*
|
|
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,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
411
|
+
function ApplyDiff(rootNode, diffResult) {
|
|
504
412
|
const root = rootNode[NODE_VALUE];
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
+
}
|