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