j-templates 9.0.1 → 10.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SYNTAX_PRIMER.md +181 -101
- package/Store/Diff/channelClient.d.ts +104 -0
- package/Store/Diff/channelClient.js +225 -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 +186 -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 +38 -0
- package/Store/Diff/hostPublisher.js +147 -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 +200 -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/paths.d.ts +2 -1
- package/Store/Diff/utils/paths.js +3 -2
- package/Store/Diff/utils/routes.d.ts +18 -0
- package/Store/Diff/utils/routes.js +33 -2
- package/Store/Diff/utils/state.js +4 -3
- package/Store/Store/store.d.ts +41 -4
- package/Store/Store/store.js +62 -7
- package/Store/Store/storeAsync.d.ts +134 -25
- package/Store/Store/storeAsync.js +235 -33
- package/Store/Store/storeSync.d.ts +91 -11
- package/Store/Store/storeSync.js +107 -12
- package/Store/Tree/observableNode.d.ts +3 -0
- package/Store/Tree/observableNode.js +87 -13
- package/Store/Tree/observableScope.d.ts +3 -1
- package/Store/Tree/observableScope.js +12 -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/emitter.d.ts +11 -4
- package/Utils/emitter.js +25 -2
- package/Utils/index.d.ts +2 -0
- package/Utils/index.js +1 -0
- package/Utils/pattern.d.ts +14 -0
- package/Utils/pattern.js +20 -0
- package/Utils/subscriptionManager.d.ts +78 -0
- package/Utils/subscriptionManager.js +159 -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
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
import { AsyncQueue } from "../../Utils/asyncQueue";
|
|
2
|
-
import {
|
|
2
|
+
import { EmitterEvent } from "../../Utils/emitter";
|
|
3
|
+
import { PatternMatches } from "../../Utils/pattern";
|
|
4
|
+
import { _queueMicrotask, _setTimeout } from "../../Utils/scheduling";
|
|
5
|
+
import { ChannelClient, ChannelSubscription } from "../Diff/channelClient";
|
|
3
6
|
import { RootOf, Store } from "./store";
|
|
7
|
+
/** How long a demand root no one reads stays subscribed, by default */
|
|
8
|
+
const DEMAND_GRACE = 1000;
|
|
4
9
|
/**
|
|
5
10
|
* StoreAsync class extends the base Store class to provide asynchronous data management operations.
|
|
6
11
|
* This class handles writing, patching, pushing, and splicing data in an asynchronous manner.
|
|
7
12
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
13
|
+
* The tree lives in a worker, served by a `HostPublisher`. The store subscribes on construction and
|
|
14
|
+
* applies every commit the tree posts, whatever caused it: its own batches, other writers, or the
|
|
15
|
+
* worker's own work. A store created after the tree has committed starts from its committed state.
|
|
16
|
+
* Without writes it's a read-only view of the tree.
|
|
17
|
+
*
|
|
18
|
+
* - Changes made in one tick are sent as one batch, and at most one batch waits behind the one in
|
|
19
|
+
* flight. A change's promise resolves on its batch's `jt-ack`, which arrives after the commit's
|
|
20
|
+
* posts, so the change has been applied when it resolves.
|
|
21
|
+
* - A failed batch rejects only its own changes; later batches carry on.
|
|
22
|
+
*
|
|
23
|
+
* **Demand roots** (`IStoreAsyncOptions.demand`) are received while they're read: the first reactive
|
|
24
|
+
* read of one subscribes to it alone, and its first post brings the tree's state of it; a grace
|
|
25
|
+
* period after the last reader goes, it's unsubscribed and deleted from the store. A read outside
|
|
26
|
+
* a scope neither subscribes nor keeps one; `Hold` does, for code that reads it that way.
|
|
27
|
+
*
|
|
28
|
+
* **Over a lease** (`DiffTreeHub.Open`), the store takes its keyFunc from the lease's endpoint, owns
|
|
29
|
+
* the lease, and releases it when it's destroyed. When the hub restarts the worker,
|
|
30
|
+
* the store starts over with the new tree, as when it first subscribed: the tree is the source of
|
|
31
|
+
* truth, nothing the store wrote is written back, and changes the old tree hadn't acknowledged are
|
|
32
|
+
* rejected.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // A store over a hub's lease: it lives as long as the component that holds it
|
|
36
|
+
* @Inject(DiffTreeHub) hub!: DiffTreeHub;
|
|
37
|
+
* @Destroy() totals = new StoreAsync(this.hub.Open("totals"));
|
|
12
38
|
*
|
|
13
39
|
* @example
|
|
14
40
|
* // Creating a StoreAsync instance from a bundler-built worker entry file
|
|
@@ -28,41 +54,198 @@ import { RootOf, Store } from "./store";
|
|
|
28
54
|
* // Splicing an array asynchronously
|
|
29
55
|
* await store.Splice("users", 0, 1, { id: 3 });
|
|
30
56
|
*
|
|
31
|
-
* // Cleaning up resources
|
|
57
|
+
* // Cleaning up resources: the store stops listening; the worker is its creator's to terminate
|
|
32
58
|
* store.Destroy();
|
|
59
|
+
* worker.terminate();
|
|
33
60
|
*
|
|
34
61
|
* @see Store
|
|
35
62
|
* @see StoreSync
|
|
36
63
|
*/
|
|
37
64
|
export class StoreAsync extends Store {
|
|
38
65
|
/**
|
|
39
|
-
* Creates an instance of StoreAsync.
|
|
40
|
-
* @param
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
66
|
+
* Creates an instance of StoreAsync and subscribes to the tree.
|
|
67
|
+
* @param channel A channel to an `ObservableDiffTree` served by `HostPublisher`: a `Worker` (e.g. a
|
|
68
|
+
* bundled `defaultDiffTreeWorker` entry), a `MessagePort`, or a lease from `DiffTreeHub.Open`. Other
|
|
69
|
+
* messages may share the channel. Given a lease, the store owns it from then on and releases it
|
|
70
|
+
* when it's destroyed, and follows the worker across restarts. A lease on a tree on this thread
|
|
71
|
+
* (`DiffTreeHub.OpenLocal`) is for a `StoreSync`, and doesn't compile here.
|
|
72
|
+
* @param options `keyFunc`, `roots` and `opaque` (`IStoreAsyncOptions`). `keyFunc` is for a plain
|
|
73
|
+
* channel: pass the key logic the worker's tree was built with. A lease's is the endpoint's.
|
|
74
|
+
* `opaque` is the store's own: the tree must write those keys whole too, from its config
|
|
75
|
+
* (`IObservableDiffTreeConfig.opaque`), so share them from a file both import.
|
|
76
|
+
* @throws When `keyFunc` is given with a lease, whose endpoint has its own.
|
|
46
77
|
*/
|
|
47
|
-
constructor(
|
|
48
|
-
|
|
78
|
+
constructor(channel, options = {}) {
|
|
79
|
+
const lease = IsLease(channel) ? channel : undefined;
|
|
80
|
+
if (lease && options.keyFunc)
|
|
81
|
+
throw new Error(`StoreAsync: the endpoint ${lease.name} gives the store its keyFunc; leave it out of the options.`);
|
|
82
|
+
if (options.demand && options.demand.length > 0 && !options.roots)
|
|
83
|
+
throw new Error("StoreAsync: demand roots need roots: a store that receives every root has none left to demand.");
|
|
84
|
+
const keyFunc = lease ? lease.endpoint.keyFunc : options.keyFunc;
|
|
85
|
+
super(keyFunc, options.opaque, options.demand ?? [], (key, event) => this.DemandWatched(key, event));
|
|
49
86
|
this.writeKeySet = new Set();
|
|
87
|
+
this.asyncQueue = new AsyncQueue();
|
|
50
88
|
this.currentBatch = [];
|
|
51
89
|
this.currentBatchPromise = Promise.resolve();
|
|
52
|
-
|
|
53
|
-
this.
|
|
90
|
+
/** Batches not yet acknowledged, for `Flush` */
|
|
91
|
+
this.inFlight = new Set();
|
|
92
|
+
/** Demand roots a reactive scope reads now, from their watchers */
|
|
93
|
+
this.demandRead = new Set();
|
|
94
|
+
/** `Hold`s, by demand root */
|
|
95
|
+
this.demandHolds = new Map();
|
|
96
|
+
/** A subscription for each demand root the store holds */
|
|
97
|
+
this.demandSubscriptions = new Map();
|
|
98
|
+
/** The release timers of demand roots no one reads, by key */
|
|
99
|
+
this.demandReleasing = new Map();
|
|
100
|
+
/** Demand roots whose readers or holds changed since the last flush */
|
|
101
|
+
this.demandChanged = new Set();
|
|
102
|
+
this.demandFlushQueued = false;
|
|
103
|
+
this.destroyed = false;
|
|
104
|
+
this.options = options;
|
|
105
|
+
this.lease = lease;
|
|
106
|
+
if (lease)
|
|
107
|
+
this.stopLease = lease.OnPort(port => this.Restarted(port));
|
|
108
|
+
this.subscription = new ChannelSubscription({ roots: options.roots }, (post, first) => this.Apply(post, first));
|
|
109
|
+
this.client = this.Connect(lease ? lease.port : channel);
|
|
110
|
+
this.subscription.Connect(this.client);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The hub restarted the lease's worker: the store moves to the new tree's port, keeping its
|
|
114
|
+
* readers, and starts over as when it first subscribed. The changes the old tree hadn't
|
|
115
|
+
* acknowledged are rejected, not sent again, and nothing is written back: the new tree's first
|
|
116
|
+
* state is diffed into the store's roots in place, with the roots the old tree posted that the
|
|
117
|
+
* new one lacks deleted (`ChannelSubscription`).
|
|
118
|
+
*/
|
|
119
|
+
Restarted(port) {
|
|
120
|
+
this.client.RejectBatches(new Error(`${this.lease.name} restarted before this change was acknowledged.`));
|
|
121
|
+
this.client.Destroy();
|
|
122
|
+
this.client = this.Connect(port);
|
|
123
|
+
this.subscription.Connect(this.client);
|
|
124
|
+
for (const subscription of this.demandSubscriptions.values())
|
|
125
|
+
subscription.Connect(this.client);
|
|
126
|
+
}
|
|
127
|
+
Connect(channel) {
|
|
128
|
+
return new ChannelClient(channel);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Holds a demand root without reading it reactively, for code that reads it outside a scope: the
|
|
132
|
+
* store subscribes to it as for a reader, and keeps it until every hold is released. Its first
|
|
133
|
+
* post arrives later; until then it reads as its default.
|
|
134
|
+
* @returns What releases this hold; calling it again does nothing.
|
|
135
|
+
* @throws When the key is neither a demand root nor one of the store's `roots`. A `roots` root is
|
|
136
|
+
* always received: its hold does nothing.
|
|
137
|
+
*/
|
|
138
|
+
Hold(key) {
|
|
139
|
+
if (this.IsRootsKey(key))
|
|
140
|
+
return () => { };
|
|
141
|
+
if (!this.IsDemandKey(key))
|
|
142
|
+
throw new Error(`StoreAsync: ${key} isn't a demand root or one of the store's roots.`);
|
|
143
|
+
this.demandHolds.set(key, (this.demandHolds.get(key) ?? 0) + 1);
|
|
144
|
+
this.DemandChanged(key);
|
|
145
|
+
let held = true;
|
|
146
|
+
return () => {
|
|
147
|
+
if (!held)
|
|
148
|
+
return;
|
|
149
|
+
held = false;
|
|
150
|
+
const holds = (this.demandHolds.get(key) ?? 1) - 1;
|
|
151
|
+
holds > 0 ? this.demandHolds.set(key, holds) : this.demandHolds.delete(key);
|
|
152
|
+
this.DemandChanged(key);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
IsRootsKey(key) {
|
|
156
|
+
return !this.options.roots || this.options.roots.some(pattern => PatternMatches(pattern, key));
|
|
157
|
+
}
|
|
158
|
+
IsDemandKey(key) {
|
|
159
|
+
return !this.IsRootsKey(key) && (this.options.demand ?? []).some(pattern => PatternMatches(pattern, key));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* A demand root's watcher: it gained its first reactive reader or lost its last. This runs inside
|
|
163
|
+
* a scope's update, so it only records the change for the next flush.
|
|
164
|
+
*/
|
|
165
|
+
DemandWatched(key, event) {
|
|
166
|
+
if (this.IsRootsKey(key))
|
|
167
|
+
return;
|
|
168
|
+
event === EmitterEvent.SUBSCRIBED ? this.demandRead.add(key) : this.demandRead.delete(key);
|
|
169
|
+
this.DemandChanged(key);
|
|
170
|
+
}
|
|
171
|
+
DemandChanged(key) {
|
|
172
|
+
this.demandChanged.add(key);
|
|
173
|
+
if (this.demandFlushQueued)
|
|
174
|
+
return;
|
|
175
|
+
this.demandFlushQueued = true;
|
|
176
|
+
_queueMicrotask(() => this.FlushDemand());
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Subscribes the demand roots read or held now and not yet subscribed, and starts the release of
|
|
180
|
+
* the ones no one reads or holds. A root read and dropped since the last flush is never subscribed.
|
|
181
|
+
*/
|
|
182
|
+
FlushDemand() {
|
|
183
|
+
this.demandFlushQueued = false;
|
|
184
|
+
if (this.destroyed)
|
|
185
|
+
return;
|
|
186
|
+
const keys = Array.from(this.demandChanged);
|
|
187
|
+
this.demandChanged.clear();
|
|
188
|
+
for (const key of keys) {
|
|
189
|
+
if (this.IsDemanded(key)) {
|
|
190
|
+
const timer = this.demandReleasing.get(key);
|
|
191
|
+
if (timer !== undefined) {
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
this.demandReleasing.delete(key);
|
|
194
|
+
}
|
|
195
|
+
!this.demandSubscriptions.has(key) && this.SubscribeDemand(key);
|
|
196
|
+
}
|
|
197
|
+
else if (this.demandSubscriptions.has(key) && !this.demandReleasing.has(key))
|
|
198
|
+
this.ReleaseLater(key);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
IsDemanded(key) {
|
|
202
|
+
return this.demandRead.has(key) || this.demandHolds.has(key);
|
|
203
|
+
}
|
|
204
|
+
SubscribeDemand(key) {
|
|
205
|
+
// A post for a key released since is dropped: an unsubscribe and a commit can cross on the channel
|
|
206
|
+
const subscription = new ChannelSubscription({ roots: [key] }, (post, first) => this.demandSubscriptions.get(key) === subscription && this.Apply(post, first));
|
|
207
|
+
this.demandSubscriptions.set(key, subscription);
|
|
208
|
+
subscription.Connect(this.client);
|
|
209
|
+
}
|
|
210
|
+
ReleaseLater(key) {
|
|
211
|
+
const grace = this.options.demandDebounce ?? DEMAND_GRACE;
|
|
212
|
+
if (grace <= 0)
|
|
213
|
+
return this.ReleaseDemand(key);
|
|
214
|
+
let fired = false;
|
|
215
|
+
const timer = _setTimeout(() => {
|
|
216
|
+
fired = true;
|
|
217
|
+
this.demandReleasing.delete(key);
|
|
218
|
+
!this.destroyed && !this.IsDemanded(key) && this.ReleaseDemand(key);
|
|
219
|
+
}, grace);
|
|
220
|
+
// A timer that ran at once (synchronous scheduling) has nothing left to cancel
|
|
221
|
+
!fired && this.demandReleasing.set(key, timer);
|
|
222
|
+
}
|
|
223
|
+
/** Unsubscribes a demand root and deletes it from the store, so no later reader sees it stale */
|
|
224
|
+
ReleaseDemand(key) {
|
|
225
|
+
const subscription = this.demandSubscriptions.get(key);
|
|
226
|
+
if (!subscription)
|
|
227
|
+
return;
|
|
228
|
+
this.demandSubscriptions.delete(key);
|
|
229
|
+
subscription.Stop();
|
|
230
|
+
this.Has(key) && this.UpdateRootMap([{ type: "delete", path: [key] }]);
|
|
231
|
+
}
|
|
232
|
+
/** Applies a post. A tree's first state is diffed into the roots in place, its deletes with it. */
|
|
233
|
+
Apply(post, firstState) {
|
|
234
|
+
firstState ? this.UpdateRootMapInPlace(post.diff) : this.UpdateRootMap(post.diff);
|
|
54
235
|
}
|
|
55
236
|
async Schedule(method) {
|
|
56
237
|
if (this.currentBatch.length === 0) {
|
|
57
238
|
const batch = this.currentBatch;
|
|
58
|
-
|
|
239
|
+
const sent = this.asyncQueue.Next(async () => {
|
|
59
240
|
await Promise.resolve();
|
|
60
241
|
if (batch === this.currentBatch)
|
|
61
242
|
this.currentBatch = [];
|
|
62
|
-
// The
|
|
63
|
-
|
|
64
|
-
this.UpdateRootMap(diff);
|
|
243
|
+
// The batch's commit is applied by the subscription, before its ack arrives
|
|
244
|
+
await this.client.Batch(batch);
|
|
65
245
|
});
|
|
246
|
+
this.currentBatchPromise = sent;
|
|
247
|
+
this.inFlight.add(sent);
|
|
248
|
+
sent.then(() => this.inFlight.delete(sent), () => this.inFlight.delete(sent));
|
|
66
249
|
}
|
|
67
250
|
this.currentBatch.push(method);
|
|
68
251
|
await this.currentBatchPromise;
|
|
@@ -75,8 +258,8 @@ export class StoreAsync extends Store {
|
|
|
75
258
|
* @param data The data to be written. Can be of any type.
|
|
76
259
|
* @param key Optional key for the data. If not provided, the keyFunc will be used to generate a key.
|
|
77
260
|
* @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
|
-
*
|
|
261
|
+
* @throws Will throw an error if no key is provided for the data, or reject with the error of
|
|
262
|
+
* the batch it was sent in.
|
|
80
263
|
*/
|
|
81
264
|
async Write(data, key) {
|
|
82
265
|
key = key || this.keyFunc?.(data);
|
|
@@ -98,7 +281,7 @@ export class StoreAsync extends Store {
|
|
|
98
281
|
* @param patch The patch data to be merged with the existing value.
|
|
99
282
|
* @returns A promise that resolves once the patch has been applied to the store.
|
|
100
283
|
* @throws Will throw if the key isn't in the store and has no pending Write, or reject with
|
|
101
|
-
* the
|
|
284
|
+
* the error of the batch it was sent in.
|
|
102
285
|
*/
|
|
103
286
|
async Patch(key, patch) {
|
|
104
287
|
if (!this.HasRootOf(key) && !this.writeKeySet.has(RootOf(key)))
|
|
@@ -114,7 +297,7 @@ export class StoreAsync extends Store {
|
|
|
114
297
|
* @param data The data items to be pushed into the array.
|
|
115
298
|
* @returns A promise that resolves once the items have been applied to the store.
|
|
116
299
|
* @throws Will throw if the key isn't in the store and has no pending Write, or reject with
|
|
117
|
-
* the
|
|
300
|
+
* the error of the batch it was sent in.
|
|
118
301
|
*/
|
|
119
302
|
async Push(key, ...data) {
|
|
120
303
|
if (!this.HasRootOf(key) && !this.writeKeySet.has(RootOf(key)))
|
|
@@ -133,7 +316,7 @@ export class StoreAsync extends Store {
|
|
|
133
316
|
* @param items Optional elements to insert into the array.
|
|
134
317
|
* @returns A promise that resolves once the splice has been applied to the store.
|
|
135
318
|
* @throws Will throw if the key isn't in the store and has no pending Write, or reject with
|
|
136
|
-
* the
|
|
319
|
+
* the error of the batch it was sent in.
|
|
137
320
|
*/
|
|
138
321
|
async Splice(key, start, deleteCount, ...items) {
|
|
139
322
|
if (!this.HasRootOf(key) && !this.writeKeySet.has(RootOf(key)))
|
|
@@ -145,20 +328,39 @@ export class StoreAsync extends Store {
|
|
|
145
328
|
}
|
|
146
329
|
/**
|
|
147
330
|
* Returns a promise that resolves once every change made before this call has been applied
|
|
148
|
-
* to the store
|
|
149
|
-
*
|
|
150
|
-
*
|
|
331
|
+
* to the store: when the last batch sent before it is acknowledged, and at once when none is in
|
|
332
|
+
* flight. Changes made after this call are sent in a later batch, so they aren't waited for and
|
|
333
|
+
* haven't been applied when the promise resolves.
|
|
334
|
+
* @throws Rejects with the first error of the batches it waited for.
|
|
151
335
|
*/
|
|
152
336
|
async Flush() {
|
|
153
337
|
this.currentBatch = [];
|
|
154
|
-
|
|
338
|
+
const pending = Array.from(this.inFlight);
|
|
339
|
+
const results = await Promise.allSettled(pending);
|
|
340
|
+
const failed = results.find(result => result.status === "rejected");
|
|
341
|
+
if (failed)
|
|
342
|
+
throw failed.reason;
|
|
155
343
|
}
|
|
156
344
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
345
|
+
* Unsubscribes and stops listening to the channel. Changes not yet acknowledged never settle.
|
|
346
|
+
* Over a lease, the store then releases it: the last lease on a worker ends it. Over a channel, the
|
|
347
|
+
* channel stays open: whoever created the worker terminates it.
|
|
159
348
|
*/
|
|
160
349
|
Destroy() {
|
|
350
|
+
this.destroyed = true;
|
|
351
|
+
this.stopLease?.();
|
|
161
352
|
this.asyncQueue.Stop();
|
|
162
|
-
this.
|
|
353
|
+
this.subscription.Stop();
|
|
354
|
+
for (const subscription of this.demandSubscriptions.values())
|
|
355
|
+
subscription.Stop();
|
|
356
|
+
this.demandSubscriptions.clear();
|
|
357
|
+
for (const timer of this.demandReleasing.values())
|
|
358
|
+
clearTimeout(timer);
|
|
359
|
+
this.demandReleasing.clear();
|
|
360
|
+
this.client.Destroy();
|
|
361
|
+
this.lease?.Release();
|
|
163
362
|
}
|
|
164
363
|
}
|
|
364
|
+
function IsLease(channel) {
|
|
365
|
+
return typeof channel.OnPort === "function";
|
|
366
|
+
}
|
|
@@ -1,5 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { EmitterEvent } from "../../Utils/emitter";
|
|
2
|
+
import type { ILocalLease } from "../Diff/diffTree.types";
|
|
3
|
+
import { ObservableDiffTree } from "../Diff/observableDiffTree";
|
|
4
|
+
import { IStoreOptions, Store } from "./store";
|
|
5
|
+
/** Options for a `StoreSync` */
|
|
6
|
+
export interface IStoreSyncOptions extends IStoreOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The tree's root keys the store holds: exact keys and patterns ending in `*`. Omitted, every
|
|
9
|
+
* root. With a `keyFunc`, include the entity roots the held roots refer to.
|
|
10
|
+
*/
|
|
11
|
+
roots?: readonly string[];
|
|
12
|
+
/**
|
|
13
|
+
* Root keys whose readers the store reports to `watchCallback`: exact keys and patterns ending in
|
|
14
|
+
* `*`. A watched root reports when a reactive scope first reads it with `Get`, and when its last
|
|
15
|
+
* reader stops; a read outside a scope reports nothing.
|
|
16
|
+
*/
|
|
17
|
+
watch?: readonly string[];
|
|
18
|
+
/**
|
|
19
|
+
* Told when a watched root gains its first reader or loses its last, with the root's key. It runs
|
|
20
|
+
* inside the scope update that caused it: record the change and act on it later, without reading
|
|
21
|
+
* or writing the store. Needed with `watch`.
|
|
22
|
+
*/
|
|
23
|
+
watchCallback?: (rootKey: string, event: EmitterEvent) => void;
|
|
24
|
+
}
|
|
3
25
|
/**
|
|
4
26
|
* StoreSync class extends the base Store class to provide synchronous data management operations.
|
|
5
27
|
* This class handles writing, patching, pushing, and splicing data in a synchronous manner.
|
|
@@ -24,24 +46,69 @@ import { Store } from "./store";
|
|
|
24
46
|
* // Splicing an array
|
|
25
47
|
* store.Splice("user.array", 0, 1, { id: 3 });
|
|
26
48
|
*
|
|
49
|
+
* The store subscribes to an `ObservableDiffTree` and applies every commit the tree delivers, so
|
|
50
|
+
* anything else on the thread can subscribe to the same tree (`Tree`), and a commit made on the
|
|
51
|
+
* tree directly reaches the store too. A change runs the tree's projections and commits
|
|
52
|
+
* before it returns.
|
|
53
|
+
*
|
|
54
|
+
* - **Its own tree** (`new StoreSync()`, `new StoreSync(undefined, { keyFunc })`): the store creates an
|
|
55
|
+
* empty tree, and it lives as long as the store.
|
|
56
|
+
* - **A tree it's given** (`new StoreSync(new ObservableDiffTree({ keyFunc, routes }))`): for a tree
|
|
57
|
+
* with routes and projections, whose keyFunc the store takes. Anything else on the thread may hold it too.
|
|
58
|
+
* - **A hub's tree** (`new StoreSync(hub.OpenLocal(name))`): a tree on this thread a `DiffTreeHub` runs
|
|
59
|
+
* for an endpoint, shared by every store opened on it and followed into by the hub. A change from
|
|
60
|
+
* any of them reaches all of them, and the trees that follow it, before it returns. The store
|
|
61
|
+
* owns the lease and releases it when it's destroyed. When the hub restarts the tree, the store
|
|
62
|
+
* moves to the new one, as when it first subscribed.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* // A store on a hub's tree: it lives as long as the component that holds it
|
|
66
|
+
* @Inject(DiffTreeHub) hub!: DiffTreeHub;
|
|
67
|
+
* @Destroy() budget = new StoreSync(this.hub.OpenLocal("budget"));
|
|
68
|
+
*
|
|
27
69
|
* @see Store
|
|
28
70
|
* @see StoreAsync
|
|
29
|
-
* @see
|
|
71
|
+
* @see ObservableDiffTree
|
|
30
72
|
*/
|
|
31
73
|
export declare class StoreSync extends Store {
|
|
32
74
|
/**
|
|
33
|
-
* The tree each change is applied to.
|
|
75
|
+
* The tree each change is applied to. The store applies every commit it delivers.
|
|
34
76
|
* @private
|
|
35
77
|
*/
|
|
36
78
|
private diff;
|
|
79
|
+
private unsubscribe?;
|
|
80
|
+
/** The lease the store owns, when it was given one */
|
|
81
|
+
private lease?;
|
|
82
|
+
private stopLease?;
|
|
83
|
+
private roots?;
|
|
84
|
+
/** Over a lease: the roots delivered and not deleted, to delete the ones a restarted tree doesn't hold */
|
|
85
|
+
private delivered;
|
|
86
|
+
/**
|
|
87
|
+
* Creates a store and subscribes it to its tree: the tree's state is the store's when this returns.
|
|
88
|
+
* @param tree The tree:
|
|
89
|
+
* - A lease from `DiffTreeHub.OpenLocal`, on a hub's tree on this thread. The store owns the
|
|
90
|
+
* lease from then on, and releases it when it's destroyed, and follows the tree across
|
|
91
|
+
* restarts. A lease on a worker's tree (`DiffTreeHub.Open`) is for a `StoreAsync`, and doesn't
|
|
92
|
+
* compile here.
|
|
93
|
+
* - An `ObservableDiffTree`, for a tree with routes (`new ObservableDiffTree({ keyFunc, routes })`).
|
|
94
|
+
* A projection in the root route (`"/"`) is readable through `Get` at `$projection_<key>`. The
|
|
95
|
+
* store doesn't own it: `Destroy` stops listening.
|
|
96
|
+
* - Left out, the store creates an empty tree of its own, with the store's `keyFunc` and `opaque`.
|
|
97
|
+
* @param options `keyFunc`, `roots`, `opaque`, `watch` and `watchCallback` (`IStoreSyncOptions`).
|
|
98
|
+
* `keyFunc` is for the tree the
|
|
99
|
+
* store creates: a leased tree's is the endpoint's, and a given tree's its own. A tree given or
|
|
100
|
+
* leased must write the `opaque` keys whole already (`IObservableDiffTreeConfig.opaque`): share
|
|
101
|
+
* them from one file.
|
|
102
|
+
* @throws When `keyFunc` is given with a tree or a lease, which has its own; or `watch` without
|
|
103
|
+
* `watchCallback`.
|
|
104
|
+
*/
|
|
105
|
+
constructor(tree?: ObservableDiffTree | ILocalLease, options?: IStoreSyncOptions);
|
|
37
106
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* readable through `Get` at `$projection_<key>`.
|
|
42
|
-
* @throws When projections depend on each other in a cycle.
|
|
107
|
+
* The store's tree: its own, or the hub's. Subscribe to it to receive the
|
|
108
|
+
* same commits the store applies; a commit made on it reaches the store too. A subscriber can't
|
|
109
|
+
* write to the store during a delivery.
|
|
43
110
|
*/
|
|
44
|
-
|
|
111
|
+
get Tree(): ObservableDiffTree;
|
|
45
112
|
/**
|
|
46
113
|
* Writes data to the store synchronously.
|
|
47
114
|
* @param data The data to be written. Can be of any type.
|
|
@@ -72,7 +139,20 @@ export declare class StoreSync extends Store {
|
|
|
72
139
|
*/
|
|
73
140
|
Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): void;
|
|
74
141
|
/**
|
|
75
|
-
*
|
|
142
|
+
* Stops applying the tree's commits. Over a lease, the store then releases it: the last lease on
|
|
143
|
+
* a hub's tree ends it. The store's values stay readable, and stop changing.
|
|
144
|
+
*/
|
|
145
|
+
Destroy(): void;
|
|
146
|
+
/**
|
|
147
|
+
* Subscribes to a hub's tree in place of the last. A restarted tree's state is applied in place, so
|
|
148
|
+
* readers keep their objects, with a delete for each root the last tree had and this one hasn't, in
|
|
149
|
+
* one update. The first delivery arrives during `Subscribe`.
|
|
150
|
+
*/
|
|
151
|
+
private Attach;
|
|
152
|
+
private Track;
|
|
153
|
+
/**
|
|
154
|
+
* Runs projections and commits: the commit's delivery applies everything changed since the
|
|
155
|
+
* last commit to the store.
|
|
76
156
|
* @private
|
|
77
157
|
*/
|
|
78
158
|
private Sync;
|
package/Store/Store/storeSync.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ObservableDiffTree } from "../Diff/observableDiffTree";
|
|
2
2
|
import { Store } from "./store";
|
|
3
3
|
/**
|
|
4
4
|
* StoreSync class extends the base Store class to provide synchronous data management operations.
|
|
@@ -24,21 +24,76 @@ import { Store } from "./store";
|
|
|
24
24
|
* // Splicing an array
|
|
25
25
|
* store.Splice("user.array", 0, 1, { id: 3 });
|
|
26
26
|
*
|
|
27
|
+
* The store subscribes to an `ObservableDiffTree` and applies every commit the tree delivers, so
|
|
28
|
+
* anything else on the thread can subscribe to the same tree (`Tree`), and a commit made on the
|
|
29
|
+
* tree directly reaches the store too. A change runs the tree's projections and commits
|
|
30
|
+
* before it returns.
|
|
31
|
+
*
|
|
32
|
+
* - **Its own tree** (`new StoreSync()`, `new StoreSync(undefined, { keyFunc })`): the store creates an
|
|
33
|
+
* empty tree, and it lives as long as the store.
|
|
34
|
+
* - **A tree it's given** (`new StoreSync(new ObservableDiffTree({ keyFunc, routes }))`): for a tree
|
|
35
|
+
* with routes and projections, whose keyFunc the store takes. Anything else on the thread may hold it too.
|
|
36
|
+
* - **A hub's tree** (`new StoreSync(hub.OpenLocal(name))`): a tree on this thread a `DiffTreeHub` runs
|
|
37
|
+
* for an endpoint, shared by every store opened on it and followed into by the hub. A change from
|
|
38
|
+
* any of them reaches all of them, and the trees that follow it, before it returns. The store
|
|
39
|
+
* owns the lease and releases it when it's destroyed. When the hub restarts the tree, the store
|
|
40
|
+
* moves to the new one, as when it first subscribed.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // A store on a hub's tree: it lives as long as the component that holds it
|
|
44
|
+
* @Inject(DiffTreeHub) hub!: DiffTreeHub;
|
|
45
|
+
* @Destroy() budget = new StoreSync(this.hub.OpenLocal("budget"));
|
|
46
|
+
*
|
|
27
47
|
* @see Store
|
|
28
48
|
* @see StoreAsync
|
|
29
|
-
* @see
|
|
49
|
+
* @see ObservableDiffTree
|
|
30
50
|
*/
|
|
31
51
|
export class StoreSync extends Store {
|
|
32
52
|
/**
|
|
33
|
-
* Creates
|
|
34
|
-
* @param
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
53
|
+
* Creates a store and subscribes it to its tree: the tree's state is the store's when this returns.
|
|
54
|
+
* @param tree The tree:
|
|
55
|
+
* - A lease from `DiffTreeHub.OpenLocal`, on a hub's tree on this thread. The store owns the
|
|
56
|
+
* lease from then on, and releases it when it's destroyed, and follows the tree across
|
|
57
|
+
* restarts. A lease on a worker's tree (`DiffTreeHub.Open`) is for a `StoreAsync`, and doesn't
|
|
58
|
+
* compile here.
|
|
59
|
+
* - An `ObservableDiffTree`, for a tree with routes (`new ObservableDiffTree({ keyFunc, routes })`).
|
|
60
|
+
* A projection in the root route (`"/"`) is readable through `Get` at `$projection_<key>`. The
|
|
61
|
+
* store doesn't own it: `Destroy` stops listening.
|
|
62
|
+
* - Left out, the store creates an empty tree of its own, with the store's `keyFunc` and `opaque`.
|
|
63
|
+
* @param options `keyFunc`, `roots`, `opaque`, `watch` and `watchCallback` (`IStoreSyncOptions`).
|
|
64
|
+
* `keyFunc` is for the tree the
|
|
65
|
+
* store creates: a leased tree's is the endpoint's, and a given tree's its own. A tree given or
|
|
66
|
+
* leased must write the `opaque` keys whole already (`IObservableDiffTreeConfig.opaque`): share
|
|
67
|
+
* them from one file.
|
|
68
|
+
* @throws When `keyFunc` is given with a tree or a lease, which has its own; or `watch` without
|
|
69
|
+
* `watchCallback`.
|
|
70
|
+
*/
|
|
71
|
+
constructor(tree, options = {}) {
|
|
72
|
+
const lease = tree && !(tree instanceof ObservableDiffTree) ? tree : undefined;
|
|
73
|
+
if (tree && options.keyFunc)
|
|
74
|
+
throw new Error(lease
|
|
75
|
+
? `StoreSync: the endpoint ${lease.name} gives the store its keyFunc; leave it out of the options.`
|
|
76
|
+
: "StoreSync: the tree has its keyFunc, which the store takes; leave it out of the options.");
|
|
77
|
+
super(lease ? lease.endpoint.keyFunc : tree ? tree.KeyFunc : options.keyFunc, options.opaque, options.watch, options.watchCallback);
|
|
78
|
+
/** Over a lease: the roots delivered and not deleted, to delete the ones a restarted tree doesn't hold */
|
|
79
|
+
this.delivered = new Set();
|
|
80
|
+
this.roots = options.roots;
|
|
81
|
+
if (lease) {
|
|
82
|
+
this.lease = lease;
|
|
83
|
+
this.Attach(lease.tree, false);
|
|
84
|
+
this.stopLease = lease.OnTree(tree => this.Attach(tree, true));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.diff = tree ?? new ObservableDiffTree({ keyFunc: options.keyFunc, opaque: options.opaque });
|
|
88
|
+
this.unsubscribe = this.diff.Subscribe({ roots: this.roots }, post => this.UpdateRootMap(post.diff));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The store's tree: its own, or the hub's. Subscribe to it to receive the
|
|
92
|
+
* same commits the store applies; a commit made on it reaches the store too. A subscriber can't
|
|
93
|
+
* write to the store during a delivery.
|
|
38
94
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.diff = new DiffTree(keyFunc, routes);
|
|
95
|
+
get Tree() {
|
|
96
|
+
return this.diff;
|
|
42
97
|
}
|
|
43
98
|
/**
|
|
44
99
|
* Writes data to the store synchronously.
|
|
@@ -91,11 +146,51 @@ export class StoreSync extends Store {
|
|
|
91
146
|
this.Sync();
|
|
92
147
|
}
|
|
93
148
|
/**
|
|
94
|
-
*
|
|
149
|
+
* Stops applying the tree's commits. Over a lease, the store then releases it: the last lease on
|
|
150
|
+
* a hub's tree ends it. The store's values stay readable, and stop changing.
|
|
151
|
+
*/
|
|
152
|
+
Destroy() {
|
|
153
|
+
this.stopLease?.();
|
|
154
|
+
this.stopLease = undefined;
|
|
155
|
+
this.unsubscribe?.();
|
|
156
|
+
this.unsubscribe = undefined;
|
|
157
|
+
this.lease?.Release();
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Subscribes to a hub's tree in place of the last. A restarted tree's state is applied in place, so
|
|
161
|
+
* readers keep their objects, with a delete for each root the last tree had and this one hasn't, in
|
|
162
|
+
* one update. The first delivery arrives during `Subscribe`.
|
|
163
|
+
*/
|
|
164
|
+
Attach(tree, again) {
|
|
165
|
+
this.unsubscribe?.();
|
|
166
|
+
this.diff = tree;
|
|
167
|
+
const previous = this.delivered;
|
|
168
|
+
this.delivered = new Set();
|
|
169
|
+
let subscribing = true;
|
|
170
|
+
let first = [];
|
|
171
|
+
this.unsubscribe = tree.Subscribe({ roots: this.roots }, post => {
|
|
172
|
+
this.Track(post.diff);
|
|
173
|
+
subscribing ? (first = post.diff) : this.UpdateRootMap(post.diff);
|
|
174
|
+
});
|
|
175
|
+
subscribing = false;
|
|
176
|
+
const diff = [...first];
|
|
177
|
+
for (const key of previous)
|
|
178
|
+
this.delivered.has(key) || diff.push({ type: "delete", path: [key] });
|
|
179
|
+
diff.length > 0 && (again ? this.UpdateRootMapInPlace(diff) : this.UpdateRootMap(diff));
|
|
180
|
+
}
|
|
181
|
+
Track(diff) {
|
|
182
|
+
for (const record of diff) {
|
|
183
|
+
const key = String(record.path[0]);
|
|
184
|
+
record.type === "delete" && record.path.length === 1 ? this.delivered.delete(key) : this.delivered.add(key);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Runs projections and commits: the commit's delivery applies everything changed since the
|
|
189
|
+
* last commit to the store.
|
|
95
190
|
* @private
|
|
96
191
|
*/
|
|
97
192
|
Sync() {
|
|
98
193
|
this.diff.RunProjections();
|
|
99
|
-
this.
|
|
194
|
+
this.diff.Commit();
|
|
100
195
|
}
|
|
101
196
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EmitterEventCallback } from "../../Utils/emitter";
|
|
1
2
|
import { JsonDiffResult } from "../../Utils/json";
|
|
2
3
|
export declare namespace ObservableNode {
|
|
3
4
|
/**
|
|
@@ -39,6 +40,8 @@ export declare namespace ObservableNode {
|
|
|
39
40
|
* @returns A proxied version of the value that emits change events.
|
|
40
41
|
*/
|
|
41
42
|
function Create<T>(value: T, unwrapValue?: boolean): T;
|
|
43
|
+
function WatchProperty<T>(proxy: T, prop: keyof T, callback: EmitterEventCallback): void;
|
|
44
|
+
function UnwatchProperty<T>(proxy: T, prop: keyof T, callback: EmitterEventCallback): void;
|
|
42
45
|
/**
|
|
43
46
|
* Returns an immutable, cached snapshot of an observable node's current value (plain
|
|
44
47
|
* objects/arrays, with keyed children read through their root). The same snapshot is
|