@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
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
|
+
}
|