@mcpwarp/ws-mixer 0.6.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/dist/conn.d.ts ADDED
@@ -0,0 +1,328 @@
1
+ /**
2
+ * MixerConn: the per-connection ws-mixer.v1 protocol engine. Owns the
3
+ * handshake, the read-dispatch loop, the control-priority + round-robin
4
+ * write scheduler (WIRE.md section 2.6 rule 3), keepalive and drain.
5
+ * The JS SDK is always the answering peer (client): it never calls
6
+ * OpenStream, only receives OPEN from the server.
7
+ *
8
+ * Mirrors `go/wsmixer/conn.go`, `dispatch.go`, `sched.go`, `keepalive.go` and
9
+ * `drain.go`, adapted to Node's single-threaded event loop (no goroutines:
10
+ * one write scheduler driven by a wake/notify queue instead of channels).
11
+ */
12
+ import { EventEmitter } from "node:events";
13
+ import { type AgentInfo, type ControlMessage, type DrainMsg, type WelcomeMsg } from "./control.js";
14
+ import { WsMixerError } from "./errors.js";
15
+ import { MixerStream } from "./stream.js";
16
+ /** Structural subset of `ws`'s WebSocket that MixerConn depends on (keeps the transport swappable). */
17
+ export interface WSLike {
18
+ readonly readyState: number;
19
+ send(data: Uint8Array, cb?: (err?: Error) => void): void;
20
+ close(code?: number, reason?: string): void;
21
+ terminate?(): void;
22
+ on(event: "message", listener: (data: Uint8Array | Buffer, isBinary: boolean) => void): void;
23
+ on(event: "close", listener: (code: number, reason: Buffer) => void): void;
24
+ on(event: "error", listener: (err: Error) => void): void;
25
+ }
26
+ export interface ConnOptions {
27
+ token: string;
28
+ agent: AgentInfo;
29
+ meta?: unknown;
30
+ window?: number;
31
+ maxStreams?: number;
32
+ capabilities?: string[];
33
+ /** Timeout waiting for `welcome` after sending `hello`. Default 10000ms per WIRE.md section 2.9. */
34
+ helloTimeoutMs?: number;
35
+ }
36
+ /**
37
+ * Fires (and clears) once for every welcome/close event; used for the
38
+ * `welcome`-then-timeout handshake race and to unblock `close()`'s grace wait.
39
+ */
40
+ export declare interface MixerConn {
41
+ on(event: "welcome", listener: (welcome: WelcomeMsg) => void): this;
42
+ on(event: "stream", listener: (stream: MixerStream) => void): this;
43
+ on(event: "app", listener: (body: Record<string, unknown>) => void): this;
44
+ on(event: "drain", listener: (msg: DrainMsg) => void): this;
45
+ on(event: "pong", listener: (info: {
46
+ id: number;
47
+ rttMs: number;
48
+ }) => void): this;
49
+ on(event: "error", listener: (err: WsMixerError) => void): this;
50
+ on(event: "fatal", listener: (err: WsMixerError) => void): this;
51
+ on(event: "close", listener: (info: {
52
+ wsCode: number;
53
+ errorCode?: number;
54
+ message: string;
55
+ closeReason?: string;
56
+ }) => void): this;
57
+ /** Fires when a `'stream'`/`'app'`/`'drain'` listener throws or rejects; delivery continues with the next queued event regardless (see stats().handlerErrors). */
58
+ on(event: "handlerError", listener: (info: {
59
+ event: string;
60
+ error: Error;
61
+ }) => void): this;
62
+ }
63
+ /** One live, handshaken (or handshaking) ws-mixer connection. */
64
+ export declare class MixerConn extends EventEmitter {
65
+ #private;
66
+ private readonly ws;
67
+ private readonly opts;
68
+ private closed;
69
+ private handshakeDone;
70
+ session: string;
71
+ peerWindow: number;
72
+ ourWindow: number;
73
+ maxStreams: number;
74
+ pingIntervalMs: number;
75
+ pingTimeoutMs: number;
76
+ welcomeMeta: unknown;
77
+ private draining;
78
+ /** The `last_stream_id` from the most recently received `drain`; set only once draining. */
79
+ private lastStreamId?;
80
+ private readonly streams;
81
+ private highestOpened;
82
+ /**
83
+ * Minimal "ignore and count" counters (item 11 of the review: full
84
+ * escalation/STREAM_LIMIT bucketing is intentionally out of scope for v1 --
85
+ * see README.md and docs/DESIGN.md). Exposed via `stats()`.
86
+ */
87
+ private readonly counters;
88
+ /** Snapshot of the counters above. */
89
+ stats(): Readonly<typeof this.counters>;
90
+ private readonly controlQueue;
91
+ private readonly rotation;
92
+ private readonly inRotation;
93
+ private readonly outbox;
94
+ private wakeWaiters;
95
+ private writerRunning;
96
+ /** Resolved once the stream table is empty; used by close() instead of polling. */
97
+ private idleWaiters;
98
+ private readonly deliveryQueue;
99
+ private deliveryRunning;
100
+ private readonly stream0Bucket;
101
+ private nextPingId;
102
+ /** Watermark: every id below this has been acked (or pruned as stale) at least once. Mirrors go/wsmixer's Conn.lowestUnacked. */
103
+ private lowestUnacked;
104
+ private lastPongAt;
105
+ private readonly outstandingPings;
106
+ private pingTimer;
107
+ private watchdogTimer;
108
+ private handshakeTimer;
109
+ private jitterTimer;
110
+ /**
111
+ * The most recent pre-welcome `'error'` event's message, recorded so
112
+ * `onSocketClose`'s report -- the sole reporter, see its own doc comment
113
+ * -- can use it as `message` when `ws`'s own close carries no reason of
114
+ * its own.
115
+ */
116
+ private pendingHandshakeErrorMessage;
117
+ constructor(ws: WSLike, opts: ConnOptions);
118
+ /** Sends `hello` and resolves once `welcome` is validated, or rejects (a WsMixerError, `fatal` set for UNSUPPORTED). */
119
+ handshake(): Promise<WelcomeMsg>;
120
+ private applyWelcome;
121
+ private onMessage;
122
+ private handleDispatchError;
123
+ private dispatchFrame;
124
+ private dispatchControl;
125
+ /**
126
+ * Queues one `'stream'`/`'app'`/`'drain'` event for in-order, async
127
+ * delivery, mirroring go/wsmixer's deliveryLoop: the read/dispatch path
128
+ * above never blocks on application code, but all three still fire in
129
+ * wire order, off a single loop, one at a time. Handlers registered via
130
+ * `on('stream'|'app'|'drain', ...)` must not block for long -- a handler
131
+ * that never returns/resolves permanently wedges this loop, exactly like
132
+ * the Go side. Throws ConnError(ENHANCE_YOUR_CALM) if the bounded queue
133
+ * (`max(128, max_streams + 64)`) is full, which the caller (dispatchFrame/
134
+ * dispatchControl, inside onMessage's try/catch) turns into fail().
135
+ *
136
+ * Refuses once `this.closed` (CLIENT-SDK.md's "Handler delivery" row):
137
+ * `dispatchFrame`/`dispatchControl` -- the only callers -- always run
138
+ * before whatever ends the connection sets `this.closed` (every
139
+ * fail()/close()/handlePeerError path runs teardownConn synchronously,
140
+ * `error` is always the last message on the wire), so nothing legitimate
141
+ * should ever reach here once closed; this is the defensive backstop that
142
+ * makes runDeliveryLoop's post-close drain below provably bounded and
143
+ * terminating regardless.
144
+ */
145
+ private enqueueDelivery;
146
+ /**
147
+ * Drains `deliveryQueue` until empty -- deliberately NOT gated on
148
+ * `!this.closed` (CLIENT-SDK.md's "Handler delivery" row): an event
149
+ * already queued when the connection ends is still owed to its handler
150
+ * (`error` is the last message on the wire, so a stream OPEN/`app`/`drain`
151
+ * queued ahead of it arrived before the connection ended), so this loop
152
+ * finishes flushing that backlog even after `teardownConn` has already set
153
+ * `this.closed` and emitted `'close'` -- a handler MAY therefore run
154
+ * shortly after `MixerClient.close()`/`MixerConn.close()`'s own promise has
155
+ * already resolved (see those methods' doc comments and README). `if
156
+ * (!ev) break` is what actually ends the loop; since `enqueueDelivery`
157
+ * above refuses once closed, the queue can only shrink from here, so this
158
+ * always terminates.
159
+ */
160
+ private runDeliveryLoop;
161
+ /**
162
+ * Invokes every listener for `event` in registration order, awaiting each
163
+ * one's return value before starting the next -- unlike EventEmitter's own
164
+ * synchronous `emit`, which fires listeners back to back without waiting
165
+ * for a returned Promise to settle.
166
+ *
167
+ * A listener that throws synchronously or returns a rejected promise is
168
+ * caught here, counted (stats().handlerErrors) and surfaced via a guarded
169
+ * `'handlerError'` emit -- never an unhandled rejection, and never fatal
170
+ * to delivery: the next queued stream/app/drain event still runs.
171
+ */
172
+ private emitOrdered;
173
+ /**
174
+ * Queues one control-channel frame and returns a promise that resolves
175
+ * once `ws.send`'s callback confirms it was actually written (or rejects
176
+ * with the connection's terminal error if the conn fails first) --
177
+ * mirroring how `sendData()`'s per-stream outbox already works.
178
+ */
179
+ private enqueueControl;
180
+ /**
181
+ * Sends a control message (hello/ping/pong/drain/app/error) as stream-0
182
+ * DATA, queued ahead of any DATA. Resolves once the frame is actually
183
+ * written to the socket.
184
+ */
185
+ sendControl(msg: ControlMessage): Promise<void>;
186
+ /**
187
+ * Sends a control message at the head of the control queue, ahead of
188
+ * anything already queued (item 6: "A pong MUST ... jump ahead of queued
189
+ * DATA" -- and ahead of other queued control frames too).
190
+ */
191
+ private sendControlPriority;
192
+ /** Backs `streamHost.sendControlFrame`: send a control-priority frame (WINDOW/CLOSE/RESET) immediately. */
193
+ private sendControlFrame;
194
+ /** Backs `streamHost.sendData`: enqueue a DATA chunk for round-robin transmission. */
195
+ private sendData;
196
+ /** Backs `streamHost.retireStream`: retire a fully-closed stream's id. */
197
+ private retireStream;
198
+ /** Resolves every pending close()'s wait for the stream table to empty, instead of polling. */
199
+ private notifyIdle;
200
+ private waitForEmpty;
201
+ private markReady;
202
+ private wake;
203
+ private waitForWork;
204
+ private nextChunk;
205
+ private startWriter;
206
+ private runWriter;
207
+ /**
208
+ * Writes one frame and resolves once `ws.send`'s callback reports it
209
+ * flushed. This is how rule 2 (§2.6, "gate the write loop on the socket
210
+ * send buffer") is satisfied without polling `bufferedAmount` on a timer:
211
+ * `ws`'s `send(data, cb)` callback fires only once the chunk has actually
212
+ * been handed to the socket, so awaiting it before writing the next frame
213
+ * already keeps exactly one write in flight and applies backpressure for
214
+ * free (runWriter's loop awaits writeRaw() before picking the next chunk).
215
+ */
216
+ private writeRaw;
217
+ /** Resolves once the `app` frame is actually written to the socket, or rejects with the connection's terminal error if it fails first. */
218
+ sendApp(body: Record<string, unknown>): Promise<void>;
219
+ private startKeepalive;
220
+ private sendPing;
221
+ /**
222
+ * Pong watermark scheme, mirroring go/wsmixer/dispatch.go's handlePong:
223
+ * every id below lowestUnacked has been acked (or pruned as stale) at
224
+ * least once, and no id >= nextPingId has ever been sent.
225
+ * - id >= nextPingId: never sent -> PROTOCOL_ERROR, connection-fatal.
226
+ * - id < lowestUnacked, or in-range but already absent from the map
227
+ * (already acked, or pruned by the watchdog as stale/late): a
228
+ * duplicate or late pong -- tolerated, just counted.
229
+ */
230
+ private handlePong;
231
+ private checkWatchdog;
232
+ /**
233
+ * Shared teardown: WS close `4000 + code`, reject everything outstanding,
234
+ * emit `'fatal'`/`'error'` (guarded) then `'close'`, synchronously and in
235
+ * that order every time. `sendErrorFrame` controls whether an
236
+ * `error{code,message}` control frame is sent first -- true for a
237
+ * locally-detected failure (fail()), false when the peer already sent its
238
+ * own `error` and WIRE.md section 2.7 forbids replying with another
239
+ * one (handlePeerError()). Either way `this.closed` is set *before*
240
+ * `ws.close()` is called, so the peer's echo of this close (or any close
241
+ * frame it happens to send around the same time) is ignored by
242
+ * onSocketClose below rather than reported as `closeReason` -- per
243
+ * CLIENT-SDK.md's `closeReason` row, an outgoing reason this side sent is
244
+ * never legitimate `closeReason` data, and there is nothing to gain by
245
+ * waiting for the peer's own close frame here: WIRE.md section 2.7
246
+ * already allows closing on `error` "without reading the close frame that
247
+ * followed".
248
+ */
249
+ private teardownConn;
250
+ /**
251
+ * Connection-fatal failure path: error{code,message} on stream 0, WS close
252
+ * 4000+code, teardown. Three steps, in order (WIRE.md section 2.8).
253
+ *
254
+ * `streamErrorFactory`, when given, overrides the error every live stream
255
+ * is torn down with (`err` is still used for the connection-level frame/
256
+ * close/rejects) -- used by MixerClient when it fails a *superseded*
257
+ * (drained) conn: the streams on it aren't erroring, the conn they were
258
+ * riding on is being replaced, so each gets a stream-scoped CANCEL
259
+ * ("connection drained") instead of the connection's own NO_ERROR.
260
+ *
261
+ * Synchronous, but any stream/app/drain event still queued for delivery at
262
+ * this point is not: runDeliveryLoop keeps flushing it after this returns
263
+ * (see close()'s doc comment above for why).
264
+ */
265
+ fail(err: WsMixerError, streamErrorFactory?: (streamId: number) => WsMixerError): void;
266
+ /**
267
+ * The peer sent `error{code,message}`: record it and close with
268
+ * `4000 + code` immediately, without waiting for the peer to do anything
269
+ * else -- mirrors go/wsmixer/dispatch.go's handlePeerError. `error` is
270
+ * always the last message on the wire (WIRE.md section 2.7), so
271
+ * there is nothing left to negotiate.
272
+ */
273
+ private handlePeerError;
274
+ /**
275
+ * Graceful client shutdown: drain{client_requested}, brief grace period,
276
+ * then close(NO_ERROR). This resolves once teardownConn's synchronous
277
+ * steps are done (frame/WS close/rejects/'close' emitted) -- it does NOT
278
+ * wait for runDeliveryLoop to finish flushing whatever stream/app/drain
279
+ * backlog was still queued at that point. A handler for an event received
280
+ * before this close MAY therefore still be invoked shortly after this
281
+ * promise (and 'close') has already resolved/fired (CLIENT-SDK.md's
282
+ * "Handler delivery" row).
283
+ */
284
+ close(graceMs?: number): Promise<void>;
285
+ /**
286
+ * A close frame this side actually *received* from `ws`'s 'close' event.
287
+ * `code`/`reason` here are always the peer's, per `ws`'s own contract
288
+ * (`receiverOnConclude` in `ws/lib/websocket.js` only ever updates
289
+ * `_closeCode`/`_closeMessage` from a frame it parsed off the wire, never
290
+ * from what this side sent via `.close()`) -- so whenever `this.closed` is
291
+ * already `true`, this is a close frame arriving after teardownConn()
292
+ * already ran and reported (never our own echoed close winning a race),
293
+ * and is correctly ignored rather than folded in after the fact.
294
+ *
295
+ * Pre-welcome, this is also the SOLE reporter for a transport failure
296
+ * before `welcome` -- measured against real `ws` (8.21): a TCP reset,
297
+ * half-close or peer close frame after the 101 delivers a bare 'close'
298
+ * with no 'error' at all, and a protocol-level failure `ws` itself
299
+ * detects (an invalid frame: 1002, an oversized message: 1009, ...)
300
+ * delivers 'error' immediately followed by 'close' a macrotask or more
301
+ * later -- 'close' is never racing 'error' for which one wins; it always
302
+ * arrives after it, if it arrives at all. `onSocketError` below therefore
303
+ * never finalizes anything itself, only records the error's message here
304
+ * for `message` to fall back on when the close carries no reason of its
305
+ * own -- so this always reports the *actual* close code `ws` delivers
306
+ * (1006, or whatever real close code it sent), never a wsCode fabricated
307
+ * for a close that never happened.
308
+ */
309
+ private onSocketClose;
310
+ /**
311
+ * Pre-welcome, `'error'` never arrives in the same microtask as (or after)
312
+ * a 'close' that never comes -- see onSocketClose's own doc comment for
313
+ * what real `ws` actually does. So this deliberately does NOT reject or
314
+ * finalize the handshake itself; it just records the error's message for
315
+ * onSocketClose (the sole reporter) to use as `message` when the close
316
+ * that follows carries no reason of its own. If `'close'` somehow never
317
+ * follows at all (a transport that violates `ws`'s own contract), nothing
318
+ * here is left waiting on it: `handshake()`'s own hello/welcome timeout
319
+ * (`handshakeTimer`, 10s default) still fires and produces the one report
320
+ * (`wsCode` 4001) regardless -- deliberately the only fallback for that
321
+ * case, not a second mechanism grafted on here.
322
+ */
323
+ private onSocketError;
324
+ private stopTimers;
325
+ private rejectOutstanding;
326
+ isDraining(): boolean;
327
+ liveStreamCount(): number;
328
+ }
@@ -0,0 +1,74 @@
1
+ export interface AgentInfo {
2
+ sdk: string;
3
+ sdk_version: string;
4
+ runtime?: string;
5
+ os?: string;
6
+ }
7
+ export interface ServerInfo {
8
+ name?: string;
9
+ version?: string;
10
+ node?: string;
11
+ }
12
+ export interface HelloMsg {
13
+ t: "hello";
14
+ v: number;
15
+ token: string;
16
+ agent: AgentInfo;
17
+ window?: number;
18
+ max_streams?: number;
19
+ capabilities?: string[];
20
+ meta?: unknown;
21
+ }
22
+ export interface WelcomeMsg {
23
+ t: "welcome";
24
+ v: number;
25
+ session: string;
26
+ window: number;
27
+ max_streams: number;
28
+ ping_interval: number;
29
+ ping_timeout: number;
30
+ server?: ServerInfo;
31
+ capabilities?: string[];
32
+ meta?: unknown;
33
+ }
34
+ export interface PingMsg {
35
+ t: "ping";
36
+ id: number;
37
+ ts?: number;
38
+ }
39
+ export interface PongMsg {
40
+ t: "pong";
41
+ id: number;
42
+ ts?: number;
43
+ }
44
+ export interface DrainMsg {
45
+ t: "drain";
46
+ reason: string;
47
+ last_stream_id: number;
48
+ deadline_ms?: number;
49
+ retry_after_ms?: number;
50
+ message?: string;
51
+ }
52
+ export interface ErrorMsg {
53
+ t: "error";
54
+ code: number;
55
+ message: string;
56
+ stream_id?: number;
57
+ last_stream_id?: number;
58
+ }
59
+ export interface AppMsg {
60
+ t: "app";
61
+ body: Record<string, unknown>;
62
+ }
63
+ export type ControlMessage = HelloMsg | WelcomeMsg | PingMsg | PongMsg | DrainMsg | ErrorMsg | AppMsg;
64
+ /** Reports whether reason is one of the documented drain reasons. An unknown reason is tolerated at runtime. */
65
+ export declare function knownDrainReason(reason: string): boolean;
66
+ /**
67
+ * Validates and decodes one stream-0 DATA payload (already UTF-8 decoded to
68
+ * a string). Implements WIRE.md section 2.7's envelope table plus every
69
+ * message type's per-field checks. Every validation failure is a ConnError
70
+ * with PROTOCOL_ERROR (or ENHANCE_YOUR_CALM for the oversize case).
71
+ */
72
+ export declare function parseControl(raw: string | Uint8Array): ControlMessage;
73
+ /** Serializes a control message to its stream-0 DATA payload bytes. */
74
+ export declare function encodeControl(msg: ControlMessage): Uint8Array;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * ws-mixer.v1 error code table (WIRE.md section 2.8). Shared by stream
3
+ * RESET frames and the connection-level `error` control message.
4
+ *
5
+ * 0x0000_0000-0x0000_0fff is reserved for ws-mixer; codes >= 0x1000_0000 are
6
+ * free for the layer above and are never produced by this package. Unknown
7
+ * codes received on the wire are treated as INTERNAL_ERROR (see codeName).
8
+ */
9
+ export declare const ErrorCode: {
10
+ readonly NO_ERROR: 0;
11
+ readonly PROTOCOL_ERROR: 1;
12
+ readonly INTERNAL_ERROR: 2;
13
+ readonly FLOW_CONTROL_ERROR: 3;
14
+ readonly FRAME_SIZE_ERROR: 4;
15
+ readonly STREAM_CLOSED: 5;
16
+ readonly REFUSED_STREAM: 6;
17
+ readonly CANCEL: 7;
18
+ readonly STREAM_LIMIT: 8;
19
+ readonly ENHANCE_YOUR_CALM: 9;
20
+ readonly UNSUPPORTED: 10;
21
+ readonly UNAUTHORIZED: 11;
22
+ readonly GOING_AWAY: 12;
23
+ readonly KEEPALIVE_TIMEOUT: 13;
24
+ readonly APPLICATION_CLOSE: 14;
25
+ };
26
+ export type ErrorCodeValue = (typeof ErrorCode)[keyof typeof ErrorCode];
27
+ /** Renders an error code's wire name, e.g. "FLOW_CONTROL_ERROR". Unrecognized codes render "INTERNAL_ERROR". */
28
+ export declare function codeName(code: number): string;
29
+ /** Looks up an error code by its wire name (e.g. "STREAM_LIMIT"). Returns undefined if unknown. */
30
+ export declare function parseErrorCode(name: string): number | undefined;
31
+ /** ws_close = 4000 + error_code, with NO_ERROR mapping to 1000 (WIRE.md section 2.8). */
32
+ export declare function closeCode(code: number): number;
33
+ /**
34
+ * WsMixerError is the single error type this package throws or emits.
35
+ * `fatal` marks a close code the client SDK must never reconnect after
36
+ * (UNSUPPORTED always; UNAUTHORIZED once it's actually final -- after
37
+ * `welcome`, or before `welcome` once the one provider refresh-retry has
38
+ * already been spent (or immediately, with a static token that has nothing
39
+ * to refresh) -- see client.ts's connectOnce/dialAndHandshakeOnce; and the
40
+ * client's own auth/handshake failures).
41
+ */
42
+ export declare class WsMixerError extends Error {
43
+ readonly code: number;
44
+ name: string;
45
+ readonly fatal: boolean;
46
+ readonly streamId?: number;
47
+ /**
48
+ * The peer's observed WS close-frame code, set only when this error was
49
+ * built from an actually-observed close frame (e.g. MixerConn.onSocketClose)
50
+ * rather than a locally-raised protocol violation.
51
+ */
52
+ readonly wsCode?: number;
53
+ /** The peer's observed WS close-frame reason, verbatim, under the same condition as `wsCode`. */
54
+ readonly closeReason?: string;
55
+ constructor(code: number, message: string, opts?: {
56
+ fatal?: boolean;
57
+ streamId?: number;
58
+ wsCode?: number;
59
+ closeReason?: string;
60
+ });
61
+ get codeName(): string;
62
+ toString(): string;
63
+ }
64
+ /** A connection-fatal error: desynchronizes shared connection state. Always error{code} + WS close. */
65
+ export declare class ConnError extends WsMixerError {
66
+ readonly lastStreamId?: number;
67
+ constructor(code: number, message: string, opts?: {
68
+ streamId?: number;
69
+ lastStreamId?: number;
70
+ fatal?: boolean;
71
+ });
72
+ }
73
+ /** A stream-scoped error: produces RESET(code, message) on one stream; the connection stays up. */
74
+ export declare class StreamError extends WsMixerError {
75
+ constructor(code: number, streamId: number, message: string);
76
+ }
77
+ /**
78
+ * A token provider's explicit signal that it could not OBTAIN a token for a
79
+ * temporary reason (a laptop waking before the network is back, the auth
80
+ * server briefly unreachable while refreshing an expired token) -- as
81
+ * opposed to a genuinely fatal provider failure (a malformed credential, a
82
+ * permanently revoked client). Throwing/rejecting with an instance of this
83
+ * (directly, or wrapped via `cause`) is how a provider opts a dial failure
84
+ * into client.ts's ordinary non-fatal dial-retry path instead of the
85
+ * fatal-by-default one every other provider throw/reject still gets --
86
+ * detection is `instanceof` only, deliberately not duck-typed on any
87
+ * property or method, so an accidental match on some unrelated library's
88
+ * error can never turn a genuinely fatal provider failure into an endless
89
+ * retry loop.
90
+ */
91
+ export declare class TokenUnavailableError extends Error {
92
+ name: string;
93
+ constructor(message?: string, options?: {
94
+ cause?: unknown;
95
+ });
96
+ }
@@ -0,0 +1,52 @@
1
+ export declare const FrameType: {
2
+ readonly OPEN: 0;
3
+ readonly DATA: 1;
4
+ readonly WINDOW: 2;
5
+ readonly CLOSE: 3;
6
+ readonly RESET: 4;
7
+ };
8
+ export type FrameTypeValue = (typeof FrameType)[keyof typeof FrameType];
9
+ /** Renders a frame type's wire name, or "UNKNOWN(0xNN)" outside the v1 set. */
10
+ export declare function frameTypeName(t: number): string;
11
+ /** Largest legal WebSocket message: 8-byte header plus a 64 KiB payload. */
12
+ export declare const MAX_MESSAGE_SIZE: number;
13
+ /** Control-channel (stream 0) message size cap. */
14
+ export declare const MAX_STREAM_ZERO_PAYLOAD = 16384;
15
+ /** Recommended DATA chunk size: a sender-side default, not a wire limit. */
16
+ export declare const MAX_CHUNK = 16384;
17
+ /** Largest legal cumulative send-credit window: 2^31-1 (WIRE.md section 2.6 decision 1). */
18
+ export declare const MAX_SEND_WINDOW = 2147483647;
19
+ /** One decoded ws-mixer mux frame: the 8-byte header plus its payload. */
20
+ export interface Frame {
21
+ type: number;
22
+ flags: number;
23
+ streamId: number;
24
+ payload: Uint8Array;
25
+ }
26
+ /**
27
+ * Decodes one WebSocket message into a Frame per WIRE.md sections
28
+ * 2.2-2.4. Throws ConnError (connection-fatal) or StreamError (scoped to the
29
+ * frame's stream id).
30
+ */
31
+ export declare function decodeFrame(msg: Uint8Array): Frame;
32
+ /** Returns the 4-byte increment carried by a WINDOW frame. Caller must have checked f.type === FrameType.WINDOW. */
33
+ export declare function windowIncrement(f: Frame): number;
34
+ /** Returns the error code carried by a RESET frame. Caller must have checked f.type === FrameType.RESET. */
35
+ export declare function resetCode(f: Frame): number;
36
+ /**
37
+ * Returns the (UTF-8 sanitized) message carried by a RESET frame. Invalid
38
+ * UTF-8 is replaced with U+FFFD by TextDecoder, matching the wire spec's
39
+ * "replace chars, do not kill the connection" rule.
40
+ */
41
+ export declare function resetMessage(f: Frame): string;
42
+ /** Serializes a Frame to its wire form: 8-byte header followed by the payload. */
43
+ export declare function encodeFrame(f: {
44
+ type: number;
45
+ streamId: number;
46
+ payload?: Uint8Array;
47
+ }): Uint8Array;
48
+ export declare function encodeOpen(streamId: number): Uint8Array;
49
+ export declare function encodeData(streamId: number, payload: Uint8Array): Uint8Array;
50
+ export declare function encodeWindow(streamId: number, increment: number): Uint8Array;
51
+ export declare function encodeClose(streamId: number): Uint8Array;
52
+ export declare function encodeReset(streamId: number, code: number, message?: string): Uint8Array;