@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +517 -0
  2. package/LICENSE-APACHE +190 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +666 -0
  5. package/dist/async-queue.d.ts +26 -0
  6. package/dist/async-queue.js +62 -0
  7. package/dist/credential.d.ts +70 -0
  8. package/dist/credential.js +95 -0
  9. package/dist/errors.d.ts +400 -0
  10. package/dist/errors.js +726 -0
  11. package/dist/events.d.ts +232 -0
  12. package/dist/events.js +298 -0
  13. package/dist/identity.d.ts +40 -0
  14. package/dist/identity.js +78 -0
  15. package/dist/index.bundle.js +2 -0
  16. package/dist/index.d.ts +38 -0
  17. package/dist/index.js +46 -0
  18. package/dist/leader/events.d.ts +111 -0
  19. package/dist/leader/events.js +116 -0
  20. package/dist/leader/index.d.ts +14 -0
  21. package/dist/leader/index.js +11 -0
  22. package/dist/leader/session.d.ts +539 -0
  23. package/dist/leader/session.js +770 -0
  24. package/dist/leader/wasm.d.ts +123 -0
  25. package/dist/leader/wasm.js +39 -0
  26. package/dist/lobby.d.ts +183 -0
  27. package/dist/lobby.js +504 -0
  28. package/dist/local.d.ts +80 -0
  29. package/dist/local.js +160 -0
  30. package/dist/net_leaf.d.ts +1494 -0
  31. package/dist/net_leaf.js +3753 -0
  32. package/dist/net_leaf_bg.wasm +0 -0
  33. package/dist/net_leaf_bg.wasm.d.ts +137 -0
  34. package/dist/netcode/client.d.ts +102 -0
  35. package/dist/netcode/client.js +296 -0
  36. package/dist/netcode/clock.d.ts +45 -0
  37. package/dist/netcode/clock.js +54 -0
  38. package/dist/netcode/host.d.ts +118 -0
  39. package/dist/netcode/host.js +276 -0
  40. package/dist/netcode/index.d.ts +19 -0
  41. package/dist/netcode/index.js +14 -0
  42. package/dist/netcode/interpolate.d.ts +50 -0
  43. package/dist/netcode/interpolate.js +100 -0
  44. package/dist/netcode/wire.d.ts +125 -0
  45. package/dist/netcode/wire.js +117 -0
  46. package/dist/node.d.ts +908 -0
  47. package/dist/node.js +1085 -0
  48. package/dist/org.d.ts +412 -0
  49. package/dist/org.js +567 -0
  50. package/dist/peer-driver.d.ts +117 -0
  51. package/dist/peer-driver.js +224 -0
  52. package/dist/store/assembly.d.ts +176 -0
  53. package/dist/store/assembly.js +325 -0
  54. package/dist/store/chunker.d.ts +83 -0
  55. package/dist/store/chunker.js +190 -0
  56. package/dist/store/core.d.ts +136 -0
  57. package/dist/store/core.js +477 -0
  58. package/dist/store/definition.d.ts +23 -0
  59. package/dist/store/definition.js +83 -0
  60. package/dist/store/errors.d.ts +77 -0
  61. package/dist/store/errors.js +94 -0
  62. package/dist/store/host.d.ts +247 -0
  63. package/dist/store/host.js +675 -0
  64. package/dist/store/index.d.ts +50 -0
  65. package/dist/store/index.js +40 -0
  66. package/dist/store/interest.d.ts +40 -0
  67. package/dist/store/interest.js +77 -0
  68. package/dist/store/inventory.d.ts +81 -0
  69. package/dist/store/inventory.js +140 -0
  70. package/dist/store/join.d.ts +118 -0
  71. package/dist/store/join.js +724 -0
  72. package/dist/store/json.d.ts +50 -0
  73. package/dist/store/json.js +283 -0
  74. package/dist/store/ledger.d.ts +187 -0
  75. package/dist/store/ledger.js +292 -0
  76. package/dist/store/owner.d.ts +616 -0
  77. package/dist/store/owner.js +1711 -0
  78. package/dist/store/patch.d.ts +70 -0
  79. package/dist/store/patch.js +166 -0
  80. package/dist/store/player.d.ts +65 -0
  81. package/dist/store/player.js +261 -0
  82. package/dist/store/replica.d.ts +187 -0
  83. package/dist/store/replica.js +740 -0
  84. package/dist/store/state.d.ts +42 -0
  85. package/dist/store/state.js +165 -0
  86. package/dist/store/types.d.ts +200 -0
  87. package/dist/store/types.js +25 -0
  88. package/dist/store/visibility.d.ts +118 -0
  89. package/dist/store/visibility.js +282 -0
  90. package/dist/store/wire.d.ts +309 -0
  91. package/dist/store/wire.js +834 -0
  92. package/dist/stream.d.ts +174 -0
  93. package/dist/stream.js +238 -0
  94. package/dist/three/index.d.ts +109 -0
  95. package/dist/three/index.js +130 -0
  96. package/dist/udp-probe.d.ts +159 -0
  97. package/dist/udp-probe.js +245 -0
  98. package/dist/wasm.d.ts +642 -0
  99. package/dist/wasm.js +98 -0
  100. package/dist/world/border.d.ts +84 -0
  101. package/dist/world/border.js +99 -0
  102. package/dist/world/handoff.d.ts +187 -0
  103. package/dist/world/handoff.js +222 -0
  104. package/dist/world/index.d.ts +13 -0
  105. package/dist/world/index.js +9 -0
  106. package/dist/world/link.d.ts +179 -0
  107. package/dist/world/link.js +476 -0
  108. package/dist/world/regions.d.ts +154 -0
  109. package/dist/world/regions.js +314 -0
  110. 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
+ }