@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
package/dist/errors.js ADDED
@@ -0,0 +1,726 @@
1
+ /**
2
+ * The leaf's error taxonomy, mirrored in TypeScript.
3
+ *
4
+ * One class per variant of `net_leaf::error::LeafError`, with the two
5
+ * nested enums (`RtcError`, `RpcError`) flattened into `kind` so a
6
+ * caller — or a Playwright assertion — can branch on one stable
7
+ * string. `message` is verbatim the Rust `Display` text that crossed
8
+ * the wasm boundary, so nothing is lost in the re-typing;
9
+ * `errors.test.ts` pins that round-trip.
10
+ *
11
+ * **The failure-typing correction lives in this file's types.** There
12
+ * is no way to build a {@link RtcErrorFailure} of type `udpBlocked`
13
+ * without a {@link UdpBlockedEvidence}, and `udpBlockedEvidence()` —
14
+ * the mirror of Rust's `UdpBlockedEvidence::new` — returns `null`
15
+ * unless both observations hold. An ICE timeout on its own therefore
16
+ * cannot become `udp-blocked` anywhere in this package.
17
+ */
18
+ /**
19
+ * `UdpBlockedEvidence` only when both observations hold; `null`
20
+ * otherwise, which is the caller's instruction to stay with
21
+ * `ice-timeout`. Mirror of `UdpBlockedEvidence::new`.
22
+ */
23
+ export function udpBlockedEvidence(bootstrapOk, stunProbeFailed, probed) {
24
+ if (!bootstrapOk || !stunProbeFailed || probed.length === 0)
25
+ return null;
26
+ return { bootstrapOk: true, stunProbeFailed: true, probed };
27
+ }
28
+ /** The base class of everything this package rejects with. */
29
+ export class LeafError extends Error {
30
+ constructor(message) {
31
+ super(message);
32
+ this.name = 'LeafError';
33
+ }
34
+ }
35
+ /** The wire layer refused a packet: framing, AEAD, replay window. */
36
+ export class WireError extends LeafError {
37
+ detail;
38
+ kind = 'wire';
39
+ constructor(detail) {
40
+ super(`wire: ${detail}`);
41
+ this.detail = detail;
42
+ this.name = 'WireError';
43
+ }
44
+ }
45
+ /** No session for a peer, an incomplete handshake, a replaced session. */
46
+ export class SessionError extends LeafError {
47
+ detail;
48
+ kind = 'session';
49
+ constructor(detail) {
50
+ super(`session: ${detail}`);
51
+ this.detail = detail;
52
+ this.name = 'SessionError';
53
+ }
54
+ }
55
+ /** The control plane could not carry what the leaf asked it to. */
56
+ export class ControlPlaneError extends LeafError {
57
+ detail;
58
+ kind = 'control-plane';
59
+ constructor(detail) {
60
+ super(`control plane: ${detail}`);
61
+ this.detail = detail;
62
+ this.name = 'ControlPlaneError';
63
+ }
64
+ }
65
+ /** Identity storage, generation or custody. */
66
+ export class IdentityError extends LeafError {
67
+ detail;
68
+ kind = 'identity';
69
+ constructor(detail) {
70
+ super(`identity: ${detail}`);
71
+ this.detail = detail;
72
+ this.name = 'IdentityError';
73
+ }
74
+ }
75
+ /**
76
+ * A supplied `iceServers` entry named **this connection's own peer**
77
+ * as its STUN server.
78
+ *
79
+ * The mirror of Rust's `LeafError::IceServerConflictsWithPeer`. An
80
+ * anchor's `rtc_addr` is the ICE peer of a connection with that
81
+ * anchor, not a STUN server for it: the connection gathers no
82
+ * server-reflexive candidate and times out. The leaf refuses this
83
+ * **before any ICE work**, and refuses rather than silently
84
+ * stripping the entry — stripping would turn an explicit
85
+ * NAT-traversal configuration into a host-candidate-only attempt
86
+ * while appearing to have accepted it.
87
+ *
88
+ * The fix is to omit `iceServers` and let `connect()` use the
89
+ * endpoint the anchor announces as `stun_addr` on
90
+ * `GET /rtc/anchor`, or to name a STUN server that is not this
91
+ * peer. {@link diagnosticStunUrl} exists for the throwaway UDP
92
+ * probe and is not a source of `iceServers`.
93
+ *
94
+ * **Detection is endpoint equality only**, after default-port
95
+ * normalisation. A URL naming a DNS alias that resolves to the peer
96
+ * is not detected: the leaf resolves no names. The announced
97
+ * endpoint is what makes detection unnecessary for the configuration
98
+ * Net supplies.
99
+ */
100
+ export class IceServerConflictError extends LeafError {
101
+ entry;
102
+ peerRtcAddr;
103
+ kind = 'ice-server-conflict';
104
+ constructor(entry, peerRtcAddr) {
105
+ super(`ice configuration: the iceServers entry ${entry} names this connection's peer RTC ` +
106
+ `endpoint ${peerRtcAddr}; a peer cannot be its own STUN server. Omit iceServers to ` +
107
+ 'use the STUN endpoint the anchor announces (the stun_addr field of GET /rtc/anchor), ' +
108
+ 'or name a STUN server that is not this peer');
109
+ this.entry = entry;
110
+ this.peerRtcAddr = peerRtcAddr;
111
+ this.name = 'IceServerConflictError';
112
+ }
113
+ }
114
+ /**
115
+ * This tab is not the leader for the origin and the operation needs
116
+ * the node. Carries the generation the caller presented so a stale
117
+ * follower's failure is legible.
118
+ */
119
+ export class NotLeaderError extends LeafError {
120
+ presented;
121
+ current;
122
+ kind = 'not-leader';
123
+ constructor(presented, current = null) {
124
+ super(current === null
125
+ ? `not the leader: this tab holds generation ${presented}`
126
+ : `not the leader: this tab holds generation ${presented}, the leader holds ${current}`);
127
+ this.presented = presented;
128
+ this.current = current;
129
+ this.name = 'NotLeaderError';
130
+ }
131
+ }
132
+ /** The RTC transport itself. `kind` is the flattened failure. */
133
+ export class RtcError extends LeafError {
134
+ failure;
135
+ kind;
136
+ constructor(failure) {
137
+ super(`rtc: ${rtcDisplay(failure)}`);
138
+ this.failure = failure;
139
+ this.name = 'RtcError';
140
+ this.kind = RTC_KINDS[failure.type];
141
+ }
142
+ }
143
+ /** An nRPC call failed or was disposed of. `kind` is the flattened failure. */
144
+ export class RpcError extends LeafError {
145
+ failure;
146
+ kind;
147
+ constructor(failure) {
148
+ super(`rpc: ${rpcDisplay(failure)}`);
149
+ this.failure = failure;
150
+ this.name = 'RpcError';
151
+ this.kind = RPC_KINDS[failure.type];
152
+ }
153
+ }
154
+ /**
155
+ * Something crossed the boundary that this taxonomy does not
156
+ * recognise — a browser exception, or a leaf `Display` string this
157
+ * version of the package predates.
158
+ *
159
+ * Deliberately not folded into a neighbouring variant: mis-typing a
160
+ * failure is the exact mistake the Stage 5 correction exists to
161
+ * prevent, so an unrecognised message stays unrecognised.
162
+ */
163
+ export class UnknownLeafError extends LeafError {
164
+ cause;
165
+ kind = 'unknown';
166
+ constructor(message,
167
+ /** The value as it crossed the boundary, for diagnosis. */
168
+ cause) {
169
+ super(message);
170
+ this.cause = cause;
171
+ this.name = 'UnknownLeafError';
172
+ }
173
+ }
174
+ /**
175
+ * The base class of the org terminal vocabulary — what
176
+ * {@link OrgByteStream}'s terminal error item carries and what the org
177
+ * verbs reject with. `message` is the boundary's own text, verbatim,
178
+ * exactly as the rest of this taxonomy keeps it.
179
+ */
180
+ export class OrgStreamError extends LeafError {
181
+ }
182
+ /**
183
+ * The provider's admission engine refused the call (`RpcStatus 0x0009`).
184
+ * `coarse` is the frozen three-bucket reason and nothing finer.
185
+ */
186
+ export class OrgAdmissionDeniedError extends OrgStreamError {
187
+ coarse;
188
+ kind = 'org-admission-denied';
189
+ constructor(coarse, message) {
190
+ super(message ?? `org:admission_denied:${WIRE_COARSE[coarse]}`);
191
+ this.coarse = coarse;
192
+ this.name = 'OrgAdmissionDeniedError';
193
+ }
194
+ }
195
+ /**
196
+ * Credentials were revoked mid-call. **Is** an admission denial:
197
+ * `coarse` is fixed to `'denied'` (the frozen Revoked → Denied coarse
198
+ * map), so `instanceof OrgAdmissionDeniedError` and
199
+ * `coarse === 'denied'` hold — `instanceof OrgRevokedError` is how a
200
+ * page that wants the cause gets it.
201
+ */
202
+ export class OrgRevokedError extends OrgAdmissionDeniedError {
203
+ kind = 'org-revoked';
204
+ constructor(message = 'org:admission_denied:denied') {
205
+ super('denied', message);
206
+ this.name = 'OrgRevokedError';
207
+ }
208
+ }
209
+ /** The call's deadline elapsed. Nothing is re-issued. */
210
+ export class OrgTimeoutError extends OrgStreamError {
211
+ kind = 'org-timeout';
212
+ constructor(message = 'org:rpc:timeout') {
213
+ super(message);
214
+ this.name = 'OrgTimeoutError';
215
+ }
216
+ }
217
+ /** The call was cancelled — by its owner, or by a teardown that owns it. */
218
+ export class OrgCancelledError extends OrgStreamError {
219
+ kind = 'org-cancelled';
220
+ constructor(message = 'org:rpc:cancelled') {
221
+ super(message);
222
+ this.name = 'OrgCancelledError';
223
+ }
224
+ }
225
+ /**
226
+ * The generation holding the call was replaced (the shared-session
227
+ * surface's typed `LeaderLost`). `generation` is the generation that
228
+ * owned the call, as an exact decimal string — the u64 never becomes a
229
+ * JS number here. Never resumed: a successor generation's calls are
230
+ * fresh calls with fresh correlation and fresh proofs.
231
+ */
232
+ export class OrgLeaderLostError extends OrgStreamError {
233
+ generation;
234
+ kind = 'org-leader-lost';
235
+ constructor(generation, message = 'org:rpc:leader_lost') {
236
+ super(message);
237
+ this.generation = generation;
238
+ this.name = 'OrgLeaderLostError';
239
+ }
240
+ }
241
+ /** The session carrying the call went away. */
242
+ export class OrgSessionLostError extends OrgStreamError {
243
+ kind = 'org-session-lost';
244
+ constructor(message = 'org:rpc:session_lost') {
245
+ super(message);
246
+ this.name = 'OrgSessionLostError';
247
+ }
248
+ }
249
+ /**
250
+ * The caller's own deadline elapsed before an answer arrived; the
251
+ * remote operation may still have executed and was not retried.
252
+ */
253
+ export class OrgIndeterminateError extends OrgStreamError {
254
+ deadlineMs;
255
+ kind = 'org-indeterminate';
256
+ constructor(deadlineMs, message = 'org:rpc:indeterminate') {
257
+ super(message);
258
+ this.deadlineMs = deadlineMs;
259
+ this.name = 'OrgIndeterminateError';
260
+ }
261
+ }
262
+ /**
263
+ * The application refused the call. `status` is its status code.
264
+ *
265
+ * Also the frozen wire kinds `server_error` (a server-answered failure
266
+ * status — the vocabulary's second name for this same condition) and
267
+ * `capability_denied` (the capability gate's refusal of the call): a
268
+ * re-typed wire string carries no recoverable `status` (`0`) and its
269
+ * `message` keeps the detail verbatim.
270
+ */
271
+ export class OrgRefusedError extends OrgStreamError {
272
+ status;
273
+ kind = 'org-refused';
274
+ constructor(status, message = 'org:rpc:refused') {
275
+ super(message);
276
+ this.status = status;
277
+ this.name = 'OrgRefusedError';
278
+ }
279
+ }
280
+ /**
281
+ * The boundary failed internally. Also what a terminal item whose
282
+ * `kind` this build does not know becomes: mis-typing a failure is the
283
+ * exact mistake this taxonomy exists to prevent, so an unknown kind is
284
+ * named internal rather than guessed into a merits bucket. The frozen
285
+ * wire kinds `no_route` and `transport` land here for the same reason:
286
+ * they carry no merits verdict (nobody refused the call).
287
+ */
288
+ export class OrgInternalError extends OrgStreamError {
289
+ kind = 'org-internal';
290
+ constructor(message = 'org:rpc:internal') {
291
+ super(message);
292
+ this.name = 'OrgInternalError';
293
+ }
294
+ }
295
+ /**
296
+ * The reply or item did not decode. Also the frozen wire kinds
297
+ * `codec_encode` / `codec_decode` — the one codec class, both
298
+ * directions: an item that did not survive the codec is malformed
299
+ * whichever side failed.
300
+ */
301
+ export class OrgMalformedError extends OrgStreamError {
302
+ kind = 'org-malformed';
303
+ constructor(message = 'org:rpc:malformed') {
304
+ super(message);
305
+ this.name = 'OrgMalformedError';
306
+ }
307
+ }
308
+ /**
309
+ * THE typed closed refusal of a handler-side sink after retirement —
310
+ * one text whatever the verdict, so the wrapper's belt (its own
311
+ * short-circuit) and the leaf's hardening (its rejection) are the
312
+ * same observable. The verdict itself is what `retired` reports.
313
+ */
314
+ export const ORG_SINK_CLOSED_REFUSAL = 'org: the response sink is closed: the call was retired';
315
+ /**
316
+ * The upload sink's spelling of the SAME typed closed refusal —
317
+ * `sink_error(SinkError::Closed, "the upload sink")` at the leaf —
318
+ * pinned verbatim like {@link ORG_SINK_CLOSED_REFUSAL}. One text per
319
+ * sink, one text per verdict: what the leaf's `sink_error` spells is
320
+ * exactly this family, and anything outside it is not a closed refusal.
321
+ */
322
+ export const ORG_UPLOAD_SINK_CLOSED_REFUSAL = 'org: the upload sink is closed: the call was retired';
323
+ /**
324
+ * The byte-budget closed refusals — `sink_error(SinkError::ResourceExhausted, ..)`
325
+ * for the response sink and the upload sink. The item overran the
326
+ * call's byte budget and the call retired as `ResourceExhausted`,
327
+ * whose precise observable is the `admission-denied` / `unavailable`
328
+ * denial (the leaf's frozen map; C4, coarse `Unavailable`) — so these
329
+ * re-type as exactly that, never as a cancel.
330
+ */
331
+ export const ORG_SINK_BUDGET_REFUSAL = "org: the response sink is closed: the call's byte budget refused the item and retired the call";
332
+ export const ORG_UPLOAD_SINK_BUDGET_REFUSAL = "org: the upload sink is closed: the call's byte budget refused the item and retired the call";
333
+ const ORG_WIRE_PREFIX = 'org:';
334
+ /** The three coarse buckets' wire tokens (`OrgSdkError::to_wire`). */
335
+ const WIRE_COARSE = {
336
+ 'denied': 'denied',
337
+ 'not-supported': 'not_supported',
338
+ 'unavailable': 'unavailable',
339
+ };
340
+ /**
341
+ * Parse one wire token — or the one-byte coarse body a `0x0009`
342
+ * refusal carries — into the coarse bucket. Anything undecodable is
343
+ * the least-informative bucket (`denied`), never an error about an
344
+ * error: the caller still learns it was denied.
345
+ */
346
+ export function coarseAdmissionReason(token) {
347
+ const text = (token ?? '').trim();
348
+ if (text === 'unavailable')
349
+ return 'unavailable';
350
+ if (text === 'not_supported' || text === 'not-supported')
351
+ return 'not-supported';
352
+ // The `map_rpc_error` body: the coarse reason's wire byte 0/1/2, as
353
+ // a raw byte or its decimal digit.
354
+ if (text === '\x00' || text === '0')
355
+ return 'denied';
356
+ if (text === '\x01' || text === '1')
357
+ return 'not-supported';
358
+ if (text === '\x02' || text === '2')
359
+ return 'unavailable';
360
+ return 'denied';
361
+ }
362
+ /** The terminal error a retirement verdict maps to (the frozen map). */
363
+ export function orgRetireError(reason) {
364
+ switch (reason) {
365
+ case 'timeout':
366
+ return new OrgTimeoutError();
367
+ case 'revoked':
368
+ return new OrgRevokedError();
369
+ case 'session-lost':
370
+ return new OrgSessionLostError();
371
+ case 'leader-lost':
372
+ // The retire signal carries no generation; the call sites that
373
+ // know one build `OrgLeaderLostError` themselves.
374
+ return new OrgLeaderLostError('');
375
+ case 'cancelled':
376
+ case 'node-closed':
377
+ case 'replaced':
378
+ return new OrgCancelledError();
379
+ case 'resource-exhausted':
380
+ // The byte-budget retirement: the leaf's frozen map sends it as
381
+ // `admission-denied` / `unavailable`, and the send refused on
382
+ // the same call re-types as exactly that denial.
383
+ return new OrgAdmissionDeniedError('unavailable');
384
+ }
385
+ }
386
+ /**
387
+ * Type one terminal item from the boundary — the frozen `kind`
388
+ * vocabulary plus its fields — as the class §4.3 names.
389
+ *
390
+ * `message` is kept verbatim as the error's message.
391
+ */
392
+ export function orgTerminalError(item) {
393
+ const message = item.message;
394
+ switch (item.kind) {
395
+ case 'admission-denied':
396
+ return new OrgAdmissionDeniedError(coarseAdmissionReason(item.coarse), message);
397
+ case 'revoked':
398
+ return message === undefined ? new OrgRevokedError() : new OrgRevokedError(message);
399
+ case 'timeout':
400
+ return message === undefined ? new OrgTimeoutError() : new OrgTimeoutError(message);
401
+ case 'cancelled':
402
+ return message === undefined ? new OrgCancelledError() : new OrgCancelledError(message);
403
+ case 'leader-lost':
404
+ return new OrgLeaderLostError(item.generation ?? '', message);
405
+ case 'session-lost':
406
+ return message === undefined ? new OrgSessionLostError() : new OrgSessionLostError(message);
407
+ case 'indeterminate':
408
+ return new OrgIndeterminateError(item.deadlineMs ?? 0, message);
409
+ case 'refused':
410
+ return new OrgRefusedError(item.status ?? 0, message);
411
+ case 'malformed':
412
+ return message === undefined ? new OrgMalformedError() : new OrgMalformedError(message);
413
+ case 'internal':
414
+ return message === undefined ? new OrgInternalError() : new OrgInternalError(message);
415
+ default:
416
+ return new OrgInternalError(`org:rpc:internal: unrecognized terminal kind ${JSON.stringify(item.kind)}` +
417
+ (message === undefined ? '' : `: ${message}`));
418
+ }
419
+ }
420
+ /**
421
+ * Type one retirement verdict coming back from the boundary's
422
+ * `retired()`. A string this build does not know is a boundary
423
+ * disagreement — named `replaced`-as-cancelled would lie about which
424
+ * teardown ran, so it becomes the cancelled terminal and the raw
425
+ * string stays in `OrgRetireReason`'s consumers' hands unchanged.
426
+ */
427
+ export function orgRetireReason(raw) {
428
+ switch (raw) {
429
+ case 'timeout':
430
+ case 'cancelled':
431
+ case 'revoked':
432
+ case 'session-lost':
433
+ case 'leader-lost':
434
+ case 'node-closed':
435
+ case 'replaced':
436
+ case 'resource-exhausted':
437
+ return raw;
438
+ default:
439
+ return 'replaced';
440
+ }
441
+ }
442
+ /**
443
+ * Parse one `org:` wire string (`org:<domain>:<kind>[:detail]`, the
444
+ * `OrgSdkError::to_wire` vocabulary) or a `0x0009` admission refusal,
445
+ * or `null` if the message is not one.
446
+ *
447
+ * The domains and kind tokens mirror `classifyOrgError` from the Node
448
+ * binding where names coincide; the `rpc` domain reuses the frozen
449
+ * nRPC kind vocabulary rather than minting second names for the same
450
+ * conditions. Every frozen `org:rpc:` kind classifies (the frozen
451
+ * wire-kind map at the `rpc` switch) — a binding that returned `null`
452
+ * there forced the caller into `UnknownLeafError`, which is the
453
+ * cross-language drift this closes.
454
+ */
455
+ export function parseOrgError(message) {
456
+ // `sink_error`'s closed-refusal family comes first: it starts with
457
+ // the wire prefix but is prose, not `org:<domain>:<kind>`. BOTH
458
+ // verdict texts, for EVERY sink the leaf spells (the upload sink,
459
+ // the response sink) — the wording is pinned in the
460
+ // `ORG_*_REFUSAL` constants and the match is generic in the sink
461
+ // name only. The closed refusal is one text whatever the verdict
462
+ // (the verdict itself is what `retired` reports) → cancelled.
463
+ if (/^org: .+ is closed: the call was retired$/.test(message)) {
464
+ return new OrgCancelledError(message);
465
+ }
466
+ // The byte-budget refusal retires the call as `ResourceExhausted`,
467
+ // whose precise observable is `admission-denied` / `unavailable`
468
+ // (the leaf's frozen map) — typed as exactly that denial.
469
+ if (/^org: .+ is closed: the call's byte budget refused the item and retired the call$/.test(message)) {
470
+ return new OrgAdmissionDeniedError('unavailable', message);
471
+ }
472
+ if (message.startsWith(ORG_WIRE_PREFIX)) {
473
+ const rest = message.slice(ORG_WIRE_PREFIX.length);
474
+ const firstColon = rest.indexOf(':');
475
+ if (firstColon <= 0)
476
+ return null;
477
+ const domain = rest.slice(0, firstColon);
478
+ const afterDomain = rest.slice(firstColon + 1);
479
+ const secondColon = afterDomain.indexOf(':');
480
+ const token = secondColon === -1 ? afterDomain : afterDomain.slice(0, secondColon);
481
+ if (token.length === 0)
482
+ return null;
483
+ if (domain === 'admission_denied') {
484
+ return new OrgAdmissionDeniedError(coarseAdmissionReason(token), message);
485
+ }
486
+ if (domain !== 'rpc')
487
+ return null;
488
+ // The frozen `org:rpc:` wire-kind map — EVERY kind `rpc_wire_kind`
489
+ // (and the terminal seam's spellings) can emit classifies here, in
490
+ // lockstep with node's `classifyOrgError`. Classification is by
491
+ // TOKEN ONLY: the detail is human-facing and never parsed for
492
+ // semantics, so fields the wire string does not carry keep their
493
+ // zero value and `message` keeps the detail verbatim.
494
+ switch (token) {
495
+ case 'revoked':
496
+ return new OrgRevokedError(message);
497
+ case 'timeout':
498
+ return new OrgTimeoutError(message);
499
+ case 'cancelled':
500
+ return new OrgCancelledError(message);
501
+ case 'leader_lost':
502
+ case 'leader-lost':
503
+ return new OrgLeaderLostError('', message);
504
+ case 'session_lost':
505
+ case 'session-lost':
506
+ return new OrgSessionLostError(message);
507
+ case 'indeterminate':
508
+ return new OrgIndeterminateError(0, message);
509
+ // `server_error` is the frozen vocabulary's second name for the
510
+ // condition `refused` names (a server-answered failure status),
511
+ // and `capability_denied` is the capability gate's refusal of
512
+ // the call — both land in the refusal class, with no status the
513
+ // wire string carries.
514
+ case 'refused':
515
+ case 'server_error':
516
+ case 'capability_denied':
517
+ return new OrgRefusedError(0, message);
518
+ // The codec family: an item that did not survive the codec is
519
+ // malformed whichever direction failed.
520
+ case 'malformed':
521
+ case 'codec_encode':
522
+ case 'codec_decode':
523
+ return new OrgMalformedError(message);
524
+ // `no_route` / `transport` carry NO merits verdict — nobody
525
+ // refused the call — so they take the taxonomy's non-merits
526
+ // bucket and are never guessed into refused/admission.
527
+ case 'internal':
528
+ case 'no_route':
529
+ case 'transport':
530
+ return new OrgInternalError(message);
531
+ default:
532
+ return null;
533
+ }
534
+ }
535
+ // The leaf's own Display for an admission refusal: `RpcStatus
536
+ // 0x0009` carrying the single coarse reason byte as its body —
537
+ // exactly the mapping `map_rpc_error` performs one layer up. The
538
+ // seam renders that byte as the coarse token text verbatim
539
+ // (`denied` | `not-supported` | `unavailable`); anything
540
+ // undecodable is the least-informative bucket.
541
+ const refused = /^rpc: refused \(9\): ([\s\S]*)$/.exec(message);
542
+ if (refused)
543
+ return new OrgAdmissionDeniedError(coarseAdmissionReason(refused[1]), message);
544
+ return null;
545
+ }
546
+ const RTC_KINDS = {
547
+ iceTimeout: 'ice-timeout',
548
+ udpBlocked: 'udp-blocked',
549
+ channelClosed: 'channel-closed',
550
+ unsupported: 'rtc-unsupported',
551
+ };
552
+ const RPC_KINDS = {
553
+ refused: 'rpc-refused',
554
+ timeout: 'rpc-timeout',
555
+ sessionLost: 'session-lost',
556
+ leaderLost: 'leader-lost',
557
+ indeterminate: 'rpc-indeterminate',
558
+ malformed: 'rpc-malformed',
559
+ };
560
+ /** The exact text `RtcError`'s Rust `Display` produces. */
561
+ const ICE_TIMEOUT_DISPLAY = 'ICE did not connect inside the deadline (this does not establish that UDP is blocked)';
562
+ function rtcDisplay(failure) {
563
+ switch (failure.type) {
564
+ case 'iceTimeout':
565
+ return ICE_TIMEOUT_DISPLAY;
566
+ case 'udpBlocked':
567
+ return ("UDP appears blocked: the anchor's HTTPS bootstrap succeeded " +
568
+ `but a STUN binding to ${failure.evidence.probed} was unanswered`);
569
+ case 'channelClosed':
570
+ return `the DataChannel closed: ${failure.detail}`;
571
+ case 'unsupported':
572
+ return `this browser refused the attempt: ${failure.detail}`;
573
+ }
574
+ }
575
+ function rpcDisplay(failure) {
576
+ switch (failure.type) {
577
+ case 'refused':
578
+ return `refused (${failure.status}): ${failure.message}`;
579
+ case 'timeout':
580
+ return "the call's deadline elapsed";
581
+ case 'sessionLost':
582
+ return 'the session carrying the call went away';
583
+ case 'leaderLost':
584
+ return `the leader holding generation ${failure.generation} was replaced`;
585
+ case 'indeterminate':
586
+ return (`the local deadline of ${failure.deadlineMs}ms elapsed before the tab running ` +
587
+ 'the node answered; the remote operation may still have executed (it was not retried)');
588
+ case 'malformed':
589
+ return `the reply did not decode: ${failure.detail}`;
590
+ }
591
+ }
592
+ /**
593
+ * Re-type whatever the wasm boundary threw.
594
+ *
595
+ * `wasm-bindgen` hands us a `JsError` whose `message` is the Rust
596
+ * `Display` of `LeafError`, so the taxonomy is recovered by parsing
597
+ * exactly those prefixes. Anything else — a `DOMException` from
598
+ * `RTCPeerConnection`, a string, a plain object — becomes
599
+ * {@link UnknownLeafError} with the original value as `cause`.
600
+ */
601
+ export function fromWasmError(thrown) {
602
+ if (thrown instanceof LeafError)
603
+ return thrown;
604
+ const message = messageOf(thrown);
605
+ return parseLeafError(message) ?? new UnknownLeafError(message, thrown);
606
+ }
607
+ function messageOf(thrown) {
608
+ if (typeof thrown === 'string')
609
+ return thrown;
610
+ if (thrown instanceof Error)
611
+ return thrown.message;
612
+ if (thrown !== null && typeof thrown === 'object' && 'message' in thrown) {
613
+ const message = thrown.message;
614
+ if (typeof message === 'string')
615
+ return message;
616
+ }
617
+ return String(thrown);
618
+ }
619
+ /**
620
+ * Parse one `LeafError` `Display` string, or `null` if it is not one.
621
+ *
622
+ * Exported because it is the whole of the wasm→TS error contract and
623
+ * deserves to be unit-testable without a wasm module.
624
+ */
625
+ export function parseLeafError(message) {
626
+ // The org vocabulary first: a `0x0009` refusal would otherwise
627
+ // become a plain `rpc-refused` and lose that it was an admission
628
+ // denial.
629
+ const org = parseOrgError(message);
630
+ if (org !== null)
631
+ return org;
632
+ const rtc = after(message, 'rtc: ');
633
+ if (rtc !== null) {
634
+ const failure = parseRtcFailure(rtc);
635
+ return failure === null ? null : new RtcError(failure);
636
+ }
637
+ const rpc = after(message, 'rpc: ');
638
+ if (rpc !== null) {
639
+ const failure = parseRpcFailure(rpc);
640
+ return failure === null ? null : new RpcError(failure);
641
+ }
642
+ const wire = after(message, 'wire: ');
643
+ if (wire !== null)
644
+ return new WireError(wire);
645
+ const session = after(message, 'session: ');
646
+ if (session !== null)
647
+ return new SessionError(session);
648
+ const controlPlane = after(message, 'control plane: ');
649
+ if (controlPlane !== null)
650
+ return new ControlPlaneError(controlPlane);
651
+ const identity = after(message, 'identity: ');
652
+ if (identity !== null)
653
+ return new IdentityError(identity);
654
+ // Re-typed, not flattened to `unknown`: a page that catches this
655
+ // has a configuration to fix, and `kind` is what tells it apart
656
+ // from a transient ICE failure it might retry.
657
+ const iceConflict = after(message, 'ice configuration: ');
658
+ if (iceConflict !== null) {
659
+ const parsed = /^the iceServers entry (.+?) names this connection's peer RTC endpoint (.+?); a peer cannot be its own STUN server\./.exec(iceConflict);
660
+ if (parsed)
661
+ return new IceServerConflictError(parsed[1] ?? '', parsed[2] ?? '');
662
+ }
663
+ const notLeader = /^not the leader: this tab holds generation (\d+)(?:, the leader holds (\d+))?$/.exec(message);
664
+ if (notLeader) {
665
+ const presented = Number(notLeader[1]);
666
+ const current = notLeader[2] === undefined ? null : Number(notLeader[2]);
667
+ return new NotLeaderError(presented, current);
668
+ }
669
+ return null;
670
+ }
671
+ /** Parse the nested `RtcError` `Display`, or `null`. */
672
+ export function parseRtcFailure(text) {
673
+ if (text === ICE_TIMEOUT_DISPLAY)
674
+ return { type: 'iceTimeout' };
675
+ const blocked = /^UDP appears blocked: the anchor's HTTPS bootstrap succeeded but a STUN binding to (.+) was unanswered$/.exec(text);
676
+ if (blocked) {
677
+ const probed = blocked[1] ?? '';
678
+ const evidence = udpBlockedEvidence(true, true, probed);
679
+ // The regex matched the message the Rust side only emits when it
680
+ // holds the evidence, so `probed` is non-empty and this is
681
+ // `UdpBlockedEvidence`; an empty subject is not evidence.
682
+ if (evidence === null)
683
+ return null;
684
+ return { type: 'udpBlocked', evidence };
685
+ }
686
+ const closed = after(text, 'the DataChannel closed: ');
687
+ if (closed !== null)
688
+ return { type: 'channelClosed', detail: closed };
689
+ const unsupported = after(text, 'this browser refused the attempt: ');
690
+ if (unsupported !== null)
691
+ return { type: 'unsupported', detail: unsupported };
692
+ return null;
693
+ }
694
+ /** Parse the nested `RpcError` `Display`, or `null`. */
695
+ export function parseRpcFailure(text) {
696
+ const refused = /^refused \((\d+)\): ([\s\S]*)$/.exec(text);
697
+ if (refused) {
698
+ return { type: 'refused', status: Number(refused[1]), message: refused[2] ?? '' };
699
+ }
700
+ if (text === "the call's deadline elapsed")
701
+ return { type: 'timeout' };
702
+ if (text === 'the session carrying the call went away')
703
+ return { type: 'sessionLost' };
704
+ const leaderLost = /^the leader holding generation (\d+) was replaced$/.exec(text);
705
+ if (leaderLost)
706
+ return { type: 'leaderLost', generation: Number(leaderLost[1]) };
707
+ // A follower's own deadline, not the node's. The distinction is
708
+ // the whole point: `rpc-timeout` says the call died on the leaf's
709
+ // deadline, this says nobody knows — the request may be executing
710
+ // in the tab that holds the node, and this package does not
711
+ // reissue it.
712
+ const indeterminate = /^the local deadline of (\d+)ms elapsed before the tab running the node answered; the remote operation may still have executed \(it was not retried\)$/.exec(text);
713
+ if (indeterminate)
714
+ return { type: 'indeterminate', deadlineMs: Number(indeterminate[1]) };
715
+ const malformed = after(text, 'the reply did not decode: ');
716
+ if (malformed !== null)
717
+ return { type: 'malformed', detail: malformed };
718
+ return null;
719
+ }
720
+ /** `true` when this error carries the UDP-blocked evidence. */
721
+ export function isUdpBlocked(error) {
722
+ return error instanceof RtcError && error.failure.type === 'udpBlocked';
723
+ }
724
+ function after(text, prefix) {
725
+ return text.startsWith(prefix) ? text.slice(prefix.length) : null;
726
+ }