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.
Files changed (64) hide show
  1. package/SYNTAX_PRIMER.md +181 -101
  2. package/Store/Diff/channelClient.d.ts +104 -0
  3. package/Store/Diff/channelClient.js +225 -0
  4. package/Store/Diff/defaultDiffTreeWorker.js +3 -4
  5. package/Store/Diff/diffTree.d.ts +56 -0
  6. package/Store/Diff/diffTree.js +110 -4
  7. package/Store/Diff/diffTree.types.d.ts +186 -21
  8. package/Store/Diff/diffTree.types.js +2 -10
  9. package/Store/Diff/diffTreeEndpoint.d.ts +101 -0
  10. package/Store/Diff/diffTreeEndpoint.js +173 -0
  11. package/Store/Diff/diffTreeEndpoint.types.d.ts +71 -0
  12. package/Store/Diff/diffTreeEndpoint.types.js +1 -0
  13. package/Store/Diff/diffTreeHub.d.ts +139 -0
  14. package/Store/Diff/diffTreeHub.js +346 -0
  15. package/Store/Diff/hostPublisher.d.ts +38 -0
  16. package/Store/Diff/hostPublisher.js +147 -0
  17. package/Store/Diff/index.d.ts +3 -0
  18. package/Store/Diff/index.js +3 -0
  19. package/Store/Diff/observableDiffTree.d.ts +93 -0
  20. package/Store/Diff/observableDiffTree.js +200 -0
  21. package/Store/Diff/onConnect.d.ts +33 -0
  22. package/Store/Diff/onConnect.js +83 -0
  23. package/Store/Diff/utils/diffTree.types.d.ts +9 -0
  24. package/Store/Diff/utils/homes.d.ts +12 -0
  25. package/Store/Diff/utils/homes.js +40 -0
  26. package/Store/Diff/utils/paths.d.ts +2 -1
  27. package/Store/Diff/utils/paths.js +3 -2
  28. package/Store/Diff/utils/routes.d.ts +18 -0
  29. package/Store/Diff/utils/routes.js +33 -2
  30. package/Store/Diff/utils/state.js +4 -3
  31. package/Store/Store/store.d.ts +41 -4
  32. package/Store/Store/store.js +62 -7
  33. package/Store/Store/storeAsync.d.ts +134 -25
  34. package/Store/Store/storeAsync.js +235 -33
  35. package/Store/Store/storeSync.d.ts +91 -11
  36. package/Store/Store/storeSync.js +107 -12
  37. package/Store/Tree/observableNode.d.ts +3 -0
  38. package/Store/Tree/observableNode.js +87 -13
  39. package/Store/Tree/observableScope.d.ts +3 -1
  40. package/Store/Tree/observableScope.js +12 -0
  41. package/Store/Tree/opaque.d.ts +30 -0
  42. package/Store/Tree/opaque.js +35 -0
  43. package/Store/Tree/snapshot.js +5 -4
  44. package/Store/index.d.ts +4 -1
  45. package/Store/index.js +1 -1
  46. package/Utils/decorators.d.ts +0 -98
  47. package/Utils/decorators.js +0 -156
  48. package/Utils/emitter.d.ts +11 -4
  49. package/Utils/emitter.js +25 -2
  50. package/Utils/index.d.ts +2 -0
  51. package/Utils/index.js +1 -0
  52. package/Utils/pattern.d.ts +14 -0
  53. package/Utils/pattern.js +20 -0
  54. package/Utils/subscriptionManager.d.ts +78 -0
  55. package/Utils/subscriptionManager.js +159 -0
  56. package/package.json +1 -1
  57. package/Store/Diff/diffTreeWorker.d.ts +0 -6
  58. package/Store/Diff/diffTreeWorker.js +0 -22
  59. package/Store/Diff/diffTreeWriter.d.ts +0 -92
  60. package/Store/Diff/diffTreeWriter.js +0 -137
  61. package/Store/Diff/workerQueue.d.ts +0 -7
  62. package/Store/Diff/workerQueue.js +0 -29
  63. package/Store/Store/pushStore.d.ts +0 -53
  64. package/Store/Store/pushStore.js +0 -77
@@ -1,14 +1,40 @@
1
1
  import { AsyncQueue } from "../../Utils/asyncQueue";
2
- import { WorkerQueue } from "../Diff/workerQueue";
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
- * StoreAsync is designed to work with observable data structures, allowing for efficient updates
9
- * and notifications when data changes. It is particularly useful for scenarios where asynchronous
10
- * operations are preferred or required, such as handling large datasets or performing complex diffs
11
- * without blocking the main thread.
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 diffWorker A `Worker` running a `DiffTree` connected via `ConnectWorkerToDiffTree`
41
- * (e.g. a bundled `defaultDiffTreeWorker` entry). Any `keyFunc`/routes the diff needs
42
- * must be baked into that worker's own entry file.
43
- * @param keyFunc Optional function to generate a key for a given data value, used locally by
44
- * this store to resolve root objects. Pass the same key logic the worker's `DiffTree` was
45
- * constructed with, if any.
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(diffWorker, keyFunc) {
48
- super(keyFunc);
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
- this.workerQueue = new WorkerQueue(diffWorker);
53
- this.asyncQueue = new AsyncQueue(true);
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
- this.currentBatchPromise = this.asyncQueue.Next(async () => {
239
+ const sent = this.asyncQueue.Next(async () => {
59
240
  await Promise.resolve();
60
241
  if (batch === this.currentBatch)
61
242
  this.currentBatch = [];
62
- // The worker's Commit() diff covers every change in the batch
63
- const diff = await this.workerQueue.Push(batch);
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 original
79
- * error if an earlier change failed.
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 original error if an earlier change failed.
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 original error if an earlier change failed.
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 original error if an earlier change failed.
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. Changes made after this call are sent in a later batch, so they aren't
149
- * waited for and haven't been applied when the promise resolves.
150
- * @throws Rejects with the original error if a change made before this call failed.
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
- return this.asyncQueue.Flush();
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
- * Destroys the StoreAsync instance, stopping the async queue and cleaning up the diff instance.
158
- * This method should be called when the instance is no longer needed to free up resources.
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.workerQueue.Destroy();
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 { IRouteDefinitions } from "../Diff/utils/diffTree.types";
2
- import { Store } from "./store";
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 DiffTree
71
+ * @see ObservableDiffTree
30
72
  */
31
73
  export declare class StoreSync extends Store {
32
74
  /**
33
- * The tree each change is applied to. Its `Commit()` diff keeps the store in sync.
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
- * Creates an instance of StoreSync.
39
- * @param keyFunc Optional function to generate a key for a given data value.
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.
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
- constructor(keyFunc?: (value: any) => string | undefined, routes?: IRouteDefinitions);
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
- * Runs projections and applies everything changed since the last sync to the store.
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;
@@ -1,4 +1,4 @@
1
- import { DiffTree } from "../Diff/diffTree";
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 DiffTree
49
+ * @see ObservableDiffTree
30
50
  */
31
51
  export class StoreSync extends Store {
32
52
  /**
33
- * Creates an instance of StoreSync.
34
- * @param keyFunc Optional function to generate a key for a given data value.
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.
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
- constructor(keyFunc, routes) {
40
- super(keyFunc);
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
- * Runs projections and applies everything changed since the last sync to the store.
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.UpdateRootMap(this.diff.Commit());
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