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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { JsonDiffResult } from "../../../Utils/json";
|
|
2
|
+
import { ISnapshotState, SnapshotPath, SnapshotResolver } from "../../Tree/snapshot";
|
|
3
|
+
export declare const ROOT_ROUTE = "/";
|
|
4
|
+
export declare const PROJECTION_PREFIX = "$projection_";
|
|
5
|
+
export declare const ROUTE_STATE: unique symbol;
|
|
6
|
+
type KeyFunc = {
|
|
7
|
+
(value: any): string | undefined;
|
|
8
|
+
};
|
|
9
|
+
/** Returns the keyFunc for a route path (`/channel/1/`, or `""`/`/` for the root) */
|
|
10
|
+
export type RouteKeyFunc = {
|
|
11
|
+
(route: string): KeyFunc | undefined;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The path a keyed value lives at: its key joined to the route it was found in. A key
|
|
15
|
+
* starting with `/` is a route path from the root.
|
|
16
|
+
* @returns The path, or undefined when the value isn't keyed
|
|
17
|
+
*/
|
|
18
|
+
export declare function ResolveKey(keyFunc: KeyFunc | undefined, route: string, value: any): string;
|
|
19
|
+
export declare function ExpandKeyedDiffFromValue(route: string, path: string, keyFunc: KeyFunc, value: any, keyedDiff: {
|
|
20
|
+
path: string;
|
|
21
|
+
value: any;
|
|
22
|
+
}[]): void;
|
|
23
|
+
/**
|
|
24
|
+
* Applies a diff to `diffRoot`, then each keyed value and rebased row it produced, until none are
|
|
25
|
+
* left. Each rebase moves a row past one keyed value, so the rows run out.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ApplyDiff(snapshot: ISnapshotState, treeRootState: any, routeKeyFunc: RouteKeyFunc, route: string, diffRoot: any, diff: JsonDiffResult): void;
|
|
28
|
+
export declare function ApplyPathValues(snapshot: ISnapshotState, root: any, pathValues: {
|
|
29
|
+
path: string;
|
|
30
|
+
value: any;
|
|
31
|
+
}[]): void;
|
|
32
|
+
/**
|
|
33
|
+
* A snapshot resolver reading each keyed child through the path it lives at, keyed with the
|
|
34
|
+
* keyFunc of the route the snapshot walk is in.
|
|
35
|
+
*/
|
|
36
|
+
export declare function CreateKeyResolver(root: any, routeKeyFunc: RouteKeyFunc): SnapshotResolver;
|
|
37
|
+
export declare function ApplyValueToPath(snapshot: ISnapshotState, root: any, routeKeyFunc: RouteKeyFunc, basePath: string, path: string, value: any): void;
|
|
38
|
+
/**
|
|
39
|
+
* Converts a route path to the snapshot path of the value it names, following how
|
|
40
|
+
* `WritePath` stores route state: `/channel/1/item.v` is `[ROUTE_STATE, "channel", ROUTE_STATE, "1", "item", "v"]`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function ToSnapshotPath(path: string): SnapshotPath;
|
|
43
|
+
/** The route path a snapshot path is in: each segment following a `ROUTE_STATE`, e.g. `/channel/1/` */
|
|
44
|
+
export declare function SnapshotPathRoute(path: Readonly<SnapshotPath>): string;
|
|
45
|
+
export declare function WritePath(snapshot: ISnapshotState, root: any, path: string, value: any, guardPath?: boolean, pathIndex?: number): void;
|
|
46
|
+
export declare function ReadPath(root: any, path: string, pathIndex?: number): any;
|
|
47
|
+
export declare function ReadPathFound(root: any, path: string, pathIndex?: number): any[];
|
|
48
|
+
export declare function SplitRouteAndPath(path: string): readonly [string, string];
|
|
49
|
+
export declare function GetKeyedPath(root: any, dotPath: string, keyFunc?: (data: any) => string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Deletes a route's state, child routes included, from its parent route. Every cached snapshot
|
|
52
|
+
* that embedded a value from it, such as one reading a keyed value through its home in the
|
|
53
|
+
* route, is invalidated.
|
|
54
|
+
* @param route A non-root route path, e.g. `/channel/1/`
|
|
55
|
+
*/
|
|
56
|
+
export declare function DeleteRouteState(snapshot: ISnapshotState, root: any, route: string): void;
|
|
57
|
+
/** Whether a path's route has a wildcard segment, e.g. a `row_*` segment */
|
|
58
|
+
export declare function IsWildcardPath(path: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Whether a route matches a route pattern segment by segment. A pattern segment ending in `*`
|
|
61
|
+
* matches any segment starting with the text before it: a `row_*` segment matches `row_1`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function RouteMatches(pattern: string, route: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Expands a wildcard path's route against the route state under `root`.
|
|
66
|
+
* @returns Each matching path, keyed by the segment the wildcard matched
|
|
67
|
+
*/
|
|
68
|
+
export declare function ExpandWildcardPath(root: any, path: string): {
|
|
69
|
+
[segment: string]: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Throws when a read path's wildcard is invalid: more than one wildcard segment, a `*` that
|
|
73
|
+
* doesn't end its segment, or a `*` in the property path.
|
|
74
|
+
*/
|
|
75
|
+
export declare function ValidateWildcardPath(path: string): void;
|
|
76
|
+
/** Whether `path` is `basePath` or a property below it: `/a.b` is within `/a`, `/ab` isn't */
|
|
77
|
+
export declare function IsPathWithin(path: string, basePath: string): boolean;
|
|
78
|
+
export declare function JoinPaths(basePath: string, targetPath: string): string;
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { JsonApplyDiff, JsonDeepClone, JsonDiff, JsonType } from "../../../Utils/json";
|
|
2
|
+
import { Snapshot } from "../../Tree/snapshot";
|
|
3
|
+
export const ROOT_ROUTE = "/";
|
|
4
|
+
export const PROJECTION_PREFIX = "$projection_";
|
|
5
|
+
export const ROUTE_STATE = Symbol("ROUTE_STATE");
|
|
6
|
+
const ROUTE_DELIMETER = "/";
|
|
7
|
+
const PROP_DELIMETER = ".";
|
|
8
|
+
const WILDCARD = "*";
|
|
9
|
+
/**
|
|
10
|
+
* The path a keyed value lives at: its key joined to the route it was found in. A key
|
|
11
|
+
* starting with `/` is a route path from the root.
|
|
12
|
+
* @returns The path, or undefined when the value isn't keyed
|
|
13
|
+
*/
|
|
14
|
+
export function ResolveKey(keyFunc, route, value) {
|
|
15
|
+
const key = keyFunc?.(value);
|
|
16
|
+
return key ? JoinPaths(route, key) : undefined;
|
|
17
|
+
}
|
|
18
|
+
export function ExpandKeyedDiffFromValue(route, path, keyFunc, value, keyedDiff) {
|
|
19
|
+
const type = JsonType(value);
|
|
20
|
+
if (type === 'value')
|
|
21
|
+
return;
|
|
22
|
+
const filterPrefix = JoinPaths(route, path);
|
|
23
|
+
const key = keyFunc(value);
|
|
24
|
+
if (key) {
|
|
25
|
+
const [keyRoute] = SplitRouteAndPath(key);
|
|
26
|
+
const invalidKeyRoute = keyRoute && !keyRoute.startsWith('/') && route.endsWith(keyRoute);
|
|
27
|
+
const fullKeyPath = JoinPaths(route, key);
|
|
28
|
+
if (!invalidKeyRoute && fullKeyPath && !IsPathWithin(fullKeyPath, filterPrefix))
|
|
29
|
+
keyedDiff.push({
|
|
30
|
+
path: fullKeyPath,
|
|
31
|
+
value: value
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const keys = type === "array" ? undefined : Object.keys(value);
|
|
35
|
+
const length = keys ? keys.length : value.length;
|
|
36
|
+
for (let x = 0; x < length; x++) {
|
|
37
|
+
const child = value[keys ? keys[x] : x];
|
|
38
|
+
ExpandKeyedDiffFromValue(route, path, keyFunc, child, keyedDiff);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Applies a diff to `diffRoot`, then each keyed value and rebased row it produced, until none are
|
|
43
|
+
* left. Each rebase moves a row past one keyed value, so the rows run out.
|
|
44
|
+
*/
|
|
45
|
+
export function ApplyDiff(snapshot, treeRootState, routeKeyFunc, route, diffRoot, diff) {
|
|
46
|
+
const keyFunc = routeKeyFunc(route);
|
|
47
|
+
const fixupDiff = [];
|
|
48
|
+
for (let x = 0; keyFunc && x < diff.length; x++) {
|
|
49
|
+
const row = diff[x];
|
|
50
|
+
const path = row.path;
|
|
51
|
+
let curr = diffRoot;
|
|
52
|
+
let key = null;
|
|
53
|
+
let keyIndex = -1;
|
|
54
|
+
let y = 0;
|
|
55
|
+
for (; curr && y < path.length; y++) {
|
|
56
|
+
const currKey = keyFunc(curr);
|
|
57
|
+
if (currKey) {
|
|
58
|
+
key = currKey;
|
|
59
|
+
keyIndex = y;
|
|
60
|
+
}
|
|
61
|
+
curr = curr[path[y]];
|
|
62
|
+
}
|
|
63
|
+
if (y < path.length && key) {
|
|
64
|
+
const joinedKey = JoinPaths(route, key);
|
|
65
|
+
const keyedValue = ReadPath(treeRootState, joinedKey);
|
|
66
|
+
if (keyedValue) {
|
|
67
|
+
const write = {
|
|
68
|
+
type: "write",
|
|
69
|
+
value: keyedValue,
|
|
70
|
+
path: path.slice(0, keyIndex)
|
|
71
|
+
};
|
|
72
|
+
fixupDiff.push(write);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
JsonApplyDiff(diffRoot, fixupDiff, function (parent, prop, value) {
|
|
77
|
+
parent[prop] = value;
|
|
78
|
+
Snapshot.Invalidate(snapshot, parent);
|
|
79
|
+
});
|
|
80
|
+
// The raw value can be a stale copy missing paths the diff's snapshot has; the rebased rows
|
|
81
|
+
// below write those to the value's home
|
|
82
|
+
JsonApplyDiff(diffRoot, diff, function (parent, prop, value) {
|
|
83
|
+
parent[prop] = value;
|
|
84
|
+
Snapshot.Invalidate(snapshot, parent);
|
|
85
|
+
});
|
|
86
|
+
if (!keyFunc)
|
|
87
|
+
return;
|
|
88
|
+
const rebasedRouteMap = {};
|
|
89
|
+
const keyedWrites = [];
|
|
90
|
+
for (let x = 0; x < diff.length; x++) {
|
|
91
|
+
const diffRow = diff[x];
|
|
92
|
+
const path = diffRow.path;
|
|
93
|
+
const pathStr = path.join('.');
|
|
94
|
+
const routeAndPath = JoinPaths(route, pathStr);
|
|
95
|
+
if (diffRow.type === "write")
|
|
96
|
+
ExpandKeyedDiffFromValue(route, pathStr, keyFunc, diffRow.value, keyedWrites);
|
|
97
|
+
let curr = diffRoot;
|
|
98
|
+
let key = null;
|
|
99
|
+
let keyIndex = -1;
|
|
100
|
+
let keyCurr = null;
|
|
101
|
+
for (let y = 0; curr && y < path.length; y++) {
|
|
102
|
+
const currKey = keyFunc(curr);
|
|
103
|
+
if (currKey) {
|
|
104
|
+
key = currKey;
|
|
105
|
+
keyIndex = y;
|
|
106
|
+
keyCurr = curr;
|
|
107
|
+
}
|
|
108
|
+
curr = curr[path[y]];
|
|
109
|
+
}
|
|
110
|
+
if (key) {
|
|
111
|
+
const joinedPath = JoinPaths(route, key);
|
|
112
|
+
if (!IsPathWithin(routeAndPath, joinedPath)) {
|
|
113
|
+
const exists = !!ReadPath(treeRootState, joinedPath);
|
|
114
|
+
const [targetRoute, targetPath] = SplitRouteAndPath(joinedPath);
|
|
115
|
+
rebasedRouteMap[targetRoute] ??= [];
|
|
116
|
+
if (!exists) {
|
|
117
|
+
rebasedRouteMap[targetRoute].push({
|
|
118
|
+
type: 'write',
|
|
119
|
+
path: targetPath.split('.'),
|
|
120
|
+
value: keyCurr
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const rebasedDiffRow = diffRow.type === "move" ? {
|
|
125
|
+
...diffRow,
|
|
126
|
+
path: targetPath.split('.').concat(diffRow.path.slice(keyIndex))
|
|
127
|
+
} : {
|
|
128
|
+
...diffRow,
|
|
129
|
+
path: targetPath.split('.').concat(diffRow.path.slice(keyIndex))
|
|
130
|
+
};
|
|
131
|
+
if (rebasedDiffRow.type === "move" || rebasedDiffRow.value !== ReadPath(treeRootState, JoinPaths(targetRoute, rebasedDiffRow.path.join('.'))))
|
|
132
|
+
rebasedRouteMap[targetRoute].push(rebasedDiffRow);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (let x = 0; x < keyedWrites.length; x++) {
|
|
138
|
+
const [keyedRoute, keyedPath] = SplitRouteAndPath(keyedWrites[x].path);
|
|
139
|
+
ApplyValueToPath(snapshot, treeRootState, routeKeyFunc, keyedRoute, keyedPath, keyedWrites[x].value);
|
|
140
|
+
}
|
|
141
|
+
for (const rebasedRoute in rebasedRouteMap) {
|
|
142
|
+
const rebasedRoot = ReadPath(treeRootState, rebasedRoute);
|
|
143
|
+
ApplyDiff(snapshot, treeRootState, routeKeyFunc, rebasedRoute, rebasedRoot, rebasedRouteMap[rebasedRoute]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export function ApplyPathValues(snapshot, root, pathValues) {
|
|
147
|
+
for (let x = 0; x < pathValues.length; x++)
|
|
148
|
+
WritePath(snapshot, root, pathValues[x].path, JsonDeepClone(pathValues[x].value));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* A snapshot resolver reading each keyed child through the path it lives at, keyed with the
|
|
152
|
+
* keyFunc of the route the snapshot walk is in.
|
|
153
|
+
*/
|
|
154
|
+
export function CreateKeyResolver(root, routeKeyFunc) {
|
|
155
|
+
return function (child, snapshotPath) {
|
|
156
|
+
const route = SnapshotPathRoute(snapshotPath);
|
|
157
|
+
const keyedPath = ResolveKey(routeKeyFunc(route), route, child);
|
|
158
|
+
if (!keyedPath)
|
|
159
|
+
return undefined;
|
|
160
|
+
const value = ReadPath(root, keyedPath);
|
|
161
|
+
return value === undefined ? undefined : { value, path: ToSnapshotPath(keyedPath) };
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The snapshot of `routeRoot[property]`, read through its home value when it's keyed, as a
|
|
166
|
+
* snapshot of the whole route would hold it.
|
|
167
|
+
*/
|
|
168
|
+
function ResolvedSnapshot(snapshot, root, routeKeyFunc, routeRoot, route, property) {
|
|
169
|
+
const value = routeRoot[property];
|
|
170
|
+
if (JsonType(value) === "value")
|
|
171
|
+
return value;
|
|
172
|
+
const resolver = CreateKeyResolver(root, routeKeyFunc);
|
|
173
|
+
const path = ToSnapshotPath(JoinPaths(route, property));
|
|
174
|
+
const resolved = resolver(value, path);
|
|
175
|
+
return resolved ? Snapshot.Get(snapshot, resolved.value, resolver, resolved.path) : Snapshot.Get(snapshot, value, resolver, path);
|
|
176
|
+
}
|
|
177
|
+
export function ApplyValueToPath(snapshot, root, routeKeyFunc, basePath, path, value) {
|
|
178
|
+
const [route, property] = SplitRouteAndPath(path);
|
|
179
|
+
if (!property)
|
|
180
|
+
throw new Error("Unable to write to root path");
|
|
181
|
+
let combinedRoute = JoinPaths(basePath, route);
|
|
182
|
+
let diffRoot = ReadPath(root, combinedRoute);
|
|
183
|
+
if (diffRoot === undefined) {
|
|
184
|
+
WritePath(snapshot, root, combinedRoute, undefined);
|
|
185
|
+
diffRoot = ReadPath(root, combinedRoute);
|
|
186
|
+
}
|
|
187
|
+
// Only the written value is snapshotted. A snapshot of the whole route would cache values that
|
|
188
|
+
// embed keys whose home values this apply hasn't written yet, and nothing would update them.
|
|
189
|
+
const propertyIndex = property.indexOf(PROP_DELIMETER);
|
|
190
|
+
const rootProperty = propertyIndex < 0 ? property : property.slice(0, propertyIndex);
|
|
191
|
+
const currentValue = ReadPath(ResolvedSnapshot(snapshot, root, routeKeyFunc, diffRoot, combinedRoute, rootProperty), propertyIndex < 0 ? "" : property.slice(propertyIndex + 1));
|
|
192
|
+
const diff = JsonDiff(value, currentValue, property);
|
|
193
|
+
ApplyDiff(snapshot, root, routeKeyFunc, combinedRoute, diffRoot, diff);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Converts a route path to the snapshot path of the value it names, following how
|
|
197
|
+
* `WritePath` stores route state: `/channel/1/item.v` is `[ROUTE_STATE, "channel", ROUTE_STATE, "1", "item", "v"]`.
|
|
198
|
+
*/
|
|
199
|
+
export function ToSnapshotPath(path) {
|
|
200
|
+
const snapshotPath = [];
|
|
201
|
+
const [route, dotPath] = SplitRouteAndPath(path);
|
|
202
|
+
const routeParts = route.split(ROUTE_DELIMETER);
|
|
203
|
+
for (let x = 0; x < routeParts.length; x++)
|
|
204
|
+
routeParts[x] !== "" && snapshotPath.push(ROUTE_STATE, routeParts[x]);
|
|
205
|
+
if (dotPath !== "")
|
|
206
|
+
snapshotPath.push(...dotPath.split(PROP_DELIMETER));
|
|
207
|
+
return snapshotPath;
|
|
208
|
+
}
|
|
209
|
+
/** The route path a snapshot path is in: each segment following a `ROUTE_STATE`, e.g. `/channel/1/` */
|
|
210
|
+
export function SnapshotPathRoute(path) {
|
|
211
|
+
let route = ROUTE_DELIMETER;
|
|
212
|
+
for (let x = 0; x < path.length - 1; x++)
|
|
213
|
+
if (path[x] === ROUTE_STATE)
|
|
214
|
+
route += `${String(path[x + 1])}${ROUTE_DELIMETER}`;
|
|
215
|
+
return route;
|
|
216
|
+
}
|
|
217
|
+
export function WritePath(snapshot, root, path, value, guardPath = true, pathIndex = 0) {
|
|
218
|
+
const startingSlash = path[pathIndex] === ROUTE_DELIMETER;
|
|
219
|
+
if (startingSlash && path.includes(ROUTE_DELIMETER, pathIndex + 1)) {
|
|
220
|
+
const routeKey = path.slice(pathIndex + 1, path.indexOf(ROUTE_DELIMETER, pathIndex + 1));
|
|
221
|
+
if (!Object.hasOwn(root, ROUTE_STATE))
|
|
222
|
+
Object.defineProperty(root, ROUTE_STATE, {
|
|
223
|
+
value: {}
|
|
224
|
+
});
|
|
225
|
+
root[ROUTE_STATE][routeKey] ??= {};
|
|
226
|
+
WritePath(snapshot, root[ROUTE_STATE][routeKey], path, value, guardPath, pathIndex + routeKey.length + 1);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const sliceIndex = startingSlash ? pathIndex + 1 : pathIndex;
|
|
230
|
+
const dotPath = path.slice(sliceIndex);
|
|
231
|
+
if (guardPath && dotPath.startsWith(PROJECTION_PREFIX))
|
|
232
|
+
throw new Error(`Cannot write to ${path}: ${PROJECTION_PREFIX} keys are reserved for projection results.`);
|
|
233
|
+
if (dotPath === '')
|
|
234
|
+
return;
|
|
235
|
+
const parts = dotPath.split(PROP_DELIMETER);
|
|
236
|
+
let curr = root;
|
|
237
|
+
let x = 0;
|
|
238
|
+
for (; curr && x < parts.length - 1; x++)
|
|
239
|
+
curr = curr[parts[x]];
|
|
240
|
+
if (!curr)
|
|
241
|
+
throw new Error("Unable to write value to path: " + path);
|
|
242
|
+
if (curr[parts[x]])
|
|
243
|
+
Snapshot.Invalidate(snapshot, curr[parts[x]]);
|
|
244
|
+
curr[parts[x]] = value;
|
|
245
|
+
Snapshot.Invalidate(snapshot, curr);
|
|
246
|
+
}
|
|
247
|
+
export function ReadPath(root, path, pathIndex = 0) {
|
|
248
|
+
if (root === undefined)
|
|
249
|
+
return undefined;
|
|
250
|
+
if (path === '')
|
|
251
|
+
return root;
|
|
252
|
+
const startingSlash = path[pathIndex] === ROUTE_DELIMETER;
|
|
253
|
+
if (startingSlash && pathIndex === path.length - 1)
|
|
254
|
+
return root;
|
|
255
|
+
if (startingSlash && path.includes(ROUTE_DELIMETER, pathIndex + 1)) {
|
|
256
|
+
const routeKey = path.slice(pathIndex + 1, path.indexOf(ROUTE_DELIMETER, pathIndex + 1));
|
|
257
|
+
const nextRoot = root[ROUTE_STATE]?.[routeKey];
|
|
258
|
+
return ReadPath(nextRoot, path, pathIndex + routeKey.length + 1);
|
|
259
|
+
}
|
|
260
|
+
const sliceIndex = startingSlash ? pathIndex + 1 : pathIndex;
|
|
261
|
+
const dotPath = path.slice(sliceIndex);
|
|
262
|
+
const parts = dotPath.split(PROP_DELIMETER);
|
|
263
|
+
let curr = root;
|
|
264
|
+
let x = 0;
|
|
265
|
+
for (; curr && x < parts.length - 1; x++)
|
|
266
|
+
curr = curr[parts[x]];
|
|
267
|
+
return curr?.[parts[x]];
|
|
268
|
+
}
|
|
269
|
+
export function ReadPathFound(root, path, pathIndex = 0) {
|
|
270
|
+
if (root === undefined)
|
|
271
|
+
return [undefined, false];
|
|
272
|
+
if (path === '')
|
|
273
|
+
return [root, !!root];
|
|
274
|
+
const startingSlash = path[pathIndex] === ROUTE_DELIMETER;
|
|
275
|
+
if (startingSlash && pathIndex === path.length - 1)
|
|
276
|
+
return [root, !!root];
|
|
277
|
+
if (startingSlash && path.includes(ROUTE_DELIMETER, pathIndex + 1)) {
|
|
278
|
+
const routeKey = path.slice(pathIndex + 1, path.indexOf(ROUTE_DELIMETER, pathIndex + 1));
|
|
279
|
+
const nextRoot = root[ROUTE_STATE]?.[routeKey];
|
|
280
|
+
return ReadPathFound(nextRoot, path, pathIndex + routeKey.length + 1);
|
|
281
|
+
}
|
|
282
|
+
const sliceIndex = startingSlash ? pathIndex + 1 : pathIndex;
|
|
283
|
+
const dotPath = path.slice(sliceIndex);
|
|
284
|
+
const parts = dotPath.split(PROP_DELIMETER);
|
|
285
|
+
let curr = root;
|
|
286
|
+
let x = 0;
|
|
287
|
+
for (; curr && x < parts.length - 1; x++)
|
|
288
|
+
curr = curr[parts[x]];
|
|
289
|
+
return [curr?.[parts[x]], !!curr];
|
|
290
|
+
}
|
|
291
|
+
export function SplitRouteAndPath(path) {
|
|
292
|
+
const lastSlash = path.lastIndexOf(ROUTE_DELIMETER);
|
|
293
|
+
const route = path.slice(0, lastSlash + 1);
|
|
294
|
+
const propertyPath = path.slice(lastSlash + 1);
|
|
295
|
+
return [route, propertyPath];
|
|
296
|
+
}
|
|
297
|
+
export function GetKeyedPath(root, dotPath, keyFunc) {
|
|
298
|
+
if (!keyFunc)
|
|
299
|
+
return undefined;
|
|
300
|
+
const parts = dotPath.split(PROP_DELIMETER);
|
|
301
|
+
let keyPath = keyFunc(root);
|
|
302
|
+
let curr = root;
|
|
303
|
+
for (let x = 0; curr && x < parts.length - 1; x++) {
|
|
304
|
+
curr = curr[parts[x]];
|
|
305
|
+
keyPath = keyFunc(root) ?? keyPath;
|
|
306
|
+
}
|
|
307
|
+
return keyPath;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Deletes a route's state, child routes included, from its parent route. Every cached snapshot
|
|
311
|
+
* that embedded a value from it, such as one reading a keyed value through its home in the
|
|
312
|
+
* route, is invalidated.
|
|
313
|
+
* @param route A non-root route path, e.g. `/channel/1/`
|
|
314
|
+
*/
|
|
315
|
+
export function DeleteRouteState(snapshot, root, route) {
|
|
316
|
+
const parentRoute = route.slice(0, route.lastIndexOf(ROUTE_DELIMETER, route.length - 2) + 1);
|
|
317
|
+
const segment = route.slice(parentRoute.length, -1);
|
|
318
|
+
const routeStates = ReadPath(root, parentRoute)?.[ROUTE_STATE];
|
|
319
|
+
if (!routeStates || !Object.hasOwn(routeStates, segment))
|
|
320
|
+
return;
|
|
321
|
+
InvalidateState(snapshot, routeStates[segment]);
|
|
322
|
+
delete routeStates[segment];
|
|
323
|
+
}
|
|
324
|
+
function InvalidateState(snapshot, value) {
|
|
325
|
+
if (JsonType(value) === "value")
|
|
326
|
+
return;
|
|
327
|
+
Snapshot.Invalidate(snapshot, value);
|
|
328
|
+
const keys = Object.keys(value);
|
|
329
|
+
for (let x = 0; x < keys.length; x++)
|
|
330
|
+
InvalidateState(snapshot, value[keys[x]]);
|
|
331
|
+
const routeStates = value[ROUTE_STATE];
|
|
332
|
+
for (const segment in routeStates)
|
|
333
|
+
InvalidateState(snapshot, routeStates[segment]);
|
|
334
|
+
}
|
|
335
|
+
/** Whether a path's route has a wildcard segment, e.g. a `row_*` segment */
|
|
336
|
+
export function IsWildcardPath(path) {
|
|
337
|
+
return SplitRouteAndPath(path)[0].includes(WILDCARD);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Whether a route matches a route pattern segment by segment. A pattern segment ending in `*`
|
|
341
|
+
* matches any segment starting with the text before it: a `row_*` segment matches `row_1`.
|
|
342
|
+
*/
|
|
343
|
+
export function RouteMatches(pattern, route) {
|
|
344
|
+
if (pattern === route)
|
|
345
|
+
return true;
|
|
346
|
+
// Every route but a few fails on the text before the wildcard, so that's checked before splitting
|
|
347
|
+
const wildcard = pattern.indexOf(WILDCARD);
|
|
348
|
+
if (wildcard < 0 || !route.startsWith(pattern.slice(0, wildcard)))
|
|
349
|
+
return false;
|
|
350
|
+
const patternParts = pattern.split(ROUTE_DELIMETER);
|
|
351
|
+
const routeParts = route.split(ROUTE_DELIMETER);
|
|
352
|
+
if (patternParts.length !== routeParts.length)
|
|
353
|
+
return false;
|
|
354
|
+
for (let x = 0; x < patternParts.length; x++)
|
|
355
|
+
if (patternParts[x].endsWith(WILDCARD) ? !routeParts[x].startsWith(patternParts[x].slice(0, -1)) : patternParts[x] !== routeParts[x])
|
|
356
|
+
return false;
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Expands a wildcard path's route against the route state under `root`.
|
|
361
|
+
* @returns Each matching path, keyed by the segment the wildcard matched
|
|
362
|
+
*/
|
|
363
|
+
export function ExpandWildcardPath(root, path) {
|
|
364
|
+
const [route, property] = SplitRouteAndPath(path);
|
|
365
|
+
const parts = route.split(ROUTE_DELIMETER);
|
|
366
|
+
const index = parts.findIndex(part => part.endsWith(WILDCARD));
|
|
367
|
+
const prefix = parts[index].slice(0, -1);
|
|
368
|
+
const parent = ReadPath(root, parts.slice(0, index).join(ROUTE_DELIMETER) + ROUTE_DELIMETER);
|
|
369
|
+
const children = parent?.[ROUTE_STATE] ?? {};
|
|
370
|
+
const paths = {};
|
|
371
|
+
for (const segment of Object.keys(children))
|
|
372
|
+
if (segment.startsWith(prefix)) {
|
|
373
|
+
parts[index] = segment;
|
|
374
|
+
paths[segment] = parts.join(ROUTE_DELIMETER) + property;
|
|
375
|
+
}
|
|
376
|
+
return paths;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Throws when a read path's wildcard is invalid: more than one wildcard segment, a `*` that
|
|
380
|
+
* doesn't end its segment, or a `*` in the property path.
|
|
381
|
+
*/
|
|
382
|
+
export function ValidateWildcardPath(path) {
|
|
383
|
+
const [route, property] = SplitRouteAndPath(path);
|
|
384
|
+
const wildcards = route.split(ROUTE_DELIMETER).filter(part => part.includes(WILDCARD));
|
|
385
|
+
if (property.includes(WILDCARD) || wildcards.length > 1 || wildcards.some(part => part.indexOf(WILDCARD) !== part.length - 1))
|
|
386
|
+
throw new Error(`Invalid wildcard read ${path}: only one route segment may end in ${WILDCARD}.`);
|
|
387
|
+
}
|
|
388
|
+
/** Whether `path` is `basePath` or a property below it: `/a.b` is within `/a`, `/ab` isn't */
|
|
389
|
+
export function IsPathWithin(path, basePath) {
|
|
390
|
+
return path === basePath || path.startsWith(basePath + PROP_DELIMETER);
|
|
391
|
+
}
|
|
392
|
+
export function JoinPaths(basePath, targetPath) {
|
|
393
|
+
if (targetPath.startsWith(ROUTE_DELIMETER))
|
|
394
|
+
return targetPath;
|
|
395
|
+
if (basePath === '')
|
|
396
|
+
return `/${targetPath}`;
|
|
397
|
+
return basePath.concat(targetPath);
|
|
398
|
+
}
|
|
@@ -43,12 +43,8 @@ export declare class PushStore extends ReadStore {
|
|
|
43
43
|
* @param keyFunc Optional function to generate a key for a given data value, used to
|
|
44
44
|
* resolve root objects. Pass the same key logic the `DiffTreeWriter` was constructed
|
|
45
45
|
* with, if any.
|
|
46
|
-
* @param cloneData Whether to deep-clone the values the store adopts (false by default).
|
|
47
|
-
* A `Worker` or `MessagePort` already delivers a structured-clone copy of every message.
|
|
48
|
-
* A same-thread `EventTarget` delivers the writer's own objects, so either the writer
|
|
49
|
-
* posts copies or this is true; otherwise the store and the writer's tree share objects.
|
|
50
46
|
*/
|
|
51
|
-
constructor(eventTarget: EventTarget, keyFunc?: (value: any) => string | undefined
|
|
47
|
+
constructor(eventTarget: EventTarget, keyFunc?: (value: any) => string | undefined);
|
|
52
48
|
/**
|
|
53
49
|
* Stops listening to the source and destroys the error scope. The source stays open (a
|
|
54
50
|
* worker keeps running); closing or terminating it is up to its owner.
|
package/Store/Store/pushStore.js
CHANGED
|
@@ -43,35 +43,16 @@ export class PushStore extends ReadStore {
|
|
|
43
43
|
* @param keyFunc Optional function to generate a key for a given data value, used to
|
|
44
44
|
* resolve root objects. Pass the same key logic the `DiffTreeWriter` was constructed
|
|
45
45
|
* with, if any.
|
|
46
|
-
* @param cloneData Whether to deep-clone the values the store adopts (false by default).
|
|
47
|
-
* A `Worker` or `MessagePort` already delivers a structured-clone copy of every message.
|
|
48
|
-
* A same-thread `EventTarget` delivers the writer's own objects, so either the writer
|
|
49
|
-
* posts copies or this is true; otherwise the store and the writer's tree share objects.
|
|
50
46
|
*/
|
|
51
|
-
constructor(eventTarget, keyFunc
|
|
52
|
-
super(keyFunc
|
|
47
|
+
constructor(eventTarget, keyFunc) {
|
|
48
|
+
super(keyFunc);
|
|
53
49
|
this.eventTarget = eventTarget;
|
|
54
50
|
this.error = null;
|
|
55
51
|
this.errorScope = ObservableScope.Basic(() => this.error);
|
|
56
52
|
this.messageHandler = (event) => {
|
|
57
53
|
const messageData = event.data;
|
|
58
54
|
if (messageData && messageData.type === DIFF_TREE_EVENT) {
|
|
59
|
-
|
|
60
|
-
for (let x = 0; x < data.length; x++) {
|
|
61
|
-
const result = data[x];
|
|
62
|
-
switch (result.type) {
|
|
63
|
-
case "diff": {
|
|
64
|
-
this.UpdateRootMap(result.result);
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
case "splice": {
|
|
68
|
-
const spliceResult = result.result;
|
|
69
|
-
this.UpdateRootMap(spliceResult.diffResult);
|
|
70
|
-
this.SpliceRootObject(spliceResult.path[0], spliceResult.start, spliceResult.deleteCount, spliceResult.items);
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
55
|
+
this.UpdateRootMap(messageData.data);
|
|
75
56
|
if (this.error !== null) {
|
|
76
57
|
this.error = null;
|
|
77
58
|
ObservableScope.Update(this.errorScope);
|
package/Store/Store/store.d.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { JsonDiffResult } from "../../Utils/json";
|
|
|
9
9
|
*/
|
|
10
10
|
export declare abstract class ReadStore {
|
|
11
11
|
protected keyFunc?: (value: any) => string | undefined;
|
|
12
|
-
private cloneData;
|
|
13
12
|
private rootMap;
|
|
14
13
|
private createNode;
|
|
15
14
|
/**
|
|
@@ -17,7 +16,7 @@ export declare abstract class ReadStore {
|
|
|
17
16
|
* @param keyFunc Optional function to generate a key for a given data value.
|
|
18
17
|
* When provided, enables alias functionality where values can reference other root objects.
|
|
19
18
|
*/
|
|
20
|
-
constructor(keyFunc?: (value: any) => string | undefined
|
|
19
|
+
constructor(keyFunc?: (value: any) => string | undefined);
|
|
21
20
|
/**
|
|
22
21
|
* Retrieves a value from the store by id.
|
|
23
22
|
* @template O - The type of the value to retrieve
|
|
@@ -40,7 +39,6 @@ export declare abstract class ReadStore {
|
|
|
40
39
|
* @protected
|
|
41
40
|
*/
|
|
42
41
|
protected UpdateRootMap(results: JsonDiffResult): void;
|
|
43
|
-
protected SpliceRootObject(rootPath: string | number, start: number, deleteCount: number, items: any[]): void;
|
|
44
42
|
/**
|
|
45
43
|
* Updates a specific root object with diff results.
|
|
46
44
|
* Creates the root object if it doesn't exist.
|
|
@@ -62,5 +60,5 @@ export declare abstract class Store extends ReadStore {
|
|
|
62
60
|
abstract Write(data: unknown, key?: string): Promise<void> | void;
|
|
63
61
|
abstract Patch(key: string, patch: unknown): Promise<void> | void;
|
|
64
62
|
abstract Push(key: string, ...data: unknown[]): Promise<void> | void;
|
|
65
|
-
abstract Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<
|
|
63
|
+
abstract Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<void> | void;
|
|
66
64
|
}
|
package/Store/Store/store.js
CHANGED
|
@@ -14,14 +14,15 @@ export class ReadStore {
|
|
|
14
14
|
* @param keyFunc Optional function to generate a key for a given data value.
|
|
15
15
|
* When provided, enables alias functionality where values can reference other root objects.
|
|
16
16
|
*/
|
|
17
|
-
constructor(keyFunc
|
|
17
|
+
constructor(keyFunc) {
|
|
18
18
|
this.keyFunc = keyFunc;
|
|
19
|
-
this.cloneData = cloneData;
|
|
20
19
|
this.rootMap = new Map();
|
|
21
20
|
const aliasFunc = keyFunc &&
|
|
22
21
|
((value, reactive = true) => {
|
|
23
22
|
const key = keyFunc(value);
|
|
24
|
-
|
|
23
|
+
// A route key names an entity in a DiffTree route instance, which never reaches the
|
|
24
|
+
// store: read the embedded copy
|
|
25
|
+
if (key === undefined || key.startsWith("/"))
|
|
25
26
|
return undefined;
|
|
26
27
|
const rootObject = this.rootMap.get(key);
|
|
27
28
|
if (rootObject === undefined)
|
|
@@ -61,10 +62,6 @@ export class ReadStore {
|
|
|
61
62
|
this.UpdateRootObject(rootGroup[0].path[0], rootGroup);
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
|
-
SpliceRootObject(rootPath, start, deleteCount, items) {
|
|
65
|
-
const rootObject = this.rootMap.get(rootPath);
|
|
66
|
-
ObservableNode.ApplySplice(rootObject[rootPath], start, deleteCount, items, this.cloneData);
|
|
67
|
-
}
|
|
68
65
|
/**
|
|
69
66
|
* Updates a specific root object with diff results.
|
|
70
67
|
* Creates the root object if it doesn't exist.
|
|
@@ -76,14 +73,18 @@ export class ReadStore {
|
|
|
76
73
|
UpdateRootObject(rootPath, results) {
|
|
77
74
|
const rootObject = this.rootMap.get(rootPath);
|
|
78
75
|
if (rootObject === undefined) {
|
|
79
|
-
if (results.
|
|
80
|
-
throw `Unable to initialize root path ${rootPath} with
|
|
81
|
-
const rootData =
|
|
76
|
+
if (results[0].type !== "write" || results[0].path.length > 1)
|
|
77
|
+
throw `Unable to initialize root path ${rootPath} with initial path ${results[0].path}`;
|
|
78
|
+
const rootData = JsonDeepClone(results[0].value);
|
|
82
79
|
const newRootObject = this.createNode({ [rootPath]: rootData }, false);
|
|
83
80
|
this.rootMap.set(rootPath, newRootObject);
|
|
81
|
+
// Later entries for the same root (e.g. route summaries written one instance at a time)
|
|
82
|
+
// apply to the root just created
|
|
83
|
+
if (results.length > 1)
|
|
84
|
+
ObservableNode.ApplyDiff(newRootObject, results.slice(1));
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
86
|
-
ObservableNode.ApplyDiff(rootObject, results
|
|
87
|
+
ObservableNode.ApplyDiff(rootObject, results);
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
/**
|
|
@@ -24,7 +24,7 @@ import { Store } from "./store";
|
|
|
24
24
|
* await store.Push("users", { id: 1 }, { id: 2 });
|
|
25
25
|
*
|
|
26
26
|
* // Splicing an array asynchronously
|
|
27
|
-
*
|
|
27
|
+
* await store.Splice("users", 0, 1, { id: 3 });
|
|
28
28
|
*
|
|
29
29
|
* // Cleaning up resources
|
|
30
30
|
* store.Destroy();
|
|
@@ -41,7 +41,7 @@ export declare class StoreAsync extends Store {
|
|
|
41
41
|
/**
|
|
42
42
|
* Creates an instance of StoreAsync.
|
|
43
43
|
* @param diffWorker A `Worker` running a `DiffTree` connected via `ConnectWorkerToDiffTree`
|
|
44
|
-
* (e.g. a bundled `defaultDiffTreeWorker` entry). Any `keyFunc`/
|
|
44
|
+
* (e.g. a bundled `defaultDiffTreeWorker` entry). Any `keyFunc`/routes the diff needs
|
|
45
45
|
* must be baked into that worker's own entry file.
|
|
46
46
|
* @param keyFunc Optional function to generate a key for a given data value, used locally by
|
|
47
47
|
* this store to resolve root objects. Pass the same key logic the worker's `DiffTree` was
|
|
@@ -88,12 +88,11 @@ export declare class StoreAsync extends Store {
|
|
|
88
88
|
* @param start The position at which to start changing the array.
|
|
89
89
|
* @param deleteCount Optional number of elements to delete. If not provided, all elements from start to end will be deleted.
|
|
90
90
|
* @param items Optional elements to insert into the array.
|
|
91
|
-
* @returns A promise that resolves
|
|
92
|
-
* applied to the store.
|
|
91
|
+
* @returns A promise that resolves once the splice has been applied to the store.
|
|
93
92
|
* @throws Will throw if the key isn't in the store and has no pending Write, or reject with
|
|
94
93
|
* the original error if an earlier change failed.
|
|
95
94
|
*/
|
|
96
|
-
Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<
|
|
95
|
+
Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<void>;
|
|
97
96
|
/**
|
|
98
97
|
* Returns a promise that resolves once every change made before this call has been applied
|
|
99
98
|
* to the store. Changes made after this call are sent in a later batch, so they aren't
|