j-templates 8.0.11 → 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.
- package/Store/Diff/diffAsync.d.ts +5 -2
- package/Store/Diff/diffAsync.js +25 -2
- package/Store/Diff/diffSync.d.ts +3 -2
- package/Store/Diff/diffSync.js +5 -3
- package/Store/Diff/diffTree.d.ts +37 -2
- package/Store/Diff/diffTree.js +229 -12
- package/Store/Diff/diffWorker.js +2 -1
- package/Store/Store/storeAsync.d.ts +7 -1
- package/Store/Store/storeAsync.js +7 -2
- package/Store/Store/storeSync.d.ts +4 -1
- package/Store/Store/storeSync.js +4 -2
- package/Store/Tree/observableScope.js +8 -1
- package/Utils/array.d.ts +1 -0
- package/Utils/array.js +7 -0
- package/package.json +1 -1
|
@@ -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
|
package/Store/Diff/diffAsync.js
CHANGED
|
@@ -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:
|
|
42
|
+
arguments: [
|
|
43
|
+
keyFunc ? keyFunc.toString() : undefined,
|
|
44
|
+
projections ? SerializeProjections(projections) : undefined,
|
|
45
|
+
],
|
|
23
46
|
});
|
|
24
47
|
}
|
|
25
48
|
/**
|
package/Store/Diff/diffSync.d.ts
CHANGED
|
@@ -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
|
package/Store/Diff/diffSync.js
CHANGED
|
@@ -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.
|
package/Store/Diff/diffTree.d.ts
CHANGED
|
@@ -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
|
|
96
|
+
export declare function DiffTreeFactory(jsonDiffFactory: () => JsonDiffFactoryResult, arraysEqual: (left: any[], right: any[]) => boolean, worker?: boolean): IDiffTreeConstructor;
|
package/Store/Diff/diffTree.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
109
|
+
let x = 0;
|
|
110
|
+
for (; curr && x < parts.length - 1; x++)
|
|
88
111
|
curr = curr[parts[x]];
|
|
89
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/Store/Diff/diffWorker.js
CHANGED
|
@@ -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.
|
package/Store/Store/storeSync.js
CHANGED
|
@@ -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.
|
|
@@ -191,7 +191,7 @@ function RegisterScope(scope) {
|
|
|
191
191
|
*/
|
|
192
192
|
function GetScopeValue(scope) {
|
|
193
193
|
if (scope.type === "basic")
|
|
194
|
-
return scope
|
|
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
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
|
+
}
|