j-templates 8.1.2 → 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.
- package/SYNTAX_PRIMER.md +3 -4
- package/Store/Diff/diffTree.d.ts +39 -33
- package/Store/Diff/diffTree.js +147 -435
- 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 +2 -0
- package/Store/Diff/index.js +2 -0
- package/Store/Diff/utils/diffTree.types.d.ts +113 -0
- package/Store/Diff/utils/diffTree.types.js +1 -0
- 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/pushStore.d.ts +1 -5
- package/Store/Store/pushStore.js +3 -22
- package/Store/Store/store.d.ts +10 -4
- package/Store/Store/store.js +25 -11
- package/Store/Store/storeAsync.d.ts +4 -5
- package/Store/Store/storeAsync.js +19 -36
- package/Store/Store/storeSync.d.ts +14 -9
- package/Store/Store/storeSync.js +27 -22
- package/Store/Tree/observableNode.d.ts +2 -23
- package/Store/Tree/observableNode.js +46 -183
- package/Store/Tree/snapshot.d.ts +68 -0
- package/Store/Tree/snapshot.js +147 -0
- package/Utils/json.d.ts +180 -10
- package/Utils/json.js +532 -67
- package/package.json +1 -1
- package/Store/Diff/diffSync.d.ts +0 -36
- package/Store/Diff/diffSync.js +0 -43
|
@@ -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
|
+
}
|