j-templates 9.0.0 → 10.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/Node/vNode.js +2 -0
- package/SYNTAX_PRIMER.md +181 -101
- package/Store/Diff/channelClient.d.ts +106 -0
- package/Store/Diff/channelClient.js +228 -0
- package/Store/Diff/defaultDiffTreeWorker.js +3 -4
- package/Store/Diff/diffTree.d.ts +56 -0
- package/Store/Diff/diffTree.js +110 -4
- package/Store/Diff/diffTree.types.d.ts +191 -21
- package/Store/Diff/diffTree.types.js +2 -10
- package/Store/Diff/diffTreeEndpoint.d.ts +101 -0
- package/Store/Diff/diffTreeEndpoint.js +173 -0
- package/Store/Diff/diffTreeEndpoint.types.d.ts +71 -0
- package/Store/Diff/diffTreeEndpoint.types.js +1 -0
- package/Store/Diff/diffTreeHub.d.ts +139 -0
- package/Store/Diff/diffTreeHub.js +346 -0
- package/Store/Diff/hostPublisher.d.ts +39 -0
- package/Store/Diff/hostPublisher.js +151 -0
- package/Store/Diff/index.d.ts +3 -0
- package/Store/Diff/index.js +3 -0
- package/Store/Diff/observableDiffTree.d.ts +93 -0
- package/Store/Diff/observableDiffTree.js +199 -0
- package/Store/Diff/onConnect.d.ts +33 -0
- package/Store/Diff/onConnect.js +83 -0
- package/Store/Diff/utils/diffTree.types.d.ts +9 -0
- package/Store/Diff/utils/homes.d.ts +12 -0
- package/Store/Diff/utils/homes.js +40 -0
- package/Store/Diff/utils/routes.d.ts +18 -0
- package/Store/Diff/utils/routes.js +33 -2
- package/Store/Store/store.d.ts +36 -4
- package/Store/Store/store.js +47 -6
- package/Store/Store/storeAsync.d.ts +80 -25
- package/Store/Store/storeAsync.js +102 -33
- package/Store/Store/storeSync.d.ts +76 -11
- package/Store/Store/storeSync.js +105 -12
- package/Store/Tree/observableNode.js +68 -13
- package/Store/Tree/observableScope.d.ts +1 -0
- package/Store/Tree/observableScope.js +4 -0
- package/Store/Tree/opaque.d.ts +30 -0
- package/Store/Tree/opaque.js +35 -0
- package/Store/Tree/snapshot.js +5 -4
- package/Store/index.d.ts +4 -1
- package/Store/index.js +1 -1
- package/Utils/decorators.d.ts +0 -98
- package/Utils/decorators.js +0 -156
- package/Utils/index.d.ts +2 -0
- package/Utils/index.js +1 -0
- package/Utils/subscriptionManager.d.ts +78 -0
- package/Utils/subscriptionManager.js +162 -0
- package/package.json +1 -1
- package/Store/Diff/diffTreeWorker.d.ts +0 -6
- package/Store/Diff/diffTreeWorker.js +0 -22
- package/Store/Diff/diffTreeWriter.d.ts +0 -92
- package/Store/Diff/diffTreeWriter.js +0 -137
- package/Store/Diff/workerQueue.d.ts +0 -7
- package/Store/Diff/workerQueue.js +0 -29
- package/Store/Store/pushStore.d.ts +0 -53
- package/Store/Store/pushStore.js +0 -77
package/Store/Store/store.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { JsonDeepClone } from "../../Utils/json";
|
|
1
|
+
import { JsonDeepClone, JsonDiff, JsonType } from "../../Utils/json";
|
|
2
2
|
import { ObservableNode } from "../Tree/observableNode";
|
|
3
|
+
import { Opaque } from "../Tree/opaque";
|
|
3
4
|
/**
|
|
4
5
|
* Base class for reading observable data: holds root objects as ObservableNode instances
|
|
5
|
-
* and applies diff results to them. Subclasses decide where the diffs come from: `
|
|
6
|
-
*
|
|
6
|
+
* and applies diff results to them. Subclasses decide where the diffs come from: `StoreSync`
|
|
7
|
+
* and `StoreAsync` apply what an `ObservableDiffTree` delivers to them.
|
|
7
8
|
*
|
|
8
9
|
* @see Store
|
|
9
|
-
* @see PushStore
|
|
10
10
|
*/
|
|
11
11
|
export class ReadStore {
|
|
12
12
|
/**
|
|
13
13
|
* Creates an instance of 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
|
+
* @param opaque Root keys held whole, see `IStoreOptions.opaque`.
|
|
16
17
|
*/
|
|
17
|
-
constructor(keyFunc) {
|
|
18
|
+
constructor(keyFunc, opaque = []) {
|
|
18
19
|
this.keyFunc = keyFunc;
|
|
19
20
|
this.rootMap = new Map();
|
|
21
|
+
this.opaque = new Set(opaque);
|
|
20
22
|
const aliasFunc = keyFunc &&
|
|
21
23
|
((value, reactive = true) => {
|
|
22
24
|
const key = keyFunc(value);
|
|
@@ -24,6 +26,8 @@ export class ReadStore {
|
|
|
24
26
|
// store: read the embedded copy
|
|
25
27
|
if (key === undefined || key.startsWith("/"))
|
|
26
28
|
return undefined;
|
|
29
|
+
if (this.opaque.has(key))
|
|
30
|
+
throw `Opaque root ${key} can't be read through a key`;
|
|
27
31
|
const rootObject = this.rootMap.get(key);
|
|
28
32
|
if (rootObject === undefined)
|
|
29
33
|
throw `No root object found for key: ${key}`;
|
|
@@ -39,7 +43,7 @@ export class ReadStore {
|
|
|
39
43
|
Get(id, defaultValue) {
|
|
40
44
|
let result = this.rootMap.get(id);
|
|
41
45
|
if (result === undefined) {
|
|
42
|
-
result = this.createNode({ [id]: defaultValue });
|
|
46
|
+
result = this.createNode({ [id]: this.opaque.has(id) ? Box(defaultValue) : defaultValue });
|
|
43
47
|
this.rootMap.set(id, result);
|
|
44
48
|
}
|
|
45
49
|
return result[id];
|
|
@@ -70,6 +74,25 @@ export class ReadStore {
|
|
|
70
74
|
this.UpdateRootObject(rootGroup[0].path[0], rootGroup);
|
|
71
75
|
}
|
|
72
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Applies whole-root writes by diffing each one into the root the store already holds, so
|
|
79
|
+
* readers of an unchanged part aren't notified and keep its objects. Anything else, an opaque
|
|
80
|
+
* root, or a root the store doesn't hold, is applied as it is.
|
|
81
|
+
* @param results - Diff results, e.g. a tree's first state after reconnecting
|
|
82
|
+
* @protected
|
|
83
|
+
*/
|
|
84
|
+
UpdateRootMapInPlace(results) {
|
|
85
|
+
const applied = [];
|
|
86
|
+
for (const result of results) {
|
|
87
|
+
const key = result.path[0];
|
|
88
|
+
const rootObject = this.rootMap.get(key);
|
|
89
|
+
if (result.type !== "write" || result.path.length > 1 || rootObject === undefined || this.opaque.has(key))
|
|
90
|
+
applied.push(result);
|
|
91
|
+
else
|
|
92
|
+
JsonDiff({ [key]: result.value }, ObservableNode.Unwrap(rootObject)).forEach(record => applied.push(record));
|
|
93
|
+
}
|
|
94
|
+
applied.length > 0 && this.UpdateRootMap(applied);
|
|
95
|
+
}
|
|
73
96
|
/**
|
|
74
97
|
* Updates a specific root object with diff results.
|
|
75
98
|
* Creates the root object if it doesn't exist.
|
|
@@ -79,6 +102,8 @@ export class ReadStore {
|
|
|
79
102
|
* @private
|
|
80
103
|
*/
|
|
81
104
|
UpdateRootObject(rootPath, results) {
|
|
105
|
+
if (this.opaque.has(rootPath))
|
|
106
|
+
results = OpaqueResults(rootPath, results);
|
|
82
107
|
const rootObject = this.rootMap.get(rootPath);
|
|
83
108
|
if (rootObject === undefined) {
|
|
84
109
|
if (results[0].type !== "write" || results[0].path.length > 1)
|
|
@@ -104,6 +129,22 @@ export class ReadStore {
|
|
|
104
129
|
*/
|
|
105
130
|
export class Store extends ReadStore {
|
|
106
131
|
}
|
|
132
|
+
/** An opaque root's value as the store holds it: an object or array boxed, anything else as it is */
|
|
133
|
+
function Box(value) {
|
|
134
|
+
return JsonType(value) === "value" ? value : new Opaque(value);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* An opaque root's diff, each value boxed. A DiffTree that marks the root opaque writes it whole or
|
|
138
|
+
* deletes it, so any other change means it wasn't told.
|
|
139
|
+
* @throws When the diff writes inside the root
|
|
140
|
+
*/
|
|
141
|
+
function OpaqueResults(rootPath, results) {
|
|
142
|
+
return results.map(result => {
|
|
143
|
+
if (result.path.length > 1 || (result.type !== "write" && result.type !== "delete"))
|
|
144
|
+
throw `Opaque root ${rootPath} was changed at ${result.path.join(".")}: mark it opaque in the DiffTree too (DiffTree.MarkOpaque)`;
|
|
145
|
+
return result.type === "write" ? { ...result, value: Box(result.value) } : result;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
107
148
|
/** The root key a path is in: its text before the first `.` */
|
|
108
149
|
export function RootOf(path) {
|
|
109
150
|
const dot = path.indexOf(".");
|
|
@@ -1,12 +1,38 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IChannel, IWorkerLease } from "../Diff/diffTree.types";
|
|
2
|
+
import { IStoreOptions, Store } from "./store";
|
|
3
|
+
/** Options for a `StoreAsync` */
|
|
4
|
+
export interface IStoreAsyncOptions extends IStoreOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The root keys the store receives: exact keys and patterns ending in `*`. Omitted, every root.
|
|
7
|
+
* A root the store writes but doesn't receive is written but never read back. With a
|
|
8
|
+
* `keyFunc`, include the entity roots the received roots refer to.
|
|
9
|
+
*/
|
|
10
|
+
roots?: readonly string[];
|
|
11
|
+
}
|
|
2
12
|
/**
|
|
3
13
|
* StoreAsync class extends the base Store class to provide asynchronous data management operations.
|
|
4
14
|
* This class handles writing, patching, pushing, and splicing data in an asynchronous manner.
|
|
5
15
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
16
|
+
* The tree lives in a worker, served by a `HostPublisher`. The store subscribes on construction and
|
|
17
|
+
* applies every commit the tree posts, whatever caused it: its own batches, other writers, or the
|
|
18
|
+
* worker's own work. A store created after the tree has committed starts from its committed state.
|
|
19
|
+
* Without writes it's a read-only view of the tree.
|
|
20
|
+
*
|
|
21
|
+
* - Changes made in one tick are sent as one batch, and at most one batch waits behind the one in
|
|
22
|
+
* flight. A change's promise resolves on its batch's `jt-ack`, which arrives after the commit's
|
|
23
|
+
* posts, so the change has been applied when it resolves.
|
|
24
|
+
* - A failed batch rejects only its own changes; later batches carry on.
|
|
25
|
+
*
|
|
26
|
+
* **Over a lease** (`DiffTreeHub.Open`), the store takes its keyFunc from the lease's endpoint, owns
|
|
27
|
+
* the lease, and releases it when it's destroyed. When the hub restarts the worker,
|
|
28
|
+
* the store starts over with the new tree, as when it first subscribed: the tree is the source of
|
|
29
|
+
* truth, nothing the store wrote is written back, and changes the old tree hadn't acknowledged are
|
|
30
|
+
* rejected.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // A store over a hub's lease: it lives as long as the component that holds it
|
|
34
|
+
* @Inject(DiffTreeHub) hub!: DiffTreeHub;
|
|
35
|
+
* @Destroy() totals = new StoreAsync(this.hub.Open("totals"));
|
|
10
36
|
*
|
|
11
37
|
* @example
|
|
12
38
|
* // Creating a StoreAsync instance from a bundler-built worker entry file
|
|
@@ -26,28 +52,55 @@ import { Store } from "./store";
|
|
|
26
52
|
* // Splicing an array asynchronously
|
|
27
53
|
* await store.Splice("users", 0, 1, { id: 3 });
|
|
28
54
|
*
|
|
29
|
-
* // Cleaning up resources
|
|
55
|
+
* // Cleaning up resources: the store stops listening; the worker is its creator's to terminate
|
|
30
56
|
* store.Destroy();
|
|
57
|
+
* worker.terminate();
|
|
31
58
|
*
|
|
32
59
|
* @see Store
|
|
33
60
|
* @see StoreSync
|
|
34
61
|
*/
|
|
35
62
|
export declare class StoreAsync extends Store {
|
|
36
63
|
private writeKeySet;
|
|
37
|
-
private
|
|
64
|
+
private client;
|
|
65
|
+
private options;
|
|
66
|
+
/**
|
|
67
|
+
* The store's subscription, kept across a restart of the lease's worker: a new tree's first state
|
|
68
|
+
* deletes the roots the old one posted that it doesn't hold
|
|
69
|
+
*/
|
|
70
|
+
private subscription;
|
|
71
|
+
/** The lease the store owns, when it was given one */
|
|
72
|
+
private lease?;
|
|
73
|
+
private stopLease?;
|
|
38
74
|
private asyncQueue;
|
|
39
75
|
private currentBatch;
|
|
40
76
|
private currentBatchPromise;
|
|
77
|
+
/** Batches not yet acknowledged, for `Flush` */
|
|
78
|
+
private inFlight;
|
|
79
|
+
/**
|
|
80
|
+
* Creates an instance of StoreAsync and subscribes to the tree.
|
|
81
|
+
* @param channel A channel to an `ObservableDiffTree` served by `HostPublisher`: a `Worker` (e.g. a
|
|
82
|
+
* bundled `defaultDiffTreeWorker` entry), a `MessagePort`, or a lease from `DiffTreeHub.Open`. Other
|
|
83
|
+
* messages may share the channel. Given a lease, the store owns it from then on and releases it
|
|
84
|
+
* when it's destroyed, and follows the worker across restarts. A lease on a tree on this thread
|
|
85
|
+
* (`DiffTreeHub.OpenLocal`) is for a `StoreSync`, and doesn't compile here.
|
|
86
|
+
* @param options `keyFunc`, `roots` and `opaque` (`IStoreAsyncOptions`). `keyFunc` is for a plain
|
|
87
|
+
* channel: pass the key logic the worker's tree was built with. A lease's is the endpoint's.
|
|
88
|
+
* `opaque` is sent to the tree before the store subscribes; over a lease, share it with the tree
|
|
89
|
+
* (`IObservableDiffTreeConfig.opaque`) from a file both import.
|
|
90
|
+
* @throws When `keyFunc` is given with a lease, whose endpoint has its own.
|
|
91
|
+
*/
|
|
92
|
+
constructor(channel: IChannel | IWorkerLease, options?: IStoreAsyncOptions);
|
|
41
93
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* this store to resolve root objects. Pass the same key logic the worker's `DiffTree` was
|
|
48
|
-
* constructed with, if any.
|
|
94
|
+
* The hub restarted the lease's worker: the store moves to the new tree's port, keeping its
|
|
95
|
+
* readers, and starts over as when it first subscribed. The changes the old tree hadn't
|
|
96
|
+
* acknowledged are rejected, not sent again, and nothing is written back: the new tree's first
|
|
97
|
+
* state is diffed into the store's roots in place, with the roots the old tree posted that the
|
|
98
|
+
* new one lacks deleted (`ChannelSubscription`).
|
|
49
99
|
*/
|
|
50
|
-
|
|
100
|
+
private Restarted;
|
|
101
|
+
private Connect;
|
|
102
|
+
/** Applies a post. A tree's first state is diffed into the roots in place, its deletes with it. */
|
|
103
|
+
private Apply;
|
|
51
104
|
private Schedule;
|
|
52
105
|
/**
|
|
53
106
|
* Writes data to the store asynchronously.
|
|
@@ -57,8 +110,8 @@ export declare class StoreAsync extends Store {
|
|
|
57
110
|
* @param data The data to be written. Can be of any type.
|
|
58
111
|
* @param key Optional key for the data. If not provided, the keyFunc will be used to generate a key.
|
|
59
112
|
* @returns A promise that resolves once the write has been applied to the store.
|
|
60
|
-
* @throws Will throw an error if no key is provided for the data, or reject with the
|
|
61
|
-
*
|
|
113
|
+
* @throws Will throw an error if no key is provided for the data, or reject with the error of
|
|
114
|
+
* the batch it was sent in.
|
|
62
115
|
*/
|
|
63
116
|
Write(data: unknown, key?: string): Promise<void>;
|
|
64
117
|
/**
|
|
@@ -69,7 +122,7 @@ export declare class StoreAsync extends Store {
|
|
|
69
122
|
* @param patch The patch data to be merged with the existing value.
|
|
70
123
|
* @returns A promise that resolves once the patch has been applied to the store.
|
|
71
124
|
* @throws Will throw if the key isn't in the store and has no pending Write, or reject with
|
|
72
|
-
* the
|
|
125
|
+
* the error of the batch it was sent in.
|
|
73
126
|
*/
|
|
74
127
|
Patch(key: string, patch: unknown): Promise<void>;
|
|
75
128
|
/**
|
|
@@ -78,7 +131,7 @@ export declare class StoreAsync extends Store {
|
|
|
78
131
|
* @param data The data items to be pushed into the array.
|
|
79
132
|
* @returns A promise that resolves once the items have been applied to the store.
|
|
80
133
|
* @throws Will throw if the key isn't in the store and has no pending Write, or reject with
|
|
81
|
-
* the
|
|
134
|
+
* the error of the batch it was sent in.
|
|
82
135
|
*/
|
|
83
136
|
Push(key: string, ...data: unknown[]): Promise<void>;
|
|
84
137
|
/**
|
|
@@ -90,19 +143,21 @@ export declare class StoreAsync extends Store {
|
|
|
90
143
|
* @param items Optional elements to insert into the array.
|
|
91
144
|
* @returns A promise that resolves once the splice has been applied to the store.
|
|
92
145
|
* @throws Will throw if the key isn't in the store and has no pending Write, or reject with
|
|
93
|
-
* the
|
|
146
|
+
* the error of the batch it was sent in.
|
|
94
147
|
*/
|
|
95
148
|
Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<void>;
|
|
96
149
|
/**
|
|
97
150
|
* Returns a promise that resolves once every change made before this call has been applied
|
|
98
|
-
* to the store
|
|
99
|
-
*
|
|
100
|
-
*
|
|
151
|
+
* to the store: when the last batch sent before it is acknowledged, and at once when none is in
|
|
152
|
+
* flight. Changes made after this call are sent in a later batch, so they aren't waited for and
|
|
153
|
+
* haven't been applied when the promise resolves.
|
|
154
|
+
* @throws Rejects with the first error of the batches it waited for.
|
|
101
155
|
*/
|
|
102
156
|
Flush(): Promise<void>;
|
|
103
157
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
158
|
+
* Unsubscribes and stops listening to the channel. Changes not yet acknowledged never settle.
|
|
159
|
+
* Over a lease, the store then releases it: the last lease on a worker ends it. Over a channel, the
|
|
160
|
+
* channel stays open: whoever created the worker terminates it.
|
|
106
161
|
*/
|
|
107
162
|
Destroy(): void;
|
|
108
163
|
}
|
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
import { AsyncQueue } from "../../Utils/asyncQueue";
|
|
2
|
-
import {
|
|
2
|
+
import { ChannelClient, ChannelSubscription } from "../Diff/channelClient";
|
|
3
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.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* The tree lives in a worker, served by a `HostPublisher`. The store subscribes on construction and
|
|
9
|
+
* applies every commit the tree posts, whatever caused it: its own batches, other writers, or the
|
|
10
|
+
* worker's own work. A store created after the tree has committed starts from its committed state.
|
|
11
|
+
* Without writes it's a read-only view of the tree.
|
|
12
|
+
*
|
|
13
|
+
* - Changes made in one tick are sent as one batch, and at most one batch waits behind the one in
|
|
14
|
+
* flight. A change's promise resolves on its batch's `jt-ack`, which arrives after the commit's
|
|
15
|
+
* posts, so the change has been applied when it resolves.
|
|
16
|
+
* - A failed batch rejects only its own changes; later batches carry on.
|
|
17
|
+
*
|
|
18
|
+
* **Over a lease** (`DiffTreeHub.Open`), the store takes its keyFunc from the lease's endpoint, owns
|
|
19
|
+
* the lease, and releases it when it's destroyed. When the hub restarts the worker,
|
|
20
|
+
* the store starts over with the new tree, as when it first subscribed: the tree is the source of
|
|
21
|
+
* truth, nothing the store wrote is written back, and changes the old tree hadn't acknowledged are
|
|
22
|
+
* rejected.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // A store over a hub's lease: it lives as long as the component that holds it
|
|
26
|
+
* @Inject(DiffTreeHub) hub!: DiffTreeHub;
|
|
27
|
+
* @Destroy() totals = new StoreAsync(this.hub.Open("totals"));
|
|
12
28
|
*
|
|
13
29
|
* @example
|
|
14
30
|
* // Creating a StoreAsync instance from a bundler-built worker entry file
|
|
@@ -28,41 +44,82 @@ import { RootOf, Store } from "./store";
|
|
|
28
44
|
* // Splicing an array asynchronously
|
|
29
45
|
* await store.Splice("users", 0, 1, { id: 3 });
|
|
30
46
|
*
|
|
31
|
-
* // Cleaning up resources
|
|
47
|
+
* // Cleaning up resources: the store stops listening; the worker is its creator's to terminate
|
|
32
48
|
* store.Destroy();
|
|
49
|
+
* worker.terminate();
|
|
33
50
|
*
|
|
34
51
|
* @see Store
|
|
35
52
|
* @see StoreSync
|
|
36
53
|
*/
|
|
37
54
|
export class StoreAsync extends Store {
|
|
38
55
|
/**
|
|
39
|
-
* Creates an instance of StoreAsync.
|
|
40
|
-
* @param
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
56
|
+
* Creates an instance of StoreAsync and subscribes to the tree.
|
|
57
|
+
* @param channel A channel to an `ObservableDiffTree` served by `HostPublisher`: a `Worker` (e.g. a
|
|
58
|
+
* bundled `defaultDiffTreeWorker` entry), a `MessagePort`, or a lease from `DiffTreeHub.Open`. Other
|
|
59
|
+
* messages may share the channel. Given a lease, the store owns it from then on and releases it
|
|
60
|
+
* when it's destroyed, and follows the worker across restarts. A lease on a tree on this thread
|
|
61
|
+
* (`DiffTreeHub.OpenLocal`) is for a `StoreSync`, and doesn't compile here.
|
|
62
|
+
* @param options `keyFunc`, `roots` and `opaque` (`IStoreAsyncOptions`). `keyFunc` is for a plain
|
|
63
|
+
* channel: pass the key logic the worker's tree was built with. A lease's is the endpoint's.
|
|
64
|
+
* `opaque` is sent to the tree before the store subscribes; over a lease, share it with the tree
|
|
65
|
+
* (`IObservableDiffTreeConfig.opaque`) from a file both import.
|
|
66
|
+
* @throws When `keyFunc` is given with a lease, whose endpoint has its own.
|
|
46
67
|
*/
|
|
47
|
-
constructor(
|
|
48
|
-
|
|
68
|
+
constructor(channel, options = {}) {
|
|
69
|
+
const lease = IsLease(channel) ? channel : undefined;
|
|
70
|
+
if (lease && options.keyFunc)
|
|
71
|
+
throw new Error(`StoreAsync: the endpoint ${lease.name} gives the store its keyFunc; leave it out of the options.`);
|
|
72
|
+
const keyFunc = lease ? lease.endpoint.keyFunc : options.keyFunc;
|
|
73
|
+
super(keyFunc, options.opaque);
|
|
49
74
|
this.writeKeySet = new Set();
|
|
75
|
+
this.asyncQueue = new AsyncQueue();
|
|
50
76
|
this.currentBatch = [];
|
|
51
77
|
this.currentBatchPromise = Promise.resolve();
|
|
52
|
-
|
|
53
|
-
this.
|
|
78
|
+
/** Batches not yet acknowledged, for `Flush` */
|
|
79
|
+
this.inFlight = new Set();
|
|
80
|
+
this.options = options;
|
|
81
|
+
this.lease = lease;
|
|
82
|
+
if (lease)
|
|
83
|
+
this.stopLease = lease.OnPort(port => this.Restarted(port));
|
|
84
|
+
this.subscription = new ChannelSubscription({ roots: options.roots }, (post, first) => this.Apply(post, first));
|
|
85
|
+
this.client = this.Connect(lease ? lease.port : channel);
|
|
86
|
+
this.subscription.Connect(this.client);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The hub restarted the lease's worker: the store moves to the new tree's port, keeping its
|
|
90
|
+
* readers, and starts over as when it first subscribed. The changes the old tree hadn't
|
|
91
|
+
* acknowledged are rejected, not sent again, and nothing is written back: the new tree's first
|
|
92
|
+
* state is diffed into the store's roots in place, with the roots the old tree posted that the
|
|
93
|
+
* new one lacks deleted (`ChannelSubscription`).
|
|
94
|
+
*/
|
|
95
|
+
Restarted(port) {
|
|
96
|
+
this.client.RejectBatches(new Error(`${this.lease.name} restarted before this change was acknowledged.`));
|
|
97
|
+
this.client.Destroy();
|
|
98
|
+
this.client = this.Connect(port);
|
|
99
|
+
this.subscription.Connect(this.client);
|
|
100
|
+
}
|
|
101
|
+
Connect(channel) {
|
|
102
|
+
const client = new ChannelClient(channel);
|
|
103
|
+
this.options.opaque && this.options.opaque.length > 0 && client.MarkOpaque(this.options.opaque);
|
|
104
|
+
return client;
|
|
105
|
+
}
|
|
106
|
+
/** Applies a post. A tree's first state is diffed into the roots in place, its deletes with it. */
|
|
107
|
+
Apply(post, firstState) {
|
|
108
|
+
firstState ? this.UpdateRootMapInPlace(post.diff) : this.UpdateRootMap(post.diff);
|
|
54
109
|
}
|
|
55
110
|
async Schedule(method) {
|
|
56
111
|
if (this.currentBatch.length === 0) {
|
|
57
112
|
const batch = this.currentBatch;
|
|
58
|
-
|
|
113
|
+
const sent = this.asyncQueue.Next(async () => {
|
|
59
114
|
await Promise.resolve();
|
|
60
115
|
if (batch === this.currentBatch)
|
|
61
116
|
this.currentBatch = [];
|
|
62
|
-
// The
|
|
63
|
-
|
|
64
|
-
this.UpdateRootMap(diff);
|
|
117
|
+
// The batch's commit is applied by the subscription, before its ack arrives
|
|
118
|
+
await this.client.Batch(batch);
|
|
65
119
|
});
|
|
120
|
+
this.currentBatchPromise = sent;
|
|
121
|
+
this.inFlight.add(sent);
|
|
122
|
+
sent.then(() => this.inFlight.delete(sent), () => this.inFlight.delete(sent));
|
|
66
123
|
}
|
|
67
124
|
this.currentBatch.push(method);
|
|
68
125
|
await this.currentBatchPromise;
|
|
@@ -75,8 +132,8 @@ export class StoreAsync extends Store {
|
|
|
75
132
|
* @param data The data to be written. Can be of any type.
|
|
76
133
|
* @param key Optional key for the data. If not provided, the keyFunc will be used to generate a key.
|
|
77
134
|
* @returns A promise that resolves once the write has been applied to the store.
|
|
78
|
-
* @throws Will throw an error if no key is provided for the data, or reject with the
|
|
79
|
-
*
|
|
135
|
+
* @throws Will throw an error if no key is provided for the data, or reject with the error of
|
|
136
|
+
* the batch it was sent in.
|
|
80
137
|
*/
|
|
81
138
|
async Write(data, key) {
|
|
82
139
|
key = key || this.keyFunc?.(data);
|
|
@@ -98,7 +155,7 @@ export class StoreAsync extends Store {
|
|
|
98
155
|
* @param patch The patch data to be merged with the existing value.
|
|
99
156
|
* @returns A promise that resolves once the patch has been applied to the store.
|
|
100
157
|
* @throws Will throw if the key isn't in the store and has no pending Write, or reject with
|
|
101
|
-
* the
|
|
158
|
+
* the error of the batch it was sent in.
|
|
102
159
|
*/
|
|
103
160
|
async Patch(key, patch) {
|
|
104
161
|
if (!this.HasRootOf(key) && !this.writeKeySet.has(RootOf(key)))
|
|
@@ -114,7 +171,7 @@ export class StoreAsync extends Store {
|
|
|
114
171
|
* @param data The data items to be pushed into the array.
|
|
115
172
|
* @returns A promise that resolves once the items have been applied to the store.
|
|
116
173
|
* @throws Will throw if the key isn't in the store and has no pending Write, or reject with
|
|
117
|
-
* the
|
|
174
|
+
* the error of the batch it was sent in.
|
|
118
175
|
*/
|
|
119
176
|
async Push(key, ...data) {
|
|
120
177
|
if (!this.HasRootOf(key) && !this.writeKeySet.has(RootOf(key)))
|
|
@@ -133,7 +190,7 @@ export class StoreAsync extends Store {
|
|
|
133
190
|
* @param items Optional elements to insert into the array.
|
|
134
191
|
* @returns A promise that resolves once the splice has been applied to the store.
|
|
135
192
|
* @throws Will throw if the key isn't in the store and has no pending Write, or reject with
|
|
136
|
-
* the
|
|
193
|
+
* the error of the batch it was sent in.
|
|
137
194
|
*/
|
|
138
195
|
async Splice(key, start, deleteCount, ...items) {
|
|
139
196
|
if (!this.HasRootOf(key) && !this.writeKeySet.has(RootOf(key)))
|
|
@@ -145,20 +202,32 @@ export class StoreAsync extends Store {
|
|
|
145
202
|
}
|
|
146
203
|
/**
|
|
147
204
|
* Returns a promise that resolves once every change made before this call has been applied
|
|
148
|
-
* to the store
|
|
149
|
-
*
|
|
150
|
-
*
|
|
205
|
+
* to the store: when the last batch sent before it is acknowledged, and at once when none is in
|
|
206
|
+
* flight. Changes made after this call are sent in a later batch, so they aren't waited for and
|
|
207
|
+
* haven't been applied when the promise resolves.
|
|
208
|
+
* @throws Rejects with the first error of the batches it waited for.
|
|
151
209
|
*/
|
|
152
210
|
async Flush() {
|
|
153
211
|
this.currentBatch = [];
|
|
154
|
-
|
|
212
|
+
const pending = Array.from(this.inFlight);
|
|
213
|
+
const results = await Promise.allSettled(pending);
|
|
214
|
+
const failed = results.find(result => result.status === "rejected");
|
|
215
|
+
if (failed)
|
|
216
|
+
throw failed.reason;
|
|
155
217
|
}
|
|
156
218
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
219
|
+
* Unsubscribes and stops listening to the channel. Changes not yet acknowledged never settle.
|
|
220
|
+
* Over a lease, the store then releases it: the last lease on a worker ends it. Over a channel, the
|
|
221
|
+
* channel stays open: whoever created the worker terminates it.
|
|
159
222
|
*/
|
|
160
223
|
Destroy() {
|
|
224
|
+
this.stopLease?.();
|
|
161
225
|
this.asyncQueue.Stop();
|
|
162
|
-
this.
|
|
226
|
+
this.subscription.Stop();
|
|
227
|
+
this.client.Destroy();
|
|
228
|
+
this.lease?.Release();
|
|
163
229
|
}
|
|
164
230
|
}
|
|
231
|
+
function IsLease(channel) {
|
|
232
|
+
return typeof channel.OnPort === "function";
|
|
233
|
+
}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { ILocalLease } from "../Diff/diffTree.types";
|
|
2
|
+
import { ObservableDiffTree } from "../Diff/observableDiffTree";
|
|
3
|
+
import { IStoreOptions, Store } from "./store";
|
|
4
|
+
/** Options for a `StoreSync` */
|
|
5
|
+
export interface IStoreSyncOptions extends IStoreOptions {
|
|
6
|
+
/**
|
|
7
|
+
* The tree's root keys the store holds: exact keys and patterns ending in `*`. Omitted, every
|
|
8
|
+
* root. With a `keyFunc`, include the entity roots the held roots refer to.
|
|
9
|
+
*/
|
|
10
|
+
roots?: readonly string[];
|
|
11
|
+
}
|
|
3
12
|
/**
|
|
4
13
|
* StoreSync class extends the base Store class to provide synchronous data management operations.
|
|
5
14
|
* This class handles writing, patching, pushing, and splicing data in a synchronous manner.
|
|
@@ -24,24 +33,67 @@ import { Store } from "./store";
|
|
|
24
33
|
* // Splicing an array
|
|
25
34
|
* store.Splice("user.array", 0, 1, { id: 3 });
|
|
26
35
|
*
|
|
36
|
+
* The store subscribes to an `ObservableDiffTree` and applies every commit the tree delivers, so
|
|
37
|
+
* anything else on the thread can subscribe to the same tree (`Tree`), and a commit made on the
|
|
38
|
+
* tree directly reaches the store too. A change runs the tree's projections and commits
|
|
39
|
+
* before it returns.
|
|
40
|
+
*
|
|
41
|
+
* - **Its own tree** (`new StoreSync()`, `new StoreSync(undefined, { keyFunc })`): the store creates an
|
|
42
|
+
* empty tree, and it lives as long as the store.
|
|
43
|
+
* - **A tree it's given** (`new StoreSync(new ObservableDiffTree({ keyFunc, routes }))`): for a tree
|
|
44
|
+
* with routes and projections, whose keyFunc the store takes. Anything else on the thread may hold it too.
|
|
45
|
+
* - **A hub's tree** (`new StoreSync(hub.OpenLocal(name))`): a tree on this thread a `DiffTreeHub` runs
|
|
46
|
+
* for an endpoint, shared by every store opened on it and followed into by the hub. A change from
|
|
47
|
+
* any of them reaches all of them, and the trees that follow it, before it returns. The store
|
|
48
|
+
* owns the lease and releases it when it's destroyed. When the hub restarts the tree, the store
|
|
49
|
+
* moves to the new one, as when it first subscribed.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // A store on a hub's tree: it lives as long as the component that holds it
|
|
53
|
+
* @Inject(DiffTreeHub) hub!: DiffTreeHub;
|
|
54
|
+
* @Destroy() budget = new StoreSync(this.hub.OpenLocal("budget"));
|
|
55
|
+
*
|
|
27
56
|
* @see Store
|
|
28
57
|
* @see StoreAsync
|
|
29
|
-
* @see
|
|
58
|
+
* @see ObservableDiffTree
|
|
30
59
|
*/
|
|
31
60
|
export declare class StoreSync extends Store {
|
|
32
61
|
/**
|
|
33
|
-
* The tree each change is applied to.
|
|
62
|
+
* The tree each change is applied to. The store applies every commit it delivers.
|
|
34
63
|
* @private
|
|
35
64
|
*/
|
|
36
65
|
private diff;
|
|
66
|
+
private unsubscribe?;
|
|
67
|
+
/** The lease the store owns, when it was given one */
|
|
68
|
+
private lease?;
|
|
69
|
+
private stopLease?;
|
|
70
|
+
private roots?;
|
|
71
|
+
/** Over a lease: the roots delivered and not deleted, to delete the ones a restarted tree doesn't hold */
|
|
72
|
+
private delivered;
|
|
37
73
|
/**
|
|
38
|
-
* Creates
|
|
39
|
-
* @param
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
74
|
+
* Creates a store and subscribes it to its tree: the tree's state is the store's when this returns.
|
|
75
|
+
* @param tree The tree:
|
|
76
|
+
* - A lease from `DiffTreeHub.OpenLocal`, on a hub's tree on this thread. The store owns the
|
|
77
|
+
* lease from then on, and releases it when it's destroyed, and follows the tree across
|
|
78
|
+
* restarts. A lease on a worker's tree (`DiffTreeHub.Open`) is for a `StoreAsync`, and doesn't
|
|
79
|
+
* compile here.
|
|
80
|
+
* - An `ObservableDiffTree`, for a tree with routes (`new ObservableDiffTree({ keyFunc, routes })`).
|
|
81
|
+
* A projection in the root route (`"/"`) is readable through `Get` at `$projection_<key>`. The
|
|
82
|
+
* store doesn't own it: `Destroy` stops listening.
|
|
83
|
+
* - Left out, the store creates an empty tree of its own, with the store's `keyFunc` and `opaque`.
|
|
84
|
+
* @param options `keyFunc`, `roots` and `opaque` (`IStoreSyncOptions`). `keyFunc` is for the tree the
|
|
85
|
+
* store creates: a leased tree's is the endpoint's, and a given tree's its own. A tree given or
|
|
86
|
+
* leased must write the `opaque` keys whole already (`IObservableDiffTreeConfig.opaque`): share
|
|
87
|
+
* them from one file.
|
|
88
|
+
* @throws When `keyFunc` is given with a tree or a lease, which has its own.
|
|
43
89
|
*/
|
|
44
|
-
constructor(
|
|
90
|
+
constructor(tree?: ObservableDiffTree | ILocalLease, options?: IStoreSyncOptions);
|
|
91
|
+
/**
|
|
92
|
+
* The store's tree: its own, or the hub's. Subscribe to it to receive the
|
|
93
|
+
* same commits the store applies; a commit made on it reaches the store too. A subscriber can't
|
|
94
|
+
* write to the store during a delivery.
|
|
95
|
+
*/
|
|
96
|
+
get Tree(): ObservableDiffTree;
|
|
45
97
|
/**
|
|
46
98
|
* Writes data to the store synchronously.
|
|
47
99
|
* @param data The data to be written. Can be of any type.
|
|
@@ -72,7 +124,20 @@ export declare class StoreSync extends Store {
|
|
|
72
124
|
*/
|
|
73
125
|
Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): void;
|
|
74
126
|
/**
|
|
75
|
-
*
|
|
127
|
+
* Stops applying the tree's commits. Over a lease, the store then releases it: the last lease on
|
|
128
|
+
* a hub's tree ends it. The store's values stay readable, and stop changing.
|
|
129
|
+
*/
|
|
130
|
+
Destroy(): void;
|
|
131
|
+
/**
|
|
132
|
+
* Subscribes to a hub's tree in place of the last. A restarted tree's state is applied in place, so
|
|
133
|
+
* readers keep their objects, with a delete for each root the last tree had and this one hasn't, in
|
|
134
|
+
* one update. The first delivery arrives during `Subscribe`.
|
|
135
|
+
*/
|
|
136
|
+
private Attach;
|
|
137
|
+
private Track;
|
|
138
|
+
/**
|
|
139
|
+
* Runs projections and commits: the commit's delivery applies everything changed since the
|
|
140
|
+
* last commit to the store.
|
|
76
141
|
* @private
|
|
77
142
|
*/
|
|
78
143
|
private Sync;
|