j-templates 8.2.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ };