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.
Files changed (40) hide show
  1. package/SYNTAX_PRIMER.md +3 -4
  2. package/Store/Diff/diffTree.d.ts +39 -33
  3. package/Store/Diff/diffTree.js +147 -435
  4. package/Store/Diff/diffTree.types.d.ts +5 -79
  5. package/Store/Diff/diffTree.types.js +1 -3
  6. package/Store/Diff/diffTreeWorker.d.ts +4 -0
  7. package/Store/Diff/diffTreeWorker.js +17 -39
  8. package/Store/Diff/diffTreeWriter.d.ts +27 -38
  9. package/Store/Diff/diffTreeWriter.js +44 -95
  10. package/Store/Diff/index.d.ts +2 -0
  11. package/Store/Diff/index.js +2 -0
  12. package/Store/Diff/utils/diffTree.types.d.ts +113 -0
  13. package/Store/Diff/utils/diffTree.types.js +1 -0
  14. package/Store/Diff/utils/homes.d.ts +46 -0
  15. package/Store/Diff/utils/homes.js +213 -0
  16. package/Store/Diff/utils/paths.d.ts +42 -0
  17. package/Store/Diff/utils/paths.js +87 -0
  18. package/Store/Diff/utils/projections.d.ts +55 -0
  19. package/Store/Diff/utils/projections.js +290 -0
  20. package/Store/Diff/utils/routes.d.ts +27 -0
  21. package/Store/Diff/utils/routes.js +168 -0
  22. package/Store/Diff/utils/state.d.ts +56 -0
  23. package/Store/Diff/utils/state.js +155 -0
  24. package/Store/Store/pushStore.d.ts +1 -5
  25. package/Store/Store/pushStore.js +3 -22
  26. package/Store/Store/store.d.ts +10 -4
  27. package/Store/Store/store.js +25 -11
  28. package/Store/Store/storeAsync.d.ts +4 -5
  29. package/Store/Store/storeAsync.js +19 -36
  30. package/Store/Store/storeSync.d.ts +14 -9
  31. package/Store/Store/storeSync.js +27 -22
  32. package/Store/Tree/observableNode.d.ts +2 -23
  33. package/Store/Tree/observableNode.js +46 -183
  34. package/Store/Tree/snapshot.d.ts +68 -0
  35. package/Store/Tree/snapshot.js +147 -0
  36. package/Utils/json.d.ts +180 -10
  37. package/Utils/json.js +532 -67
  38. package/package.json +1 -1
  39. package/Store/Diff/diffSync.d.ts +0 -36
  40. package/Store/Diff/diffSync.js +0 -43
