j-templates 8.2.0 → 9.0.1
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/Node/vNode.js +2 -0
- package/Store/Diff/diffTree.d.ts +24 -49
- package/Store/Diff/diffTree.js +112 -511
- package/Store/Diff/index.d.ts +1 -0
- package/Store/Diff/index.js +1 -0
- package/Store/Diff/utils/diffTree.types.d.ts +6 -1
- package/Store/Diff/utils/homes.d.ts +46 -0
- package/Store/Diff/utils/homes.js +213 -0
- package/Store/Diff/utils/paths.d.ts +42 -0
- package/Store/Diff/utils/paths.js +87 -0
- package/Store/Diff/utils/projections.d.ts +55 -0
- package/Store/Diff/utils/projections.js +290 -0
- package/Store/Diff/utils/routes.d.ts +27 -0
- package/Store/Diff/utils/routes.js +168 -0
- package/Store/Diff/utils/state.d.ts +56 -0
- package/Store/Diff/utils/state.js +155 -0
- package/Store/Store/store.d.ts +8 -0
- package/Store/Store/store.js +13 -0
- package/Store/Store/storeAsync.js +7 -6
- package/Store/Store/storeSync.js +3 -3
- package/Store/Tree/observableNode.js +18 -15
- package/Store/Tree/observableScope.d.ts +1 -0
- package/Store/Tree/observableScope.js +4 -0
- package/Store/Tree/snapshot.d.ts +19 -21
- package/Store/Tree/snapshot.js +29 -23
- package/Utils/json.d.ts +166 -26
- package/Utils/json.js +510 -183
- package/package.json +1 -1
- package/Store/Diff/utils/diffTree.utils.d.ts +0 -79
- package/Store/Diff/utils/diffTree.utils.js +0 -398
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { JsonMaintainUnchanged, JsonType, SetOwnProperty } from "../../../Utils/json";
|
|
2
|
+
import { Snapshot } from "../../Tree/snapshot";
|
|
3
|
+
import { IsWildcardPath, PROJECTION_PREFIX, PropertiesOverlap, RouteMatches, SplitRouteAndPath } from "./paths";
|
|
4
|
+
import { ExpandWildcardPath, KeyUnchanged, ReadPath, ReadSnapshot, WritePath } from "./state";
|
|
5
|
+
/** Runs the projections of a DiffTree's route instances against its state */
|
|
6
|
+
export class Projections {
|
|
7
|
+
constructor(snapshot, rootState, routes) {
|
|
8
|
+
this.snapshot = snapshot;
|
|
9
|
+
this.rootState = rootState;
|
|
10
|
+
this.routes = routes;
|
|
11
|
+
this.orderVersion = -1;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Runs every projection of every route instance, dependencies first. A projection is skipped
|
|
15
|
+
* when no route it reads through changed since its last run, when a read's parent is missing,
|
|
16
|
+
* or when every read value is the same as its last run. Outputs are written to
|
|
17
|
+
* `$projection_<key>` in their route without a diff, and are their own snapshots, so they're
|
|
18
|
+
* never changed in place. Instead, each part of an output with no changes from the last one
|
|
19
|
+
* written is replaced by the last one's object (JsonMaintainUnchanged): unchanged parts keep
|
|
20
|
+
* their identity, so the projections and readers using them are skipped and `Commit` passes
|
|
21
|
+
* over them. An output with no changes at all isn't written.
|
|
22
|
+
* @throws When projections depend on each other in a cycle
|
|
23
|
+
*/
|
|
24
|
+
Run() {
|
|
25
|
+
// Rebuilt once after any number of instances were created or deleted
|
|
26
|
+
if (this.order === undefined || this.orderVersion !== this.routes.version) {
|
|
27
|
+
this.order = OrderProjections(this.routes.root);
|
|
28
|
+
this.orderVersion = this.routes.version;
|
|
29
|
+
}
|
|
30
|
+
const order = this.order;
|
|
31
|
+
for (let x = 0; x < order.length; x++) {
|
|
32
|
+
const projection = order[x];
|
|
33
|
+
if (this.SourcesUnchanged(projection.sources))
|
|
34
|
+
continue;
|
|
35
|
+
const previous = projection.sources;
|
|
36
|
+
const values = this.ReadValues(projection.reads, projection.sources = new Array(projection.reads.length), previous);
|
|
37
|
+
if (!values || SameValues(projection.reads, values, projection.values))
|
|
38
|
+
continue;
|
|
39
|
+
let output;
|
|
40
|
+
if (projection.keyed) {
|
|
41
|
+
output = RunKeyed(projection, projection.keyed, values[0]);
|
|
42
|
+
// Every entry kept its output, so the record it wrote last run still holds
|
|
43
|
+
if (output === UNCHANGED) {
|
|
44
|
+
projection.values = values;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const last = ReadPath(this.rootState, projection.output);
|
|
50
|
+
// The last output is its own snapshot, so it's never changed and its parts can be shared
|
|
51
|
+
output = JsonMaintainUnchanged(projection.projection(...values), last);
|
|
52
|
+
// An output with no changes leaves the last one in place, so the projections reading it
|
|
53
|
+
// see the same objects and are skipped
|
|
54
|
+
if (projection.values !== undefined && output === last) {
|
|
55
|
+
projection.values = values;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
WritePath(this.snapshot, this.rootState, projection.output, output);
|
|
60
|
+
Snapshot.Register(this.snapshot, output);
|
|
61
|
+
if (projection.readsOutput) {
|
|
62
|
+
// Its own output is one of its reads, so the output it just wrote isn't a change
|
|
63
|
+
const read = projection.sources;
|
|
64
|
+
projection.values = this.ReadValues(projection.reads, projection.sources = new Array(projection.reads.length), read);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
projection.values = values;
|
|
68
|
+
this.RefreshSources(projection.sources, SplitRouteAndPath(projection.output)[0]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether every source a projection's reads resolved through on its last full read still has
|
|
74
|
+
* the snapshot it read: a write anywhere under a value clears its cached snapshot, so then no
|
|
75
|
+
* read can have changed. A wildcard read also needs the same route instances to exist.
|
|
76
|
+
*/
|
|
77
|
+
SourcesUnchanged(sources) {
|
|
78
|
+
if (!sources)
|
|
79
|
+
return false;
|
|
80
|
+
for (let x = 0; x < sources.length; x++) {
|
|
81
|
+
const source = sources[x];
|
|
82
|
+
if (!source)
|
|
83
|
+
return false;
|
|
84
|
+
if ("matches" in source) {
|
|
85
|
+
if (source.version !== this.routes.version)
|
|
86
|
+
return false;
|
|
87
|
+
for (let y = 0; y < source.matches.length; y++)
|
|
88
|
+
if (!this.SourceUnchanged(source.matches[y]))
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
else if (!this.SourceUnchanged(source))
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Whether a source still has the snapshot it was read with. A key's value is read from the
|
|
98
|
+
* state again, so a key written with a new value is a change too (see KeyUnchanged).
|
|
99
|
+
*/
|
|
100
|
+
SourceUnchanged(source) {
|
|
101
|
+
if (source.key === undefined)
|
|
102
|
+
return Snapshot.Peek(this.snapshot, source.state) === source.snapshot;
|
|
103
|
+
return KeyUnchanged(this.snapshot, source.state, source.key, source.snapshot);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Takes a new snapshot of every source in the route a projection just wrote its output to that
|
|
107
|
+
* the write cleared. Nothing the projection read changed between its read and the write, so
|
|
108
|
+
* without this the next run would read everything again for nothing. A source of one key keeps
|
|
109
|
+
* its snapshot unless the output is under that key.
|
|
110
|
+
*/
|
|
111
|
+
RefreshSources(sources, outputRoute) {
|
|
112
|
+
for (let x = 0; x < sources.length; x++) {
|
|
113
|
+
const source = sources[x];
|
|
114
|
+
const routes = source && ("matches" in source ? source.matches : [source]);
|
|
115
|
+
for (let y = 0; routes && y < routes.length; y++) {
|
|
116
|
+
const route = routes[y];
|
|
117
|
+
if (route.route !== outputRoute || this.SourceUnchanged(route))
|
|
118
|
+
continue;
|
|
119
|
+
route.snapshot = route.key === undefined ?
|
|
120
|
+
Snapshot.Get(this.snapshot, route.state) :
|
|
121
|
+
Snapshot.Child(this.snapshot, route.state, route.key);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Snapshots of the values at each path.
|
|
127
|
+
* @param sources Filled with what each read resolved through; a read after a missing one is left undefined
|
|
128
|
+
* @param previous The sources of the last full read, so a wildcard read reuses its unchanged matches
|
|
129
|
+
* @returns The values, or undefined when any of them is missing
|
|
130
|
+
*/
|
|
131
|
+
ReadValues(paths, sources, previous) {
|
|
132
|
+
const values = new Array(paths.length);
|
|
133
|
+
for (let x = 0; x < paths.length; x++) {
|
|
134
|
+
if (IsWildcardPath(paths[x])) {
|
|
135
|
+
const source = this.ReadWildcard(paths[x], previous?.[x]);
|
|
136
|
+
values[x] = WildcardValue(source);
|
|
137
|
+
sources[x] = source;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const [value, found, source] = ReadSnapshot(this.snapshot, this.rootState, paths[x]);
|
|
141
|
+
sources[x] = source;
|
|
142
|
+
if (!found)
|
|
143
|
+
return undefined;
|
|
144
|
+
values[x] = value;
|
|
145
|
+
}
|
|
146
|
+
return values;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Reads every route a wildcard path matches. While no route instance was added or removed since
|
|
150
|
+
* the last read, the matches are the same, and a match whose route has the same snapshot keeps
|
|
151
|
+
* the value it read.
|
|
152
|
+
*/
|
|
153
|
+
ReadWildcard(path, previous) {
|
|
154
|
+
const version = this.routes.version;
|
|
155
|
+
if (previous && previous.version === version) {
|
|
156
|
+
const matches = previous.matches.map(match => this.SourceUnchanged(match) ? match : this.ReadMatch(match.segment, match.path));
|
|
157
|
+
return { version, matches };
|
|
158
|
+
}
|
|
159
|
+
const paths = ExpandWildcardPath(this.rootState, path);
|
|
160
|
+
const matches = [];
|
|
161
|
+
for (const segment in paths)
|
|
162
|
+
matches.push(this.ReadMatch(segment, paths[segment]));
|
|
163
|
+
return { version, matches };
|
|
164
|
+
}
|
|
165
|
+
ReadMatch(segment, path) {
|
|
166
|
+
const [value, found, source] = ReadSnapshot(this.snapshot, this.rootState, path);
|
|
167
|
+
// An expanded path's route exists, so a source is always found
|
|
168
|
+
return { ...source, segment, path, value, found };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The value of a wildcard read: each match's value keyed by its segment. A match missing the
|
|
173
|
+
* value is left out, so no match reads as an empty object.
|
|
174
|
+
*/
|
|
175
|
+
function WildcardValue(source) {
|
|
176
|
+
const values = {};
|
|
177
|
+
for (let x = 0; x < source.matches.length; x++)
|
|
178
|
+
if (source.matches[x].found)
|
|
179
|
+
values[source.matches[x].segment] = source.matches[x].value;
|
|
180
|
+
return values;
|
|
181
|
+
}
|
|
182
|
+
/** Whether every read value is the same as its last run; a wildcard read compares each matched value */
|
|
183
|
+
function SameValues(paths, values, lastValues) {
|
|
184
|
+
if (!lastValues)
|
|
185
|
+
return false;
|
|
186
|
+
for (let x = 0; x < values.length; x++)
|
|
187
|
+
if (values[x] !== lastValues[x] && !(IsWildcardPath(paths[x]) && SameEntries(values[x], lastValues[x])))
|
|
188
|
+
return false;
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
function SameEntries(values, lastValues) {
|
|
192
|
+
const keys = Object.keys(values);
|
|
193
|
+
if (keys.length !== Object.keys(lastValues).length)
|
|
194
|
+
return false;
|
|
195
|
+
for (let x = 0; x < keys.length; x++)
|
|
196
|
+
if (!Object.hasOwn(lastValues, keys[x]) || values[keys[x]] !== lastValues[keys[x]])
|
|
197
|
+
return false;
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
/** What `RunKeyed` returns when no entry's output changed and none was added or removed */
|
|
201
|
+
const UNCHANGED = Symbol("UNCHANGED");
|
|
202
|
+
/**
|
|
203
|
+
* A keyed projection's output for a record: each entry's output, from its last run when its value
|
|
204
|
+
* is the same, and without the entries it returns undefined for. A value that isn't an object or
|
|
205
|
+
* array reads as an empty record.
|
|
206
|
+
* @returns The output, or `UNCHANGED` when it would hold the same entries as the last one written
|
|
207
|
+
*/
|
|
208
|
+
function RunKeyed(projection, state, record) {
|
|
209
|
+
const entries = new Map();
|
|
210
|
+
const output = {};
|
|
211
|
+
let changed = projection.values === undefined;
|
|
212
|
+
const keys = JsonType(record) === "value" ? [] : Object.keys(record);
|
|
213
|
+
for (let x = 0; x < keys.length; x++) {
|
|
214
|
+
const key = keys[x];
|
|
215
|
+
if (state.routeState && key.startsWith(PROJECTION_PREFIX))
|
|
216
|
+
continue;
|
|
217
|
+
const value = record[key];
|
|
218
|
+
const last = state.entries.get(key);
|
|
219
|
+
// A new output keeps every part of the last one with no changes (see Run)
|
|
220
|
+
const entry = last && last.value === value ? last : { value, output: JsonMaintainUnchanged(projection.projection(value, key), last?.output) };
|
|
221
|
+
entries.set(key, entry);
|
|
222
|
+
changed ||= entry !== last && entry.output !== last?.output;
|
|
223
|
+
if (entry.output !== undefined)
|
|
224
|
+
SetOwnProperty(output, key, entry.output);
|
|
225
|
+
}
|
|
226
|
+
// A key that's gone changes the output unless it was left out of it
|
|
227
|
+
for (const [key, last] of state.entries)
|
|
228
|
+
if (!entries.has(key) && last.output !== undefined)
|
|
229
|
+
changed = true;
|
|
230
|
+
state.entries = entries;
|
|
231
|
+
return changed ? output : UNCHANGED;
|
|
232
|
+
}
|
|
233
|
+
function CollectProjections(route, byRoute) {
|
|
234
|
+
for (let x = 0; x < route.projections.length; x++) {
|
|
235
|
+
const [outputRoute] = SplitRouteAndPath(route.projections[x].output);
|
|
236
|
+
let projections = byRoute.get(outputRoute);
|
|
237
|
+
projections || byRoute.set(outputRoute, projections = []);
|
|
238
|
+
projections.push(route.projections[x]);
|
|
239
|
+
}
|
|
240
|
+
for (const child of route.childRoutes.values())
|
|
241
|
+
CollectProjections(child, byRoute);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Topologically sorts every projection under a route: a projection depends on every other
|
|
245
|
+
* projection whose output overlaps one of its reads, so that projection is sorted earlier.
|
|
246
|
+
* @throws When projections depend on each other in a cycle
|
|
247
|
+
*/
|
|
248
|
+
function OrderProjections(rootRoute) {
|
|
249
|
+
const byRoute = new Map();
|
|
250
|
+
CollectProjections(rootRoute, byRoute);
|
|
251
|
+
const order = [];
|
|
252
|
+
const state = new Map();
|
|
253
|
+
// The projections of every route a wildcard read's route matches, found once per read route
|
|
254
|
+
const wildcardCandidates = new Map();
|
|
255
|
+
const CandidatesOf = (readRoute) => {
|
|
256
|
+
let candidates = wildcardCandidates.get(readRoute);
|
|
257
|
+
if (!candidates) {
|
|
258
|
+
candidates = [];
|
|
259
|
+
for (const [outputRoute, projections] of byRoute)
|
|
260
|
+
if (RouteMatches(readRoute, outputRoute))
|
|
261
|
+
candidates.push(projections);
|
|
262
|
+
wildcardCandidates.set(readRoute, candidates);
|
|
263
|
+
}
|
|
264
|
+
return candidates;
|
|
265
|
+
};
|
|
266
|
+
function Visit(projection, stack) {
|
|
267
|
+
if (state.get(projection) === "done")
|
|
268
|
+
return;
|
|
269
|
+
if (state.get(projection) === "visiting")
|
|
270
|
+
throw new Error(`DiffTree projection cycle detected: ${stack.concat(projection.output).join(" -> ")}`);
|
|
271
|
+
state.set(projection, "visiting");
|
|
272
|
+
const next = stack.concat(projection.output);
|
|
273
|
+
// A keyed projection of its route's own state reads none of the route's projection outputs
|
|
274
|
+
for (let x = 0; !projection.keyed?.routeState && x < projection.reads.length; x++) {
|
|
275
|
+
const [readRoute, readProperty] = SplitRouteAndPath(projection.reads[x]);
|
|
276
|
+
// A wildcard read can match outputs in any number of routes; the others only in their own
|
|
277
|
+
const routeCandidates = IsWildcardPath(projection.reads[x]) ? CandidatesOf(readRoute) : [byRoute.get(readRoute) ?? []];
|
|
278
|
+
for (const candidates of routeCandidates)
|
|
279
|
+
for (let y = 0; y < candidates.length; y++)
|
|
280
|
+
if (candidates[y] !== projection && PropertiesOverlap(readProperty, SplitRouteAndPath(candidates[y].output)[1]))
|
|
281
|
+
Visit(candidates[y], next);
|
|
282
|
+
}
|
|
283
|
+
state.set(projection, "done");
|
|
284
|
+
order.push(projection);
|
|
285
|
+
}
|
|
286
|
+
for (const projections of byRoute.values())
|
|
287
|
+
for (let x = 0; x < projections.length; x++)
|
|
288
|
+
Visit(projections[x], []);
|
|
289
|
+
return order;
|
|
290
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { IKeyFunc, IRoute, IRouteDefinitions } from "./diffTree.types";
|
|
2
|
+
/**
|
|
3
|
+
* A DiffTree's route instances: one per route path a definition matches, created from the
|
|
4
|
+
* definition on first use, each with its own projections and keyFunc.
|
|
5
|
+
*/
|
|
6
|
+
export declare class RouteInstances {
|
|
7
|
+
private keyFunc;
|
|
8
|
+
readonly root: IRoute;
|
|
9
|
+
/**
|
|
10
|
+
* Changes whenever an instance may have been created or removed, so the projection order can
|
|
11
|
+
* tell it needs rebuilding and a wildcard read that its matches may have changed
|
|
12
|
+
*/
|
|
13
|
+
version: number;
|
|
14
|
+
private cache;
|
|
15
|
+
constructor(keyFunc: IKeyFunc | undefined, definitions: IRouteDefinitions);
|
|
16
|
+
/**
|
|
17
|
+
* Finds the route instance for a route path (the portion of a path before the last `/`), creating it (and any missing
|
|
18
|
+
* parent instances) from the matching definition on first use.
|
|
19
|
+
* @returns The deepest route instance matched by a definition, or the root route when none matches
|
|
20
|
+
*/
|
|
21
|
+
Get(routePath: string): IRoute;
|
|
22
|
+
/**
|
|
23
|
+
* Removes every instance at or below a route.
|
|
24
|
+
* @param route A full, non-root route path, e.g. `/channel/1/`
|
|
25
|
+
*/
|
|
26
|
+
Delete(route: string): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { IsWildcardPath, JoinPaths, PathsOverlap, PROJECTION_PREFIX, ROOT_ROUTE, RouteSegments, ValidateWildcardPath } from "./paths";
|
|
2
|
+
/**
|
|
3
|
+
* A DiffTree's route instances: one per route path a definition matches, created from the
|
|
4
|
+
* definition on first use, each with its own projections and keyFunc.
|
|
5
|
+
*/
|
|
6
|
+
export class RouteInstances {
|
|
7
|
+
constructor(keyFunc, definitions) {
|
|
8
|
+
this.keyFunc = keyFunc;
|
|
9
|
+
/**
|
|
10
|
+
* Changes whenever an instance may have been created or removed, so the projection order can
|
|
11
|
+
* tell it needs rebuilding and a wildcard read that its matches may have changed
|
|
12
|
+
*/
|
|
13
|
+
this.version = 0;
|
|
14
|
+
// Each route path looked up, by the instance it found; cleared below a deleted route
|
|
15
|
+
this.cache = new Map();
|
|
16
|
+
if (!Object.hasOwn(definitions, ROOT_ROUTE))
|
|
17
|
+
definitions = { ...definitions, [ROOT_ROUTE]: { projections: {} } };
|
|
18
|
+
this.root = CreateRoute([], ROOT_ROUTE, definitions[ROOT_ROUTE], keyFunc);
|
|
19
|
+
// Every other pattern in the map is a child of the root route
|
|
20
|
+
this.root.routeDefinitions = definitions;
|
|
21
|
+
this.root.childPartCounts = PartCounts(definitions);
|
|
22
|
+
// Both spellings of the root route, so the first lookup of either isn't taken for a new instance
|
|
23
|
+
this.cache.set("", this.root);
|
|
24
|
+
this.cache.set(ROOT_ROUTE, this.root);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Finds the route instance for a route path (the portion of a path before the last `/`), creating it (and any missing
|
|
28
|
+
* parent instances) from the matching definition on first use.
|
|
29
|
+
* @returns The deepest route instance matched by a definition, or the root route when none matches
|
|
30
|
+
*/
|
|
31
|
+
Get(routePath) {
|
|
32
|
+
let route = this.cache.get(routePath);
|
|
33
|
+
if (route)
|
|
34
|
+
return route;
|
|
35
|
+
this.version++;
|
|
36
|
+
const segments = RouteSegments(routePath);
|
|
37
|
+
route = segments.length === 0 ? this.root : FindRoute(this.root, segments, 0, this.keyFunc) ?? this.root;
|
|
38
|
+
this.cache.set(routePath, route);
|
|
39
|
+
return route;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Removes every instance at or below a route.
|
|
43
|
+
* @param route A full, non-root route path, e.g. `/channel/1/`
|
|
44
|
+
*/
|
|
45
|
+
Delete(route) {
|
|
46
|
+
RemoveRouteInstances(this.root, route);
|
|
47
|
+
for (const cachedPath of this.cache.keys())
|
|
48
|
+
if (cachedPath.startsWith(route))
|
|
49
|
+
this.cache.delete(cachedPath);
|
|
50
|
+
this.version++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function FindRoute(parent, segments, index, defaultKeyFunc) {
|
|
54
|
+
let route = FindChildRoute(parent, segments, index);
|
|
55
|
+
if (!route) {
|
|
56
|
+
const match = MatchRouteDefinition(parent.routeDefinitions, segments, index);
|
|
57
|
+
if (!match)
|
|
58
|
+
return undefined;
|
|
59
|
+
const end = index + match.length;
|
|
60
|
+
const parts = segments.slice(index, end);
|
|
61
|
+
route = CreateRoute(parts, `/${segments.slice(0, end).join("/")}/`, match.definition, defaultKeyFunc);
|
|
62
|
+
// Kept even when a deeper segment matches no definition, so a write below an undefined
|
|
63
|
+
// child route still creates the parent routes whose projections may read it
|
|
64
|
+
parent.childRoutes.set(parts.join("/"), route);
|
|
65
|
+
}
|
|
66
|
+
const nextIndex = index + route.parts.length;
|
|
67
|
+
// Segments below the deepest defined route belong to it
|
|
68
|
+
return nextIndex === segments.length ? route : FindRoute(route, segments, nextIndex, defaultKeyFunc) ?? route;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The existing child instance the segments from `index` name, looked up by key for each number
|
|
72
|
+
* of segments a child pattern spans, so finding one doesn't scan its siblings.
|
|
73
|
+
*/
|
|
74
|
+
function FindChildRoute(parent, segments, index) {
|
|
75
|
+
for (let x = 0; x < parent.childPartCounts.length; x++) {
|
|
76
|
+
const count = parent.childPartCounts[x];
|
|
77
|
+
if (index + count > segments.length)
|
|
78
|
+
continue;
|
|
79
|
+
const route = parent.childRoutes.get(segments.slice(index, index + count).join("/"));
|
|
80
|
+
if (route)
|
|
81
|
+
return route;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/** Removes every route instance at or below a route path from the instances under `route` */
|
|
86
|
+
function RemoveRouteInstances(route, routePath) {
|
|
87
|
+
for (const [key, child] of route.childRoutes) {
|
|
88
|
+
if (child.path.startsWith(routePath))
|
|
89
|
+
route.childRoutes.delete(key);
|
|
90
|
+
else if (routePath.startsWith(child.path))
|
|
91
|
+
RemoveRouteInstances(child, routePath);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** The distinct numbers of segments the patterns in a definition map span, in definition order */
|
|
95
|
+
function PartCounts(definitions) {
|
|
96
|
+
const counts = [];
|
|
97
|
+
for (const pattern in definitions) {
|
|
98
|
+
const count = RouteSegments(pattern).length;
|
|
99
|
+
// The root pattern has no segments and is never a child
|
|
100
|
+
if (count > 0 && !counts.includes(count))
|
|
101
|
+
counts.push(count);
|
|
102
|
+
}
|
|
103
|
+
return counts;
|
|
104
|
+
}
|
|
105
|
+
function MatchRouteDefinition(definitions, segments, index) {
|
|
106
|
+
if (!definitions)
|
|
107
|
+
return undefined;
|
|
108
|
+
for (const pattern in definitions) {
|
|
109
|
+
const patternParts = RouteSegments(pattern);
|
|
110
|
+
// The root pattern has no segments and would match anything
|
|
111
|
+
if (patternParts.length === 0 || patternParts.length > segments.length - index)
|
|
112
|
+
continue;
|
|
113
|
+
let x = 0;
|
|
114
|
+
for (; x < patternParts.length && (IsParam(patternParts[x]) || patternParts[x] === segments[index + x]); x++)
|
|
115
|
+
;
|
|
116
|
+
if (x === patternParts.length)
|
|
117
|
+
return { definition: definitions[pattern], length: patternParts.length };
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* @param routePath The instance's full route from the tree root, e.g. `/Root/1234/`. Each projection
|
|
123
|
+
* read is resolved against it, so `name` becomes `/Root/1234/name`; a read starting with `/` is kept as-is.
|
|
124
|
+
* @param defaultKeyFunc The tree's keyFunc, used when the definition doesn't define one
|
|
125
|
+
*/
|
|
126
|
+
function CreateRoute(parts, routePath, definition, defaultKeyFunc) {
|
|
127
|
+
const projections = [];
|
|
128
|
+
for (const key in definition.projections) {
|
|
129
|
+
const projectionDefinition = definition.projections[key];
|
|
130
|
+
const output = JoinPaths(routePath, `${PROJECTION_PREFIX}${key}`);
|
|
131
|
+
if (IsKeyedDefinition(projectionDefinition)) {
|
|
132
|
+
const { read, projection } = projectionDefinition;
|
|
133
|
+
if (read !== undefined && IsWildcardPath(read))
|
|
134
|
+
throw new Error(`Invalid keyed projection read ${read}: a keyed projection reads one record, without a wildcard.`);
|
|
135
|
+
// Its route's own state, without the outputs, so its own output isn't one of its reads
|
|
136
|
+
const routeRead = JoinPaths(routePath, read ?? "");
|
|
137
|
+
projections.push({
|
|
138
|
+
key,
|
|
139
|
+
reads: [routeRead],
|
|
140
|
+
projection,
|
|
141
|
+
output,
|
|
142
|
+
readsOutput: read !== undefined && PathsOverlap(routeRead, output),
|
|
143
|
+
keyed: { entries: new Map(), routeState: read === undefined }
|
|
144
|
+
});
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const { reads, projection } = projectionDefinition;
|
|
148
|
+
reads.forEach(ValidateWildcardPath);
|
|
149
|
+
const routeReads = reads.map(read => JoinPaths(routePath, read));
|
|
150
|
+
projections.push({ key, reads: routeReads, projection, output, readsOutput: routeReads.some(read => PathsOverlap(read, output)) });
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
path: routePath,
|
|
154
|
+
parts,
|
|
155
|
+
projections,
|
|
156
|
+
childRoutes: new Map(),
|
|
157
|
+
childPartCounts: PartCounts(definition.routes),
|
|
158
|
+
keyFunc: definition.keyFunc ?? defaultKeyFunc,
|
|
159
|
+
routeDefinitions: definition.routes
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** Whether a projection definition is a keyed projection: it has no `reads` */
|
|
163
|
+
function IsKeyedDefinition(definition) {
|
|
164
|
+
return definition.reads === undefined;
|
|
165
|
+
}
|
|
166
|
+
function IsParam(part) {
|
|
167
|
+
return part.startsWith("{") && part.endsWith("}");
|
|
168
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ISnapshotState } from "../../Tree/snapshot";
|
|
2
|
+
import { IRouteSource } from "./diffTree.types";
|
|
3
|
+
/**
|
|
4
|
+
* The raw state of a DiffTree's routes. A route's state hangs off its parent route's state under
|
|
5
|
+
* `ROUTE_STATE`, keyed by segment, so `Object.keys` of a route's state lists only its own values.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ROUTE_STATE: unique symbol;
|
|
8
|
+
/**
|
|
9
|
+
* A route's state object, e.g. `/channel/1/`'s; `""` and `/` are the root.
|
|
10
|
+
* @param create Creates the route's state, and its parents', when missing
|
|
11
|
+
* @returns The state, or undefined when it's missing and `create` isn't set
|
|
12
|
+
*/
|
|
13
|
+
export declare function RouteState(root: any, route: string, create?: boolean): any;
|
|
14
|
+
/**
|
|
15
|
+
* Writes a value to a path as it is, without diffing it or keeping keyed values at their homes,
|
|
16
|
+
* creating the path's route when missing. A path to a route (`/channel/1/`) only creates it.
|
|
17
|
+
* @throws When the path's parent doesn't exist
|
|
18
|
+
*/
|
|
19
|
+
export declare function WritePath(snapshot: ISnapshotState, root: any, path: string, value: any): void;
|
|
20
|
+
/** The value at a path, or undefined when any segment of it doesn't exist; see ReadPathFound */
|
|
21
|
+
export declare function ReadPath(root: any, path: string): any;
|
|
22
|
+
/**
|
|
23
|
+
* The value at a path, and whether its parent was found: a property missing from an existing
|
|
24
|
+
* parent is found as undefined.
|
|
25
|
+
*/
|
|
26
|
+
export declare function ReadPathFound(root: any, path: string): [any, boolean];
|
|
27
|
+
/** The value at the first `depth` parts of a property path */
|
|
28
|
+
export declare function ReadPathParts(value: any, parts: string[], depth: number): any;
|
|
29
|
+
/**
|
|
30
|
+
* The snapshot of the value at a path, whether it was found, and the route it was read through,
|
|
31
|
+
* when there is one. A property path is read from the snapshot of its first segment alone, not of
|
|
32
|
+
* the whole route, so a write to the route's other keys neither rebuilds what it reads nor changes it.
|
|
33
|
+
*/
|
|
34
|
+
export declare function ReadSnapshot(snapshot: ISnapshotState, root: any, path: string): [any, boolean, IRouteSource | undefined];
|
|
35
|
+
/**
|
|
36
|
+
* Whether a key's value still has the snapshot taken of it, told by a cache lookup without
|
|
37
|
+
* taking a new one. A primitive value has no snapshot of its own and is compared as it is. A
|
|
38
|
+
* value with no cached snapshot was written since, even where the snapshot was undefined.
|
|
39
|
+
*/
|
|
40
|
+
export declare function KeyUnchanged(snapshot: ISnapshotState, state: object, key: string, keySnapshot: any): boolean;
|
|
41
|
+
export declare function Remove(snapshot: ISnapshotState, parent: any, prop: string | number): void;
|
|
42
|
+
export declare function Assign(snapshot: ISnapshotState, parent: any, prop: string | number, value: any): void;
|
|
43
|
+
/**
|
|
44
|
+
* Deletes a route's state, child routes included, from its parent route. Every cached snapshot
|
|
45
|
+
* that embedded a value from it, such as one reading a keyed value through its home in the
|
|
46
|
+
* route, is invalidated.
|
|
47
|
+
* @param route A non-root route path, e.g. `/channel/1/`
|
|
48
|
+
*/
|
|
49
|
+
export declare function DeleteRouteState(snapshot: ISnapshotState, root: any, route: string): void;
|
|
50
|
+
/**
|
|
51
|
+
* Expands a wildcard path's route against the route state under `root`.
|
|
52
|
+
* @returns Each matching path, keyed by the segment the wildcard matched
|
|
53
|
+
*/
|
|
54
|
+
export declare function ExpandWildcardPath(root: any, path: string): {
|
|
55
|
+
[segment: string]: string;
|
|
56
|
+
};
|