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,155 @@
1
+ import { JsonType } from "../../../Utils/json";
2
+ import { Snapshot } from "../../Tree/snapshot";
3
+ import { PROP_DELIMETER, ROUTE_DELIMETER, RouteAndProperty, SplitRouteAndPath, WILDCARD } from "./paths";
4
+ /**
5
+ * The raw state of a DiffTree's routes. A route's state hangs off its parent route's state under
6
+ * `ROUTE_STATE`, keyed by segment, so `Object.keys` of a route's state lists only its own values.
7
+ */
8
+ export const ROUTE_STATE = Symbol("ROUTE_STATE");
9
+ /**
10
+ * A route's state object, e.g. `/channel/1/`'s; `""` and `/` are the root.
11
+ * @param create Creates the route's state, and its parents', when missing
12
+ * @returns The state, or undefined when it's missing and `create` isn't set
13
+ */
14
+ export function RouteState(root, route, create = false) {
15
+ let state = root;
16
+ for (let start = 1; state !== undefined && state !== null && start < route.length;) {
17
+ const end = route.indexOf(ROUTE_DELIMETER, start);
18
+ const segment = route.slice(start, end);
19
+ if (create && !Object.hasOwn(state, ROUTE_STATE))
20
+ Object.defineProperty(state, ROUTE_STATE, { value: {} });
21
+ state = create ? state[ROUTE_STATE][segment] ??= {} : state[ROUTE_STATE]?.[segment];
22
+ start = end + 1;
23
+ }
24
+ return state;
25
+ }
26
+ /**
27
+ * Writes a value to a path as it is, without diffing it or keeping keyed values at their homes,
28
+ * creating the path's route when missing. A path to a route (`/channel/1/`) only creates it.
29
+ * @throws When the path's parent doesn't exist
30
+ */
31
+ export function WritePath(snapshot, root, path, value) {
32
+ const [route, property] = RouteAndProperty(path);
33
+ const state = RouteState(root, route, true);
34
+ if (property === "")
35
+ return;
36
+ const parts = property.split(PROP_DELIMETER);
37
+ const parent = ReadPathParts(state, parts, parts.length - 1);
38
+ if (!parent)
39
+ throw new Error("Unable to write value to path: " + path);
40
+ Assign(snapshot, parent, parts[parts.length - 1], value);
41
+ }
42
+ /** The value at a path, or undefined when any segment of it doesn't exist; see ReadPathFound */
43
+ export function ReadPath(root, path) {
44
+ return ReadPathFound(root, path)[0];
45
+ }
46
+ /**
47
+ * The value at a path, and whether its parent was found: a property missing from an existing
48
+ * parent is found as undefined.
49
+ */
50
+ export function ReadPathFound(root, path) {
51
+ const [route, property] = RouteAndProperty(path);
52
+ const state = RouteState(root, route);
53
+ if (property === "")
54
+ return [state, !!state];
55
+ const parts = property.split(PROP_DELIMETER);
56
+ const parent = ReadPathParts(state, parts, parts.length - 1);
57
+ return [parent?.[parts[parts.length - 1]], !!parent];
58
+ }
59
+ /** The value at the first `depth` parts of a property path */
60
+ export function ReadPathParts(value, parts, depth) {
61
+ let curr = value;
62
+ for (let x = 0; curr !== undefined && curr !== null && x < depth; x++)
63
+ curr = curr[parts[x]];
64
+ return curr;
65
+ }
66
+ /**
67
+ * The snapshot of the value at a path, whether it was found, and the route it was read through,
68
+ * when there is one. A property path is read from the snapshot of its first segment alone, not of
69
+ * the whole route, so a write to the route's other keys neither rebuilds what it reads nor changes it.
70
+ */
71
+ export function ReadSnapshot(snapshot, root, path) {
72
+ const [route, property] = SplitRouteAndPath(path);
73
+ const routeState = RouteState(root, route);
74
+ if (!routeState)
75
+ return [undefined, false, undefined];
76
+ if (property === "") {
77
+ const routeSnapshot = Snapshot.Get(snapshot, routeState);
78
+ return [routeSnapshot, !!routeSnapshot, { route, state: routeState, snapshot: routeSnapshot }];
79
+ }
80
+ const dot = property.indexOf(PROP_DELIMETER);
81
+ const key = dot < 0 ? property : property.slice(0, dot);
82
+ const keySnapshot = Snapshot.Child(snapshot, routeState, key);
83
+ const source = { route, state: routeState, key, snapshot: keySnapshot };
84
+ if (dot < 0)
85
+ return [keySnapshot, true, source];
86
+ const [value, valueFound] = ReadPathFound(keySnapshot, property.slice(dot + 1));
87
+ return [value, valueFound, source];
88
+ }
89
+ /**
90
+ * Whether a key's value still has the snapshot taken of it, told by a cache lookup without
91
+ * taking a new one. A primitive value has no snapshot of its own and is compared as it is. A
92
+ * value with no cached snapshot was written since, even where the snapshot was undefined.
93
+ */
94
+ export function KeyUnchanged(snapshot, state, key, keySnapshot) {
95
+ const value = state[key];
96
+ if (JsonType(value) === "value")
97
+ return value === keySnapshot;
98
+ const cached = Snapshot.Peek(snapshot, value);
99
+ return cached !== undefined && cached === keySnapshot;
100
+ }
101
+ // Removing or replacing a value changes its parent, not the value: its snapshot stays valid. That
102
+ // matters for a slot, whose value is a home still held, and read, elsewhere.
103
+ export function Remove(snapshot, parent, prop) {
104
+ delete parent[prop];
105
+ Snapshot.Invalidate(snapshot, parent);
106
+ }
107
+ export function Assign(snapshot, parent, prop, value) {
108
+ parent[prop] = value;
109
+ Snapshot.Invalidate(snapshot, parent);
110
+ }
111
+ /**
112
+ * Deletes a route's state, child routes included, from its parent route. Every cached snapshot
113
+ * that embedded a value from it, such as one reading a keyed value through its home in the
114
+ * route, is invalidated.
115
+ * @param route A non-root route path, e.g. `/channel/1/`
116
+ */
117
+ export function DeleteRouteState(snapshot, root, route) {
118
+ const parentRoute = route.slice(0, route.lastIndexOf(ROUTE_DELIMETER, route.length - 2) + 1);
119
+ const segment = route.slice(parentRoute.length, -1);
120
+ const routeStates = RouteState(root, parentRoute)?.[ROUTE_STATE];
121
+ if (!routeStates || !Object.hasOwn(routeStates, segment))
122
+ return;
123
+ InvalidateState(snapshot, routeStates[segment]);
124
+ delete routeStates[segment];
125
+ }
126
+ function InvalidateState(snapshot, value) {
127
+ if (JsonType(value) === "value")
128
+ return;
129
+ Snapshot.Invalidate(snapshot, value);
130
+ const keys = Object.keys(value);
131
+ for (let x = 0; x < keys.length; x++)
132
+ InvalidateState(snapshot, value[keys[x]]);
133
+ const routeStates = value[ROUTE_STATE];
134
+ for (const segment in routeStates)
135
+ InvalidateState(snapshot, routeStates[segment]);
136
+ }
137
+ /**
138
+ * Expands a wildcard path's route against the route state under `root`.
139
+ * @returns Each matching path, keyed by the segment the wildcard matched
140
+ */
141
+ export function ExpandWildcardPath(root, path) {
142
+ const [route, property] = SplitRouteAndPath(path);
143
+ const parts = route.split(ROUTE_DELIMETER);
144
+ const index = parts.findIndex(part => part.endsWith(WILDCARD));
145
+ const prefix = parts[index].slice(0, -1);
146
+ const parent = RouteState(root, parts.slice(0, index).join(ROUTE_DELIMETER) + ROUTE_DELIMETER);
147
+ const children = parent?.[ROUTE_STATE] ?? {};
148
+ const paths = {};
149
+ for (const segment of Object.keys(children))
150
+ if (segment.startsWith(prefix)) {
151
+ parts[index] = segment;
152
+ paths[segment] = parts.join(ROUTE_DELIMETER) + property;
153
+ }
154
+ return paths;
155
+ }
@@ -33,6 +33,12 @@ export declare abstract class ReadStore {
33
33
  */
34
34
  Get<O>(id: string, defaultValue: O): O;
35
35
  Has(id: string): boolean;
36
+ /**
37
+ * Whether the store holds the root a path is in: its key, before the first `.`. Push, Splice
38
+ * and Patch take a path into a root, e.g. `rows.t_1`.
39
+ * @param path The path, e.g. `user.items`.
40
+ */
41
+ protected HasRootOf(path: string): boolean;
36
42
  /**
37
43
  * Updates the root map with diff results by grouping changes by root path.
38
44
  * @param results - Array of diff results to apply
@@ -62,3 +68,5 @@ export declare abstract class Store extends ReadStore {
62
68
  abstract Push(key: string, ...data: unknown[]): Promise<void> | void;
63
69
  abstract Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<void> | void;
64
70
  }
71
+ /** The root key a path is in: its text before the first `.` */
72
+ export declare function RootOf(path: string): string;
@@ -47,6 +47,14 @@ export class ReadStore {
47
47
  Has(id) {
48
48
  return this.rootMap.has(id);
49
49
  }
50
+ /**
51
+ * Whether the store holds the root a path is in: its key, before the first `.`. Push, Splice
52
+ * and Patch take a path into a root, e.g. `rows.t_1`.
53
+ * @param path The path, e.g. `user.items`.
54
+ */
55
+ HasRootOf(path) {
56
+ return this.Has(RootOf(path));
57
+ }
50
58
  /**
51
59
  * Updates the root map with diff results by grouping changes by root path.
52
60
  * @param results - Array of diff results to apply
@@ -96,3 +104,8 @@ export class ReadStore {
96
104
  */
97
105
  export class Store extends ReadStore {
98
106
  }
107
+ /** The root key a path is in: its text before the first `.` */
108
+ export function RootOf(path) {
109
+ const dot = path.indexOf(".");
110
+ return dot < 0 ? path : path.slice(0, dot);
111
+ }
@@ -1,6 +1,6 @@
1
1
  import { AsyncQueue } from "../../Utils/asyncQueue";
2
2
  import { WorkerQueue } from "../Diff/workerQueue";
3
- import { Store } from "./store";
3
+ import { RootOf, Store } from "./store";
4
4
  /**
5
5
  * StoreAsync class extends the base Store class to provide asynchronous data management operations.
6
6
  * This class handles writing, patching, pushing, and splicing data in an asynchronous manner.
@@ -82,12 +82,13 @@ export class StoreAsync extends Store {
82
82
  key = key || this.keyFunc?.(data);
83
83
  if (!key)
84
84
  throw new Error("No key provided for data");
85
- this.writeKeySet.add(key);
85
+ // Pending writes by root, so a change to a path in a root being written isn't refused
86
+ this.writeKeySet.add(RootOf(key));
86
87
  await this.Schedule({
87
88
  method: "writepath",
88
89
  arguments: [key, data]
89
90
  });
90
- this.writeKeySet.delete(key);
91
+ this.writeKeySet.delete(RootOf(key));
91
92
  }
92
93
  /**
93
94
  * Patches an existing value in the store with new data asynchronously.
@@ -100,7 +101,7 @@ export class StoreAsync extends Store {
100
101
  * the original error if an earlier change failed.
101
102
  */
102
103
  async Patch(key, patch) {
103
- if (!this.Has(key) && !this.writeKeySet.has(key))
104
+ if (!this.HasRootOf(key) && !this.writeKeySet.has(RootOf(key)))
104
105
  throw "Key not found in store";
105
106
  await this.Schedule({
106
107
  method: "patchpath",
@@ -116,7 +117,7 @@ export class StoreAsync extends Store {
116
117
  * the original error if an earlier change failed.
117
118
  */
118
119
  async Push(key, ...data) {
119
- if (!this.Has(key) && !this.writeKeySet.has(key))
120
+ if (!this.HasRootOf(key) && !this.writeKeySet.has(RootOf(key)))
120
121
  throw "Key not found in store";
121
122
  await this.Schedule({
122
123
  method: "pushpath",
@@ -135,7 +136,7 @@ export class StoreAsync extends Store {
135
136
  * the original error if an earlier change failed.
136
137
  */
137
138
  async Splice(key, start, deleteCount, ...items) {
138
- if (!this.Has(key) && !this.writeKeySet.has(key))
139
+ if (!this.HasRootOf(key) && !this.writeKeySet.has(RootOf(key)))
139
140
  throw "Key not found in store";
140
141
  await this.Schedule({
141
142
  method: "splicepath",
@@ -60,7 +60,7 @@ export class StoreSync extends Store {
60
60
  * @throws Will throw an error if the value to be patched is undefined.
61
61
  */
62
62
  Patch(key, patch) {
63
- if (!this.Has(key))
63
+ if (!this.HasRootOf(key))
64
64
  throw "Key not found in store";
65
65
  this.diff.PatchPath(key, patch);
66
66
  this.Sync();
@@ -71,7 +71,7 @@ export class StoreSync extends Store {
71
71
  * @param data The data items to be pushed into the array.
72
72
  */
73
73
  Push(key, ...data) {
74
- if (!this.Has(key))
74
+ if (!this.HasRootOf(key))
75
75
  throw "Key not found in store";
76
76
  this.diff.PushPath(key, data);
77
77
  this.Sync();
@@ -85,7 +85,7 @@ export class StoreSync extends Store {
85
85
  * @param items Optional elements to insert into the array.
86
86
  */
87
87
  Splice(key, start, deleteCount, ...items) {
88
- if (!this.Has(key))
88
+ if (!this.HasRootOf(key))
89
89
  throw "Key not found in store";
90
90
  this.diff.SplicePath(key, start, deleteCount ?? Infinity, items);
91
91
  this.Sync();
@@ -109,9 +109,7 @@ function GetPropertyScope(object, prop) {
109
109
  return object[prop];
110
110
  }
111
111
  function SetObjectValue(object, prop, value) {
112
- const propExists = Object.hasOwn(object[NODE_VALUE], prop);
113
- object[NODE_VALUE][prop] = value;
114
- NodeValueSet(object[NODE_VALUE], prop, !propExists, object);
112
+ AssignNodeValue(object[NODE_VALUE], prop, value, object);
115
113
  }
116
114
  function SetArrayValue(object, prop, value) {
117
115
  object[NODE_VALUE][prop] = value;
@@ -121,25 +119,33 @@ function NodeValueSet(obj, prop, added, wrapper = wrapperCache.get(obj)) {
121
119
  InvalidateSnapshotForValue(obj);
122
120
  if (!wrapper)
123
121
  return;
124
- const propScopeExists = !!prop && Object.hasOwn(wrapper, prop);
122
+ // An array's wrapper is an array with no property scopes: its own `length` isn't one, and a
123
+ // diff that only shrinks an array writes nothing but `length`
124
+ const propScopeExists = !!prop && !wrapper[ARRAY_SCOPE] && Object.hasOwn(wrapper, prop);
125
125
  if (!prop || added || !propScopeExists)
126
126
  ObservableScope.Update(wrapper[ARRAY_SCOPE] ?? wrapper[OBJECT_SCOPE]);
127
127
  if (propScopeExists)
128
128
  ObservableScope.Update(wrapper[prop]);
129
129
  }
130
- function AssignNodeValue(obj, prop, value) {
130
+ /** A deleted property: the object's keys changed, so its scope updates, and so does the property's. */
131
+ function RemoveNodeValue(obj, prop) {
132
+ delete obj[prop];
133
+ NodeValueSet(obj, prop, true);
134
+ }
135
+ /** An array spliced in place: its elements changed, so its scope updates. Moved elements keep their nodes. */
136
+ function SplicedNodeValue(array) {
137
+ NodeValueSet(array, "", false);
138
+ }
139
+ /** A property assigned: the object's scope updates when it's new, and the property's does */
140
+ function AssignNodeValue(obj, prop, value, wrapper) {
131
141
  const added = !Object.hasOwn(obj, prop);
132
142
  obj[prop] = value;
133
- NodeValueSet(obj, prop, added);
143
+ NodeValueSet(obj, prop, added, wrapper);
134
144
  }
135
145
  function CreateProxyFactory(alias) {
136
146
  // Keyed children are read through their root. Every factory shares the module-level
137
147
  // snapshotState, matching wrapperCache; only the resolver differs per factory.
138
- // The alias ignores paths, so the root is snapshotted at the child's own path
139
- const resolveSnapshot = alias && function (child, path) {
140
- const value = alias(child, false);
141
- return value === undefined ? undefined : { value, path: path.slice() };
142
- };
148
+ const resolveSnapshot = alias && (child => alias(child, false));
143
149
  function CreateSnapshot(value) {
144
150
  return Snapshot.Get(snapshotState, value, resolveSnapshot);
145
151
  }
@@ -410,10 +416,7 @@ export var ObservableNode;
410
416
  */
411
417
  function ApplyDiff(rootNode, diffResult) {
412
418
  const root = rootNode[NODE_VALUE];
413
- JsonApplyDiff(root, diffResult, AssignNodeValue);
414
- // A root replacement is applied in place without calling AssignNodeValue
415
- if (diffResult.length === 1 && diffResult[0].path.length === 0)
416
- NodeValueSet(root, "", false, rootNode[NODE_WRAPPER]);
419
+ JsonApplyDiff(root, diffResult, AssignNodeValue, RemoveNodeValue, SplicedNodeValue);
417
420
  }
418
421
  ObservableNode.ApplyDiff = ApplyDiff;
419
422
  /**
@@ -1,18 +1,8 @@
1
1
  /**
2
- * The keys walked from the snapshot's seed to a value: property names, array indices and
3
- * hidden (symbol) props. A resolved child continues from the path its resolver returns.
2
+ * Resolves a keyed child to the root entity it should be read through. Returns the entity, or
3
+ * undefined when the child isn't keyed (or has no root), in which case the child itself is used.
4
4
  */
5
- export type SnapshotPath = (string | number | symbol)[];
6
- /**
7
- * Resolves a keyed child to the root entity it should be read through, given the child's
8
- * path. Returns the entity and its path, or undefined when the child isn't keyed (or has no
9
- * root), in which case the child itself is used. `path` is the walk's own stack, so a
10
- * resolver must not keep it.
11
- */
12
- export type SnapshotResolver = (child: any, path: Readonly<SnapshotPath>) => {
13
- value: object;
14
- path: SnapshotPath;
15
- } | undefined;
5
+ export type SnapshotResolver = (child: any) => object | undefined;
16
6
  /**
17
7
  * Snapshot cache state. Each owner (a `DiffTree` instance, or `ObservableNode` at module
18
8
  * level) creates its own state, so cached snapshots are never shared between owners.
@@ -22,28 +12,36 @@ export interface ISnapshotState {
22
12
  rawToSnapshotCache: WeakMap<object, any>;
23
13
  /** Raw value -> every parent whose cached snapshot embedded it */
24
14
  hierarchy: WeakMap<object, Set<object>>;
25
- /** Default resolver for keyed children; a call to `Snapshot.Get` may override it */
26
- resolve: SnapshotResolver | undefined;
27
15
  }
28
16
  export declare namespace Snapshot {
29
17
  /**
30
18
  * Creates a new, empty snapshot cache.
31
- * @param resolve Optional default resolver for keyed children.
32
19
  * @returns A new snapshot state.
33
20
  */
34
- function Create(resolve?: SnapshotResolver): ISnapshotState;
21
+ function Create(): ISnapshotState;
35
22
  /**
36
23
  * Returns an immutable, cached snapshot of a raw value. The same snapshot is returned
37
24
  * until a write invalidates it, and unchanged nested parts are shared between snapshots,
38
25
  * so `===` holds for any part that hasn't changed.
39
26
  * @param state The snapshot cache to read from and populate.
40
27
  * @param value The raw value to snapshot.
41
- * @param resolve Resolver for keyed children; defaults to the state's resolver.
42
- * @param path The value's own path. Snapshots are cached per raw value, so a value must
43
- * always be seeded with the same path or a resolver may see a different one.
28
+ * @param resolve Resolver for keyed children. Snapshots are cached per raw value, so a value
29
+ * must always be read with the same resolver.
44
30
  * @returns The snapshot.
45
31
  */
46
- function Get<T>(state: ISnapshotState, value: T, resolve?: SnapshotResolver | undefined, path?: SnapshotPath): T;
32
+ function Get<T>(state: ISnapshotState, value: T, resolve?: SnapshotResolver): T;
33
+ /**
34
+ * The snapshot of one property of a raw value, as the value's own snapshot would hold it:
35
+ * cached, keyed children read through their root, and linked to the value so a write to the
36
+ * property invalidates the value's snapshot. Reads one property without building a snapshot of
37
+ * the whole value.
38
+ * @param state The snapshot cache to read from and populate.
39
+ * @param parent The raw value the property is on.
40
+ * @param key The property.
41
+ * @param resolve Resolver for keyed children.
42
+ * @returns The property's snapshot.
43
+ */
44
+ function Child(state: ISnapshotState, parent: any, key: string | number, resolve?: SnapshotResolver): any;
47
45
  /**
48
46
  * Invalidates the cached snapshot of a raw value that was just written to, along with
49
47
  * every cached ancestor that embedded it.
@@ -27,7 +27,7 @@ function InvalidateValue(state, value) {
27
27
  * unchanged children, so a snapshot handed out never changes and unchanged parts are
28
28
  * shared between snapshots. Keyed children are read through their root.
29
29
  */
30
- function CreateSnapshot(state, resolve, value, path) {
30
+ function CreateSnapshot(state, resolve, value) {
31
31
  if (JsonType(value) === "value")
32
32
  return value;
33
33
  const cached = state.rawToSnapshotCache.get(value);
@@ -37,13 +37,13 @@ function CreateSnapshot(state, resolve, value, path) {
37
37
  if (Array.isArray(value)) {
38
38
  snapshot = new Array(value.length);
39
39
  for (let x = 0; x < value.length; x++)
40
- snapshot[x] = ChildSnapshot(state, resolve, value, x, path);
40
+ snapshot[x] = ChildSnapshot(state, resolve, value, x);
41
41
  }
42
42
  else {
43
43
  snapshot = {};
44
44
  const keys = Object.keys(value);
45
45
  for (let x = 0; x < keys.length; x++)
46
- snapshot[keys[x]] = ChildSnapshot(state, resolve, value, keys[x], path);
46
+ snapshot[keys[x]] = ChildSnapshot(state, resolve, value, keys[x]);
47
47
  }
48
48
  state.rawToSnapshotCache.set(value, snapshot);
49
49
  return snapshot;
@@ -54,23 +54,17 @@ function CreateSnapshot(state, resolve, value, path) {
54
54
  * linked too; the parent's own embedded copy stays linked because a positional write
55
55
  * (one that changes which entity a slot holds) mutates that copy in place.
56
56
  */
57
- function ChildSnapshot(state, resolve, parent, key, path) {
57
+ function ChildSnapshot(state, resolve, parent, key) {
58
58
  const child = parent[key];
59
59
  if (JsonType(child) === "value")
60
60
  return child;
61
61
  CreateSnapshotLink(state, child, parent);
62
- path.push(key);
63
- try {
64
- const resolved = resolve?.(child, path);
65
- if (resolved !== undefined && resolved.value !== child) {
66
- CreateSnapshotLink(state, resolved.value, parent);
67
- return CreateSnapshot(state, resolve, resolved.value, resolved.path);
68
- }
69
- return CreateSnapshot(state, resolve, child, path);
70
- }
71
- finally {
72
- path.pop();
62
+ const resolved = resolve?.(child);
63
+ if (resolved !== undefined && resolved !== child) {
64
+ CreateSnapshotLink(state, resolved, parent);
65
+ return CreateSnapshot(state, resolve, resolved);
73
66
  }
67
+ return CreateSnapshot(state, resolve, child);
74
68
  }
75
69
  function RegisterSnapshot(state, snapshot) {
76
70
  if (JsonType(snapshot) !== 'value')
@@ -80,14 +74,12 @@ export var Snapshot;
80
74
  (function (Snapshot) {
81
75
  /**
82
76
  * Creates a new, empty snapshot cache.
83
- * @param resolve Optional default resolver for keyed children.
84
77
  * @returns A new snapshot state.
85
78
  */
86
- function Create(resolve) {
79
+ function Create() {
87
80
  return {
88
81
  rawToSnapshotCache: new WeakMap(),
89
82
  hierarchy: new WeakMap(),
90
- resolve,
91
83
  };
92
84
  }
93
85
  Snapshot.Create = Create;
@@ -97,15 +89,29 @@ export var Snapshot;
97
89
  * so `===` holds for any part that hasn't changed.
98
90
  * @param state The snapshot cache to read from and populate.
99
91
  * @param value The raw value to snapshot.
100
- * @param resolve Resolver for keyed children; defaults to the state's resolver.
101
- * @param path The value's own path. Snapshots are cached per raw value, so a value must
102
- * always be seeded with the same path or a resolver may see a different one.
92
+ * @param resolve Resolver for keyed children. Snapshots are cached per raw value, so a value
93
+ * must always be read with the same resolver.
103
94
  * @returns The snapshot.
104
95
  */
105
- function Get(state, value, resolve = state.resolve, path = []) {
106
- return CreateSnapshot(state, resolve, value, path.slice());
96
+ function Get(state, value, resolve) {
97
+ return CreateSnapshot(state, resolve, value);
107
98
  }
108
99
  Snapshot.Get = Get;
100
+ /**
101
+ * The snapshot of one property of a raw value, as the value's own snapshot would hold it:
102
+ * cached, keyed children read through their root, and linked to the value so a write to the
103
+ * property invalidates the value's snapshot. Reads one property without building a snapshot of
104
+ * the whole value.
105
+ * @param state The snapshot cache to read from and populate.
106
+ * @param parent The raw value the property is on.
107
+ * @param key The property.
108
+ * @param resolve Resolver for keyed children.
109
+ * @returns The property's snapshot.
110
+ */
111
+ function Child(state, parent, key, resolve) {
112
+ return ChildSnapshot(state, resolve, parent, key);
113
+ }
114
+ Snapshot.Child = Child;
109
115
  /**
110
116
  * Invalidates the cached snapshot of a raw value that was just written to, along with
111
117
  * every cached ancestor that embedded it.