@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/CHANGELOG.md +357 -0
- package/LICENSE +202 -0
- package/README.md +431 -0
- package/dist/client.d.ts +453 -0
- package/dist/conn.d.ts +328 -0
- package/dist/control.d.ts +74 -0
- package/dist/errors.d.ts +96 -0
- package/dist/frame.d.ts +52 -0
- package/dist/index.cjs +2866 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +2806 -0
- package/dist/index.js.map +1 -0
- package/dist/stream.d.ts +124 -0
- package/dist/util.d.ts +3 -0
- package/package.json +59 -0
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;
|
package/dist/errors.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/frame.d.ts
ADDED
|
@@ -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;
|