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.
@@ -1,2 +1,3 @@
1
1
  export * from "./diffTree.types";
2
2
  export * from "./utils/diffTree.types";
3
+ export { PROJECTION_PREFIX } from "./utils/paths";
@@ -1,2 +1,3 @@
1
1
  export * from "./diffTree.types";
2
2
  export * from "./utils/diffTree.types";
3
+ export { PROJECTION_PREFIX } from "./utils/paths";
@@ -43,10 +43,15 @@ export type IKeyedState = {
43
43
  /** Whether it reads its route's own state, which leaves out the route's projection outputs */
44
44
  routeState: boolean;
45
45
  };
46
- /** A route a read resolved through on its last full read: its path, raw state and the snapshot read from it */
46
+ /**
47
+ * A route a read resolved through on its last full read: its path, raw state and the snapshot read
48
+ * from it. A read of a property path reads only the route state's `key`, the path's first segment,
49
+ * and `snapshot` is that key's; a read of the whole route has no `key`, and `snapshot` is the route's.
50
+ */
47
51
  export type IRouteSource = {
48
52
  route: string;
49
53
  state: object;
54
+ key?: string;
50
55
  snapshot: any;
51
56
  };
52
57
  /** A route a wildcard read matched: the segment, the path read in it, and what was read */
@@ -0,0 +1,46 @@
1
+ import { ISnapshotState } from "../../Tree/snapshot";
2
+ type KeyFunc = {
3
+ (value: any): string | undefined;
4
+ };
5
+ /** Returns the keyFunc for a route path (`/channel/1/`, or `""`/`/` for the root) */
6
+ export type RouteKeyFunc = {
7
+ (route: string): KeyFunc | undefined;
8
+ };
9
+ /** What a write needs: the tree's snapshot cache, its root state, and each route's keyFunc */
10
+ export type WriteContext = {
11
+ snapshot: ISnapshotState;
12
+ root: any;
13
+ routeKeyFunc: RouteKeyFunc;
14
+ };
15
+ /**
16
+ * Writes a value to a path, keeping every keyed value at its home.
17
+ *
18
+ * A keyed value is written to its home, and every other place it appears (a "slot") holds that
19
+ * home object itself rather than a copy. So reading through a slot reads the home, a write through a
20
+ * slot writes the home, and a home's snapshot is shared by every slot. Homes keep their identity: a
21
+ * home is only ever changed in place.
22
+ *
23
+ * A write is applied in three steps:
24
+ * 1. A path that runs through a slot is a write to the first such slot's home. When the write would change
25
+ * the entity's key, the changed entity is written to the slot instead, which writes it to its new
26
+ * home and leaves the old one alone.
27
+ * 2. The value is flattened: each keyed value in it is written to its home and replaced by the home
28
+ * object. A keyed value whose home is inside the written path stays where it is: it's its own home.
29
+ * 3. The flattened value is diffed into what's at the path, in place. A slot is never diffed into,
30
+ * since that would change another entity's home: a slot written with anything else is replaced,
31
+ * and so is an array of keyed values, which holds nothing but slots.
32
+ *
33
+ * @param path The path, e.g. `value.item` or `/First/Second/value.item`
34
+ * @throws When the path's parent doesn't exist, or a home would be replaced with a value of
35
+ * another type, which would break the slots holding it
36
+ */
37
+ export declare function ApplyValueToPath(context: WriteContext, path: string, value: any): void;
38
+ /**
39
+ * Splices the array at a path in place, as `Array.prototype.splice` would: a start past the end
40
+ * splices at the end, and a delete past the end deletes to it. A path through a slot splices the
41
+ * slot's home's array. Inserted items are flattened like a written value (see ApplyValueToPath).
42
+ * @param path The array's path, e.g. `items` or `/channel/1/value.list`
43
+ * @throws When there's no array at the path
44
+ */
45
+ export declare function SpliceValueAtPath(context: WriteContext, path: string, start: number, deleteCount: number, items: readonly unknown[]): void;
46
+ export {};
@@ -0,0 +1,213 @@
1
+ import { JsonApplyDiff, JsonApplySplices, JsonDiff, JsonType, SPLICE_ACTION } from "../../../Utils/json";
2
+ import { ArraysEqual } from "../../../Utils/array";
3
+ import { Snapshot } from "../../Tree/snapshot";
4
+ import { IsPathWithin, JoinPaths, PROP_DELIMETER, ROUTE_DELIMETER, SplitRouteAndPath } from "./paths";
5
+ import { Assign, ReadPath, ReadPathParts, Remove, RouteState } from "./state";
6
+ /**
7
+ * Where a keyed value lives ("its home"): its key joined to the route it's written in. A key
8
+ * starting with `/` is a route path from the root.
9
+ * @returns The home's path, or undefined when the value isn't keyed or its key names a route the
10
+ * value's own route ends with, which can't be its home
11
+ */
12
+ function HomePath(keyFunc, route, value) {
13
+ const key = JsonType(value) === "value" ? undefined : keyFunc?.(value);
14
+ if (!key)
15
+ return undefined;
16
+ if (key.includes(ROUTE_DELIMETER)) {
17
+ const [keyRoute] = SplitRouteAndPath(key);
18
+ if (keyRoute && !keyRoute.startsWith(ROUTE_DELIMETER) && route.endsWith(keyRoute))
19
+ return undefined;
20
+ }
21
+ return JoinPaths(route, key);
22
+ }
23
+ /**
24
+ * Writes a value to a path, keeping every keyed value at its home.
25
+ *
26
+ * A keyed value is written to its home, and every other place it appears (a "slot") holds that
27
+ * home object itself rather than a copy. So reading through a slot reads the home, a write through a
28
+ * slot writes the home, and a home's snapshot is shared by every slot. Homes keep their identity: a
29
+ * home is only ever changed in place.
30
+ *
31
+ * A write is applied in three steps:
32
+ * 1. A path that runs through a slot is a write to the first such slot's home. When the write would change
33
+ * the entity's key, the changed entity is written to the slot instead, which writes it to its new
34
+ * home and leaves the old one alone.
35
+ * 2. The value is flattened: each keyed value in it is written to its home and replaced by the home
36
+ * object. A keyed value whose home is inside the written path stays where it is: it's its own home.
37
+ * 3. The flattened value is diffed into what's at the path, in place. A slot is never diffed into,
38
+ * since that would change another entity's home: a slot written with anything else is replaced,
39
+ * and so is an array of keyed values, which holds nothing but slots.
40
+ *
41
+ * @param path The path, e.g. `value.item` or `/First/Second/value.item`
42
+ * @throws When the path's parent doesn't exist, or a home would be replaced with a value of
43
+ * another type, which would break the slots holding it
44
+ */
45
+ export function ApplyValueToPath(context, path, value) {
46
+ const [route, property] = WriteRoute(path);
47
+ if (!property)
48
+ throw new Error("Unable to write to root path");
49
+ RouteState(context.root, route, true);
50
+ WriteValue(context, route, property.split(PROP_DELIMETER), value);
51
+ }
52
+ /**
53
+ * Splices the array at a path in place, as `Array.prototype.splice` would: a start past the end
54
+ * splices at the end, and a delete past the end deletes to it. A path through a slot splices the
55
+ * slot's home's array. Inserted items are flattened like a written value (see ApplyValueToPath).
56
+ * @param path The array's path, e.g. `items` or `/channel/1/value.list`
57
+ * @throws When there's no array at the path
58
+ */
59
+ export function SpliceValueAtPath(context, path, start, deleteCount, items) {
60
+ const [route, property] = WriteRoute(path);
61
+ const keyFunc = context.routeKeyFunc(route);
62
+ const targetPath = JoinPaths(route, property);
63
+ // Flattened first: a home it writes could be the one holding the array
64
+ const insert = items.map(item => ({ type: SPLICE_ACTION.INSERT, value: Flatten(context, keyFunc, route, targetPath, item) }));
65
+ const array = ReadPath(context.root, path);
66
+ if (!array)
67
+ throw new Error(`Array not found at path: ${path}`);
68
+ if (!Array.isArray(array))
69
+ throw new Error(`Value at path ${path} is not an array`);
70
+ const index = Math.min(start, array.length);
71
+ const actualDelete = Math.max(0, Math.min(deleteCount, array.length - index));
72
+ if (actualDelete === 0 && insert.length === 0)
73
+ return;
74
+ JsonApplySplices(array, [{ index, delete: actualDelete, insert }], KeepValue);
75
+ Snapshot.Invalidate(context.snapshot, array);
76
+ }
77
+ /** A written path's route from the root (`/` for the root route) and its property path */
78
+ function WriteRoute(path) {
79
+ const [route, property] = SplitRouteAndPath(path);
80
+ return [JoinPaths("", route), property];
81
+ }
82
+ function WriteValue(context, route, parts, value) {
83
+ const keyFunc = context.routeKeyFunc(route);
84
+ const routeRoot = ReadPath(context.root, route);
85
+ // Step 1: the first slot the path runs through, below the route's root and above the target.
86
+ // Values inside a home are keyed by the home's own route, so any slot past this one is found by
87
+ // the write to the home.
88
+ let slotDepth = -1;
89
+ let slotHome;
90
+ for (let x = 0, curr = routeRoot; keyFunc && x < parts.length - 1; x++) {
91
+ curr = curr?.[parts[x]];
92
+ if (JsonType(curr) === "value")
93
+ break;
94
+ const homePath = HomePath(keyFunc, route, curr);
95
+ // The path is only joined for a keyed value
96
+ if (homePath !== undefined && IsSlot(homePath, JoinPaths(route, parts.slice(0, x + 1).join(PROP_DELIMETER)))) {
97
+ slotDepth = x + 1;
98
+ slotHome = homePath;
99
+ break;
100
+ }
101
+ }
102
+ if (slotHome !== undefined) {
103
+ const home = ReadPathParts(routeRoot, parts, slotDepth);
104
+ const rest = parts.slice(slotDepth);
105
+ const changed = SetPath(home, rest, value);
106
+ if (keyFunc(changed) === keyFunc(home)) {
107
+ const [homeRoute, homeProperty] = SplitRouteAndPath(slotHome);
108
+ WriteValue(context, homeRoute, homeProperty.split(PROP_DELIMETER).concat(rest), value);
109
+ }
110
+ else
111
+ WriteValue(context, route, parts.slice(0, slotDepth), changed);
112
+ return;
113
+ }
114
+ // Step 2: flatten first, so the homes it writes exist before the slots pointing to them
115
+ const targetPath = JoinPaths(route, parts.join(PROP_DELIMETER));
116
+ const flattened = Flatten(context, keyFunc, route, targetPath, value);
117
+ // Step 3
118
+ const parent = ReadPathParts(routeRoot, parts, parts.length - 1);
119
+ if (JsonType(parent) === "value")
120
+ throw new Error("Unable to write value to path: " + targetPath);
121
+ const prop = parts[parts.length - 1];
122
+ const current = parent[prop];
123
+ if (current === flattened)
124
+ return;
125
+ const currentType = JsonType(current);
126
+ const currentHome = HomePath(keyFunc, route, current);
127
+ if (currentType !== "value" && currentType === JsonType(flattened) && !IsSlot(currentHome, targetPath) && !IsSlot(HomePath(keyFunc, route, flattened), targetPath)) {
128
+ const diff = JsonDiff(flattened, current, undefined, IngestStrategy(keyFunc, route, flattened));
129
+ if (diff.length > 0) {
130
+ JsonApplyDiff(current, diff, (target, key, written) => Assign(context.snapshot, target, key, written), (target, key) => Remove(context.snapshot, target, key), (array) => Snapshot.Invalidate(context.snapshot, array), KeepValue);
131
+ }
132
+ return;
133
+ }
134
+ // The current value is the home at this path
135
+ if (currentHome === targetPath && flattened !== undefined)
136
+ throw new Error(`Unable to replace the keyed value at ${targetPath}: a keyed value keeps its type and key at its home`);
137
+ Assign(context.snapshot, parent, prop, flattened);
138
+ }
139
+ /** Whether a value with this home, held at `path`, is a slot: its home is somewhere else */
140
+ function IsSlot(homePath, path) {
141
+ return homePath !== undefined && homePath !== path;
142
+ }
143
+ /**
144
+ * How a flattened value is diffed into the tree: a slot, or an array of them, is replaced rather
145
+ * than diffed into, and so is any value replacing a slot. The value being written is diffed into
146
+ * its target even when it's keyed, which is how a home is updated in place.
147
+ *
148
+ * An array of slots holding the same homes as before, in the same order, is skipped rather than
149
+ * replaced, so it keeps its object and its snapshot: rewriting an entity whose keyed children didn't
150
+ * change leaves the entity's snapshot, and every projection reading it, alone.
151
+ */
152
+ function IngestStrategy(keyFunc, route, rootValue) {
153
+ return keyFunc && function (newValue, oldValue) {
154
+ if (newValue === rootValue)
155
+ return undefined;
156
+ if (Array.isArray(newValue)) {
157
+ if (newValue.length === 0 || HomePath(keyFunc, route, newValue[0]) === undefined)
158
+ return undefined;
159
+ return Array.isArray(oldValue) && ArraysEqual(newValue, oldValue) ? "skip" : "write";
160
+ }
161
+ return HomePath(keyFunc, route, newValue) !== undefined || HomePath(keyFunc, route, oldValue) !== undefined ? "write" : undefined;
162
+ };
163
+ }
164
+ /**
165
+ * A copy of a value the tree owns, with each keyed value written to its home and replaced by the
166
+ * home object. A keyed value whose home is inside `targetPath`, the path being written, is copied
167
+ * in place instead.
168
+ */
169
+ function Flatten(context, keyFunc, route, targetPath, value) {
170
+ const type = JsonType(value);
171
+ if (type === "value")
172
+ return value;
173
+ const homePath = HomePath(keyFunc, route, value);
174
+ if (homePath !== undefined && !IsPathWithin(homePath, targetPath)) {
175
+ // A home written back to itself, e.g. one read through a slot, has nothing to write
176
+ const home = ReadPath(context.root, homePath);
177
+ if (home === value)
178
+ return home;
179
+ const [homeRoute, homeProperty] = SplitRouteAndPath(homePath);
180
+ home === undefined && RouteState(context.root, homeRoute, true);
181
+ WriteValue(context, homeRoute, homeProperty.split(PROP_DELIMETER), value);
182
+ // An existing home is updated in place, so only a new one needs reading
183
+ return home ?? ReadPath(context.root, homePath);
184
+ }
185
+ if (type === "array") {
186
+ const result = new Array(value.length);
187
+ for (let x = 0; x < value.length; x++)
188
+ result[x] = Flatten(context, keyFunc, route, targetPath, value[x]);
189
+ return result;
190
+ }
191
+ const result = {};
192
+ const keys = Object.keys(value);
193
+ for (let x = 0; x < keys.length; x++)
194
+ result[keys[x]] = Flatten(context, keyFunc, route, targetPath, value[keys[x]]);
195
+ return result;
196
+ }
197
+ /** The flattened value is already the tree's own, slots included, so it's written as is */
198
+ function KeepValue(value) {
199
+ return value;
200
+ }
201
+ /**
202
+ * A copy of `value` with `path` set, copying only the objects along the path
203
+ * @throws When an object along the path doesn't exist
204
+ */
205
+ function SetPath(value, path, set) {
206
+ if (path.length === 0)
207
+ return set;
208
+ if (JsonType(value) === "value")
209
+ throw new Error("Unable to write value to path: " + path.join(PROP_DELIMETER));
210
+ const copy = Array.isArray(value) ? value.slice() : { ...value };
211
+ copy[path[0]] = SetPath(value[path[0]], path.slice(1), set);
212
+ return copy;
213
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Path strings. A path is a route and a property path: `/channel/1/value.item` is the property
3
+ * path `value.item` in the route `/channel/1/`. A path without a route (`value.item`) is in the
4
+ * root route.
5
+ */
6
+ export declare const ROOT_ROUTE = "/";
7
+ export declare const PROJECTION_PREFIX = "$projection_";
8
+ export declare const ROUTE_DELIMETER = "/";
9
+ export declare const PROP_DELIMETER = ".";
10
+ export declare const WILDCARD = "*";
11
+ export declare function SplitRouteAndPath(path: string): readonly [string, string];
12
+ /**
13
+ * A path's route and property path. Only a path starting with `/` has a route: any other path
14
+ * is a property path from the root.
15
+ */
16
+ export declare function RouteAndProperty(path: string): readonly [string, string];
17
+ export declare function JoinPaths(basePath: string, targetPath: string): string;
18
+ /** A route's segments: `/channel/1/` is `channel` and `1` */
19
+ export declare function RouteSegments(route: string): string[];
20
+ /** Whether `path` is `basePath` or a property below it: `/a.b` is within `/a`, `/ab` isn't */
21
+ export declare function IsPathWithin(path: string, basePath: string): boolean;
22
+ /** Whether a path's route has a wildcard segment, e.g. a `row_*` segment */
23
+ export declare function IsWildcardPath(path: string): boolean;
24
+ /**
25
+ * Whether a route matches a route pattern segment by segment. A pattern segment ending in `*`
26
+ * matches any segment starting with the text before it: a `row_*` segment matches `row_1`.
27
+ */
28
+ export declare function RouteMatches(pattern: string, route: string): boolean;
29
+ /**
30
+ * Throws when a read path's wildcard is invalid: more than one wildcard segment, a `*` that
31
+ * doesn't end its segment, or a `*` in the property path.
32
+ */
33
+ export declare function ValidateWildcardPath(path: string): void;
34
+ /**
35
+ * Whether a read and a projection output overlap: the read's route matches the output's (see
36
+ * `RouteMatches` for wildcards), and one dot path contains the other. A read of the route's root
37
+ * object (`/channel/1/`) overlaps every output in that route, but not those of its child routes,
38
+ * which a snapshot of the route doesn't include.
39
+ */
40
+ export declare function PathsOverlap(read: string, output: string): boolean;
41
+ /** Whether a read's property path and an output's in a matching route overlap; see `PathsOverlap` */
42
+ export declare function PropertiesOverlap(readProperty: string, outputProperty: string): boolean;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Path strings. A path is a route and a property path: `/channel/1/value.item` is the property
3
+ * path `value.item` in the route `/channel/1/`. A path without a route (`value.item`) is in the
4
+ * root route.
5
+ */
6
+ export const ROOT_ROUTE = "/";
7
+ export const PROJECTION_PREFIX = "$projection_";
8
+ export const ROUTE_DELIMETER = "/";
9
+ export const PROP_DELIMETER = ".";
10
+ export const WILDCARD = "*";
11
+ export function SplitRouteAndPath(path) {
12
+ const lastSlash = path.lastIndexOf(ROUTE_DELIMETER);
13
+ const route = path.slice(0, lastSlash + 1);
14
+ const propertyPath = path.slice(lastSlash + 1);
15
+ return [route, propertyPath];
16
+ }
17
+ /**
18
+ * A path's route and property path. Only a path starting with `/` has a route: any other path
19
+ * is a property path from the root.
20
+ */
21
+ export function RouteAndProperty(path) {
22
+ return path.startsWith(ROUTE_DELIMETER) ? SplitRouteAndPath(path) : ["", path];
23
+ }
24
+ export function JoinPaths(basePath, targetPath) {
25
+ if (targetPath.startsWith(ROUTE_DELIMETER))
26
+ return targetPath;
27
+ if (basePath === '')
28
+ return `/${targetPath}`;
29
+ return basePath.concat(targetPath);
30
+ }
31
+ /** A route's segments: `/channel/1/` is `channel` and `1` */
32
+ export function RouteSegments(route) {
33
+ return route.split(ROUTE_DELIMETER).filter(part => part !== "");
34
+ }
35
+ /** Whether `path` is `basePath` or a property below it: `/a.b` is within `/a`, `/ab` isn't */
36
+ export function IsPathWithin(path, basePath) {
37
+ return path.startsWith(basePath) && (path.length === basePath.length || path[basePath.length] === PROP_DELIMETER);
38
+ }
39
+ /** Whether a path's route has a wildcard segment, e.g. a `row_*` segment */
40
+ export function IsWildcardPath(path) {
41
+ return SplitRouteAndPath(path)[0].includes(WILDCARD);
42
+ }
43
+ /**
44
+ * Whether a route matches a route pattern segment by segment. A pattern segment ending in `*`
45
+ * matches any segment starting with the text before it: a `row_*` segment matches `row_1`.
46
+ */
47
+ export function RouteMatches(pattern, route) {
48
+ if (pattern === route)
49
+ return true;
50
+ // Every route but a few fails on the text before the wildcard, so that's checked before splitting
51
+ const wildcard = pattern.indexOf(WILDCARD);
52
+ if (wildcard < 0 || !route.startsWith(pattern.slice(0, wildcard)))
53
+ return false;
54
+ const patternParts = pattern.split(ROUTE_DELIMETER);
55
+ const routeParts = route.split(ROUTE_DELIMETER);
56
+ if (patternParts.length !== routeParts.length)
57
+ return false;
58
+ for (let x = 0; x < patternParts.length; x++)
59
+ if (patternParts[x].endsWith(WILDCARD) ? !routeParts[x].startsWith(patternParts[x].slice(0, -1)) : patternParts[x] !== routeParts[x])
60
+ return false;
61
+ return true;
62
+ }
63
+ /**
64
+ * Throws when a read path's wildcard is invalid: more than one wildcard segment, a `*` that
65
+ * doesn't end its segment, or a `*` in the property path.
66
+ */
67
+ export function ValidateWildcardPath(path) {
68
+ const [route, property] = SplitRouteAndPath(path);
69
+ const wildcards = route.split(ROUTE_DELIMETER).filter(part => part.includes(WILDCARD));
70
+ if (property.includes(WILDCARD) || wildcards.length > 1 || wildcards.some(part => part.indexOf(WILDCARD) !== part.length - 1))
71
+ throw new Error(`Invalid wildcard read ${path}: only one route segment may end in ${WILDCARD}.`);
72
+ }
73
+ /**
74
+ * Whether a read and a projection output overlap: the read's route matches the output's (see
75
+ * `RouteMatches` for wildcards), and one dot path contains the other. A read of the route's root
76
+ * object (`/channel/1/`) overlaps every output in that route, but not those of its child routes,
77
+ * which a snapshot of the route doesn't include.
78
+ */
79
+ export function PathsOverlap(read, output) {
80
+ const [readRoute, readProperty] = SplitRouteAndPath(read);
81
+ const [outputRoute, outputProperty] = SplitRouteAndPath(output);
82
+ return RouteMatches(readRoute, outputRoute) && PropertiesOverlap(readProperty, outputProperty);
83
+ }
84
+ /** Whether a read's property path and an output's in a matching route overlap; see `PathsOverlap` */
85
+ export function PropertiesOverlap(readProperty, outputProperty) {
86
+ return readProperty === "" || IsPathWithin(readProperty, outputProperty) || IsPathWithin(outputProperty, readProperty);
87
+ }
@@ -0,0 +1,55 @@
1
+ import { ISnapshotState } from "../../Tree/snapshot";
2
+ import { RouteInstances } from "./routes";
3
+ /** Runs the projections of a DiffTree's route instances against its state */
4
+ export declare class Projections {
5
+ private snapshot;
6
+ private rootState;
7
+ private routes;
8
+ private order;
9
+ private orderVersion;
10
+ constructor(snapshot: ISnapshotState, rootState: object, routes: RouteInstances);
11
+ /**
12
+ * Runs every projection of every route instance, dependencies first. A projection is skipped
13
+ * when no route it reads through changed since its last run, when a read's parent is missing,
14
+ * or when every read value is the same as its last run. Outputs are written to
15
+ * `$projection_<key>` in their route without a diff, and are their own snapshots, so they're
16
+ * never changed in place. Instead, each part of an output with no changes from the last one
17
+ * written is replaced by the last one's object (JsonMaintainUnchanged): unchanged parts keep
18
+ * their identity, so the projections and readers using them are skipped and `Commit` passes
19
+ * over them. An output with no changes at all isn't written.
20
+ * @throws When projections depend on each other in a cycle
21
+ */
22
+ Run(): void;
23
+ /**
24
+ * Whether every source a projection's reads resolved through on its last full read still has
25
+ * the snapshot it read: a write anywhere under a value clears its cached snapshot, so then no
26
+ * read can have changed. A wildcard read also needs the same route instances to exist.
27
+ */
28
+ private SourcesUnchanged;
29
+ /**
30
+ * Whether a source still has the snapshot it was read with. A key's value is read from the
31
+ * state again, so a key written with a new value is a change too (see KeyUnchanged).
32
+ */
33
+ private SourceUnchanged;
34
+ /**
35
+ * Takes a new snapshot of every source in the route a projection just wrote its output to that
36
+ * the write cleared. Nothing the projection read changed between its read and the write, so
37
+ * without this the next run would read everything again for nothing. A source of one key keeps
38
+ * its snapshot unless the output is under that key.
39
+ */
40
+ private RefreshSources;
41
+ /**
42
+ * Snapshots of the values at each path.
43
+ * @param sources Filled with what each read resolved through; a read after a missing one is left undefined
44
+ * @param previous The sources of the last full read, so a wildcard read reuses its unchanged matches
45
+ * @returns The values, or undefined when any of them is missing
46
+ */
47
+ private ReadValues;
48
+ /**
49
+ * Reads every route a wildcard path matches. While no route instance was added or removed since
50
+ * the last read, the matches are the same, and a match whose route has the same snapshot keeps
51
+ * the value it read.
52
+ */
53
+ private ReadWildcard;
54
+ private ReadMatch;
55
+ }