@net-mesh/browser 0.37.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/CHANGELOG.md +517 -0
- package/LICENSE-APACHE +190 -0
- package/LICENSE-MIT +21 -0
- package/README.md +666 -0
- package/dist/async-queue.d.ts +26 -0
- package/dist/async-queue.js +62 -0
- package/dist/credential.d.ts +70 -0
- package/dist/credential.js +95 -0
- package/dist/errors.d.ts +400 -0
- package/dist/errors.js +726 -0
- package/dist/events.d.ts +232 -0
- package/dist/events.js +298 -0
- package/dist/identity.d.ts +40 -0
- package/dist/identity.js +78 -0
- package/dist/index.bundle.js +2 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +46 -0
- package/dist/leader/events.d.ts +111 -0
- package/dist/leader/events.js +116 -0
- package/dist/leader/index.d.ts +14 -0
- package/dist/leader/index.js +11 -0
- package/dist/leader/session.d.ts +539 -0
- package/dist/leader/session.js +770 -0
- package/dist/leader/wasm.d.ts +123 -0
- package/dist/leader/wasm.js +39 -0
- package/dist/lobby.d.ts +183 -0
- package/dist/lobby.js +504 -0
- package/dist/local.d.ts +80 -0
- package/dist/local.js +160 -0
- package/dist/net_leaf.d.ts +1494 -0
- package/dist/net_leaf.js +3753 -0
- package/dist/net_leaf_bg.wasm +0 -0
- package/dist/net_leaf_bg.wasm.d.ts +137 -0
- package/dist/netcode/client.d.ts +102 -0
- package/dist/netcode/client.js +296 -0
- package/dist/netcode/clock.d.ts +45 -0
- package/dist/netcode/clock.js +54 -0
- package/dist/netcode/host.d.ts +118 -0
- package/dist/netcode/host.js +276 -0
- package/dist/netcode/index.d.ts +19 -0
- package/dist/netcode/index.js +14 -0
- package/dist/netcode/interpolate.d.ts +50 -0
- package/dist/netcode/interpolate.js +100 -0
- package/dist/netcode/wire.d.ts +125 -0
- package/dist/netcode/wire.js +117 -0
- package/dist/node.d.ts +908 -0
- package/dist/node.js +1085 -0
- package/dist/org.d.ts +412 -0
- package/dist/org.js +567 -0
- package/dist/peer-driver.d.ts +117 -0
- package/dist/peer-driver.js +224 -0
- package/dist/store/assembly.d.ts +176 -0
- package/dist/store/assembly.js +325 -0
- package/dist/store/chunker.d.ts +83 -0
- package/dist/store/chunker.js +190 -0
- package/dist/store/core.d.ts +136 -0
- package/dist/store/core.js +477 -0
- package/dist/store/definition.d.ts +23 -0
- package/dist/store/definition.js +83 -0
- package/dist/store/errors.d.ts +77 -0
- package/dist/store/errors.js +94 -0
- package/dist/store/host.d.ts +247 -0
- package/dist/store/host.js +675 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.js +40 -0
- package/dist/store/interest.d.ts +40 -0
- package/dist/store/interest.js +77 -0
- package/dist/store/inventory.d.ts +81 -0
- package/dist/store/inventory.js +140 -0
- package/dist/store/join.d.ts +118 -0
- package/dist/store/join.js +724 -0
- package/dist/store/json.d.ts +50 -0
- package/dist/store/json.js +283 -0
- package/dist/store/ledger.d.ts +187 -0
- package/dist/store/ledger.js +292 -0
- package/dist/store/owner.d.ts +616 -0
- package/dist/store/owner.js +1711 -0
- package/dist/store/patch.d.ts +70 -0
- package/dist/store/patch.js +166 -0
- package/dist/store/player.d.ts +65 -0
- package/dist/store/player.js +261 -0
- package/dist/store/replica.d.ts +187 -0
- package/dist/store/replica.js +740 -0
- package/dist/store/state.d.ts +42 -0
- package/dist/store/state.js +165 -0
- package/dist/store/types.d.ts +200 -0
- package/dist/store/types.js +25 -0
- package/dist/store/visibility.d.ts +118 -0
- package/dist/store/visibility.js +282 -0
- package/dist/store/wire.d.ts +309 -0
- package/dist/store/wire.js +834 -0
- package/dist/stream.d.ts +174 -0
- package/dist/stream.js +238 -0
- package/dist/three/index.d.ts +109 -0
- package/dist/three/index.js +130 -0
- package/dist/udp-probe.d.ts +159 -0
- package/dist/udp-probe.js +245 -0
- package/dist/wasm.d.ts +642 -0
- package/dist/wasm.js +98 -0
- package/dist/world/border.d.ts +84 -0
- package/dist/world/border.js +99 -0
- package/dist/world/handoff.d.ts +187 -0
- package/dist/world/handoff.js +222 -0
- package/dist/world/index.d.ts +13 -0
- package/dist/world/index.js +9 -0
- package/dist/world/link.d.ts +179 -0
- package/dist/world/link.js +476 -0
- package/dist/world/regions.d.ts +154 -0
- package/dist/world/regions.js +314 -0
- package/package.json +72 -0
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MeshSession` — one node per origin, whichever tab you opened.
|
|
3
|
+
*
|
|
4
|
+
* §8's decision is that a browser origin runs exactly one Net node:
|
|
5
|
+
* tabs contend for a Web Lock, the holder runs the node on the main
|
|
6
|
+
* thread, and the others attach over a `BroadcastChannel` and get the
|
|
7
|
+
* same API. This is that API in TypeScript.
|
|
8
|
+
*
|
|
9
|
+
* The difference from {@link BrowserNode} is one of scope, not of
|
|
10
|
+
* capability: `connect()` gives a page *this tab's* node, and
|
|
11
|
+
* `openSession()` gives it *the origin's* node. Everything a page can
|
|
12
|
+
* do with one it can do with the other — nRPC calls, channels,
|
|
13
|
+
* streams, announcements, queries, enrollment — and three methods are
|
|
14
|
+
* promises here that are synchronous there, because on a follower the
|
|
15
|
+
* answer lives in another tab:
|
|
16
|
+
*
|
|
17
|
+
* | | `BrowserNode` | `MeshSession` |
|
|
18
|
+
* |---|---|---|
|
|
19
|
+
* | `openStream` | synchronous | promise |
|
|
20
|
+
* | `counters` | synchronous | promise |
|
|
21
|
+
* | `isEnrolled` | synchronous | promise |
|
|
22
|
+
*
|
|
23
|
+
* Failures are the same taxonomy, re-typed by the same mapper, so a
|
|
24
|
+
* proxied refusal and a direct one arrive as the same class. The one
|
|
25
|
+
* addition is {@link NotLeaderError}: a tab that was superseded — a
|
|
26
|
+
* suspended tab that resumed, say — is refused rather than served, and
|
|
27
|
+
* that refusal is typed.
|
|
28
|
+
*/
|
|
29
|
+
import { fromWasmError, OrgCancelledError, OrgLeaderLostError, } from '../errors.js';
|
|
30
|
+
import { clientStreamOrgTrampoline, duplexOrgTrampoline, OrgDuplex, OrgHandleRegistry, OrgServe, OrgStream, OrgUpload, streamingOrgTrampoline, toWasmOrgCallOptions, toWasmOrgServeOptions, unaryOrgTrampoline, } from '../org.js';
|
|
31
|
+
import { buildConnectRequest, parseAttemptStatus, parseCounters, parseDescriptors, } from '../node.js';
|
|
32
|
+
import { acceptPeer as driveAccept, connectPeer as driveConnect, } from '../peer-driver.js';
|
|
33
|
+
import { LeafStream } from '../stream.js';
|
|
34
|
+
import { idArg, loadLeafWasm } from '../wasm.js';
|
|
35
|
+
import { isLifecycleEvent, parseSessionEvent, } from './events.js';
|
|
36
|
+
import { asSessionModule } from './wasm.js';
|
|
37
|
+
import { EventHub } from '../events.js';
|
|
38
|
+
/** The origin's Net node, as this tab sees it. */
|
|
39
|
+
export class MeshSession {
|
|
40
|
+
inner;
|
|
41
|
+
/**
|
|
42
|
+
* The same fan-out the direct surface uses, instantiated over the
|
|
43
|
+
* wider event union. One implementation, so a throwing listener,
|
|
44
|
+
* the never-unwind-into-Rust rule and the iterator buffering behave
|
|
45
|
+
* identically on both surfaces.
|
|
46
|
+
*/
|
|
47
|
+
hub = new EventHub();
|
|
48
|
+
/**
|
|
49
|
+
* Streams this session handed out, so the ones still open can be
|
|
50
|
+
* ended when leadership moves.
|
|
51
|
+
*
|
|
52
|
+
* A stream is session-scoped and is **not** restored across a
|
|
53
|
+
* leader change, which the Rust side enforces by binding each
|
|
54
|
+
* handle to its opening generation. That enforcement makes a stale
|
|
55
|
+
* `send` reject — but it says nothing about a consumer sitting in
|
|
56
|
+
* `await stream[Symbol.asyncIterator]().next()`, which would simply
|
|
57
|
+
* never settle: the wasm stream stops emitting and nothing tells
|
|
58
|
+
* the queue to end. So the session owns the set and closes it,
|
|
59
|
+
* which is what turns "no more data, ever" into the end of the
|
|
60
|
+
* iteration a page is already written to handle.
|
|
61
|
+
*/
|
|
62
|
+
streams = new Set();
|
|
63
|
+
/**
|
|
64
|
+
* The generation the streams in that set were opened under.
|
|
65
|
+
*
|
|
66
|
+
* Tracked because the generation moving is the *only* signal an
|
|
67
|
+
* abrupt leader disappearance gives. A page that crashes or
|
|
68
|
+
* navigates away never sends its `leader_lost` announcement: the
|
|
69
|
+
* promoted follower fails its own pending requests and reports
|
|
70
|
+
* `leader_changed`, and so does every other surviving follower
|
|
71
|
+
* when the successor's Leadership arrives. The Rust handle then
|
|
72
|
+
* correctly suppresses the old stream's bytes by its opening
|
|
73
|
+
* generation — which is precisely what leaves a consumer sitting
|
|
74
|
+
* in `await iterator.next()` forever, waiting on a stream that is
|
|
75
|
+
* now guaranteed never to emit again.
|
|
76
|
+
*/
|
|
77
|
+
streamGeneration;
|
|
78
|
+
/**
|
|
79
|
+
* The live org handles (plan §4.5), retired when leadership moves
|
|
80
|
+
* or the session closes: pending calls fail typed and each dropped
|
|
81
|
+
* caller handle emits exactly one CANCEL.
|
|
82
|
+
*/
|
|
83
|
+
orgHandles = new OrgHandleRegistry();
|
|
84
|
+
closed = false;
|
|
85
|
+
/** @internal — use {@link openSession}. */
|
|
86
|
+
constructor(inner) {
|
|
87
|
+
this.inner = inner;
|
|
88
|
+
this.streamGeneration = inner.generation();
|
|
89
|
+
inner.on_event((json) => this.hub.deliver(json, parseSessionEvent));
|
|
90
|
+
this.hub.onAny((event) => {
|
|
91
|
+
// `leader_lost` is this tab losing the leader it was talking
|
|
92
|
+
// to; `not_leader` is this tab discovering it *was* the leader
|
|
93
|
+
// and is not any more. Either way every stream this session
|
|
94
|
+
// handed out belonged to a node that is gone — and neither
|
|
95
|
+
// moves this tab's generation, so both are named explicitly.
|
|
96
|
+
if (event.type === 'leader_lost' || event.type === 'not_leader') {
|
|
97
|
+
// The generation that owned the calls is the one they were
|
|
98
|
+
// issued under — typed `LeaderLost`, never resumed.
|
|
99
|
+
this.orgHandles.retireAll(new OrgLeaderLostError(this.streamGeneration));
|
|
100
|
+
this.endStreams();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// And the abrupt case: a generation that moved under us. Read
|
|
104
|
+
// off the session rather than out of the event, so a
|
|
105
|
+
// `leader_changed` that merely repeats the generation this
|
|
106
|
+
// tab already holds ends nothing — a duplicate notification
|
|
107
|
+
// must not kill the streams the new generation just opened.
|
|
108
|
+
const current = this.inner.generation();
|
|
109
|
+
if (current === this.streamGeneration)
|
|
110
|
+
return;
|
|
111
|
+
const previous = this.streamGeneration;
|
|
112
|
+
this.streamGeneration = current;
|
|
113
|
+
this.orgHandles.retireAll(new OrgLeaderLostError(previous));
|
|
114
|
+
this.endStreams();
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** `'leader'` if this tab runs the node, `'follower'` if another does. */
|
|
118
|
+
role() {
|
|
119
|
+
return this.inner.role() === 'leader' ? 'leader' : 'follower';
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The generation in force for this tab, as an exact decimal string.
|
|
123
|
+
*
|
|
124
|
+
* A string because it is a `u64`: a number would round above 2^53,
|
|
125
|
+
* and a rounded generation is a fence that has stopped fencing.
|
|
126
|
+
*/
|
|
127
|
+
generation() {
|
|
128
|
+
return this.inner.generation();
|
|
129
|
+
}
|
|
130
|
+
/** The node's id, 16 lowercase hex digits, or `null` before one is known. */
|
|
131
|
+
nodeIdHex() {
|
|
132
|
+
return this.inner.node_id_hex() ?? null;
|
|
133
|
+
}
|
|
134
|
+
/** The identity fingerprint the Web Lock is named after. */
|
|
135
|
+
fingerprint() {
|
|
136
|
+
return this.inner.fingerprint();
|
|
137
|
+
}
|
|
138
|
+
/** The Web Lock and `BroadcastChannel` name. */
|
|
139
|
+
scope() {
|
|
140
|
+
return this.inner.scope();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* How long this tab took to take over, in milliseconds, or `null` if
|
|
144
|
+
* it was the origin's first leader.
|
|
145
|
+
*
|
|
146
|
+
* Measured from the lock coming free to the node being re-bootstrapped
|
|
147
|
+
* and the subscriptions and announcement restored. It is a
|
|
148
|
+
* measurement, not a promise: the interruption a *peer* observes is
|
|
149
|
+
* bounded by Net's own failure detection, which is a different and
|
|
150
|
+
* much larger number.
|
|
151
|
+
*/
|
|
152
|
+
interruptionMs() {
|
|
153
|
+
return this.inner.interruption_ms() ?? null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Call an nRPC service.
|
|
157
|
+
*
|
|
158
|
+
* Rejects with {@link RpcError} — including `leader-lost` when the
|
|
159
|
+
* tab running the node was replaced while the call was in flight.
|
|
160
|
+
* Never a silent retry: §8's rule is that the caller is told and
|
|
161
|
+
* decides.
|
|
162
|
+
*
|
|
163
|
+
* On a follower the deadline is armed here as well as on the
|
|
164
|
+
* leader, and that is true whether or not `timeoutMs` was given:
|
|
165
|
+
* omitting it takes the leaf's own 30 s default rather than
|
|
166
|
+
* leaving the promise to a leader that may be frozen. Either way
|
|
167
|
+
* expiry is `rpc-indeterminate` — the work may already have been
|
|
168
|
+
* admitted in the other tab, and nothing here re-issues it.
|
|
169
|
+
*/
|
|
170
|
+
async call(service, payload, timeoutMs) {
|
|
171
|
+
return await this.guard(() => this.inner.call(service, payload, timeoutMs));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Subscribe to a channel.
|
|
175
|
+
*
|
|
176
|
+
* Also adds it to what a future leader restores, so a handoff does
|
|
177
|
+
* not silently drop it.
|
|
178
|
+
*/
|
|
179
|
+
async subscribe(channel) {
|
|
180
|
+
await this.guard(() => this.inner.subscribe(channel));
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Release this tab's claim on a channel.
|
|
184
|
+
*
|
|
185
|
+
* Not `subscribe` run backwards. The origin runs one node and the
|
|
186
|
+
* leader holds **one** membership for the union of every tab's
|
|
187
|
+
* declarations, so this drops this tab's claim and the membership
|
|
188
|
+
* itself is given up only when no other tab still wants the
|
|
189
|
+
* channel. One tab closing a subscription therefore cannot stop
|
|
190
|
+
* another tab that is still reading the same channel.
|
|
191
|
+
*
|
|
192
|
+
* Also removes it from what a future leader restores, so a handoff
|
|
193
|
+
* does not silently bring back a channel this tab released.
|
|
194
|
+
*/
|
|
195
|
+
async unsubscribe(channel) {
|
|
196
|
+
await this.guard(() => this.inner.unsubscribe(channel));
|
|
197
|
+
}
|
|
198
|
+
/** Publish one payload to a channel. */
|
|
199
|
+
async publish(channel, payload) {
|
|
200
|
+
await this.guard(() => this.inner.publish(channel, payload));
|
|
201
|
+
}
|
|
202
|
+
/** Publish this node's capability announcement. */
|
|
203
|
+
async announce(capabilities) {
|
|
204
|
+
await this.guard(() => this.inner.announce([...capabilities]));
|
|
205
|
+
}
|
|
206
|
+
/** Find the nodes offering a capability. */
|
|
207
|
+
async query(capability) {
|
|
208
|
+
return parseDescriptors(await this.guard(() => this.inner.query(capability)));
|
|
209
|
+
}
|
|
210
|
+
/** Every leaf counter. Values stay strings because they are `u64`. */
|
|
211
|
+
async counters() {
|
|
212
|
+
return parseCounters(await this.guard(() => this.inner.counters_json()));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Run the enrollment exchange if the node is not enrolled.
|
|
216
|
+
*
|
|
217
|
+
* Works on a follower too: one node per origin means one enrollment
|
|
218
|
+
* per origin, so a follower asking to enroll is asking the only
|
|
219
|
+
* session there is.
|
|
220
|
+
*/
|
|
221
|
+
async enroll() {
|
|
222
|
+
await this.guard(() => this.inner.enroll());
|
|
223
|
+
}
|
|
224
|
+
/** Whether the anchor has admitted this node. */
|
|
225
|
+
async isEnrolled() {
|
|
226
|
+
return await this.guard(() => this.inner.is_enrolled());
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Sign and send a `0x0D02` signalling envelope — the
|
|
230
|
+
* session-independent path, so no session with `peer` is needed.
|
|
231
|
+
*/
|
|
232
|
+
async signal(peerHex, dialogHex, kind, payload) {
|
|
233
|
+
// The same id marshaling `BrowserNode.signal` applies (finding
|
|
234
|
+
// #52): strings verbatim, a non-string refused by the parsers'
|
|
235
|
+
// names — peer first — before the wasm seam.
|
|
236
|
+
await this.guard(() => this.inner.signal(idArg(peerHex, 'peer'), idArg(dialogHex, 'dialog'), kind, payload));
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Connect directly to `nodeIdHex` (§9), wherever the node is.
|
|
240
|
+
*
|
|
241
|
+
* The same drive loop `BrowserNode.connectPeer` runs — literally
|
|
242
|
+
* the same function, from `peer-driver.ts`, with this session's
|
|
243
|
+
* primitives instead of the direct node's. One loop means the
|
|
244
|
+
* offerer and the answerer cannot disagree about what supersession
|
|
245
|
+
* or a terminal reading looks like, which is a disagreement neither
|
|
246
|
+
* side could detect.
|
|
247
|
+
*
|
|
248
|
+
* On a follower every step is a proxy round trip and the attempt
|
|
249
|
+
* runs on the leader tab's node. A step whose attempt was replaced
|
|
250
|
+
* while the request was in flight is refused by the leaf before it
|
|
251
|
+
* touches the replacement, and arrives here as `superseded`.
|
|
252
|
+
*
|
|
253
|
+
* **Idempotent on a healthy pair**, as `BrowserNode.connectPeer` is: an
|
|
254
|
+
* already-direct, open pair resolves `direct` with its live dialog and
|
|
255
|
+
* nothing is offered — a second offer would replace the working link
|
|
256
|
+
* and close it under the peer. A leader too old to answer that reading
|
|
257
|
+
* is asked for an offer, as before.
|
|
258
|
+
*/
|
|
259
|
+
async connectPeer(nodeIdHex) {
|
|
260
|
+
let settled;
|
|
261
|
+
try {
|
|
262
|
+
settled = (await this.inner.peer_direct_dialog(nodeIdHex)) ?? undefined;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// A closed session is refused by the offer below, typed; an older
|
|
266
|
+
// leader simply has no such reading.
|
|
267
|
+
settled = undefined;
|
|
268
|
+
}
|
|
269
|
+
if (settled !== undefined)
|
|
270
|
+
return { type: 'direct', peer: nodeIdHex, dialog: settled };
|
|
271
|
+
return driveConnect(nodeIdHex, this.peerPrimitives(), parseAttemptStatus);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Answer the offer `nodeIdHex` filed — {@link connectPeer}'s
|
|
275
|
+
* counterpart, on whichever tab holds the lock.
|
|
276
|
+
*/
|
|
277
|
+
async acceptPeer(nodeIdHex) {
|
|
278
|
+
return driveAccept(nodeIdHex, this.peerPrimitives(), parseAttemptStatus);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The proxied primitives, each re-typed by the same mapper every
|
|
282
|
+
* other method here uses, so a proxied refusal is the same class a
|
|
283
|
+
* direct one is — which is what lets one drive loop classify both.
|
|
284
|
+
*/
|
|
285
|
+
peerPrimitives() {
|
|
286
|
+
return {
|
|
287
|
+
offer: (peer) => this.guard(() => this.inner.peer_offer(peer)),
|
|
288
|
+
acceptOffer: (peer) => this.guard(() => this.inner.peer_accept_offer(peer)),
|
|
289
|
+
candidate: (peer, dialog) => this.guard(() => this.inner.peer_candidate(peer, dialog)),
|
|
290
|
+
handshake: (peer, dialog) => this.guard(() => this.inner.peer_handshake(peer, dialog)),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Open a reliable or fire-and-forget stream.
|
|
295
|
+
*
|
|
296
|
+
* A promise where {@link BrowserNode.openStream} is synchronous: on
|
|
297
|
+
* a follower the stream is opened by the tab that owns the
|
|
298
|
+
* DataChannel. The object it resolves to is the same
|
|
299
|
+
* {@link LeafStream}, so `await stream.send(bytes)` and
|
|
300
|
+
* `for await (const payload of stream)` read the same either way.
|
|
301
|
+
*
|
|
302
|
+
* A stream is **not** restored across a leader change — it is
|
|
303
|
+
* session-scoped, and pretending otherwise would hide a real
|
|
304
|
+
* interruption. A page that wants one back opens one. What this
|
|
305
|
+
* session does guarantee is that the interruption is *observable*:
|
|
306
|
+
* the handle is retained here and ended when leadership moves, so
|
|
307
|
+
* `for await (const payload of stream)` finishes instead of hanging
|
|
308
|
+
* on a node that is gone.
|
|
309
|
+
*
|
|
310
|
+
* That guarantee has to cover the open itself. On a follower this
|
|
311
|
+
* is a proxy round trip, so a result can arrive *after* the
|
|
312
|
+
* leadership-loss notification that already drained the retained
|
|
313
|
+
* set — and the handle it carries was stamped by Rust with the
|
|
314
|
+
* generation the request was issued under, so it is stale on
|
|
315
|
+
* arrival. Registering it would put a permanently silent stream
|
|
316
|
+
* into a set nothing will drain again. The generation is therefore
|
|
317
|
+
* captured before the await and compared after it, and a crossing
|
|
318
|
+
* result is ended exactly as a consumer a microtask later would
|
|
319
|
+
* have seen it.
|
|
320
|
+
*/
|
|
321
|
+
async openStream(options) {
|
|
322
|
+
const openedUnder = this.inner.generation();
|
|
323
|
+
const stream = new LeafStream(await this.guard(() => this.inner.open_stream(options)));
|
|
324
|
+
if (this.closed || this.inner.generation() !== openedUnder) {
|
|
325
|
+
// Leadership (or this session) went away while the open was in
|
|
326
|
+
// flight. Ending it here is the same disposition a consumer
|
|
327
|
+
// would have got a microtask later, rather than a live handle
|
|
328
|
+
// on a dead node.
|
|
329
|
+
stream.close();
|
|
330
|
+
return stream;
|
|
331
|
+
}
|
|
332
|
+
this.streams.add(stream);
|
|
333
|
+
return stream;
|
|
334
|
+
}
|
|
335
|
+
// ── Organization-scoped streaming (plan §4.5) ────────────────────────
|
|
336
|
+
//
|
|
337
|
+
// The same eight verbs as `BrowserNode`, on whichever tab holds the
|
|
338
|
+
// node. On a follower every call rides a **transparent envelope of
|
|
339
|
+
// the same nRPC frame bytes** over the existing proxy vocabulary —
|
|
340
|
+
// a new shape would cost proxy vocabulary — and its correlation is
|
|
341
|
+
// this tab's self-minted id plus the gate generation it presented,
|
|
342
|
+
// never the wire id or incarnation (those come back in the stream
|
|
343
|
+
// resolution). Exact peer/session attribution is preserved through
|
|
344
|
+
// direct and proxied callbacks alike and across leader replacement.
|
|
345
|
+
/**
|
|
346
|
+
* Call an org-protected unary service.
|
|
347
|
+
*
|
|
348
|
+
* Rejects with the §4.3 typed error: an **opening denial** is
|
|
349
|
+
* {@link OrgAdmissionDeniedError} carrying the coarse reason and
|
|
350
|
+
* nothing finer — a precise remote reason would be a credential
|
|
351
|
+
* oracle. Never a silent retry: the caller is told and decides.
|
|
352
|
+
*
|
|
353
|
+
* **Leader replacement.** A pending proxied call fails typed
|
|
354
|
+
* `LeaderLost` on a generation move and is NEVER resumed; a
|
|
355
|
+
* successor generation's calls carry fresh correlation and fresh
|
|
356
|
+
* proofs.
|
|
357
|
+
*
|
|
358
|
+
* **Suspension and closure (plan §4.5).** Deadlines are absolute: a
|
|
359
|
+
* frozen tab extends no lease and no window, so a call whose
|
|
360
|
+
* deadline passes while its tab is suspended retires with its
|
|
361
|
+
* deadline terminal on wake — {@link OrgTimeoutError}, or
|
|
362
|
+
* {@link OrgIndeterminateError} where the work may already be
|
|
363
|
+
* executing in another tab. Nothing resumes automatically: a
|
|
364
|
+
* retired call is never transparently re-opened, because re-opening
|
|
365
|
+
* is a fresh call with a fresh proof and MAY repeat effects. Node
|
|
366
|
+
* or tab close retires ownership — pending calls fail typed, and
|
|
367
|
+
* each dropped caller handle emits exactly one CANCEL.
|
|
368
|
+
*/
|
|
369
|
+
async callOrg(service, payload, options) {
|
|
370
|
+
return await this.guard(() => this.inner.call_org(service, payload, toWasmOrgCallOptions(options)));
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Call an org-protected server-streaming service; resolve to the
|
|
374
|
+
* response stream.
|
|
375
|
+
*
|
|
376
|
+
* An **opening denial** rejects here with
|
|
377
|
+
* {@link OrgAdmissionDeniedError} (coarse) — a denial is a
|
|
378
|
+
* rejection, never a terminal item. Midstream, the stream's FINAL
|
|
379
|
+
* error item carries the §4.3 outcome: revocation is
|
|
380
|
+
* `AdmissionDenied('denied')` ({@link OrgRevokedError} where the
|
|
381
|
+
* cause is known), deadline/cancel retirement is
|
|
382
|
+
* {@link OrgTimeoutError}/{@link OrgCancelledError}. The async
|
|
383
|
+
* iterator throws that typed error at the terminal error item.
|
|
384
|
+
*
|
|
385
|
+
* **Leader replacement.** A pending proxied call fails typed
|
|
386
|
+
* `LeaderLost` on a generation move and is NEVER resumed; a
|
|
387
|
+
* successor generation's calls carry fresh correlation and fresh
|
|
388
|
+
* proofs. An open that lands after the move is retired on arrival
|
|
389
|
+
* rather than handed out live.
|
|
390
|
+
*
|
|
391
|
+
* **Suspension and closure (plan §4.5).** Deadlines are absolute: a
|
|
392
|
+
* frozen tab extends no lease and no window, so a call whose
|
|
393
|
+
* deadline passes while its tab is suspended retires with its
|
|
394
|
+
* deadline terminal on wake — {@link OrgTimeoutError}, or
|
|
395
|
+
* {@link OrgIndeterminateError} where the work may already be
|
|
396
|
+
* executing in another tab. Nothing resumes automatically: a
|
|
397
|
+
* retired call is never transparently re-opened, because re-opening
|
|
398
|
+
* is a fresh call with a fresh proof and MAY repeat effects. Node
|
|
399
|
+
* or tab close retires ownership — pending calls fail typed, and
|
|
400
|
+
* each dropped caller handle emits exactly one CANCEL.
|
|
401
|
+
*/
|
|
402
|
+
async callOrgStreaming(service, payload, options) {
|
|
403
|
+
const openedUnder = this.inner.generation();
|
|
404
|
+
let stream;
|
|
405
|
+
try {
|
|
406
|
+
const handle = await this.inner.call_org_streaming(service, payload, toWasmOrgCallOptions(options));
|
|
407
|
+
stream = new OrgStream(handle, () => handle.cancel(), () => this.orgHandles.delete(stream));
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
throw fromWasmError(error);
|
|
411
|
+
}
|
|
412
|
+
if (this.closed || this.inner.generation() !== openedUnder) {
|
|
413
|
+
// Leadership (or this session) went away while the open was in
|
|
414
|
+
// flight. Retired on arrival is the same disposition a
|
|
415
|
+
// consumer would have got a microtask later — never a live
|
|
416
|
+
// handle on a dead generation, and never resumed on the
|
|
417
|
+
// successor's.
|
|
418
|
+
stream.retire(this.closed
|
|
419
|
+
? new OrgCancelledError('org:rpc:cancelled: the session is closed')
|
|
420
|
+
: new OrgLeaderLostError(openedUnder));
|
|
421
|
+
return stream;
|
|
422
|
+
}
|
|
423
|
+
this.orgHandles.add(stream);
|
|
424
|
+
return stream;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Open an org-protected client-streaming upload. `finish()` is the
|
|
428
|
+
* half-close and the reply await, and it rejects with the typed
|
|
429
|
+
* terminal error (§4.3).
|
|
430
|
+
*
|
|
431
|
+
* An **opening denial** rejects here with
|
|
432
|
+
* {@link OrgAdmissionDeniedError} (coarse).
|
|
433
|
+
*
|
|
434
|
+
* **Leader replacement.** A pending proxied call fails typed
|
|
435
|
+
* `LeaderLost` on a generation move and is NEVER resumed; a
|
|
436
|
+
* successor generation's calls carry fresh correlation and fresh
|
|
437
|
+
* proofs. An open that lands after the move is retired on arrival
|
|
438
|
+
* rather than handed out live.
|
|
439
|
+
*
|
|
440
|
+
* **Suspension and closure (plan §4.5).** Deadlines are absolute: a
|
|
441
|
+
* frozen tab extends no lease and no window, so a call whose
|
|
442
|
+
* deadline passes while its tab is suspended retires with its
|
|
443
|
+
* deadline terminal on wake — {@link OrgTimeoutError}, or
|
|
444
|
+
* {@link OrgIndeterminateError} where the work may already be
|
|
445
|
+
* executing in another tab. Nothing resumes automatically: a
|
|
446
|
+
* retired call is never transparently re-opened, because re-opening
|
|
447
|
+
* is a fresh call with a fresh proof and MAY repeat effects. Node
|
|
448
|
+
* or tab close retires ownership — pending calls fail typed, and
|
|
449
|
+
* each dropped caller handle emits exactly one CANCEL.
|
|
450
|
+
*/
|
|
451
|
+
async callOrgClientStream(service, options) {
|
|
452
|
+
const openedUnder = this.inner.generation();
|
|
453
|
+
let upload;
|
|
454
|
+
try {
|
|
455
|
+
const handle = await this.inner.call_org_client_stream(service, toWasmOrgCallOptions(options));
|
|
456
|
+
upload = new OrgUpload(handle, () => this.orgHandles.delete(upload));
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
throw fromWasmError(error);
|
|
460
|
+
}
|
|
461
|
+
if (this.closed || this.inner.generation() !== openedUnder) {
|
|
462
|
+
upload.retire(this.closed
|
|
463
|
+
? new OrgCancelledError('org:rpc:cancelled: the session is closed')
|
|
464
|
+
: new OrgLeaderLostError(openedUnder));
|
|
465
|
+
return upload;
|
|
466
|
+
}
|
|
467
|
+
this.orgHandles.add(upload);
|
|
468
|
+
return upload;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Open an org-protected duplex call: an upload sink and an
|
|
472
|
+
* independent response stream.
|
|
473
|
+
*
|
|
474
|
+
* An **opening denial** rejects here with
|
|
475
|
+
* {@link OrgAdmissionDeniedError} (coarse). Midstream outcomes are
|
|
476
|
+
* the §4.3 terminals on each half.
|
|
477
|
+
*
|
|
478
|
+
* **Leader replacement.** A pending proxied call fails typed
|
|
479
|
+
* `LeaderLost` on a generation move and is NEVER resumed; a
|
|
480
|
+
* successor generation's calls carry fresh correlation and fresh
|
|
481
|
+
* proofs. An open that lands after the move is retired on arrival
|
|
482
|
+
* rather than handed out live.
|
|
483
|
+
*
|
|
484
|
+
* **Suspension and closure (plan §4.5).** Deadlines are absolute: a
|
|
485
|
+
* frozen tab extends no lease and no window, so a call whose
|
|
486
|
+
* deadline passes while its tab is suspended retires with its
|
|
487
|
+
* deadline terminal on wake — {@link OrgTimeoutError}, or
|
|
488
|
+
* {@link OrgIndeterminateError} where the work may already be
|
|
489
|
+
* executing in another tab. Nothing resumes automatically: a
|
|
490
|
+
* retired call is never transparently re-opened, because re-opening
|
|
491
|
+
* is a fresh call with a fresh proof and MAY repeat effects. Node
|
|
492
|
+
* or tab close retires ownership — pending calls fail typed, and
|
|
493
|
+
* each dropped caller handle emits exactly one CANCEL.
|
|
494
|
+
*/
|
|
495
|
+
async callOrgDuplex(service, options) {
|
|
496
|
+
const openedUnder = this.inner.generation();
|
|
497
|
+
let call;
|
|
498
|
+
try {
|
|
499
|
+
const handle = await this.inner.call_org_duplex(service, toWasmOrgCallOptions(options));
|
|
500
|
+
call = new OrgDuplex(handle, () => this.orgHandles.delete(call));
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
throw fromWasmError(error);
|
|
504
|
+
}
|
|
505
|
+
if (this.closed || this.inner.generation() !== openedUnder) {
|
|
506
|
+
call.retire(this.closed
|
|
507
|
+
? new OrgCancelledError('org:rpc:cancelled: the session is closed')
|
|
508
|
+
: new OrgLeaderLostError(openedUnder));
|
|
509
|
+
return call;
|
|
510
|
+
}
|
|
511
|
+
this.orgHandles.add(call);
|
|
512
|
+
return call;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Serve an org-protected unary service. `access` is `'same-org'`
|
|
516
|
+
* (owner-org traffic only) or `'granted'` (capability grants too);
|
|
517
|
+
* anything else is refused loudly at registration.
|
|
518
|
+
*
|
|
519
|
+
* On a follower the registration rides the proxy registry and
|
|
520
|
+
* inbound calls dispatch to the registering follower; teardown
|
|
521
|
+
* follows the retire discipline — a generation move fences the
|
|
522
|
+
* registration's calls and streams, never resurrected.
|
|
523
|
+
*
|
|
524
|
+
* **Handler drop (F-S3.1-2).** The retire supervisor may drop the
|
|
525
|
+
* handler future without a final poll — cancellation is observed
|
|
526
|
+
* through the retirement observables (the terminal item, the sink's
|
|
527
|
+
* typed closed refusal, and `retired`), never assumed as a
|
|
528
|
+
* handler-side event; a detached observer holding `retired`
|
|
529
|
+
* observes the signal. After retirement a handler sees typed
|
|
530
|
+
* refusals and any return value it produces is discarded.
|
|
531
|
+
*
|
|
532
|
+
* **`close()` is a retirement, not a graceful unregister (C9's
|
|
533
|
+
* protected split).** Closing the returned handle retires its live
|
|
534
|
+
* protected calls — each with its exact retirement terminal, the
|
|
535
|
+
* deadline/cancel/revocation vocabulary — and refuses new openings
|
|
536
|
+
* on the service. Deliberately NOT the public path's
|
|
537
|
+
* let-existing-calls-finish behavior.
|
|
538
|
+
*/
|
|
539
|
+
serveOrg(service, access, handler, options) {
|
|
540
|
+
try {
|
|
541
|
+
const serve = new OrgServe(this.inner.serve_org(service, access, unaryOrgTrampoline(handler), toWasmOrgServeOptions(options)), () => this.orgHandles.delete(serve));
|
|
542
|
+
this.orgHandles.add(serve);
|
|
543
|
+
return serve;
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
throw fromWasmError(error);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Serve an org-protected server-streaming service: one request in,
|
|
551
|
+
* items out through the {@link OrgResponseSink}.
|
|
552
|
+
*
|
|
553
|
+
* On a follower the registration rides the proxy registry and
|
|
554
|
+
* inbound calls dispatch to the registering follower; teardown
|
|
555
|
+
* follows the retire discipline — a generation move fences the
|
|
556
|
+
* registration's calls and streams, never resurrected.
|
|
557
|
+
*
|
|
558
|
+
* **Handler drop (F-S3.1-2).** The retire supervisor may drop the
|
|
559
|
+
* handler future without a final poll — cancellation is observed
|
|
560
|
+
* through the retirement observables (the terminal item, the sink's
|
|
561
|
+
* typed closed refusal, and `retired`), never assumed as a
|
|
562
|
+
* handler-side event; a detached observer holding `retired`
|
|
563
|
+
* observes the signal. After retirement a handler sees typed
|
|
564
|
+
* refusals and any return value it produces is discarded.
|
|
565
|
+
*
|
|
566
|
+
* **`close()` is a retirement, not a graceful unregister (C9's
|
|
567
|
+
* protected split).** Closing the returned handle retires its live
|
|
568
|
+
* protected calls — each with its exact retirement terminal, the
|
|
569
|
+
* deadline/cancel/revocation vocabulary — and refuses new openings
|
|
570
|
+
* on the service. Deliberately NOT the public path's
|
|
571
|
+
* let-existing-calls-finish behavior.
|
|
572
|
+
*/
|
|
573
|
+
serveOrgStreaming(service, access, handler, options) {
|
|
574
|
+
try {
|
|
575
|
+
const serve = new OrgServe(this.inner.serve_org_streaming(service, access, streamingOrgTrampoline(handler), toWasmOrgServeOptions(options)), () => this.orgHandles.delete(serve));
|
|
576
|
+
this.orgHandles.add(serve);
|
|
577
|
+
return serve;
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
throw fromWasmError(error);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Serve an org-protected client-streaming service: requests in
|
|
585
|
+
* through the {@link OrgRequestStream}, one reply out.
|
|
586
|
+
*
|
|
587
|
+
* On a follower the registration rides the proxy registry and
|
|
588
|
+
* inbound calls dispatch to the registering follower; teardown
|
|
589
|
+
* follows the retire discipline — a generation move fences the
|
|
590
|
+
* registration's calls and streams, never resurrected.
|
|
591
|
+
*
|
|
592
|
+
* **Handler drop (F-S3.1-2).** The retire supervisor may drop the
|
|
593
|
+
* handler future without a final poll — cancellation is observed
|
|
594
|
+
* through the retirement observables (the terminal item, the sink's
|
|
595
|
+
* typed closed refusal, and `retired`), never assumed as a
|
|
596
|
+
* handler-side event; a detached observer holding `retired`
|
|
597
|
+
* observes the signal. After retirement a handler sees typed
|
|
598
|
+
* refusals and any return value it produces is discarded.
|
|
599
|
+
*
|
|
600
|
+
* **`close()` is a retirement, not a graceful unregister (C9's
|
|
601
|
+
* protected split).** Closing the returned handle retires its live
|
|
602
|
+
* protected calls — each with its exact retirement terminal, the
|
|
603
|
+
* deadline/cancel/revocation vocabulary — and refuses new openings
|
|
604
|
+
* on the service. Deliberately NOT the public path's
|
|
605
|
+
* let-existing-calls-finish behavior.
|
|
606
|
+
*/
|
|
607
|
+
serveOrgClientStream(service, access, handler, options) {
|
|
608
|
+
try {
|
|
609
|
+
const serve = new OrgServe(this.inner.serve_org_client_stream(service, access, clientStreamOrgTrampoline(handler), toWasmOrgServeOptions(options)), () => this.orgHandles.delete(serve));
|
|
610
|
+
this.orgHandles.add(serve);
|
|
611
|
+
return serve;
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
throw fromWasmError(error);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Serve an org-protected duplex service: requests in and items out,
|
|
619
|
+
* independently, through the request stream and the sink.
|
|
620
|
+
*
|
|
621
|
+
* On a follower the registration rides the proxy registry and
|
|
622
|
+
* inbound calls dispatch to the registering follower; teardown
|
|
623
|
+
* follows the retire discipline — a generation move fences the
|
|
624
|
+
* registration's calls and streams, never resurrected.
|
|
625
|
+
*
|
|
626
|
+
* **Handler drop (F-S3.1-2).** The retire supervisor may drop the
|
|
627
|
+
* handler future without a final poll — cancellation is observed
|
|
628
|
+
* through the retirement observables (the terminal item, the sink's
|
|
629
|
+
* typed closed refusal, and `retired`), never assumed as a
|
|
630
|
+
* handler-side event; a detached observer holding `retired`
|
|
631
|
+
* observes the signal. After retirement a handler sees typed
|
|
632
|
+
* refusals and any return value it produces is discarded.
|
|
633
|
+
*
|
|
634
|
+
* **`close()` is a retirement, not a graceful unregister (C9's
|
|
635
|
+
* protected split).** Closing the returned handle retires its live
|
|
636
|
+
* protected calls — each with its exact retirement terminal, the
|
|
637
|
+
* deadline/cancel/revocation vocabulary — and refuses new openings
|
|
638
|
+
* on the service. Deliberately NOT the public path's
|
|
639
|
+
* let-existing-calls-finish behavior.
|
|
640
|
+
*/
|
|
641
|
+
serveOrgDuplex(service, access, handler, options) {
|
|
642
|
+
try {
|
|
643
|
+
const serve = new OrgServe(this.inner.serve_org_duplex(service, access, duplexOrgTrampoline(handler), toWasmOrgServeOptions(options)), () => this.orgHandles.delete(serve));
|
|
644
|
+
this.orgHandles.add(serve);
|
|
645
|
+
return serve;
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
throw fromWasmError(error);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/** Listen for one event tag. Returns a cancel handle. */
|
|
652
|
+
on(type, handler) {
|
|
653
|
+
return this.hub.on(type, handler);
|
|
654
|
+
}
|
|
655
|
+
/** Listen for every event. Returns a cancel handle. */
|
|
656
|
+
onEvent(handler) {
|
|
657
|
+
return this.hub.onAny(handler);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Listen for the five lifecycle tags only — `leader_changed`,
|
|
661
|
+
* `subscription_restored`, `leader_lost`, `generation_fenced`,
|
|
662
|
+
* `not_leader`.
|
|
663
|
+
*
|
|
664
|
+
* A page that wants to render "reconnecting" wants exactly these and
|
|
665
|
+
* none of the node traffic.
|
|
666
|
+
*/
|
|
667
|
+
onLifecycle(handler) {
|
|
668
|
+
return this.hub.onAny((event) => {
|
|
669
|
+
if (isLifecycleEvent(event))
|
|
670
|
+
handler(event);
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
/** Consume events as an async iterable. */
|
|
674
|
+
events() {
|
|
675
|
+
return this.hub.events();
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Stand down and release the lock.
|
|
679
|
+
*
|
|
680
|
+
* On a leader this is what lets a follower promote, so a page that
|
|
681
|
+
* navigates away without calling it leaves the handoff to the
|
|
682
|
+
* browser's own lock release.
|
|
683
|
+
*
|
|
684
|
+
* Every stream this session handed out is ended first, in that
|
|
685
|
+
* order: a consumer awaiting the next payload has to be settled by
|
|
686
|
+
* something, and after `inner.close()` nothing will ever emit
|
|
687
|
+
* again.
|
|
688
|
+
*/
|
|
689
|
+
close() {
|
|
690
|
+
if (this.closed)
|
|
691
|
+
return;
|
|
692
|
+
this.closed = true;
|
|
693
|
+
// Ownership retirement first, while the transport still exists:
|
|
694
|
+
// one CANCEL per dropped caller handle, each consumer settled
|
|
695
|
+
// with its typed terminal (plan §4.5).
|
|
696
|
+
this.orgHandles.retireAll(new OrgCancelledError('org:rpc:cancelled: the session is closed'));
|
|
697
|
+
this.endStreams();
|
|
698
|
+
this.hub.close();
|
|
699
|
+
this.inner.close();
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* End every stream this session handed out.
|
|
703
|
+
*
|
|
704
|
+
* `LeafStream.close` ends each waiting consumer and clears the
|
|
705
|
+
* listeners, which is precisely the disposition D2 specifies for a
|
|
706
|
+
* stream across a leader change: failed and observable, never
|
|
707
|
+
* resurrected and never left pending. Called on explicit
|
|
708
|
+
* leadership loss, on a generation that moved under this tab, and
|
|
709
|
+
* on close; idempotent, because a page may have closed a stream
|
|
710
|
+
* itself.
|
|
711
|
+
*/
|
|
712
|
+
endStreams() {
|
|
713
|
+
const open = [...this.streams];
|
|
714
|
+
this.streams.clear();
|
|
715
|
+
for (const stream of open)
|
|
716
|
+
stream.close();
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Re-type whatever the boundary threw.
|
|
720
|
+
*
|
|
721
|
+
* One wrapper for every method because they all cross the same
|
|
722
|
+
* boundary and a proxied failure must arrive as the same class a
|
|
723
|
+
* direct one does — that is the whole point of the failure text
|
|
724
|
+
* being carried verbatim through the proxy instead of re-parsed on
|
|
725
|
+
* the Rust side.
|
|
726
|
+
*/
|
|
727
|
+
async guard(operation) {
|
|
728
|
+
try {
|
|
729
|
+
return await operation();
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
throw fromWasmError(error);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Join the origin's Net node, electing a leader if there is none.
|
|
738
|
+
*
|
|
739
|
+
* Resolves as soon as this tab knows its role: the leader once it has
|
|
740
|
+
* bootstrapped, a follower once it has attached. A follower keeps
|
|
741
|
+
* queueing for the lock in the background, so when the leader goes
|
|
742
|
+
* away it promotes itself, re-bootstraps with the same identity and
|
|
743
|
+
* restores the subscriptions — without the page doing anything.
|
|
744
|
+
*
|
|
745
|
+
* Rejects with a typed {@link LeafError}. {@link IdentityError} covers
|
|
746
|
+
* the two refusals worth knowing by name: a context with no Web Locks
|
|
747
|
+
* API (where one-node-per-origin cannot be enforced, so the leaf
|
|
748
|
+
* refuses rather than let two tabs evict each other) and a context
|
|
749
|
+
* with no IndexedDB (where an identity cannot be stored, so the page
|
|
750
|
+
* must inject one).
|
|
751
|
+
*/
|
|
752
|
+
export async function openSession(options) {
|
|
753
|
+
const module = asSessionModule(await loadLeafWasm(options));
|
|
754
|
+
const request = {
|
|
755
|
+
// Shared with `connect()` deliberately: the two surfaces take the
|
|
756
|
+
// same connect keys, and a key added to one must not be silently
|
|
757
|
+
// missing from the other.
|
|
758
|
+
...buildConnectRequest(options),
|
|
759
|
+
...(options.capabilities ? { capabilities: [...options.capabilities] } : {}),
|
|
760
|
+
...(options.subscriptions ? { subscriptions: [...options.subscriptions] } : {}),
|
|
761
|
+
...(options.dbName === undefined ? {} : { dbName: options.dbName }),
|
|
762
|
+
...(options.lockScope === undefined ? {} : { lockScope: options.lockScope }),
|
|
763
|
+
};
|
|
764
|
+
try {
|
|
765
|
+
return new MeshSession(await module.MeshSession.open(request));
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
throw fromWasmError(error);
|
|
769
|
+
}
|
|
770
|
+
}
|