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
@@ -0,0 +1,139 @@
1
+ import { ILocalLease, IWorkerLease } from "./diffTree.types";
2
+ import { ObservableDiffTree } from "./observableDiffTree";
3
+ import type { IDiffTreeEndpoint } from "./diffTreeEndpoint.types";
4
+ /** What a `DiffTreeHub` needs of a worker: a `Worker`, or anything shaped like one */
5
+ export interface IDiffTreeWorker {
6
+ postMessage(message: any, transfer: Transferable[]): void;
7
+ terminate(): void;
8
+ }
9
+ /**
10
+ * Creates an endpoint's tree, on its first start and each restart: a worker hosting the tree
11
+ * (`DiffTreeEndpoint.Host` in its entry), or the `ObservableDiffTree` itself, to run on this thread. What it
12
+ * returns decides the key's kind, and a key's factory always returns the same kind.
13
+ */
14
+ export type TreeFactory = () => IDiffTreeWorker | ObservableDiffTree;
15
+ /** Where a key's tree runs: in a worker (`"worker"`), or on this thread (`"local"`) */
16
+ export type TreeKind = "worker" | "local";
17
+ /** `DiffTreeHub.Register` options */
18
+ export interface IRegisterOptions {
19
+ /**
20
+ * Milliseconds to keep the tree after its last lease ends; a lease opened meanwhile keeps the
21
+ * same tree. Omitted, the tree is released at once.
22
+ */
23
+ releaseDebounce?: number;
24
+ }
25
+ /**
26
+ * Owns the app's trees by endpoint: one key is one tree in the app's dependency graph, running in a
27
+ * worker or on this thread. The hub creates each one from its factory when it's first needed, hands
28
+ * out leases on it, connects each tree to the ones it follows, re-creates one on request, and ends
29
+ * each one when its last lease ends. It never carries data: posts go directly between each port's
30
+ * two ends.
31
+ *
32
+ * - **`Register(endpoint, factory)`** records how to create a tree, and takes the trees it follows
33
+ * from the endpoint's definition (`DiffTreeEndpoint.Define`), each registered before it. It creates
34
+ * nothing.
35
+ * - **The factory returns a worker or a tree.** A worker hosts its own tree (`DiffTreeEndpoint.Host` in its
36
+ * entry). A returned `ObservableDiffTree` runs on this thread: the hub hosts it with the same call,
37
+ * on a channel of its own. Either way, the hub manages it the same way. What the first start
38
+ * returns fixes the key's kind.
39
+ * - **`Open`** leases a worker's tree, as a port, for a `StoreAsync`. **`OpenLocal`** leases a tree on
40
+ * this thread, as the tree, for a `StoreSync`. The leases are different types, so a store given the
41
+ * wrong one doesn't compile, and each throws for a key of the other kind: that's configuration to
42
+ * fix, not a state to recover from. The first lease starts the tree; a store owns its lease and
43
+ * releases it when it's destroyed. Any number of stores can be opened on one tree.
44
+ * - **A tree is released when its last lease ends**, at once or after its `releaseDebounce`. A tree
45
+ * that must outlive every view is held by a store the app keeps.
46
+ * - **A tree that follows others** holds a lease on each while it runs, and its `OnConnect` handlers
47
+ * (`DiffTreeEndpoint.Connect`) get a port to each source's tree, again whenever either restarts.
48
+ * - **`Restart`** creates the tree again from its factory. The tree is the source of truth: every
49
+ * store on it starts over with the new tree, as when it first subscribed, and nothing a store
50
+ * wrote is written back. State that must outlive a restart belongs in a tree of its own that the
51
+ * restarted one follows.
52
+ *
53
+ * @example
54
+ * const hub = new DiffTreeHub();
55
+ * hub.Register(ledgerEndpoint, () => new Worker(new URL("./ledger-worker.ts", import.meta.url), { type: "module" }));
56
+ * hub.Register(totalsEndpoint, () => new Worker(new URL("./totals-worker.ts", import.meta.url), { type: "module" }), { releaseDebounce: 5_000 });
57
+ * hub.Register(budgetEndpoint, newBudgetTree); // follows the ledger, on this thread
58
+ *
59
+ * const totals = new StoreAsync(hub.Open("totals")); // starts totals, and the ledger for it
60
+ * const budget = new StoreSync(hub.OpenLocal("budget"));
61
+ * // teardown: each store releases its lease
62
+ * totals.Destroy();
63
+ * budget.Destroy();
64
+ */
65
+ export declare class DiffTreeHub {
66
+ private entries;
67
+ /**
68
+ * Records how to create an endpoint's tree. Creates nothing: the first lease does.
69
+ * @param endpoint The endpoint's definition: its name, and the endpoints it follows (`sources`),
70
+ * each registered first.
71
+ * @param factory Returns a worker hosting the tree, or the tree itself, to run on this thread; runs
72
+ * on every start.
73
+ * @param options `releaseDebounce`: see `IRegisterOptions`.
74
+ * @throws When the name is already registered, or a source isn't registered yet.
75
+ */
76
+ Register(endpoint: IDiffTreeEndpoint, factory: TreeFactory, options?: IRegisterOptions): void;
77
+ /**
78
+ * Opens a lease on a worker's tree, starting the worker when it isn't running. Give it to a
79
+ * `StoreAsync`.
80
+ * @throws When the name isn't registered, or its factory returns a tree to run on this thread.
81
+ */
82
+ Open(name: string): IWorkerLease;
83
+ /**
84
+ * Opens a lease on a tree on this thread, creating the tree when it isn't running. Give it to a
85
+ * `StoreSync`.
86
+ * @throws When the name isn't registered, or its factory returns a worker.
87
+ */
88
+ OpenLocal(name: string): ILocalLease;
89
+ /**
90
+ * Ends a running tree and creates a new one from its factory, then re-issues every connection to
91
+ * it: each worker lease gets a new port (`IWorkerLease.OnPort`), each tree lease the new tree
92
+ * (`ILocalLease.OnTree`), each tree following it a new `jt-connect`, and the new tree new ports to
93
+ * its own sources. The stores on it start over with the new tree. Does nothing when the tree isn't
94
+ * running.
95
+ * @throws When the name isn't registered, or the factory returns the other kind than it did before,
96
+ * or neither kind. Nothing is recovered: the tree stays ended.
97
+ */
98
+ Restart(name: string): void;
99
+ /**
100
+ * Ends the tree if it runs, ends every lease on it, releases its sources, and forgets it. `Open`
101
+ * on the name throws after.
102
+ * @throws When a registered endpoint follows it: unregister that one first.
103
+ */
104
+ Unregister(name: string): void;
105
+ /**
106
+ * Unregisters every name, the last registered first, so each tree goes before its sources: every
107
+ * tree is ended and every lease with it.
108
+ */
109
+ Destroy(): void;
110
+ /** Whether a registered tree is running */
111
+ IsRunning(name: string): boolean;
112
+ /** Where a key's tree runs, known from its first start; `undefined` before it */
113
+ KindOf(name: string): TreeKind | undefined;
114
+ private Entry;
115
+ /**
116
+ * Keeps a tree running for a new lease, starting it when it isn't. With a `kind`, a tree of the
117
+ * other kind throws; one started for this check ends first.
118
+ */
119
+ private Run;
120
+ /** Throws when the key is known to be of the other kind */
121
+ private Expect;
122
+ private Start;
123
+ /** Opens the leases a tree holds on its sources, each one a port the hub gives it */
124
+ private Hold;
125
+ /**
126
+ * Runs the factory, checks what it returned, and wires it up to match: a worker as it is, a tree
127
+ * hosted on a channel of its own on this thread. Then tells it its sources, before anything else
128
+ * the hub sends.
129
+ */
130
+ private Create;
131
+ /** Serves a new channel for the lease on its tree, and gives the other end to its holder */
132
+ private Issue;
133
+ /** Ends a lease, and releases its tree when it was the last */
134
+ private Release;
135
+ /** Ends a lease without releasing its tree */
136
+ private End;
137
+ /** Ends a tree and releases the leases it holds */
138
+ private Stop;
139
+ }
@@ -0,0 +1,346 @@
1
+ import { ObservableDiffTree } from "./observableDiffTree";
2
+ import { DiffTreeEndpoint } from "./diffTreeEndpoint";
3
+ let nextServeId = 1;
4
+ class Lease {
5
+ constructor(entry, release) {
6
+ this.entry = entry;
7
+ this.release = release;
8
+ this.released = false;
9
+ }
10
+ get name() {
11
+ return this.entry.endpoint.name;
12
+ }
13
+ get endpoint() {
14
+ return this.entry.endpoint;
15
+ }
16
+ Release() {
17
+ this.release(this);
18
+ }
19
+ }
20
+ /** A port to a tree, served by its `HostPublisher`: a store's lease on a worker, or a follower's on any tree */
21
+ class PortLease extends Lease {
22
+ /** @param holder The tree following this one over the port, for a lease the hub holds */
23
+ constructor(entry, holder, release) {
24
+ super(entry, release);
25
+ this.holder = holder;
26
+ this.serveId = 0;
27
+ this.callbacks = [];
28
+ }
29
+ OnPort(callback) {
30
+ return Add(this.callbacks, callback);
31
+ }
32
+ }
33
+ /** The tree itself, for a store on its thread */
34
+ class TreeLease extends Lease {
35
+ constructor() {
36
+ super(...arguments);
37
+ this.callbacks = [];
38
+ }
39
+ get tree() {
40
+ return this.entry.tree;
41
+ }
42
+ OnTree(callback) {
43
+ return Add(this.callbacks, callback);
44
+ }
45
+ }
46
+ /**
47
+ * Owns the app's trees by endpoint: one key is one tree in the app's dependency graph, running in a
48
+ * worker or on this thread. The hub creates each one from its factory when it's first needed, hands
49
+ * out leases on it, connects each tree to the ones it follows, re-creates one on request, and ends
50
+ * each one when its last lease ends. It never carries data: posts go directly between each port's
51
+ * two ends.
52
+ *
53
+ * - **`Register(endpoint, factory)`** records how to create a tree, and takes the trees it follows
54
+ * from the endpoint's definition (`DiffTreeEndpoint.Define`), each registered before it. It creates
55
+ * nothing.
56
+ * - **The factory returns a worker or a tree.** A worker hosts its own tree (`DiffTreeEndpoint.Host` in its
57
+ * entry). A returned `ObservableDiffTree` runs on this thread: the hub hosts it with the same call,
58
+ * on a channel of its own. Either way, the hub manages it the same way. What the first start
59
+ * returns fixes the key's kind.
60
+ * - **`Open`** leases a worker's tree, as a port, for a `StoreAsync`. **`OpenLocal`** leases a tree on
61
+ * this thread, as the tree, for a `StoreSync`. The leases are different types, so a store given the
62
+ * wrong one doesn't compile, and each throws for a key of the other kind: that's configuration to
63
+ * fix, not a state to recover from. The first lease starts the tree; a store owns its lease and
64
+ * releases it when it's destroyed. Any number of stores can be opened on one tree.
65
+ * - **A tree is released when its last lease ends**, at once or after its `releaseDebounce`. A tree
66
+ * that must outlive every view is held by a store the app keeps.
67
+ * - **A tree that follows others** holds a lease on each while it runs, and its `OnConnect` handlers
68
+ * (`DiffTreeEndpoint.Connect`) get a port to each source's tree, again whenever either restarts.
69
+ * - **`Restart`** creates the tree again from its factory. The tree is the source of truth: every
70
+ * store on it starts over with the new tree, as when it first subscribed, and nothing a store
71
+ * wrote is written back. State that must outlive a restart belongs in a tree of its own that the
72
+ * restarted one follows.
73
+ *
74
+ * @example
75
+ * const hub = new DiffTreeHub();
76
+ * hub.Register(ledgerEndpoint, () => new Worker(new URL("./ledger-worker.ts", import.meta.url), { type: "module" }));
77
+ * hub.Register(totalsEndpoint, () => new Worker(new URL("./totals-worker.ts", import.meta.url), { type: "module" }), { releaseDebounce: 5_000 });
78
+ * hub.Register(budgetEndpoint, newBudgetTree); // follows the ledger, on this thread
79
+ *
80
+ * const totals = new StoreAsync(hub.Open("totals")); // starts totals, and the ledger for it
81
+ * const budget = new StoreSync(hub.OpenLocal("budget"));
82
+ * // teardown: each store releases its lease
83
+ * totals.Destroy();
84
+ * budget.Destroy();
85
+ */
86
+ export class DiffTreeHub {
87
+ constructor() {
88
+ this.entries = new Map();
89
+ }
90
+ /**
91
+ * Records how to create an endpoint's tree. Creates nothing: the first lease does.
92
+ * @param endpoint The endpoint's definition: its name, and the endpoints it follows (`sources`),
93
+ * each registered first.
94
+ * @param factory Returns a worker hosting the tree, or the tree itself, to run on this thread; runs
95
+ * on every start.
96
+ * @param options `releaseDebounce`: see `IRegisterOptions`.
97
+ * @throws When the name is already registered, or a source isn't registered yet.
98
+ */
99
+ Register(endpoint, factory, options = {}) {
100
+ const name = endpoint.name;
101
+ if (this.entries.has(name))
102
+ throw new Error(`DiffTreeHub: ${name} is already registered.`);
103
+ for (const source of endpoint.sources)
104
+ if (!this.entries.has(source))
105
+ throw new Error(`DiffTreeHub: ${name} follows ${source}, which isn't registered; register an endpoint's sources before it.`);
106
+ this.entries.set(name, { endpoint, factory, options, leases: new Set(), held: new Map() });
107
+ }
108
+ /**
109
+ * Opens a lease on a worker's tree, starting the worker when it isn't running. Give it to a
110
+ * `StoreAsync`.
111
+ * @throws When the name isn't registered, or its factory returns a tree to run on this thread.
112
+ */
113
+ Open(name) {
114
+ const entry = this.Entry(name);
115
+ this.Run(entry, "worker");
116
+ const lease = new PortLease(entry, undefined, lease => this.Release(lease));
117
+ entry.leases.add(lease);
118
+ this.Issue(lease);
119
+ return lease;
120
+ }
121
+ /**
122
+ * Opens a lease on a tree on this thread, creating the tree when it isn't running. Give it to a
123
+ * `StoreSync`.
124
+ * @throws When the name isn't registered, or its factory returns a worker.
125
+ */
126
+ OpenLocal(name) {
127
+ const entry = this.Entry(name);
128
+ this.Run(entry, "local");
129
+ const lease = new TreeLease(entry, lease => this.Release(lease));
130
+ entry.leases.add(lease);
131
+ return lease;
132
+ }
133
+ /**
134
+ * Ends a running tree and creates a new one from its factory, then re-issues every connection to
135
+ * it: each worker lease gets a new port (`IWorkerLease.OnPort`), each tree lease the new tree
136
+ * (`ILocalLease.OnTree`), each tree following it a new `jt-connect`, and the new tree new ports to
137
+ * its own sources. The stores on it start over with the new tree. Does nothing when the tree isn't
138
+ * running.
139
+ * @throws When the name isn't registered, or the factory returns the other kind than it did before,
140
+ * or neither kind. Nothing is recovered: the tree stays ended.
141
+ */
142
+ Restart(name) {
143
+ const entry = this.Entry(name);
144
+ if (!entry.worker)
145
+ return;
146
+ entry.worker.terminate();
147
+ entry.worker = undefined;
148
+ entry.tree = undefined;
149
+ this.Create(entry);
150
+ // New leases on the sources first, so releasing the old ones doesn't release a source
151
+ const old = Array.from(entry.held.values());
152
+ entry.held = new Map();
153
+ this.Hold(entry);
154
+ old.forEach(lease => this.Release(lease));
155
+ for (const lease of entry.leases)
156
+ lease instanceof TreeLease ? lease.callbacks.slice().forEach(callback => callback(entry.tree)) : this.Issue(lease);
157
+ }
158
+ /**
159
+ * Ends the tree if it runs, ends every lease on it, releases its sources, and forgets it. `Open`
160
+ * on the name throws after.
161
+ * @throws When a registered endpoint follows it: unregister that one first.
162
+ */
163
+ Unregister(name) {
164
+ const entry = this.entries.get(name);
165
+ if (!entry)
166
+ return;
167
+ for (const other of this.entries.values())
168
+ if (other.endpoint.sources.includes(name))
169
+ throw new Error(`DiffTreeHub: ${other.endpoint.name} follows ${name}; unregister it first.`);
170
+ this.entries.delete(name);
171
+ for (const lease of Array.from(entry.leases))
172
+ this.End(lease);
173
+ this.Stop(entry);
174
+ }
175
+ /**
176
+ * Unregisters every name, the last registered first, so each tree goes before its sources: every
177
+ * tree is ended and every lease with it.
178
+ */
179
+ Destroy() {
180
+ for (const name of Array.from(this.entries.keys()).reverse())
181
+ this.Unregister(name);
182
+ }
183
+ /** Whether a registered tree is running */
184
+ IsRunning(name) {
185
+ return !!this.entries.get(name)?.worker;
186
+ }
187
+ /** Where a key's tree runs, known from its first start; `undefined` before it */
188
+ KindOf(name) {
189
+ return this.entries.get(name)?.kind;
190
+ }
191
+ Entry(name) {
192
+ const entry = this.entries.get(name);
193
+ if (!entry)
194
+ throw new Error(`DiffTreeHub: ${name} isn't registered.`);
195
+ return entry;
196
+ }
197
+ /**
198
+ * Keeps a tree running for a new lease, starting it when it isn't. With a `kind`, a tree of the
199
+ * other kind throws; one started for this check ends first.
200
+ */
201
+ Run(entry, kind) {
202
+ kind && this.Expect(entry, kind);
203
+ clearTimeout(entry.timer);
204
+ entry.timer = undefined;
205
+ if (entry.worker)
206
+ return;
207
+ this.Start(entry);
208
+ if (kind && entry.kind !== kind) {
209
+ entry.leases.size === 0 && this.Stop(entry);
210
+ this.Expect(entry, kind);
211
+ }
212
+ }
213
+ /** Throws when the key is known to be of the other kind */
214
+ Expect(entry, kind) {
215
+ if (entry.kind && entry.kind !== kind)
216
+ throw new Error(`DiffTreeHub: ${entry.endpoint.name} is ${Describe(entry.kind)}; open it with ${entry.kind === "local" ? "OpenLocal" : "Open"}.`);
217
+ }
218
+ Start(entry) {
219
+ this.Create(entry);
220
+ this.Hold(entry);
221
+ }
222
+ /** Opens the leases a tree holds on its sources, each one a port the hub gives it */
223
+ Hold(entry) {
224
+ for (const source of entry.endpoint.sources) {
225
+ const sourceEntry = this.Entry(source);
226
+ this.Run(sourceEntry);
227
+ const lease = new PortLease(sourceEntry, entry, lease => this.Release(lease));
228
+ sourceEntry.leases.add(lease);
229
+ entry.held.set(source, lease);
230
+ this.Issue(lease);
231
+ }
232
+ }
233
+ /**
234
+ * Runs the factory, checks what it returned, and wires it up to match: a worker as it is, a tree
235
+ * hosted on a channel of its own on this thread. Then tells it its sources, before anything else
236
+ * the hub sends.
237
+ */
238
+ Create(entry) {
239
+ const name = entry.endpoint.name;
240
+ const result = entry.factory();
241
+ const kind = result instanceof ObservableDiffTree ? "local" : IsWorker(result) ? "worker" : undefined;
242
+ if (!kind)
243
+ throw new Error(`DiffTreeHub: ${name}'s factory returned neither a worker nor an ObservableDiffTree (an ObservableDiffTree from another copy of j-templates isn't one).`);
244
+ if (entry.kind && entry.kind !== kind) {
245
+ kind === "worker" && result.terminate();
246
+ throw new Error(`DiffTreeHub: ${name}'s factory returned ${Describe(kind)} after ${Describe(entry.kind)}; a key's factory always returns one kind.`);
247
+ }
248
+ entry.kind = kind;
249
+ if (kind === "worker")
250
+ entry.worker = result;
251
+ else {
252
+ entry.tree = result;
253
+ entry.worker = HostLocal(entry.endpoint, entry.tree);
254
+ }
255
+ const sources = { type: "jt-sources", sources: entry.endpoint.sources };
256
+ entry.worker.postMessage(sources, []);
257
+ }
258
+ /** Serves a new channel for the lease on its tree, and gives the other end to its holder */
259
+ Issue(lease) {
260
+ const { port1, port2 } = new MessageChannel();
261
+ lease.serveId = nextServeId++;
262
+ const serve = { type: "jt-serve", id: lease.serveId, port: port2 };
263
+ lease.entry.worker.postMessage(serve, [port2]);
264
+ if (lease.holder) {
265
+ const connect = { type: "jt-connect", source: lease.name, port: port1 };
266
+ lease.holder.worker.postMessage(connect, [port1]);
267
+ return;
268
+ }
269
+ const old = lease.port;
270
+ lease.port = port1;
271
+ if (old) {
272
+ old.close();
273
+ for (const callback of lease.callbacks.slice())
274
+ callback(port1);
275
+ }
276
+ }
277
+ /** Ends a lease, and releases its tree when it was the last */
278
+ Release(lease) {
279
+ if (lease.released)
280
+ return;
281
+ this.End(lease);
282
+ const entry = lease.entry;
283
+ if (entry.leases.size > 0 || !entry.worker || !this.entries.has(entry.endpoint.name))
284
+ return;
285
+ const debounce = entry.options.releaseDebounce;
286
+ if (debounce !== undefined && debounce > 0)
287
+ entry.timer = setTimeout(() => this.Stop(entry), debounce);
288
+ else
289
+ this.Stop(entry);
290
+ }
291
+ /** Ends a lease without releasing its tree */
292
+ End(lease) {
293
+ if (lease.released)
294
+ return;
295
+ lease.released = true;
296
+ lease.entry.leases.delete(lease);
297
+ lease.callbacks = [];
298
+ if (lease instanceof PortLease) {
299
+ const unserve = { type: "jt-unserve", id: lease.serveId };
300
+ lease.entry.worker?.postMessage(unserve, []);
301
+ lease.port?.close();
302
+ }
303
+ }
304
+ /** Ends a tree and releases the leases it holds */
305
+ Stop(entry) {
306
+ clearTimeout(entry.timer);
307
+ entry.timer = undefined;
308
+ entry.worker?.terminate();
309
+ entry.worker = undefined;
310
+ entry.tree = undefined;
311
+ const held = Array.from(entry.held.values());
312
+ entry.held.clear();
313
+ held.forEach(lease => this.Release(lease));
314
+ }
315
+ }
316
+ /**
317
+ * Hosts a tree on this thread as a worker hosts its own: `DiffTreeEndpoint.Host` on one end of a
318
+ * `MessageChannel`, with the other end as the hub's "worker". Terminating it stops the host and
319
+ * closes both ends.
320
+ */
321
+ function HostLocal(endpoint, tree) {
322
+ const { port1, port2 } = new MessageChannel();
323
+ const stop = DiffTreeEndpoint.Host(endpoint, tree, port2);
324
+ port2.start();
325
+ return {
326
+ postMessage: (message, transfer) => port1.postMessage(message, transfer),
327
+ terminate: () => {
328
+ stop();
329
+ port1.close();
330
+ port2.close();
331
+ }
332
+ };
333
+ }
334
+ function IsWorker(value) {
335
+ return typeof value?.postMessage === "function" && typeof value?.terminate === "function";
336
+ }
337
+ function Describe(kind) {
338
+ return kind === "local" ? "a tree on this thread" : "a worker";
339
+ }
340
+ function Add(callbacks, callback) {
341
+ callbacks.push(callback);
342
+ return () => {
343
+ const index = callbacks.indexOf(callback);
344
+ index >= 0 && callbacks.splice(index, 1);
345
+ };
346
+ }
@@ -0,0 +1,39 @@
1
+ import { ObservableDiffTree } from "./observableDiffTree";
2
+ import { IChannel } from "./diffTree.types";
3
+ /** A `HostPublisher`: the channels it serves */
4
+ export interface IHost {
5
+ /**
6
+ * Serves another channel, such as a `MessagePort` the worker was given.
7
+ * @returns A function that stops serving it, ends its subscriptions, and closes it.
8
+ */
9
+ Serve(channel: IChannel): () => void;
10
+ /** Stops serving every channel and ends their subscriptions */
11
+ Stop(): void;
12
+ }
13
+ /**
14
+ * Serves an `ObservableDiffTree` over channels: the worker's own (`self`) and any `MessagePort` given
15
+ * to `Serve`. On each channel it answers the framework's `jt-` messages and leaves every other
16
+ * message to the app's own listeners; it never takes `onmessage`.
17
+ *
18
+ * - `jt-batch`: runs the calls, then projections, commits, and posts `jt-ack` with the batch's id
19
+ * after the commit's posts. When a call throws, the calls after it and the projections are
20
+ * skipped, what the batch applied is still committed, and the ack carries the error: only that
21
+ * batch fails.
22
+ * - `jt-subscribe` / `jt-unsubscribe`: a subscription for the channel. Its first state and every
23
+ * commit are posted as `jt-post`, then `jt-subscribed` once the first state is out.
24
+ * - `jt-disconnect`: ends every subscription on the channel.
25
+ * - `jt-markopaque`: see `DiffTree.MarkOpaque`.
26
+ * - `jt-serve` / `jt-unserve`: from a `DiffTreeHub`, a lease's port to serve, or to stop serving.
27
+ *
28
+ * The worker's own work commits on the same publisher, and its commits reach every subscriber
29
+ * without a round trip.
30
+ *
31
+ * @example
32
+ * // worker entry
33
+ * const tree = new ObservableDiffTree({ keyFunc, routes });
34
+ * HostPublisher(tree, self);
35
+ * tree.WritePath("results", compute());
36
+ * tree.RunProjections();
37
+ * tree.Commit();
38
+ */
39
+ export declare function HostPublisher(publisher: ObservableDiffTree, scope: IChannel): IHost;
@@ -0,0 +1,151 @@
1
+ import { MESSAGE_PREFIX } from "./diffTree.types";
2
+ import { ListenForConnections } from "./onConnect";
3
+ /**
4
+ * Serves an `ObservableDiffTree` over channels: the worker's own (`self`) and any `MessagePort` given
5
+ * to `Serve`. On each channel it answers the framework's `jt-` messages and leaves every other
6
+ * message to the app's own listeners; it never takes `onmessage`.
7
+ *
8
+ * - `jt-batch`: runs the calls, then projections, commits, and posts `jt-ack` with the batch's id
9
+ * after the commit's posts. When a call throws, the calls after it and the projections are
10
+ * skipped, what the batch applied is still committed, and the ack carries the error: only that
11
+ * batch fails.
12
+ * - `jt-subscribe` / `jt-unsubscribe`: a subscription for the channel. Its first state and every
13
+ * commit are posted as `jt-post`, then `jt-subscribed` once the first state is out.
14
+ * - `jt-disconnect`: ends every subscription on the channel.
15
+ * - `jt-markopaque`: see `DiffTree.MarkOpaque`.
16
+ * - `jt-serve` / `jt-unserve`: from a `DiffTreeHub`, a lease's port to serve, or to stop serving.
17
+ *
18
+ * The worker's own work commits on the same publisher, and its commits reach every subscriber
19
+ * without a round trip.
20
+ *
21
+ * @example
22
+ * // worker entry
23
+ * const tree = new ObservableDiffTree({ keyFunc, routes });
24
+ * HostPublisher(tree, self);
25
+ * tree.WritePath("results", compute());
26
+ * tree.RunProjections();
27
+ * tree.Commit();
28
+ */
29
+ export function HostPublisher(publisher, scope) {
30
+ const served = new Map();
31
+ /** Lease ports a `DiffTreeHub` asked this host to serve, by id */
32
+ const leases = new Map();
33
+ function Serve(channel) {
34
+ const existing = served.get(channel);
35
+ if (existing)
36
+ return existing;
37
+ const subscriptions = new Map();
38
+ const listener = (event) => {
39
+ const message = event.data;
40
+ if (!IsRequest(message))
41
+ return;
42
+ switch (message.type) {
43
+ case "jt-batch": {
44
+ const error = RunBatch(publisher, message.calls);
45
+ Post(channel, error === undefined ? { type: "jt-ack", id: message.id } : { type: "jt-ack", id: message.id, error });
46
+ break;
47
+ }
48
+ case "jt-subscribe": {
49
+ const sub = message.sub;
50
+ let error;
51
+ try {
52
+ subscriptions.set(sub, publisher.Subscribe({ roots: message.roots, form: message.form, group: channel }, (delivery) => Post(channel, { type: "jt-post", sub, ...delivery })));
53
+ }
54
+ catch (err) {
55
+ error = err;
56
+ }
57
+ Post(channel, error === undefined ? { type: "jt-subscribed", sub } : { type: "jt-subscribed", sub, error });
58
+ break;
59
+ }
60
+ case "jt-unsubscribe":
61
+ subscriptions.get(message.sub)?.();
62
+ subscriptions.delete(message.sub);
63
+ break;
64
+ case "jt-disconnect":
65
+ publisher.Disconnect(channel);
66
+ subscriptions.clear();
67
+ break;
68
+ case "jt-markopaque":
69
+ publisher.MarkOpaque(message.keys);
70
+ break;
71
+ case "jt-serve":
72
+ leases.set(message.id, Serve(message.port));
73
+ break;
74
+ case "jt-unserve":
75
+ leases.get(message.id)?.();
76
+ leases.delete(message.id);
77
+ break;
78
+ }
79
+ };
80
+ channel.addEventListener("message", listener);
81
+ channel.start?.();
82
+ const stop = () => {
83
+ if (served.get(channel) !== stop)
84
+ return;
85
+ served.delete(channel);
86
+ channel.removeEventListener("message", listener);
87
+ publisher.Disconnect(channel);
88
+ subscriptions.clear();
89
+ channel !== scope && channel.close?.();
90
+ };
91
+ served.set(channel, stop);
92
+ return stop;
93
+ }
94
+ Serve(scope);
95
+ // A hub's jt-connect ports can arrive before the worker's OnConnect handlers are registered
96
+ ListenForConnections(scope);
97
+ return {
98
+ Serve,
99
+ Stop() {
100
+ for (const stop of Array.from(served.values()))
101
+ stop();
102
+ },
103
+ };
104
+ }
105
+ /**
106
+ * Applies a batch, runs projections and commits. A failed call skips the rest and the
107
+ * projections; what was applied is committed either way.
108
+ * @returns The batch's error, or undefined
109
+ */
110
+ function RunBatch(publisher, calls) {
111
+ let error;
112
+ try {
113
+ for (let x = 0; x < calls.length; x++)
114
+ RunMethod(publisher, calls[x]);
115
+ publisher.RunProjections();
116
+ }
117
+ catch (err) {
118
+ error = err;
119
+ }
120
+ try {
121
+ publisher.Commit();
122
+ }
123
+ catch (err) {
124
+ error ??= err;
125
+ }
126
+ return error;
127
+ }
128
+ function RunMethod(publisher, call) {
129
+ const args = call.arguments;
130
+ switch (call.method) {
131
+ case "writepath": return publisher.WritePath(args[0], args[1]);
132
+ case "splicepath": return publisher.SplicePath(args[0], args[1], args[2], args[3]);
133
+ case "pushpath": return publisher.PushPath(args[0], args[1]);
134
+ case "patchpath": return publisher.PatchPath(args[0], args[1]);
135
+ default: throw new Error(`Unknown ObservableDiffTree method: ${call.method}`);
136
+ }
137
+ }
138
+ function IsRequest(message) {
139
+ return typeof message?.type === "string" && message.type.startsWith(MESSAGE_PREFIX);
140
+ }
141
+ /** Posts a message; an `error` that can't be cloned is sent as its text */
142
+ function Post(channel, message) {
143
+ if (!("error" in message) || message.error === undefined)
144
+ return channel.postMessage(message);
145
+ try {
146
+ channel.postMessage(message);
147
+ }
148
+ catch {
149
+ channel.postMessage({ ...message, error: String(message.error) });
150
+ }
151
+ }
@@ -1,3 +1,6 @@
1
1
  export * from "./diffTree.types";
2
2
  export * from "./utils/diffTree.types";
3
3
  export { PROJECTION_PREFIX } from "./utils/paths";
4
+ export { FollowRoutes } from "./utils/routes";
5
+ export * from "./diffTreeEndpoint.types";
6
+ export { DiffTreeEndpoint } from "./diffTreeEndpoint";