j-templates 9.0.1 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SYNTAX_PRIMER.md +181 -101
- package/Store/Diff/channelClient.d.ts +106 -0
- package/Store/Diff/channelClient.js +228 -0
- package/Store/Diff/defaultDiffTreeWorker.js +3 -4
- package/Store/Diff/diffTree.d.ts +56 -0
- package/Store/Diff/diffTree.js +110 -4
- package/Store/Diff/diffTree.types.d.ts +191 -21
- package/Store/Diff/diffTree.types.js +2 -10
- package/Store/Diff/diffTreeEndpoint.d.ts +101 -0
- package/Store/Diff/diffTreeEndpoint.js +173 -0
- package/Store/Diff/diffTreeEndpoint.types.d.ts +71 -0
- package/Store/Diff/diffTreeEndpoint.types.js +1 -0
- package/Store/Diff/diffTreeHub.d.ts +139 -0
- package/Store/Diff/diffTreeHub.js +346 -0
- package/Store/Diff/hostPublisher.d.ts +39 -0
- package/Store/Diff/hostPublisher.js +151 -0
- package/Store/Diff/index.d.ts +3 -0
- package/Store/Diff/index.js +3 -0
- package/Store/Diff/observableDiffTree.d.ts +93 -0
- package/Store/Diff/observableDiffTree.js +199 -0
- package/Store/Diff/onConnect.d.ts +33 -0
- package/Store/Diff/onConnect.js +83 -0
- package/Store/Diff/utils/diffTree.types.d.ts +9 -0
- package/Store/Diff/utils/homes.d.ts +12 -0
- package/Store/Diff/utils/homes.js +40 -0
- package/Store/Diff/utils/routes.d.ts +18 -0
- package/Store/Diff/utils/routes.js +33 -2
- package/Store/Store/store.d.ts +36 -4
- package/Store/Store/store.js +47 -6
- package/Store/Store/storeAsync.d.ts +80 -25
- package/Store/Store/storeAsync.js +102 -33
- package/Store/Store/storeSync.d.ts +76 -11
- package/Store/Store/storeSync.js +105 -12
- package/Store/Tree/observableNode.js +68 -13
- package/Store/Tree/opaque.d.ts +30 -0
- package/Store/Tree/opaque.js +35 -0
- package/Store/Tree/snapshot.js +5 -4
- package/Store/index.d.ts +4 -1
- package/Store/index.js +1 -1
- package/Utils/decorators.d.ts +0 -98
- package/Utils/decorators.js +0 -156
- package/Utils/index.d.ts +2 -0
- package/Utils/index.js +1 -0
- package/Utils/subscriptionManager.d.ts +78 -0
- package/Utils/subscriptionManager.js +162 -0
- package/package.json +1 -1
- package/Store/Diff/diffTreeWorker.d.ts +0 -6
- package/Store/Diff/diffTreeWorker.js +0 -22
- package/Store/Diff/diffTreeWriter.d.ts +0 -92
- package/Store/Diff/diffTreeWriter.js +0 -137
- package/Store/Diff/workerQueue.d.ts +0 -7
- package/Store/Diff/workerQueue.js +0 -29
- package/Store/Store/pushStore.d.ts +0 -53
- package/Store/Store/pushStore.js +0 -77
|
@@ -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
|
+
}
|
package/Store/Diff/index.d.ts
CHANGED
|
@@ -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";
|