@@ -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
+ }
@@ -43,12 +43,8 @@ export declare class PushStore extends ReadStore {
43
43
  * @param keyFunc Optional function to generate a key for a given data value, used to
44
44
  * resolve root objects. Pass the same key logic the `DiffTreeWriter` was constructed
45
45
  * with, if any.
46
- * @param cloneData Whether to deep-clone the values the store adopts (false by default).
47
- * A `Worker` or `MessagePort` already delivers a structured-clone copy of every message.
48
- * A same-thread `EventTarget` delivers the writer's own objects, so either the writer
49
- * posts copies or this is true; otherwise the store and the writer's tree share objects.
50
46
  */
51
- constructor(eventTarget: EventTarget, keyFunc?: (value: any) => string | undefined, cloneData?: boolean);
47
+ constructor(eventTarget: EventTarget, keyFunc?: (value: any) => string | undefined);
52
48
  /**
53
49
  * Stops listening to the source and destroys the error scope. The source stays open (a
54
50
  * worker keeps running); closing or terminating it is up to its owner.
@@ -43,35 +43,16 @@ export class PushStore extends ReadStore {
43
43
  * @param keyFunc Optional function to generate a key for a given data value, used to
44
44
  * resolve root objects. Pass the same key logic the `DiffTreeWriter` was constructed
45
45
  * with, if any.
46
- * @param cloneData Whether to deep-clone the values the store adopts (false by default).
47
- * A `Worker` or `MessagePort` already delivers a structured-clone copy of every message.
48
- * A same-thread `EventTarget` delivers the writer's own objects, so either the writer
49
- * posts copies or this is true; otherwise the store and the writer's tree share objects.
50
46
  */
51
- constructor(eventTarget, keyFunc, cloneData = false) {
52
- super(keyFunc, cloneData);
47
+ constructor(eventTarget, keyFunc) {
48
+ super(keyFunc);
53
49
  this.eventTarget = eventTarget;
54
50
  this.error = null;
55
51
  this.errorScope = ObservableScope.Basic(() => this.error);
56
52
  this.messageHandler = (event) => {
57
53
  const messageData = event.data;
58
54
  if (messageData && messageData.type === DIFF_TREE_EVENT) {
59
- const data = messageData.data;
60
- for (let x = 0; x < data.length; x++) {
61
- const result = data[x];
62
- switch (result.type) {
63
- case "diff": {
64
- this.UpdateRootMap(result.result);
65
- break;
66
- }
67
- case "splice": {
68
- const spliceResult = result.result;
69
- this.UpdateRootMap(spliceResult.diffResult);
70
- this.SpliceRootObject(spliceResult.path[0], spliceResult.start, spliceResult.deleteCount, spliceResult.items);
71
- break;
72
- }
73
- }
74
- }
55
+ this.UpdateRootMap(messageData.data);
75
56
  if (this.error !== null) {
76
57
  this.error = null;
77
58
  ObservableScope.Update(this.errorScope);
@@ -9,7 +9,6 @@ import { JsonDiffResult } from "../../Utils/json";
9
9
  */
10
10
  export declare abstract class ReadStore {
11
11
  protected keyFunc?: (value: any) => string | undefined;
12
- private cloneData;
13
12
  private rootMap;
14
13
  private createNode;
15
14
  /**
@@ -17,7 +16,7 @@ export declare abstract class ReadStore {
17
16
  * @param keyFunc Optional function to generate a key for a given data value.
18
17
  * When provided, enables alias functionality where values can reference other root objects.
19
18
  */
20
- constructor(keyFunc?: (value: any) => string | undefined, cloneData?: boolean);
19
+ constructor(keyFunc?: (value: any) => string | undefined);
21
20
  /**
22
21
  * Retrieves a value from the store by id.
23
22
  * @template O - The type of the value to retrieve
@@ -34,13 +33,18 @@ export declare abstract class ReadStore {
34
33
  */
35
34
  Get<O>(id: string, defaultValue: O): O;
36
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;
37
42
  /**
38
43
  * Updates the root map with diff results by grouping changes by root path.
39
44
  * @param results - Array of diff results to apply
40
45
  * @protected
41
46
  */
42
47
  protected UpdateRootMap(results: JsonDiffResult): void;
43
- protected SpliceRootObject(rootPath: string | number, start: number, deleteCount: number, items: any[]): void;
44
48
  /**
45
49
  * Updates a specific root object with diff results.
46
50
  * Creates the root object if it doesn't exist.
@@ -62,5 +66,7 @@ export declare abstract class Store extends ReadStore {
62
66
  abstract Write(data: unknown, key?: string): Promise<void> | void;
63
67
  abstract Patch(key: string, patch: unknown): Promise<void> | void;
64
68
  abstract Push(key: string, ...data: unknown[]): Promise<void> | void;
65
- abstract Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<any[]> | any[];
69
+ abstract Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<void> | void;
66
70
  }
71
+ /** The root key a path is in: its text before the first `.` */
72
+ export declare function RootOf(path: string): string;
@@ -14,14 +14,15 @@ export class ReadStore {
14
14
  * @param keyFunc Optional function to generate a key for a given data value.
15
15
  * When provided, enables alias functionality where values can reference other root objects.
16
16
  */
17
- constructor(keyFunc, cloneData = true) {
17
+ constructor(keyFunc) {
18
18
  this.keyFunc = keyFunc;
19
- this.cloneData = cloneData;
20
19
  this.rootMap = new Map();
21
20
  const aliasFunc = keyFunc &&
22
21
  ((value, reactive = true) => {
23
22
  const key = keyFunc(value);
24
- if (key === undefined)
23
+ // A route key names an entity in a DiffTree route instance, which never reaches the
24
+ // store: read the embedded copy
25
+ if (key === undefined || key.startsWith("/"))
25
26
  return undefined;
26
27
  const rootObject = this.rootMap.get(key);
27
28
  if (rootObject === undefined)
@@ -46,6 +47,14 @@ export class ReadStore {
46
47
  Has(id) {
47
48
  return this.rootMap.has(id);
48
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
+ }
49
58
  /**
50
59
  * Updates the root map with diff results by grouping changes by root path.
51
60
  * @param results - Array of diff results to apply
@@ -61,10 +70,6 @@ export class ReadStore {
61
70
  this.UpdateRootObject(rootGroup[0].path[0], rootGroup);
62
71
  }
63
72
  }
64
- SpliceRootObject(rootPath, start, deleteCount, items) {
65
- const rootObject = this.rootMap.get(rootPath);
66
- ObservableNode.ApplySplice(rootObject[rootPath], start, deleteCount, items, this.cloneData);
67
- }
68
73
  /**
69
74
  * Updates a specific root object with diff results.
70
75
  * Creates the root object if it doesn't exist.
@@ -76,14 +81,18 @@ export class ReadStore {
76
81
  UpdateRootObject(rootPath, results) {
77
82
  const rootObject = this.rootMap.get(rootPath);
78
83
  if (rootObject === undefined) {
79
- if (results.length > 1 || results[0].path.length > 1)
80
- throw `Unable to initialize root path ${rootPath} with ${results.length} results and initial path ${results[0].path}`;
81
- const rootData = this.cloneData ? JsonDeepClone(results[0].value) : results[0].value;
84
+ if (results[0].type !== "write" || results[0].path.length > 1)
85
+ throw `Unable to initialize root path ${rootPath} with initial path ${results[0].path}`;
86
+ const rootData = JsonDeepClone(results[0].value);
82
87
  const newRootObject = this.createNode({ [rootPath]: rootData }, false);
83
88
  this.rootMap.set(rootPath, newRootObject);
89
+ // Later entries for the same root (e.g. route summaries written one instance at a time)
90
+ // apply to the root just created
91
+ if (results.length > 1)
92
+ ObservableNode.ApplyDiff(newRootObject, results.slice(1));
84
93
  return;
85
94
  }
86
- ObservableNode.ApplyDiff(rootObject, results, this.cloneData);
95
+ ObservableNode.ApplyDiff(rootObject, results);
87
96
  }
88
97
  }
89
98
  /**
@@ -95,3 +104,8 @@ export class ReadStore {
95
104
  */
96
105
  export class Store extends ReadStore {
97
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
+ }
@@ -24,7 +24,7 @@ import { Store } from "./store";
24
24
  * await store.Push("users", { id: 1 }, { id: 2 });
25
25
  *
26
26
  * // Splicing an array asynchronously
27
- * const deletedItems = await store.Splice("users", 0, 1, { id: 3 });
27
+ * await store.Splice("users", 0, 1, { id: 3 });
28
28
  *
29
29
  * // Cleaning up resources
30
30
  * store.Destroy();
@@ -41,7 +41,7 @@ export declare class StoreAsync extends Store {
41
41
  /**
42
42
  * Creates an instance of StoreAsync.
43
43
  * @param diffWorker A `Worker` running a `DiffTree` connected via `ConnectWorkerToDiffTree`
44
- * (e.g. a bundled `defaultDiffTreeWorker` entry). Any `keyFunc`/projections the diff needs
44
+ * (e.g. a bundled `defaultDiffTreeWorker` entry). Any `keyFunc`/routes the diff needs
45
45
  * must be baked into that worker's own entry file.
46
46
  * @param keyFunc Optional function to generate a key for a given data value, used locally by
47
47
  * this store to resolve root objects. Pass the same key logic the worker's `DiffTree` was
@@ -88,12 +88,11 @@ export declare class StoreAsync extends Store {
88
88
  * @param start The position at which to start changing the array.
89
89
  * @param deleteCount Optional number of elements to delete. If not provided, all elements from start to end will be deleted.
90
90
  * @param items Optional elements to insert into the array.
91
- * @returns A promise that resolves to the array of deleted elements once the splice has been
92
- * applied to the store.
91
+ * @returns A promise that resolves once the splice has been applied to the store.
93
92
  * @throws Will throw if the key isn't in the store and has no pending Write, or reject with
94
93
  * the original error if an earlier change failed.
95
94
  */
96
- Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<any[]>;
95
+ Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<void>;
97
96
  /**
98
97
  * Returns a promise that resolves once every change made before this call has been applied
99
98
  * to the store. Changes made after this call are sent in a later batch, so they aren't
@@ -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.
@@ -26,7 +26,7 @@ import { Store } from "./store";
26
26
  * await store.Push("users", { id: 1 }, { id: 2 });
27
27
  *
28
28
  * // Splicing an array asynchronously
29
- * const deletedItems = await store.Splice("users", 0, 1, { id: 3 });
29
+ * await store.Splice("users", 0, 1, { id: 3 });
30
30
  *
31
31
  * // Cleaning up resources
32
32
  * store.Destroy();
@@ -38,17 +38,17 @@ export class StoreAsync extends Store {
38
38
  /**
39
39
  * Creates an instance of StoreAsync.
40
40
  * @param diffWorker A `Worker` running a `DiffTree` connected via `ConnectWorkerToDiffTree`
41
- * (e.g. a bundled `defaultDiffTreeWorker` entry). Any `keyFunc`/projections the diff needs
41
+ * (e.g. a bundled `defaultDiffTreeWorker` entry). Any `keyFunc`/routes the diff needs
42
42
  * must be baked into that worker's own entry file.
43
43
  * @param keyFunc Optional function to generate a key for a given data value, used locally by
44
44
  * this store to resolve root objects. Pass the same key logic the worker's `DiffTree` was
45
45
  * constructed with, if any.
46
46
  */
47
47
  constructor(diffWorker, keyFunc) {
48
- super(keyFunc, false);
48
+ super(keyFunc);
49
49
  this.writeKeySet = new Set();
50
50
  this.currentBatch = [];
51
- this.currentBatchPromise = Promise.resolve([]);
51
+ this.currentBatchPromise = Promise.resolve();
52
52
  this.workerQueue = new WorkerQueue(diffWorker);
53
53
  this.asyncQueue = new AsyncQueue(true);
54
54
  }
@@ -59,29 +59,13 @@ export class StoreAsync extends Store {
59
59
  await Promise.resolve();
60
60
  if (batch === this.currentBatch)
61
61
  this.currentBatch = [];
62
- const results = await this.workerQueue.Push(batch);
63
- for (let x = 0; x < results.length; x++) {
64
- const result = results[x];
65
- switch (result.type) {
66
- case "diff": {
67
- this.UpdateRootMap(result.result);
68
- break;
69
- }
70
- case "splice": {
71
- const spliceResult = result.result;
72
- this.UpdateRootMap(spliceResult.diffResult);
73
- this.SpliceRootObject(spliceResult.path[0], spliceResult.start, spliceResult.deleteCount, spliceResult.items);
74
- break;
75
- }
76
- }
77
- }
78
- return results;
62
+ // The worker's Commit() diff covers every change in the batch
63
+ const diff = await this.workerQueue.Push(batch);
64
+ this.UpdateRootMap(diff);
79
65
  });
80
66
  }
81
- const batchIndex = this.currentBatch.length;
82
67
  this.currentBatch.push(method);
83
- const batchResult = await this.currentBatchPromise;
84
- return batchResult[batchIndex].result;
68
+ await this.currentBatchPromise;
85
69
  }
86
70
  /**
87
71
  * Writes data to the store asynchronously.
@@ -98,12 +82,13 @@ export class StoreAsync extends Store {
98
82
  key = key || this.keyFunc?.(data);
99
83
  if (!key)
100
84
  throw new Error("No key provided for data");
101
- 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));
102
87
  await this.Schedule({
103
- method: "diffpath",
88
+ method: "writepath",
104
89
  arguments: [key, data]
105
90
  });
106
- this.writeKeySet.delete(key);
91
+ this.writeKeySet.delete(RootOf(key));
107
92
  }
108
93
  /**
109
94
  * Patches an existing value in the store with new data asynchronously.
@@ -116,7 +101,7 @@ export class StoreAsync extends Store {
116
101
  * the original error if an earlier change failed.
117
102
  */
118
103
  async Patch(key, patch) {
119
- if (!this.Has(key) && !this.writeKeySet.has(key))
104
+ if (!this.HasRootOf(key) && !this.writeKeySet.has(RootOf(key)))
120
105
  throw "Key not found in store";
121
106
  await this.Schedule({
122
107
  method: "patchpath",
@@ -132,7 +117,7 @@ export class StoreAsync extends Store {
132
117
  * the original error if an earlier change failed.
133
118
  */
134
119
  async Push(key, ...data) {
135
- if (!this.Has(key) && !this.writeKeySet.has(key))
120
+ if (!this.HasRootOf(key) && !this.writeKeySet.has(RootOf(key)))
136
121
  throw "Key not found in store";
137
122
  await this.Schedule({
138
123
  method: "pushpath",
@@ -146,19 +131,17 @@ export class StoreAsync extends Store {
146
131
  * @param start The position at which to start changing the array.
147
132
  * @param deleteCount Optional number of elements to delete. If not provided, all elements from start to end will be deleted.
148
133
  * @param items Optional elements to insert into the array.
149
- * @returns A promise that resolves to the array of deleted elements once the splice has been
150
- * applied to the store.
134
+ * @returns A promise that resolves once the splice has been applied to the store.
151
135
  * @throws Will throw if the key isn't in the store and has no pending Write, or reject with
152
136
  * the original error if an earlier change failed.
153
137
  */
154
138
  async Splice(key, start, deleteCount, ...items) {
155
- if (!this.Has(key) && !this.writeKeySet.has(key))
139
+ if (!this.HasRootOf(key) && !this.writeKeySet.has(RootOf(key)))
156
140
  throw "Key not found in store";
157
- const spliceResult = await this.Schedule({
141
+ await this.Schedule({
158
142
  method: "splicepath",
159
- arguments: [key, start, deleteCount, items]
143
+ arguments: [key, start, deleteCount ?? Infinity, items]
160
144
  });
161
- return spliceResult.spliceResult;
162
145
  }
163
146
  /**
164
147
  * Returns a promise that resolves once every change made before this call has been applied
@@ -1,4 +1,4 @@
1
- import { DiffTreeProjectionMap } from "../Diff/diffTree.types";
1
+ import { IRouteDefinitions } from "../Diff/utils/diffTree.types";
2
2
  import { Store } from "./store";
3
3
  /**
4
4
  * StoreSync class extends the base Store class to provide synchronous data management operations.
@@ -22,25 +22,26 @@ import { Store } from "./store";
22
22
  * store.Push("user.array", { id: 1 }, { id: 2 });
23
23
  *
24
24
  * // Splicing an array
25
- * const deletedItems = store.Splice("user.array", 0, 1, { id: 3 });
25
+ * store.Splice("user.array", 0, 1, { id: 3 });
26
26
  *
27
27
  * @see Store
28
28
  * @see StoreAsync
29
- * @see DiffSync
29
+ * @see DiffTree
30
30
  */
31
31
  export declare class StoreSync extends Store {
32
32
  /**
33
- * The diff instance used to compute differences between current and new data states.
33
+ * The tree each change is applied to. Its `Commit()` diff keeps the store in sync.
34
34
  * @private
35
35
  */
36
36
  private diff;
37
37
  /**
38
38
  * Creates an instance of StoreSync.
39
39
  * @param keyFunc Optional function to generate a key for a given data value.
40
- * @param projections Optional map of derived-value projections, keyed by id. A
41
- * projection's result is readable through `Get` at `$projection_<id>`.
40
+ * @param routes Optional route definitions. A projection in the root route (`"/"`) is
41
+ * readable through `Get` at `$projection_<key>`.
42
+ * @throws When projections depend on each other in a cycle.
42
43
  */
43
- constructor(keyFunc?: (value: any) => string | undefined, projections?: DiffTreeProjectionMap);
44
+ constructor(keyFunc?: (value: any) => string | undefined, routes?: IRouteDefinitions);
44
45
  /**
45
46
  * Writes data to the store synchronously.
46
47
  * @param data The data to be written. Can be of any type.
@@ -68,7 +69,11 @@ export declare class StoreSync extends Store {
68
69
  * @param start The position at which to start changing the array.
69
70
  * @param deleteCount Optional number of elements to delete. If not provided, all elements from start to end will be deleted.
70
71
  * @param items Optional elements to insert into the array.
71
- * @returns The array of deleted elements.
72
72
  */
73
- Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): unknown[];
73
+ Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): void;
74
+ /**
75
+ * Runs projections and applies everything changed since the last sync to the store.
76
+ * @private
77
+ */
78
+ private Sync;
74
79
  }
@@ -1,5 +1,4 @@
1
- import { JsonDeepClone } from "../../Utils/json";
2
- import { DiffSync } from "../Diff/diffSync";
1
+ import { DiffTree } from "../Diff/diffTree";
3
2
  import { Store } from "./store";
4
3
  /**
5
4
  * StoreSync class extends the base Store class to provide synchronous data management operations.
@@ -23,22 +22,23 @@ import { Store } from "./store";
23
22
  * store.Push("user.array", { id: 1 }, { id: 2 });
24
23
  *
25
24
  * // Splicing an array
26
- * const deletedItems = store.Splice("user.array", 0, 1, { id: 3 });
25
+ * store.Splice("user.array", 0, 1, { id: 3 });
27
26
  *
28
27
  * @see Store
29
28
  * @see StoreAsync
30
- * @see DiffSync
29
+ * @see DiffTree
31
30
  */
32
31
  export class StoreSync extends Store {
33
32
  /**
34
33
  * Creates an instance of StoreSync.
35
34
  * @param keyFunc Optional function to generate a key for a given data value.
36
- * @param projections Optional map of derived-value projections, keyed by id. A
37
- * projection's result is readable through `Get` at `$projection_<id>`.
35
+ * @param routes Optional route definitions. A projection in the root route (`"/"`) is
36
+ * readable through `Get` at `$projection_<key>`.
37
+ * @throws When projections depend on each other in a cycle.
38
38
  */
39
- constructor(keyFunc, projections) {
39
+ constructor(keyFunc, routes) {
40
40
  super(keyFunc);
41
- this.diff = new DiffSync(keyFunc, projections);
41
+ this.diff = new DiffTree(keyFunc, routes);
42
42
  }
43
43
  /**
44
44
  * Writes data to the store synchronously.
@@ -50,8 +50,8 @@ export class StoreSync extends Store {
50
50
  key = key || this.keyFunc?.(data);
51
51
  if (!key)
52
52
  throw "No key provided for data";
53
- const diffResult = this.diff.DiffPath(key, data);
54
- this.UpdateRootMap(diffResult);
53
+ this.diff.WritePath(key, data);
54
+ this.Sync();
55
55
  }
56
56
  /**
57
57
  * Patches an existing value in the store with new data synchronously.
@@ -60,10 +60,10 @@ 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
- const diffResult = this.diff.PatchPath(key, patch);
66
- this.UpdateRootMap(diffResult);
65
+ this.diff.PatchPath(key, patch);
66
+ this.Sync();
67
67
  }
68
68
  /**
69
69
  * Pushes data into an array stored at the specified key synchronously.
@@ -71,10 +71,10 @@ 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
- const diffResult = this.diff.PushPath(key, data);
77
- this.UpdateRootMap(diffResult);
76
+ this.diff.PushPath(key, data);
77
+ this.Sync();
78
78
  }
79
79
  /**
80
80
  * Splices an array stored at the specified key with new data synchronously.
@@ -83,14 +83,19 @@ export class StoreSync extends Store {
83
83
  * @param start The position at which to start changing the array.
84
84
  * @param deleteCount Optional number of elements to delete. If not provided, all elements from start to end will be deleted.
85
85
  * @param items Optional elements to insert into the array.
86
- * @returns The array of deleted elements.
87
86
  */
88
87
  Splice(key, start, deleteCount, ...items) {
89
- if (!this.Has(key))
88
+ if (!this.HasRootOf(key))
90
89
  throw "Key not found in store";
91
- const spliceResult = this.diff.SplicePath(key, start, deleteCount, items);
92
- this.UpdateRootMap(spliceResult.diffResult);
93
- this.SpliceRootObject(spliceResult.path[0], spliceResult.start, spliceResult.deleteCount, items);
94
- return JsonDeepClone(spliceResult.spliceResult);
90
+ this.diff.SplicePath(key, start, deleteCount ?? Infinity, items);
91
+ this.Sync();
92
+ }
93
+ /**
94
+ * Runs projections and applies everything changed since the last sync to the store.
95
+ * @private
96
+ */
97
+ Sync() {
98
+ this.diff.RunProjections();
99
+ this.UpdateRootMap(this.diff.Commit());
95
100
  }
96
101
  }