@kyneta/websocket-transport 1.4.0 → 1.5.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/browser.d.ts +2 -2
- package/dist/browser.js +1 -1
- package/dist/bun.d.ts +2 -2
- package/dist/bun.d.ts.map +1 -1
- package/dist/bun.js.map +1 -1
- package/dist/{client-transport-DIZ-LJxs.js → client-transport-B2V7s2VP.js} +21 -6
- package/dist/client-transport-B2V7s2VP.js.map +1 -0
- package/dist/{client-transport-CKjXedwS.d.ts → client-transport-D3tYQYrS.d.ts} +2 -2
- package/dist/{client-transport-CKjXedwS.d.ts.map → client-transport-D3tYQYrS.d.ts.map} +1 -1
- package/dist/server.d.ts +4 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +22 -16
- package/dist/server.js.map +1 -1
- package/dist/{types-CJAcr1Df.d.ts → types-c1S_xIRG.d.ts} +12 -5
- package/dist/types-c1S_xIRG.d.ts.map +1 -0
- package/package.json +11 -10
- package/src/__tests__/client-transport.test.ts +5 -5
- package/src/bun-websocket.ts +3 -3
- package/src/client-transport.ts +37 -11
- package/src/connection.ts +37 -12
- package/src/server-transport.ts +2 -17
- package/src/types.ts +16 -11
- package/dist/client-transport-DIZ-LJxs.js.map +0 -1
- package/dist/types-CJAcr1Df.d.ts.map +0 -1
package/dist/browser.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as SocketReadyState, c as WebSocketConstructor, d as WebsocketClientState, f as WebsocketClientStateTransition, i as Socket, l as WebSocketLike, o as TransitionListener, r as READY_STATE, s as WebSocketCloseEvent, t as DisconnectReason, u as WebSocketMessageEvent } from "./types-
|
|
2
|
-
import { a as createWebsocketClient, i as WebsocketClientTransport, n as WebsocketClientLifecycleEvents, r as WebsocketClientOptions, t as DEFAULT_FRAGMENT_THRESHOLD } from "./client-transport-
|
|
1
|
+
import { a as SocketReadyState, c as WebSocketConstructor, d as WebsocketClientState, f as WebsocketClientStateTransition, i as Socket, l as WebSocketLike, o as TransitionListener, r as READY_STATE, s as WebSocketCloseEvent, t as DisconnectReason, u as WebSocketMessageEvent } from "./types-c1S_xIRG.js";
|
|
2
|
+
import { a as createWebsocketClient, i as WebsocketClientTransport, n as WebsocketClientLifecycleEvents, r as WebsocketClientOptions, t as DEFAULT_FRAGMENT_THRESHOLD } from "./client-transport-D3tYQYrS.js";
|
|
3
3
|
import { ReconnectOptions } from "@kyneta/transport";
|
|
4
4
|
import { Program } from "@kyneta/machine";
|
|
5
5
|
|
package/dist/browser.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as READY_STATE, n as WebsocketClientTransport, r as createWebsocketClient, s as createWsClientProgram, t as DEFAULT_FRAGMENT_THRESHOLD } from "./client-transport-
|
|
1
|
+
import { i as READY_STATE, n as WebsocketClientTransport, r as createWebsocketClient, s as createWsClientProgram, t as DEFAULT_FRAGMENT_THRESHOLD } from "./client-transport-B2V7s2VP.js";
|
|
2
2
|
export { DEFAULT_FRAGMENT_THRESHOLD, READY_STATE, WebsocketClientTransport, createWebsocketClient, createWsClientProgram };
|
package/dist/bun.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as SocketReadyState, i as Socket, t as DisconnectReason } from "./types-
|
|
1
|
+
import { a as SocketReadyState, i as Socket, t as DisconnectReason } from "./types-c1S_xIRG.js";
|
|
2
2
|
import { ServerWebSocket } from "bun";
|
|
3
3
|
|
|
4
4
|
//#region src/bun-websocket.d.ts
|
|
@@ -15,7 +15,7 @@ import { ServerWebSocket } from "bun";
|
|
|
15
15
|
*/
|
|
16
16
|
type BunWebsocketData = {
|
|
17
17
|
handlers: {
|
|
18
|
-
onMessage?: (data: Uint8Array | string) => void;
|
|
18
|
+
onMessage?: (data: Uint8Array<ArrayBuffer> | string) => void;
|
|
19
19
|
onClose?: (code: number, reason: string) => void;
|
|
20
20
|
};
|
|
21
21
|
};
|
package/dist/bun.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bun.d.ts","names":[],"sources":["../src/bun-websocket.ts"],"mappings":";;;;;;AA+BA;;;;;;;;;KAAY,gBAAA;EACV,QAAA;IACE,SAAA,IAAa,IAAA,EAAM,UAAA;
|
|
1
|
+
{"version":3,"file":"bun.d.ts","names":[],"sources":["../src/bun-websocket.ts"],"mappings":";;;;;;AA+BA;;;;;;;;;KAAY,gBAAA;EACV,QAAA;IACE,SAAA,IAAa,IAAA,EAAM,UAAA,CAAW,WAAA;IAC9B,OAAA,IAAW,IAAA,UAAc,MAAA;EAAA;AAAA;;;;;;;;;;;;;;AAyG7B;;;;;;;;;;;;;;;;;;;iBAjEgB,gBAAA,CACd,EAAA,EAAI,eAAA,CAAgB,gBAAA,IACnB,MAAA;;;;;;;;;;;;;;;;;;;;;;;;iBA+Da,0BAAA,CAA2B,SAAA;EACzC,gBAAA,GAAmB,IAAA;IAAQ,MAAA,EAAQ,MAAA;EAAA;IAAe,KAAA;EAAA;AAAA;WAGvC,eAAA,CAAgB,gBAAA;cAInB,eAAA,CAAgB,gBAAA,GAAiB,GAAA,WACvB,WAAA,GAAc,MAAA;YAUpB,eAAA,CAAgB,gBAAA,GAAiB,IAAA,UAAc,MAAA;AAAA"}
|
package/dist/bun.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bun.js","names":[],"sources":["../src/bun-websocket.ts"],"sourcesContent":["// bun-websocket — Bun-specific Websocket wrapper for @kyneta/websocket-transport.\n//\n// Provides a wrapper to adapt Bun's ServerWebSocket to the Socket interface\n// expected by WebsocketServerTransport.\n//\n// Bun's WebSocket API is callback-based at the server level (not per-socket),\n// so we bridge that gap by storing handlers in ws.data.\n//\n// Ported from @loro-extended/adapter-websocket's bun.ts with kyneta\n// naming conventions applied.\n\n/// <reference types=\"bun-types\" />\n\nimport type { ServerWebSocket } from \"bun\"\nimport type { Socket, SocketReadyState } from \"./types.js\"\n\n// ---------------------------------------------------------------------------\n// BunWebsocketData — stored in ws.data for per-socket handler callbacks\n// ---------------------------------------------------------------------------\n\n/**\n * Data structure stored in `ws.data` for handler callbacks.\n * Use this type when defining your `Bun.serve()` generic.\n *\n * @example\n * ```typescript\n * Bun.serve<BunWebsocketData>({\n * websocket: { ... }\n * })\n * ```\n */\nexport type BunWebsocketData = {\n handlers: {\n onMessage?: (data: Uint8Array | string) => void\n onClose?: (code: number, reason: string) => void\n }\n}\n\n// ---------------------------------------------------------------------------\n// wrapBunWebsocket\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap Bun's `ServerWebSocket` to match the `Socket` interface.\n *\n * Bun's WebSocket API uses server-level callbacks (`websocket: { message, close }`)\n * rather than per-socket event handlers. This wrapper bridges that gap by\n * storing handlers in `ws.data` and having the server-level callbacks delegate\n * to them.\n *\n * @example\n * ```typescript\n * import { WebsocketServerTransport } from \"@kyneta/websocket-transport/server\"\n * import { wrapBunWebsocket, type BunWebsocketData } from \"@kyneta/websocket-transport/bun\"\n *\n * const serverAdapter = new WebsocketServerTransport()\n *\n * Bun.serve<BunWebsocketData>({\n * websocket: {\n * open(ws) {\n * const socket = wrapBunWebsocket(ws)\n * serverAdapter.handleConnection({ socket }).start()\n * },\n * message(ws, msg) {\n * const data = msg instanceof ArrayBuffer ? new Uint8Array(msg) : msg\n * ws.data?.handlers?.onMessage?.(data)\n * },\n * close(ws, code, reason) {\n * ws.data?.handlers?.onClose?.(code, reason)\n * },\n * },\n * })\n * ```\n */\nexport function wrapBunWebsocket(\n ws: ServerWebSocket<BunWebsocketData>,\n): Socket {\n ws.data = { handlers: {} }\n\n return {\n send(data: Uint8Array | string): void {\n ws.send(data)\n },\n\n close(code?: number, reason?: string): void {\n ws.close(code, reason)\n },\n\n onMessage(handler: (data: Uint8Array | string) => void): void {\n ws.data.handlers.onMessage = handler\n },\n\n onClose(handler: (code: number, reason: string) => void): void {\n ws.data.handlers.onClose = handler\n },\n\n onError(_handler: (error: Error) => void): void {\n // Bun handles errors at the server level, not per-socket\n },\n\n get readyState(): SocketReadyState {\n const states: SocketReadyState[] = [\n \"connecting\",\n \"open\",\n \"closing\",\n \"closed\",\n ]\n return states[ws.readyState] ?? \"closed\"\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// createBunWebsocketHandlers\n// ---------------------------------------------------------------------------\n\n/**\n * Create Bun Websocket handlers that integrate with `WebsocketServerTransport`.\n *\n * This helper eliminates boilerplate by providing pre-configured handlers\n * for `open`, `message`, and `close` events that automatically wire up\n * to the adapter's `handleConnection()` method.\n *\n * @example\n * ```typescript\n * import { WebsocketServerTransport } from \"@kyneta/websocket-transport/server\"\n * import { createBunWebsocketHandlers, type BunWebsocketData } from \"@kyneta/websocket-transport/bun\"\n *\n * const serverAdapter = new WebsocketServerTransport()\n *\n * Bun.serve<BunWebsocketData>({\n * fetch(req, server) {\n * server.upgrade(req)\n * return new Response(\"upgrade failed\", { status: 400 })\n * },\n * websocket: createBunWebsocketHandlers(serverAdapter),\n * })\n * ```\n */\nexport function createBunWebsocketHandlers(wsAdapter: {\n handleConnection: (opts: { socket: Socket }) => { start: () => void }\n}) {\n return {\n open(ws: ServerWebSocket<BunWebsocketData>) {\n wsAdapter.handleConnection({ socket: wrapBunWebsocket(ws) }).start()\n },\n message(\n ws: ServerWebSocket<BunWebsocketData>,\n msg: string | ArrayBuffer | Buffer,\n ) {\n const data =\n msg instanceof ArrayBuffer\n ? new Uint8Array(msg)\n : Buffer.isBuffer(msg)\n ? new Uint8Array(msg)\n : msg\n ws.data.handlers.onMessage?.(data)\n },\n close(ws: ServerWebSocket<BunWebsocketData>, code: number, reason: string) {\n ws.data.handlers.onClose?.(code, reason)\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0EA,SAAgB,iBACd,IACQ;AACR,IAAG,OAAO,EAAE,UAAU,EAAE,EAAE;AAE1B,QAAO;EACL,KAAK,
|
|
1
|
+
{"version":3,"file":"bun.js","names":[],"sources":["../src/bun-websocket.ts"],"sourcesContent":["// bun-websocket — Bun-specific Websocket wrapper for @kyneta/websocket-transport.\n//\n// Provides a wrapper to adapt Bun's ServerWebSocket to the Socket interface\n// expected by WebsocketServerTransport.\n//\n// Bun's WebSocket API is callback-based at the server level (not per-socket),\n// so we bridge that gap by storing handlers in ws.data.\n//\n// Ported from @loro-extended/adapter-websocket's bun.ts with kyneta\n// naming conventions applied.\n\n/// <reference types=\"bun-types\" />\n\nimport type { ServerWebSocket } from \"bun\"\nimport type { Socket, SocketReadyState } from \"./types.js\"\n\n// ---------------------------------------------------------------------------\n// BunWebsocketData — stored in ws.data for per-socket handler callbacks\n// ---------------------------------------------------------------------------\n\n/**\n * Data structure stored in `ws.data` for handler callbacks.\n * Use this type when defining your `Bun.serve()` generic.\n *\n * @example\n * ```typescript\n * Bun.serve<BunWebsocketData>({\n * websocket: { ... }\n * })\n * ```\n */\nexport type BunWebsocketData = {\n handlers: {\n onMessage?: (data: Uint8Array<ArrayBuffer> | string) => void\n onClose?: (code: number, reason: string) => void\n }\n}\n\n// ---------------------------------------------------------------------------\n// wrapBunWebsocket\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap Bun's `ServerWebSocket` to match the `Socket` interface.\n *\n * Bun's WebSocket API uses server-level callbacks (`websocket: { message, close }`)\n * rather than per-socket event handlers. This wrapper bridges that gap by\n * storing handlers in `ws.data` and having the server-level callbacks delegate\n * to them.\n *\n * @example\n * ```typescript\n * import { WebsocketServerTransport } from \"@kyneta/websocket-transport/server\"\n * import { wrapBunWebsocket, type BunWebsocketData } from \"@kyneta/websocket-transport/bun\"\n *\n * const serverAdapter = new WebsocketServerTransport()\n *\n * Bun.serve<BunWebsocketData>({\n * websocket: {\n * open(ws) {\n * const socket = wrapBunWebsocket(ws)\n * serverAdapter.handleConnection({ socket }).start()\n * },\n * message(ws, msg) {\n * const data = msg instanceof ArrayBuffer ? new Uint8Array(msg) : msg\n * ws.data?.handlers?.onMessage?.(data)\n * },\n * close(ws, code, reason) {\n * ws.data?.handlers?.onClose?.(code, reason)\n * },\n * },\n * })\n * ```\n */\nexport function wrapBunWebsocket(\n ws: ServerWebSocket<BunWebsocketData>,\n): Socket {\n ws.data = { handlers: {} }\n\n return {\n send(data: Uint8Array<ArrayBuffer> | string): void {\n ws.send(data)\n },\n\n close(code?: number, reason?: string): void {\n ws.close(code, reason)\n },\n\n onMessage(handler: (data: Uint8Array<ArrayBuffer> | string) => void): void {\n ws.data.handlers.onMessage = handler\n },\n\n onClose(handler: (code: number, reason: string) => void): void {\n ws.data.handlers.onClose = handler\n },\n\n onError(_handler: (error: Error) => void): void {\n // Bun handles errors at the server level, not per-socket\n },\n\n get readyState(): SocketReadyState {\n const states: SocketReadyState[] = [\n \"connecting\",\n \"open\",\n \"closing\",\n \"closed\",\n ]\n return states[ws.readyState] ?? \"closed\"\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// createBunWebsocketHandlers\n// ---------------------------------------------------------------------------\n\n/**\n * Create Bun Websocket handlers that integrate with `WebsocketServerTransport`.\n *\n * This helper eliminates boilerplate by providing pre-configured handlers\n * for `open`, `message`, and `close` events that automatically wire up\n * to the adapter's `handleConnection()` method.\n *\n * @example\n * ```typescript\n * import { WebsocketServerTransport } from \"@kyneta/websocket-transport/server\"\n * import { createBunWebsocketHandlers, type BunWebsocketData } from \"@kyneta/websocket-transport/bun\"\n *\n * const serverAdapter = new WebsocketServerTransport()\n *\n * Bun.serve<BunWebsocketData>({\n * fetch(req, server) {\n * server.upgrade(req)\n * return new Response(\"upgrade failed\", { status: 400 })\n * },\n * websocket: createBunWebsocketHandlers(serverAdapter),\n * })\n * ```\n */\nexport function createBunWebsocketHandlers(wsAdapter: {\n handleConnection: (opts: { socket: Socket }) => { start: () => void }\n}) {\n return {\n open(ws: ServerWebSocket<BunWebsocketData>) {\n wsAdapter.handleConnection({ socket: wrapBunWebsocket(ws) }).start()\n },\n message(\n ws: ServerWebSocket<BunWebsocketData>,\n msg: string | ArrayBuffer | Buffer,\n ) {\n const data =\n msg instanceof ArrayBuffer\n ? new Uint8Array(msg)\n : Buffer.isBuffer(msg)\n ? new Uint8Array(msg)\n : msg\n ws.data.handlers.onMessage?.(data)\n },\n close(ws: ServerWebSocket<BunWebsocketData>, code: number, reason: string) {\n ws.data.handlers.onClose?.(code, reason)\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0EA,SAAgB,iBACd,IACQ;AACR,IAAG,OAAO,EAAE,UAAU,EAAE,EAAE;AAE1B,QAAO;EACL,KAAK,MAA8C;AACjD,MAAG,KAAK,KAAK;;EAGf,MAAM,MAAe,QAAuB;AAC1C,MAAG,MAAM,MAAM,OAAO;;EAGxB,UAAU,SAAiE;AACzE,MAAG,KAAK,SAAS,YAAY;;EAG/B,QAAQ,SAAuD;AAC7D,MAAG,KAAK,SAAS,UAAU;;EAG7B,QAAQ,UAAwC;EAIhD,IAAI,aAA+B;AAOjC,UANmC;IACjC;IACA;IACA;IACA;IACD,CACa,GAAG,eAAe;;EAEnC;;;;;;;;;;;;;;;;;;;;;;;;;AA8BH,SAAgB,2BAA2B,WAExC;AACD,QAAO;EACL,KAAK,IAAuC;AAC1C,aAAU,iBAAiB,EAAE,QAAQ,iBAAiB,GAAG,EAAE,CAAC,CAAC,OAAO;;EAEtE,QACE,IACA,KACA;GACA,MAAM,OACJ,eAAe,cACX,IAAI,WAAW,IAAI,GACnB,OAAO,SAAS,IAAI,GAClB,IAAI,WAAW,IAAI,GACnB;AACR,MAAG,KAAK,SAAS,YAAY,KAAK;;EAEpC,MAAM,IAAuC,MAAc,QAAgB;AACzE,MAAG,KAAK,SAAS,UAAU,MAAM,OAAO;;EAE3C"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_RECONNECT, Transport, computeBackoffDelay } from "@kyneta/transport";
|
|
2
2
|
import { createObservableProgram } from "@kyneta/machine";
|
|
3
|
-
import { FragmentReassembler,
|
|
3
|
+
import { FragmentReassembler, applyInboundAliasing, applyOutboundAliasing, createFrameIdCounter, decodeBinaryWires, emptyAliasState, encodeWireFrameAndSend } from "@kyneta/wire";
|
|
4
4
|
//#region src/client-program.ts
|
|
5
5
|
/**
|
|
6
6
|
* Create the websocket client connection lifecycle program — a pure Mealy machine.
|
|
@@ -139,7 +139,7 @@ const READY_STATE = {
|
|
|
139
139
|
function wrapStandardWebsocket(ws) {
|
|
140
140
|
return {
|
|
141
141
|
send(data) {
|
|
142
|
-
ws.send(
|
|
142
|
+
ws.send(data);
|
|
143
143
|
},
|
|
144
144
|
close(code, reason) {
|
|
145
145
|
ws.close(code, reason);
|
|
@@ -249,6 +249,7 @@ var WebsocketClientTransport = class extends Transport {
|
|
|
249
249
|
#reconnectTimer;
|
|
250
250
|
#fragmentThreshold;
|
|
251
251
|
#reassembler;
|
|
252
|
+
#aliasState = emptyAliasState();
|
|
252
253
|
constructor(options) {
|
|
253
254
|
super({ transportType: "websocket-client" });
|
|
254
255
|
this.#options = options;
|
|
@@ -277,6 +278,7 @@ var WebsocketClientTransport = class extends Transport {
|
|
|
277
278
|
this.removeChannel(this.#serverChannel.channelId);
|
|
278
279
|
this.#serverChannel = void 0;
|
|
279
280
|
}
|
|
281
|
+
this.#reassembler.reset();
|
|
280
282
|
this.#serverChannel = this.addChannel();
|
|
281
283
|
this.establishChannel(this.#serverChannel.channelId);
|
|
282
284
|
break;
|
|
@@ -390,8 +392,17 @@ var WebsocketClientTransport = class extends Transport {
|
|
|
390
392
|
return;
|
|
391
393
|
}
|
|
392
394
|
if (data instanceof ArrayBuffer) try {
|
|
393
|
-
const
|
|
394
|
-
if (
|
|
395
|
+
const wires = decodeBinaryWires(new Uint8Array(data), this.#reassembler);
|
|
396
|
+
if (!wires) return;
|
|
397
|
+
for (const wire of wires) {
|
|
398
|
+
const result = applyInboundAliasing(this.#aliasState, wire);
|
|
399
|
+
this.#aliasState = result.state;
|
|
400
|
+
if (result.error || !result.msg) {
|
|
401
|
+
console.warn("[WebsocketClient] alias resolution failed:", result.error);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
this.#handleChannelMessage(result.msg);
|
|
405
|
+
}
|
|
395
406
|
} catch (error) {
|
|
396
407
|
console.error("Failed to decode message:", error);
|
|
397
408
|
}
|
|
@@ -461,12 +472,16 @@ var WebsocketClientTransport = class extends Transport {
|
|
|
461
472
|
return this.#handle.getState().status === "ready";
|
|
462
473
|
}
|
|
463
474
|
generate() {
|
|
475
|
+
const nextFrameId = createFrameIdCounter();
|
|
476
|
+
this.#aliasState = emptyAliasState();
|
|
464
477
|
return {
|
|
465
478
|
transportType: this.transportType,
|
|
466
479
|
send: (msg) => {
|
|
467
480
|
const socket = this.#socket;
|
|
468
481
|
if (!socket || socket.readyState !== READY_STATE.OPEN) return;
|
|
469
|
-
|
|
482
|
+
const { state, wire } = applyOutboundAliasing(this.#aliasState, msg);
|
|
483
|
+
this.#aliasState = state;
|
|
484
|
+
encodeWireFrameAndSend(wire, (data) => socket.send(new Uint8Array(data).buffer), this.#fragmentThreshold, nextFrameId);
|
|
470
485
|
},
|
|
471
486
|
stop: () => {}
|
|
472
487
|
};
|
|
@@ -507,4 +522,4 @@ function createWebsocketClient(options) {
|
|
|
507
522
|
//#endregion
|
|
508
523
|
export { wrapNodeWebsocket as a, READY_STATE as i, WebsocketClientTransport as n, wrapStandardWebsocket as o, createWebsocketClient as r, createWsClientProgram as s, DEFAULT_FRAGMENT_THRESHOLD as t };
|
|
509
524
|
|
|
510
|
-
//# sourceMappingURL=client-transport-
|
|
525
|
+
//# sourceMappingURL=client-transport-B2V7s2VP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-transport-B2V7s2VP.js","names":["#fragmentThreshold","#reassembler","#options","#WebSocketImpl","#handle","#executeEffect","#setupLifecycleEvents","#doCreateWebsocket","#socket","#serverChannel","#reconnectTimer","#startKeepalive","#stopKeepalive","#peerId","#handleMessage","#aliasState","#handleChannelMessage","#keepaliveTimer"],"sources":["../src/client-program.ts","../src/types.ts","../src/client-transport.ts"],"sourcesContent":["// client-program — pure Mealy machine for websocket client connection lifecycle.\n//\n// The client program encodes every state transition and effect as data.\n// The imperative shell (client-transport.ts) interprets effects as I/O.\n// Tests assert on data — no sockets, no timing, never flaky.\n//\n// Algebra: Program<WsClientMsg, WebsocketClientState, WsClientEffect>\n// Interpreter: client-transport.ts executeClientEffect()\n//\n// The websocket client has a 5-state lifecycle with an extra \"ready\" state\n// compared to the unix socket client. The server sends a text \"ready\" signal\n// after the connection opens, and only then does the client create a channel\n// and start the establishment handshake.\n//\n// Race condition: the server may send \"ready\" before the client's open event\n// fires (server-ready while connecting). The program handles this by\n// transitioning directly to ready, skipping the connected state.\n\nimport type { Program } from \"@kyneta/machine\"\nimport type { ReconnectOptions } from \"@kyneta/transport\"\nimport { computeBackoffDelay, DEFAULT_RECONNECT } from \"@kyneta/transport\"\n\nimport type { DisconnectReason, WebsocketClientState } from \"./types.js\"\n\n// ---------------------------------------------------------------------------\n// Messages\n// ---------------------------------------------------------------------------\n\nexport type WsClientMsg =\n | { type: \"start\" }\n | { type: \"socket-opened\" }\n | { type: \"server-ready\" }\n | { type: \"socket-closed\"; code: number; reason: string }\n | { type: \"socket-error\"; error: Error }\n | { type: \"reconnect-timer-fired\" }\n | { type: \"stop\" }\n\n// ---------------------------------------------------------------------------\n// Effects (data — interpreted by the imperative shell)\n// ---------------------------------------------------------------------------\n\nexport type WsClientEffect =\n | { type: \"create-websocket\"; attempt: number }\n | { type: \"close-websocket\" }\n | { type: \"add-channel-and-establish\" }\n | { type: \"remove-channel\" }\n | { type: \"start-reconnect-timer\"; delayMs: number }\n | { type: \"cancel-reconnect-timer\" }\n | { type: \"start-keepalive\" }\n | { type: \"stop-keepalive\" }\n\n// ---------------------------------------------------------------------------\n// Program factory\n// ---------------------------------------------------------------------------\n\nexport interface WsClientProgramOptions {\n reconnect?: Partial<ReconnectOptions>\n /** Inject jitter source for deterministic testing. Default: () => Math.random() * 1000 */\n jitterFn?: () => number\n}\n\n/**\n * Create the websocket client connection lifecycle program — a pure Mealy machine.\n *\n * The returned `Program<WsClientMsg, WebsocketClientState, WsClientEffect>`\n * encodes every state transition and effect as inspectable data. The imperative\n * shell interprets `WsClientEffect` as actual I/O.\n */\nexport function createWsClientProgram(\n options: WsClientProgramOptions = {},\n): Program<WsClientMsg, WebsocketClientState, WsClientEffect> {\n const { jitterFn = () => Math.random() * 1000 } = options\n const reconnect: ReconnectOptions = {\n ...DEFAULT_RECONNECT,\n ...options.reconnect,\n }\n\n /**\n * Attempt to transition into reconnecting, or give up and disconnect.\n *\n * Pure — computes the next state and effects from the current attempt\n * count and reconnect configuration. Returns a tuple suitable for\n * spreading into an `update` return.\n */\n function tryReconnect(\n currentAttempt: number,\n reason: DisconnectReason,\n ...extraEffects: WsClientEffect[]\n ): [WebsocketClientState, ...WsClientEffect[]] {\n if (!reconnect.enabled) {\n return [{ status: \"disconnected\", reason }, ...extraEffects]\n }\n\n if (currentAttempt >= reconnect.maxAttempts) {\n return [\n {\n status: \"disconnected\",\n reason: { type: \"max-retries-exceeded\", attempts: currentAttempt },\n },\n ...extraEffects,\n ]\n }\n\n const delay = computeBackoffDelay(\n currentAttempt + 1,\n reconnect.baseDelay,\n reconnect.maxDelay,\n jitterFn(),\n )\n\n return [\n {\n status: \"reconnecting\",\n attempt: currentAttempt + 1,\n nextAttemptMs: delay,\n },\n ...extraEffects,\n { type: \"start-reconnect-timer\", delayMs: delay },\n ]\n }\n\n return {\n init: [{ status: \"disconnected\" }],\n\n update(msg, model): [WebsocketClientState, ...WsClientEffect[]] {\n switch (msg.type) {\n // -----------------------------------------------------------------\n // start\n // -----------------------------------------------------------------\n case \"start\": {\n if (model.status !== \"disconnected\") return [model]\n return [\n { status: \"connecting\", attempt: 1 },\n { type: \"create-websocket\", attempt: 1 },\n ]\n }\n\n // -----------------------------------------------------------------\n // socket-opened\n // -----------------------------------------------------------------\n case \"socket-opened\": {\n if (model.status !== \"connecting\") return [model]\n return [{ status: \"connected\" }, { type: \"start-keepalive\" }]\n }\n\n // -----------------------------------------------------------------\n // server-ready\n // -----------------------------------------------------------------\n case \"server-ready\": {\n // Already ready — ignore duplicate\n if (model.status === \"ready\") return [model]\n\n // Normal path: connected → ready\n if (model.status === \"connected\") {\n return [{ status: \"ready\" }, { type: \"add-channel-and-establish\" }]\n }\n\n // Race condition: server sent \"ready\" before client's open event fired.\n // Skip connected, go directly to ready with both keepalive and channel effects.\n if (model.status === \"connecting\") {\n return [\n { status: \"ready\" },\n { type: \"start-keepalive\" },\n { type: \"add-channel-and-establish\" },\n ]\n }\n\n return [model]\n }\n\n // -----------------------------------------------------------------\n // socket-closed\n // -----------------------------------------------------------------\n case \"socket-closed\": {\n const reason: DisconnectReason = {\n type: \"closed\",\n code: msg.code,\n reason: msg.reason,\n }\n\n if (model.status === \"connected\") {\n return tryReconnect(0, reason, { type: \"stop-keepalive\" })\n }\n\n if (model.status === \"ready\") {\n return tryReconnect(\n 0,\n reason,\n { type: \"stop-keepalive\" },\n { type: \"remove-channel\" },\n )\n }\n\n return [model]\n }\n\n // -----------------------------------------------------------------\n // socket-error\n // -----------------------------------------------------------------\n case \"socket-error\": {\n const reason: DisconnectReason = {\n type: \"error\",\n error: msg.error,\n }\n\n if (model.status === \"connecting\") {\n return tryReconnect(model.attempt, reason)\n }\n\n if (model.status === \"connected\") {\n return tryReconnect(0, reason, { type: \"stop-keepalive\" })\n }\n\n if (model.status === \"ready\") {\n return tryReconnect(\n 0,\n reason,\n { type: \"stop-keepalive\" },\n { type: \"remove-channel\" },\n )\n }\n\n return [model]\n }\n\n // -----------------------------------------------------------------\n // reconnect-timer-fired\n // -----------------------------------------------------------------\n case \"reconnect-timer-fired\": {\n if (model.status !== \"reconnecting\") return [model]\n return [\n { status: \"connecting\", attempt: model.attempt },\n { type: \"create-websocket\", attempt: model.attempt },\n ]\n }\n\n // -----------------------------------------------------------------\n // stop\n // -----------------------------------------------------------------\n case \"stop\": {\n if (model.status === \"disconnected\") return [model]\n\n const effects: WsClientEffect[] = [{ type: \"cancel-reconnect-timer\" }]\n\n if (model.status === \"connecting\") {\n effects.push({ type: \"close-websocket\" })\n }\n\n if (model.status === \"connected\") {\n effects.push(\n { type: \"close-websocket\" },\n { type: \"stop-keepalive\" },\n )\n }\n\n if (model.status === \"ready\") {\n effects.push(\n { type: \"close-websocket\" },\n { type: \"stop-keepalive\" },\n { type: \"remove-channel\" },\n )\n }\n\n return [\n { status: \"disconnected\", reason: { type: \"intentional\" } },\n ...effects,\n ]\n }\n }\n },\n }\n}\n","// types — framework-agnostic Websocket abstractions for @kyneta/websocket-transport.\n//\n// The `Socket` interface decouples the adapter from any specific Websocket\n// library (browser WebSocket, Node `ws`, Bun ServerWebSocket). Platform-\n// specific wrappers (`wrapStandardWebsocket`, `wrapNodeWebsocket`,\n// `wrapBunWebsocket`) adapt concrete implementations to this interface.\n//\n// Ported from @loro-extended/adapter-websocket's WsSocket with kyneta\n// naming conventions applied.\n\nimport type {\n TransitionListener as GenericTransitionListener,\n PeerId,\n StateTransition,\n} from \"@kyneta/transport\"\n\n// ---------------------------------------------------------------------------\n// WebSocket readyState constants (spec values, no global dependency)\n// ---------------------------------------------------------------------------\n\n/**\n * WebSocket readyState constants per the WHATWG WebSocket spec.\n * Replaces references to `WebSocket.CONNECTING`, `WebSocket.OPEN`, etc.\n * so that shared code never depends on the browser global.\n */\nexport const READY_STATE = {\n CONNECTING: 0,\n OPEN: 1,\n CLOSING: 2,\n CLOSED: 3,\n} as const\n\n// ---------------------------------------------------------------------------\n// Structural event types (replace DOM MessageEvent / CloseEvent)\n// ---------------------------------------------------------------------------\n\n/** Minimal message event — only the fields the transport accesses. */\nexport interface WebSocketMessageEvent {\n readonly data: string | ArrayBuffer\n}\n\n/** Minimal close event — only the fields the transport accesses. */\nexport interface WebSocketCloseEvent {\n readonly code: number\n readonly reason: string\n}\n\n// ---------------------------------------------------------------------------\n// WebSocket instance and constructor structural types\n// ---------------------------------------------------------------------------\n\n/**\n * Structural type for a constructed WebSocket instance.\n *\n * Covers the browser's `WebSocket`, the `ws` library's `WebSocket`,\n * and Bun's client `WebSocket` — all satisfy this interface without casting.\n *\n * The client transport uses `addEventListener`/`removeEventListener` for\n * one-shot connection handlers with explicit cleanup during the connect\n * phase. This is why `WebSocketLike` exists alongside the server-side\n * `Socket` interface (which uses single-callback registration).\n */\nexport interface WebSocketLike {\n readonly readyState: number\n binaryType: string\n send(data: string | ArrayBuffer): void\n close(code?: number, reason?: string): void\n addEventListener(type: string, listener: (event: any) => void): void\n removeEventListener(type: string, listener: (event: any) => void): void\n}\n\n/**\n * Structural type for a WebSocket constructor.\n *\n * Type safety for constructor arguments is intentionally at the options\n * layer (`WebsocketClientOptions.headers`), not here. The `...rest: any[]`\n * absorbs both the browser's `protocols` arg and backend's `{ headers }`\n * arg without requiring the transport to know which runtime it's in.\n */\nexport type WebSocketConstructor = new (\n url: string,\n ...rest: any[]\n) => WebSocketLike\n\n// ---------------------------------------------------------------------------\n// Socket ready states (string enum for server-side Socket interface)\n// ---------------------------------------------------------------------------\n\n/**\n * Websocket ready states — mirrors the standard WebSocket readyState\n * values as human-readable strings.\n */\nexport type SocketReadyState = \"connecting\" | \"open\" | \"closing\" | \"closed\"\n\n// ---------------------------------------------------------------------------\n// Socket interface\n// ---------------------------------------------------------------------------\n\n/**\n * Framework-agnostic Websocket interface.\n *\n * This allows the adapter to work with any Websocket library:\n * - Browser `WebSocket` via `wrapStandardWebsocket()`\n * - Node.js `ws` library via `wrapNodeWebsocket()`\n * - Bun `ServerWebSocket` via `wrapBunWebsocket()`\n *\n * The interface is intentionally minimal — only the operations the\n * adapter needs are exposed.\n */\nexport interface Socket {\n /**\n * Narrowed to `Uint8Array<ArrayBuffer>` because the strictest downstream\n * runtimes reject `SharedArrayBuffer`-backed views: Bun's `BufferSource`\n * resolves to `ArrayBufferView<ArrayBuffer> | ArrayBuffer`, and Hono's\n * `WSContext.send` takes `Uint8Array<ArrayBuffer>` directly. The wire\n * pipeline allocates with `new Uint8Array(n)`, so producers satisfy this\n * without changes.\n */\n send(data: Uint8Array<ArrayBuffer> | string): void\n\n /** Close the Websocket connection. */\n close(code?: number, reason?: string): void\n\n /** Register a handler for incoming messages (binary or text). */\n onMessage(handler: (data: Uint8Array<ArrayBuffer> | string) => void): void\n\n /** Register a handler for connection close. */\n onClose(handler: (code: number, reason: string) => void): void\n\n /** Register a handler for errors. */\n onError(handler: (error: Error) => void): void\n\n /** The current ready state of the Websocket. */\n readonly readyState: SocketReadyState\n}\n\n// ---------------------------------------------------------------------------\n// Connection types — used by server adapter\n// ---------------------------------------------------------------------------\n\n/**\n * Options for handling a new Websocket connection on the server.\n */\nexport interface WebsocketConnectionOptions {\n /** The Websocket instance, wrapped in the Socket interface. */\n socket: Socket\n\n /** Optional peer ID extracted from the upgrade request. */\n peerId?: PeerId\n\n /** Optional authentication token from the upgrade request. */\n authToken?: string\n}\n\n/**\n * Handle for an active Websocket connection.\n */\nexport interface WebsocketConnectionHandle {\n /** The peer ID for this connection. */\n readonly peerId: PeerId\n\n /** The channel ID for this connection. */\n readonly channelId: number\n\n /** Close the connection. */\n close(code?: number, reason?: string): void\n}\n\n/**\n * Result of handling a Websocket connection on the server.\n */\nexport interface WebsocketConnectionResult {\n /** The connection handle for managing this peer. */\n connection: WebsocketConnectionHandle\n\n /** Call this to start processing messages. */\n start(): void\n}\n\n// ---------------------------------------------------------------------------\n// Disconnect reason\n// ---------------------------------------------------------------------------\n\n/**\n * Discriminated union describing why a Websocket connection was lost.\n */\nexport type DisconnectReason =\n | { type: \"intentional\" }\n | { type: \"error\"; error: Error }\n | { type: \"closed\"; code: number; reason: string }\n | { type: \"max-retries-exceeded\"; attempts: number }\n | { type: \"not-started\" }\n\n// ---------------------------------------------------------------------------\n// Connection state (for client adapter observability)\n// ---------------------------------------------------------------------------\n\n/**\n * All possible states of the Websocket client.\n *\n * State machine transitions:\n * ```\n * disconnected → connecting → connected → ready\n * ↓ ↓ ↓\n * reconnecting ← ─ ┴ ─ ─ ─ ─ ┘\n * ↓\n * connecting (retry)\n * ↓\n * disconnected (max retries)\n * ```\n */\nexport type WebsocketClientState =\n | { status: \"disconnected\"; reason?: DisconnectReason }\n | { status: \"connecting\"; attempt: number }\n | { status: \"connected\" }\n | { status: \"ready\" }\n | { status: \"reconnecting\"; attempt: number; nextAttemptMs: number }\n\n/**\n * A state transition event for websocket client states.\n * Specialized from the generic `StateTransition<S>`.\n */\nexport type WebsocketClientStateTransition =\n StateTransition<WebsocketClientState>\n\n/**\n * Listener for websocket client state transitions.\n * Specialized from the generic `TransitionListener<S>`.\n */\nexport type TransitionListener = GenericTransitionListener<WebsocketClientState>\n\n// ---------------------------------------------------------------------------\n// Socket wrapper — standard WebSocket API (browser + Node ws)\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a standard `WebSocket` (browser or Node.js `ws` via `ws` package\n * in `WebSocket`-compatible mode) into the `Socket` interface.\n *\n * Handles `ArrayBuffer`, `Blob`, and string messages.\n */\nexport function wrapStandardWebsocket(ws: WebSocket): Socket {\n return {\n send(data: Uint8Array<ArrayBuffer> | string): void {\n ws.send(data)\n },\n\n close(code?: number, reason?: string): void {\n ws.close(code, reason)\n },\n\n onMessage(handler: (data: Uint8Array<ArrayBuffer> | string) => void): void {\n ws.addEventListener(\"message\", event => {\n if (event.data instanceof ArrayBuffer) {\n handler(new Uint8Array(event.data))\n } else if (typeof Blob !== \"undefined\" && event.data instanceof Blob) {\n // Handle Blob data (browser)\n event.data.arrayBuffer().then(buffer => {\n handler(new Uint8Array(buffer))\n })\n } else {\n handler(event.data as string)\n }\n })\n },\n\n onClose(handler: (code: number, reason: string) => void): void {\n ws.addEventListener(\"close\", event => {\n handler(event.code, event.reason)\n })\n },\n\n onError(handler: (error: Error) => void): void {\n ws.addEventListener(\"error\", _event => {\n handler(new Error(\"WebSocket error\"))\n })\n },\n\n get readyState(): SocketReadyState {\n switch (ws.readyState) {\n case READY_STATE.CONNECTING:\n return \"connecting\"\n case READY_STATE.OPEN:\n return \"open\"\n case READY_STATE.CLOSING:\n return \"closing\"\n case READY_STATE.CLOSED:\n return \"closed\"\n default:\n return \"closed\"\n }\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Socket wrapper — Node.js `ws` library (raw API, not WebSocket-compat)\n// ---------------------------------------------------------------------------\n\n/**\n * The minimal interface we need from the Node.js `ws` library's `WebSocket`.\n *\n * Using a structural type rather than importing `ws` — consumers provide\n * the actual `ws` instance, we just need these methods.\n */\nexport interface NodeWebsocketLike {\n send(data: Uint8Array<ArrayBuffer> | string): void\n close(code?: number, reason?: string): void\n on(\n event: \"message\",\n handler: (data: Buffer | ArrayBuffer | string, isBinary: boolean) => void,\n ): void\n on(event: \"close\", handler: (code: number, reason: Buffer) => void): void\n on(event: \"error\", handler: (error: Error) => void): void\n readyState: number\n}\n\n/**\n * Wrap a Node.js `ws` library WebSocket into the `Socket` interface.\n *\n * Handles `Buffer` → `Uint8Array` conversion for binary messages.\n */\nexport function wrapNodeWebsocket(ws: NodeWebsocketLike): Socket {\n const CONNECTING = 0\n const OPEN = 1\n const CLOSING = 2\n\n return {\n send(data: Uint8Array<ArrayBuffer> | string): void {\n ws.send(data)\n },\n\n close(code?: number, reason?: string): void {\n ws.close(code, reason)\n },\n\n onMessage(handler: (data: Uint8Array<ArrayBuffer> | string) => void): void {\n ws.on(\n \"message\",\n (data: Buffer | ArrayBuffer | string, isBinary: boolean) => {\n if (isBinary) {\n if (data instanceof ArrayBuffer) {\n handler(new Uint8Array(data))\n } else if (typeof Buffer !== \"undefined\" && Buffer.isBuffer(data)) {\n handler(new Uint8Array(data))\n } else {\n handler(new Uint8Array(data as unknown as ArrayBuffer))\n }\n } else {\n if (typeof Buffer !== \"undefined\" && Buffer.isBuffer(data)) {\n handler(data.toString(\"utf-8\"))\n } else {\n handler(data as string)\n }\n }\n },\n )\n },\n\n onClose(handler: (code: number, reason: string) => void): void {\n ws.on(\"close\", (code: number, reason: Buffer) => {\n handler(code, reason.toString())\n })\n },\n\n onError(handler: (error: Error) => void): void {\n ws.on(\"error\", handler)\n },\n\n get readyState(): SocketReadyState {\n switch (ws.readyState) {\n case CONNECTING:\n return \"connecting\"\n case OPEN:\n return \"open\"\n case CLOSING:\n return \"closing\"\n default:\n return \"closed\"\n }\n },\n }\n}\n","// client-transport — Websocket client transport for @kyneta/exchange.\n//\n// Thin imperative shell around the pure client program (client-program.ts).\n// The program produces data effects; this module interprets them as I/O.\n//\n// FC/IS design:\n// - client-program.ts: pure Mealy machine (functional core)\n// - client-transport.ts: effect executor (imperative shell)\n//\n// Uses the kyneta wire format (CBOR codec + framing + fragmentation)\n// for binary messages. Text frames carry the \"ready\" handshake and\n// keepalive ping/pong.\n\nimport type { ObservableHandle, TransitionListener } from \"@kyneta/machine\"\nimport { createObservableProgram } from \"@kyneta/machine\"\nimport type {\n Channel,\n ChannelMsg,\n GeneratedChannel,\n PeerId,\n TransportFactory,\n} from \"@kyneta/transport\"\nimport { Transport } from \"@kyneta/transport\"\nimport {\n type AliasState,\n applyInboundAliasing,\n applyOutboundAliasing,\n createFrameIdCounter,\n decodeBinaryWires,\n emptyAliasState,\n encodeWireFrameAndSend,\n FragmentReassembler,\n} from \"@kyneta/wire\"\nimport {\n createWsClientProgram,\n type WsClientEffect,\n type WsClientMsg,\n} from \"./client-program.js\"\nimport type {\n DisconnectReason,\n WebsocketClientState,\n WebsocketClientStateTransition,\n} from \"./types.js\"\nimport {\n READY_STATE,\n type WebSocketCloseEvent,\n type WebSocketConstructor,\n type WebSocketLike,\n type WebSocketMessageEvent,\n} from \"./types.js\"\n\n// Re-export state types for convenience\nexport type {\n DisconnectReason,\n WebsocketClientState,\n WebsocketClientStateTransition,\n}\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\n/**\n * Default fragment threshold in bytes.\n * AWS API Gateway has a 128KB limit, so 100KB provides a safe margin.\n */\nexport const DEFAULT_FRAGMENT_THRESHOLD = 100 * 1024\n\n/**\n * Options for the Websocket client transport (browser connections).\n */\nexport interface WebsocketClientOptions {\n /** Websocket URL to connect to. Can be a string or a function of peerId. */\n url: string | ((peerId: PeerId) => string)\n\n /**\n * WebSocket constructor — caller must provide explicitly.\n *\n * In browsers, pass the global `WebSocket`. In Node.js, pass `ws`'s\n * `WebSocket`. In Bun, pass `globalThis.WebSocket`. The transport\n * never probes `globalThis` on its own.\n */\n WebSocket: WebSocketConstructor\n\n /**\n * Headers to send during Websocket upgrade.\n * Used for authentication in service-to-service communication.\n *\n * Note: Headers are a Bun/Node-specific extension. The browser WebSocket\n * API does not support custom headers per the WHATWG spec.\n */\n headers?: Record<string, string>\n\n /** Reconnection options. */\n reconnect?: {\n enabled?: boolean\n maxAttempts?: number\n baseDelay?: number\n maxDelay?: number\n }\n\n /** Keepalive interval in ms (default: 30000). */\n keepaliveInterval?: number\n\n /**\n * Fragment threshold in bytes. Messages larger than this are fragmented.\n * Set to 0 to disable fragmentation (not recommended for cloud deployments).\n * Default: 100KB\n */\n fragmentThreshold?: number\n\n /** Lifecycle event callbacks. */\n lifecycle?: WebsocketClientLifecycleEvents\n}\n\n/**\n * Lifecycle event callbacks for the Websocket client.\n */\nexport interface WebsocketClientLifecycleEvents {\n /** Called on every state transition (delivered async via microtask). */\n onStateChange?: (transition: WebsocketClientStateTransition) => void\n\n /** Called when the connection is lost. */\n onDisconnect?: (reason: DisconnectReason) => void\n\n /** Called when a reconnection attempt is scheduled. */\n onReconnecting?: (attempt: number, nextAttemptMs: number) => void\n\n /** Called when reconnection succeeds after a previous connection. */\n onReconnected?: () => void\n\n /** Called when the server sends the \"ready\" signal. */\n onReady?: () => void\n}\n\n// ---------------------------------------------------------------------------\n// WebsocketClientTransport\n// ---------------------------------------------------------------------------\n\n/**\n * Websocket client network transport for @kyneta/exchange.\n *\n * Connects to a Websocket server, sends and receives ChannelMsg via\n * the kyneta wire format (CBOR codec + framing + fragmentation).\n *\n * Internally, the connection lifecycle is a `Program<Msg, Model, Fx>` —\n * a pure Mealy machine whose transitions are deterministically testable.\n * This class is the imperative shell that interprets data effects as I/O.\n *\n * Prefer the factory functions for construction:\n * - `createWebsocketClient()` — browser-to-server (from `./browser`)\n * - `createServiceWebsocketClient()` — service-to-service with headers (from `./server`)\n */\nexport class WebsocketClientTransport extends Transport<void> {\n #peerId?: PeerId\n #options: WebsocketClientOptions\n #WebSocketImpl: WebSocketConstructor\n\n // Observable program handle — created in constructor, drives all state\n #handle: ObservableHandle<WsClientMsg, WebsocketClientState>\n\n // Executor-local I/O state — not in the program model\n #socket?: WebSocketLike\n #serverChannel?: Channel\n #keepaliveTimer?: ReturnType<typeof setInterval>\n #reconnectTimer?: ReturnType<typeof setTimeout>\n\n // Fragmentation\n readonly #fragmentThreshold: number\n readonly #reassembler: FragmentReassembler\n\n // Per-channel alias state (Phase 4). Single channel per client.\n #aliasState: AliasState = emptyAliasState()\n\n constructor(options: WebsocketClientOptions) {\n super({ transportType: \"websocket-client\" })\n this.#options = options\n this.#WebSocketImpl = options.WebSocket\n this.#fragmentThreshold =\n options.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD\n this.#reassembler = new FragmentReassembler({\n timeoutMs: 10_000,\n })\n\n const program = createWsClientProgram({\n reconnect: options.reconnect,\n })\n\n this.#handle = createObservableProgram(program, (effect, dispatch) => {\n this.#executeEffect(effect, dispatch)\n })\n\n // Set up lifecycle event forwarding\n this.#setupLifecycleEvents()\n }\n\n // ==========================================================================\n // Effect executor — interprets data effects as I/O\n // ==========================================================================\n\n #executeEffect(\n effect: WsClientEffect,\n dispatch: (msg: WsClientMsg) => void,\n ): void {\n switch (effect.type) {\n case \"create-websocket\": {\n this.#doCreateWebsocket(dispatch)\n break\n }\n\n case \"close-websocket\": {\n if (this.#socket) {\n this.#socket.close(1000, \"Client disconnecting\")\n this.#socket = undefined\n }\n break\n }\n\n case \"add-channel-and-establish\": {\n // Clean up previous channel if it exists (e.g. after reconnect)\n if (this.#serverChannel) {\n this.removeChannel(this.#serverChannel.channelId)\n this.#serverChannel = undefined\n }\n\n // Fresh reassembler for the new connection — stale fragments from\n // the old connection must not collide with new fragments.\n this.#reassembler.reset()\n\n this.#serverChannel = this.addChannel()\n\n // Establish immediately — the server already signaled ready\n this.establishChannel(this.#serverChannel.channelId)\n break\n }\n\n case \"remove-channel\": {\n if (this.#serverChannel) {\n this.removeChannel(this.#serverChannel.channelId)\n this.#serverChannel = undefined\n }\n break\n }\n\n case \"start-reconnect-timer\": {\n this.#reconnectTimer = setTimeout(() => {\n this.#reconnectTimer = undefined\n dispatch({ type: \"reconnect-timer-fired\" })\n }, effect.delayMs)\n break\n }\n\n case \"cancel-reconnect-timer\": {\n if (this.#reconnectTimer !== undefined) {\n clearTimeout(this.#reconnectTimer)\n this.#reconnectTimer = undefined\n }\n break\n }\n\n case \"start-keepalive\": {\n this.#startKeepalive()\n break\n }\n\n case \"stop-keepalive\": {\n this.#stopKeepalive()\n break\n }\n }\n }\n\n // ==========================================================================\n // WebSocket creation — the core I/O operation\n // ==========================================================================\n\n /**\n * Create a WebSocket and wire up event handlers to dispatch messages.\n *\n * The message handler is set up IMMEDIATELY after creation (before\n * the open event) to handle the race condition where the server sends\n * \"ready\" before the client's open promise resolves.\n */\n #doCreateWebsocket(dispatch: (msg: WsClientMsg) => void): void {\n const peerId = this.#peerId\n if (!peerId) {\n dispatch({\n type: \"socket-error\",\n error: new Error(\"Cannot connect: peerId not set\"),\n })\n return\n }\n\n // Resolve URL\n const url =\n typeof this.#options.url === \"function\"\n ? this.#options.url(peerId)\n : this.#options.url\n\n try {\n // Create WebSocket — pass headers as second arg when present.\n // The structural WebSocketConstructor's `...rest: any[]` absorbs\n // both the browser's protocols arg and backend's { headers } arg.\n if (\n this.#options.headers &&\n Object.keys(this.#options.headers).length > 0\n ) {\n this.#socket = new this.#WebSocketImpl(url, {\n headers: this.#options.headers,\n })\n } else {\n this.#socket = new this.#WebSocketImpl(url)\n }\n this.#socket.binaryType = \"arraybuffer\"\n\n const socket = this.#socket\n\n // Set up message handler IMMEDIATELY to handle the \"ready\" race condition.\n // The server may send \"ready\" before the open event fires.\n socket.addEventListener(\"message\", (event: WebSocketMessageEvent) => {\n this.#handleMessage(event, dispatch)\n })\n\n // Track whether we've dispatched a terminal event for this connection attempt\n let settled = false\n\n const onOpen = () => {\n cleanup()\n settled = true\n dispatch({ type: \"socket-opened\" })\n\n // After open, set up permanent close handler for post-connection closes\n socket.addEventListener(\"close\", (event: WebSocketCloseEvent) => {\n dispatch({\n type: \"socket-closed\",\n code: event.code,\n reason: event.reason,\n })\n })\n }\n\n const onError = () => {\n if (settled) return\n cleanup()\n settled = true\n dispatch({\n type: \"socket-error\",\n error: new Error(\"WebSocket connection failed\"),\n })\n }\n\n const onClose = () => {\n if (settled) return\n cleanup()\n settled = true\n dispatch({\n type: \"socket-error\",\n error: new Error(\"WebSocket closed during connection\"),\n })\n }\n\n const cleanup = () => {\n socket.removeEventListener(\"open\", onOpen)\n socket.removeEventListener(\"error\", onError)\n socket.removeEventListener(\"close\", onClose)\n }\n\n socket.addEventListener(\"open\", onOpen)\n socket.addEventListener(\"error\", onError)\n socket.addEventListener(\"close\", onClose)\n } catch (error) {\n dispatch({\n type: \"socket-error\",\n error: error instanceof Error ? error : new Error(String(error)),\n })\n }\n }\n\n // ==========================================================================\n // Message handling — I/O parsing logic\n // ==========================================================================\n\n /**\n * Handle incoming Websocket messages.\n *\n * Text frames carry the \"ready\" handshake and keepalive pong.\n * Binary frames carry CBOR-encoded ChannelMsg.\n */\n #handleMessage(\n event: WebSocketMessageEvent,\n dispatch: (msg: WsClientMsg) => void,\n ): void {\n const data = event.data\n\n // Handle text messages (keepalive and ready signal)\n if (typeof data === \"string\") {\n if (data === \"ready\") {\n dispatch({ type: \"server-ready\" })\n }\n // Ignore pong responses and other text\n return\n }\n\n // Handle binary messages through shared decode pipeline\n if (data instanceof ArrayBuffer) {\n try {\n const wires = decodeBinaryWires(new Uint8Array(data), this.#reassembler)\n if (!wires) return\n for (const wire of wires) {\n const result = applyInboundAliasing(this.#aliasState, wire)\n this.#aliasState = result.state\n if (result.error || !result.msg) {\n console.warn(\n \"[WebsocketClient] alias resolution failed:\",\n result.error,\n )\n continue\n }\n this.#handleChannelMessage(result.msg)\n }\n } catch (error) {\n console.error(\"Failed to decode message:\", error)\n }\n }\n }\n\n /**\n * Handle a decoded channel message.\n */\n #handleChannelMessage(msg: ChannelMsg): void {\n if (!this.#serverChannel) {\n return\n }\n\n // Deliver synchronously — the Synchronizer's receive queue prevents recursion\n this.#serverChannel.onReceive(msg)\n }\n\n // ==========================================================================\n // Keepalive\n // ==========================================================================\n\n #startKeepalive(): void {\n this.#stopKeepalive()\n\n const interval = this.#options.keepaliveInterval ?? 30_000\n\n this.#keepaliveTimer = setInterval(() => {\n if (this.#socket?.readyState === READY_STATE.OPEN) {\n this.#socket.send(\"ping\")\n }\n }, interval)\n }\n\n #stopKeepalive(): void {\n if (this.#keepaliveTimer) {\n clearInterval(this.#keepaliveTimer)\n this.#keepaliveTimer = undefined\n }\n }\n\n // ==========================================================================\n // Lifecycle event forwarding\n // ==========================================================================\n\n #setupLifecycleEvents(): void {\n // wasConnectedBefore is observer-local state, not in the program model\n let wasConnectedBefore = false\n\n this.#handle.subscribeToTransitions(transition => {\n // Forward to onStateChange callback\n this.#options.lifecycle?.onStateChange?.(transition)\n\n const { from, to } = transition\n\n // onDisconnect: transitioning TO disconnected\n if (to.status === \"disconnected\" && to.reason) {\n this.#options.lifecycle?.onDisconnect?.(to.reason)\n }\n\n // onReconnecting: transitioning TO reconnecting\n if (to.status === \"reconnecting\") {\n this.#options.lifecycle?.onReconnecting?.(to.attempt, to.nextAttemptMs)\n }\n\n // onReconnected: from reconnecting/connecting TO connected/ready (after prior connection)\n if (\n wasConnectedBefore &&\n (from.status === \"reconnecting\" || from.status === \"connecting\") &&\n (to.status === \"connected\" || to.status === \"ready\")\n ) {\n this.#options.lifecycle?.onReconnected?.()\n }\n\n // onReady: transitioning TO ready\n if (to.status === \"ready\") {\n this.#options.lifecycle?.onReady?.()\n wasConnectedBefore = true\n }\n })\n }\n\n // ==========================================================================\n // State observation — delegated to the observable handle\n // ==========================================================================\n\n /**\n * Get the current connection state.\n */\n getState(): WebsocketClientState {\n return this.#handle.getState()\n }\n\n /**\n * Subscribe to state transitions.\n */\n subscribeToTransitions(\n listener: TransitionListener<WebsocketClientState>,\n ): () => void {\n return this.#handle.subscribeToTransitions(listener)\n }\n\n /**\n * Wait for a specific state.\n */\n waitForState(\n predicate: (state: WebsocketClientState) => boolean,\n options?: { timeoutMs?: number },\n ): Promise<WebsocketClientState> {\n return this.#handle.waitForState(predicate, options)\n }\n\n /**\n * Wait for a specific status.\n */\n waitForStatus(\n status: WebsocketClientState[\"status\"],\n options?: { timeoutMs?: number },\n ): Promise<WebsocketClientState> {\n return this.#handle.waitForStatus(status, options)\n }\n\n /**\n * Whether the client is ready (server ready signal received).\n */\n get isReady(): boolean {\n return this.#handle.getState().status === \"ready\"\n }\n\n // ==========================================================================\n // Transport abstract method implementations\n // ==========================================================================\n\n protected generate(): GeneratedChannel {\n const nextFrameId = createFrameIdCounter()\n // New channel = fresh alias state; reset on (re)connect.\n this.#aliasState = emptyAliasState()\n return {\n transportType: this.transportType,\n send: (msg: ChannelMsg) => {\n const socket = this.#socket\n if (!socket || socket.readyState !== READY_STATE.OPEN) {\n return\n }\n\n const { state, wire } = applyOutboundAliasing(this.#aliasState, msg)\n this.#aliasState = state\n\n encodeWireFrameAndSend(\n wire,\n data => socket.send(new Uint8Array(data).buffer),\n this.#fragmentThreshold,\n nextFrameId,\n )\n },\n stop: () => {\n // Don't call disconnect here — channel.stop() is called when\n // the channel is removed, which can happen during effect execution.\n // The actual disconnect is handled by onStop() or the program.\n },\n }\n }\n\n async onStart(): Promise<void> {\n if (!this.identity) {\n throw new Error(\n \"Transport not properly initialized — identity not available\",\n )\n }\n this.#peerId = this.identity.peerId\n this.#handle.dispatch({ type: \"start\" })\n }\n\n async onStop(): Promise<void> {\n this.#reassembler.dispose()\n this.#handle.dispatch({ type: \"stop\" })\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory functions\n// ---------------------------------------------------------------------------\n\n/**\n * Create a Websocket client transport factory for browser-to-server\n * connections.\n *\n * Returns an `TransportFactory` — a closure that creates a fresh transport\n * instance when called. Pass directly to `Exchange({ transports: [...] })`.\n *\n * @example\n * ```typescript\n * import { createWebsocketClient } from \"@kyneta/websocket-transport/browser\"\n *\n * const exchange = new Exchange({\n * transports: [createWebsocketClient({\n * url: \"ws://localhost:3000/ws\",\n * WebSocket,\n * reconnect: { enabled: true },\n * })],\n * })\n * ```\n */\nexport function createWebsocketClient(\n options: WebsocketClientOptions,\n): TransportFactory {\n return () => new WebsocketClientTransport(options)\n}\n"],"mappings":";;;;;;;;;;;AAoEA,SAAgB,sBACd,UAAkC,EAAE,EACwB;CAC5D,MAAM,EAAE,iBAAiB,KAAK,QAAQ,GAAG,QAAS;CAClD,MAAM,YAA8B;EAClC,GAAG;EACH,GAAG,QAAQ;EACZ;;;;;;;;CASD,SAAS,aACP,gBACA,QACA,GAAG,cAC0C;AAC7C,MAAI,CAAC,UAAU,QACb,QAAO,CAAC;GAAE,QAAQ;GAAgB;GAAQ,EAAE,GAAG,aAAa;AAG9D,MAAI,kBAAkB,UAAU,YAC9B,QAAO,CACL;GACE,QAAQ;GACR,QAAQ;IAAE,MAAM;IAAwB,UAAU;IAAgB;GACnE,EACD,GAAG,aACJ;EAGH,MAAM,QAAQ,oBACZ,iBAAiB,GACjB,UAAU,WACV,UAAU,UACV,UAAU,CACX;AAED,SAAO;GACL;IACE,QAAQ;IACR,SAAS,iBAAiB;IAC1B,eAAe;IAChB;GACD,GAAG;GACH;IAAE,MAAM;IAAyB,SAAS;IAAO;GAClD;;AAGH,QAAO;EACL,MAAM,CAAC,EAAE,QAAQ,gBAAgB,CAAC;EAElC,OAAO,KAAK,OAAoD;AAC9D,WAAQ,IAAI,MAAZ;IAIE,KAAK;AACH,SAAI,MAAM,WAAW,eAAgB,QAAO,CAAC,MAAM;AACnD,YAAO,CACL;MAAE,QAAQ;MAAc,SAAS;MAAG,EACpC;MAAE,MAAM;MAAoB,SAAS;MAAG,CACzC;IAMH,KAAK;AACH,SAAI,MAAM,WAAW,aAAc,QAAO,CAAC,MAAM;AACjD,YAAO,CAAC,EAAE,QAAQ,aAAa,EAAE,EAAE,MAAM,mBAAmB,CAAC;IAM/D,KAAK;AAEH,SAAI,MAAM,WAAW,QAAS,QAAO,CAAC,MAAM;AAG5C,SAAI,MAAM,WAAW,YACnB,QAAO,CAAC,EAAE,QAAQ,SAAS,EAAE,EAAE,MAAM,6BAA6B,CAAC;AAKrE,SAAI,MAAM,WAAW,aACnB,QAAO;MACL,EAAE,QAAQ,SAAS;MACnB,EAAE,MAAM,mBAAmB;MAC3B,EAAE,MAAM,6BAA6B;MACtC;AAGH,YAAO,CAAC,MAAM;IAMhB,KAAK,iBAAiB;KACpB,MAAM,SAA2B;MAC/B,MAAM;MACN,MAAM,IAAI;MACV,QAAQ,IAAI;MACb;AAED,SAAI,MAAM,WAAW,YACnB,QAAO,aAAa,GAAG,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5D,SAAI,MAAM,WAAW,QACnB,QAAO,aACL,GACA,QACA,EAAE,MAAM,kBAAkB,EAC1B,EAAE,MAAM,kBAAkB,CAC3B;AAGH,YAAO,CAAC,MAAM;;IAMhB,KAAK,gBAAgB;KACnB,MAAM,SAA2B;MAC/B,MAAM;MACN,OAAO,IAAI;MACZ;AAED,SAAI,MAAM,WAAW,aACnB,QAAO,aAAa,MAAM,SAAS,OAAO;AAG5C,SAAI,MAAM,WAAW,YACnB,QAAO,aAAa,GAAG,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5D,SAAI,MAAM,WAAW,QACnB,QAAO,aACL,GACA,QACA,EAAE,MAAM,kBAAkB,EAC1B,EAAE,MAAM,kBAAkB,CAC3B;AAGH,YAAO,CAAC,MAAM;;IAMhB,KAAK;AACH,SAAI,MAAM,WAAW,eAAgB,QAAO,CAAC,MAAM;AACnD,YAAO,CACL;MAAE,QAAQ;MAAc,SAAS,MAAM;MAAS,EAChD;MAAE,MAAM;MAAoB,SAAS,MAAM;MAAS,CACrD;IAMH,KAAK,QAAQ;AACX,SAAI,MAAM,WAAW,eAAgB,QAAO,CAAC,MAAM;KAEnD,MAAM,UAA4B,CAAC,EAAE,MAAM,0BAA0B,CAAC;AAEtE,SAAI,MAAM,WAAW,aACnB,SAAQ,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAG3C,SAAI,MAAM,WAAW,YACnB,SAAQ,KACN,EAAE,MAAM,mBAAmB,EAC3B,EAAE,MAAM,kBAAkB,CAC3B;AAGH,SAAI,MAAM,WAAW,QACnB,SAAQ,KACN,EAAE,MAAM,mBAAmB,EAC3B,EAAE,MAAM,kBAAkB,EAC1B,EAAE,MAAM,kBAAkB,CAC3B;AAGH,YAAO,CACL;MAAE,QAAQ;MAAgB,QAAQ,EAAE,MAAM,eAAe;MAAE,EAC3D,GAAG,QACJ;;;;EAIR;;;;;;;;;ACrPH,MAAa,cAAc;CACzB,YAAY;CACZ,MAAM;CACN,SAAS;CACT,QAAQ;CACT;;;;;;;AAmND,SAAgB,sBAAsB,IAAuB;AAC3D,QAAO;EACL,KAAK,MAA8C;AACjD,MAAG,KAAK,KAAK;;EAGf,MAAM,MAAe,QAAuB;AAC1C,MAAG,MAAM,MAAM,OAAO;;EAGxB,UAAU,SAAiE;AACzE,MAAG,iBAAiB,YAAW,UAAS;AACtC,QAAI,MAAM,gBAAgB,YACxB,SAAQ,IAAI,WAAW,MAAM,KAAK,CAAC;aAC1B,OAAO,SAAS,eAAe,MAAM,gBAAgB,KAE9D,OAAM,KAAK,aAAa,CAAC,MAAK,WAAU;AACtC,aAAQ,IAAI,WAAW,OAAO,CAAC;MAC/B;QAEF,SAAQ,MAAM,KAAe;KAE/B;;EAGJ,QAAQ,SAAuD;AAC7D,MAAG,iBAAiB,UAAS,UAAS;AACpC,YAAQ,MAAM,MAAM,MAAM,OAAO;KACjC;;EAGJ,QAAQ,SAAuC;AAC7C,MAAG,iBAAiB,UAAS,WAAU;AACrC,4BAAQ,IAAI,MAAM,kBAAkB,CAAC;KACrC;;EAGJ,IAAI,aAA+B;AACjC,WAAQ,GAAG,YAAX;IACE,KAAK,YAAY,WACf,QAAO;IACT,KAAK,YAAY,KACf,QAAO;IACT,KAAK,YAAY,QACf,QAAO;IACT,KAAK,YAAY,OACf,QAAO;IACT,QACE,QAAO;;;EAGd;;;;;;;AA8BH,SAAgB,kBAAkB,IAA+B;CAC/D,MAAM,aAAa;CACnB,MAAM,OAAO;CACb,MAAM,UAAU;AAEhB,QAAO;EACL,KAAK,MAA8C;AACjD,MAAG,KAAK,KAAK;;EAGf,MAAM,MAAe,QAAuB;AAC1C,MAAG,MAAM,MAAM,OAAO;;EAGxB,UAAU,SAAiE;AACzE,MAAG,GACD,YACC,MAAqC,aAAsB;AAC1D,QAAI,SACF,KAAI,gBAAgB,YAClB,SAAQ,IAAI,WAAW,KAAK,CAAC;aACpB,OAAO,WAAW,eAAe,OAAO,SAAS,KAAK,CAC/D,SAAQ,IAAI,WAAW,KAAK,CAAC;QAE7B,SAAQ,IAAI,WAAW,KAA+B,CAAC;aAGrD,OAAO,WAAW,eAAe,OAAO,SAAS,KAAK,CACxD,SAAQ,KAAK,SAAS,QAAQ,CAAC;QAE/B,SAAQ,KAAe;KAI9B;;EAGH,QAAQ,SAAuD;AAC7D,MAAG,GAAG,UAAU,MAAc,WAAmB;AAC/C,YAAQ,MAAM,OAAO,UAAU,CAAC;KAChC;;EAGJ,QAAQ,SAAuC;AAC7C,MAAG,GAAG,SAAS,QAAQ;;EAGzB,IAAI,aAA+B;AACjC,WAAQ,GAAG,YAAX;IACE,KAAK,WACH,QAAO;IACT,KAAK,KACH,QAAO;IACT,KAAK,QACH,QAAO;IACT,QACE,QAAO;;;EAGd;;;;;;;;AC3TH,MAAa,6BAA6B,MAAM;;;;;;;;;;;;;;;AAuFhD,IAAa,2BAAb,cAA8C,UAAgB;CAC5D;CACA;CACA;CAGA;CAGA;CACA;CACA;CACA;CAGA;CACA;CAGA,cAA0B,iBAAiB;CAE3C,YAAY,SAAiC;AAC3C,QAAM,EAAE,eAAe,oBAAoB,CAAC;AAC5C,QAAA,UAAgB;AAChB,QAAA,gBAAsB,QAAQ;AAC9B,QAAA,oBACE,QAAQ,qBAAA;AACV,QAAA,cAAoB,IAAI,oBAAoB,EAC1C,WAAW,KACZ,CAAC;EAEF,MAAM,UAAU,sBAAsB,EACpC,WAAW,QAAQ,WACpB,CAAC;AAEF,QAAA,SAAe,wBAAwB,UAAU,QAAQ,aAAa;AACpE,SAAA,cAAoB,QAAQ,SAAS;IACrC;AAGF,QAAA,sBAA4B;;CAO9B,eACE,QACA,UACM;AACN,UAAQ,OAAO,MAAf;GACE,KAAK;AACH,UAAA,kBAAwB,SAAS;AACjC;GAGF,KAAK;AACH,QAAI,MAAA,QAAc;AAChB,WAAA,OAAa,MAAM,KAAM,uBAAuB;AAChD,WAAA,SAAe,KAAA;;AAEjB;GAGF,KAAK;AAEH,QAAI,MAAA,eAAqB;AACvB,UAAK,cAAc,MAAA,cAAoB,UAAU;AACjD,WAAA,gBAAsB,KAAA;;AAKxB,UAAA,YAAkB,OAAO;AAEzB,UAAA,gBAAsB,KAAK,YAAY;AAGvC,SAAK,iBAAiB,MAAA,cAAoB,UAAU;AACpD;GAGF,KAAK;AACH,QAAI,MAAA,eAAqB;AACvB,UAAK,cAAc,MAAA,cAAoB,UAAU;AACjD,WAAA,gBAAsB,KAAA;;AAExB;GAGF,KAAK;AACH,UAAA,iBAAuB,iBAAiB;AACtC,WAAA,iBAAuB,KAAA;AACvB,cAAS,EAAE,MAAM,yBAAyB,CAAC;OAC1C,OAAO,QAAQ;AAClB;GAGF,KAAK;AACH,QAAI,MAAA,mBAAyB,KAAA,GAAW;AACtC,kBAAa,MAAA,eAAqB;AAClC,WAAA,iBAAuB,KAAA;;AAEzB;GAGF,KAAK;AACH,UAAA,gBAAsB;AACtB;GAGF,KAAK;AACH,UAAA,eAAqB;AACrB;;;;;;;;;;CAgBN,mBAAmB,UAA4C;EAC7D,MAAM,SAAS,MAAA;AACf,MAAI,CAAC,QAAQ;AACX,YAAS;IACP,MAAM;IACN,uBAAO,IAAI,MAAM,iCAAiC;IACnD,CAAC;AACF;;EAIF,MAAM,MACJ,OAAO,MAAA,QAAc,QAAQ,aACzB,MAAA,QAAc,IAAI,OAAO,GACzB,MAAA,QAAc;AAEpB,MAAI;AAIF,OACE,MAAA,QAAc,WACd,OAAO,KAAK,MAAA,QAAc,QAAQ,CAAC,SAAS,EAE5C,OAAA,SAAe,IAAI,MAAA,cAAoB,KAAK,EAC1C,SAAS,MAAA,QAAc,SACxB,CAAC;OAEF,OAAA,SAAe,IAAI,MAAA,cAAoB,IAAI;AAE7C,SAAA,OAAa,aAAa;GAE1B,MAAM,SAAS,MAAA;AAIf,UAAO,iBAAiB,YAAY,UAAiC;AACnE,UAAA,cAAoB,OAAO,SAAS;KACpC;GAGF,IAAI,UAAU;GAEd,MAAM,eAAe;AACnB,aAAS;AACT,cAAU;AACV,aAAS,EAAE,MAAM,iBAAiB,CAAC;AAGnC,WAAO,iBAAiB,UAAU,UAA+B;AAC/D,cAAS;MACP,MAAM;MACN,MAAM,MAAM;MACZ,QAAQ,MAAM;MACf,CAAC;MACF;;GAGJ,MAAM,gBAAgB;AACpB,QAAI,QAAS;AACb,aAAS;AACT,cAAU;AACV,aAAS;KACP,MAAM;KACN,uBAAO,IAAI,MAAM,8BAA8B;KAChD,CAAC;;GAGJ,MAAM,gBAAgB;AACpB,QAAI,QAAS;AACb,aAAS;AACT,cAAU;AACV,aAAS;KACP,MAAM;KACN,uBAAO,IAAI,MAAM,qCAAqC;KACvD,CAAC;;GAGJ,MAAM,gBAAgB;AACpB,WAAO,oBAAoB,QAAQ,OAAO;AAC1C,WAAO,oBAAoB,SAAS,QAAQ;AAC5C,WAAO,oBAAoB,SAAS,QAAQ;;AAG9C,UAAO,iBAAiB,QAAQ,OAAO;AACvC,UAAO,iBAAiB,SAAS,QAAQ;AACzC,UAAO,iBAAiB,SAAS,QAAQ;WAClC,OAAO;AACd,YAAS;IACP,MAAM;IACN,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC;IACjE,CAAC;;;;;;;;;CAcN,eACE,OACA,UACM;EACN,MAAM,OAAO,MAAM;AAGnB,MAAI,OAAO,SAAS,UAAU;AAC5B,OAAI,SAAS,QACX,UAAS,EAAE,MAAM,gBAAgB,CAAC;AAGpC;;AAIF,MAAI,gBAAgB,YAClB,KAAI;GACF,MAAM,QAAQ,kBAAkB,IAAI,WAAW,KAAK,EAAE,MAAA,YAAkB;AACxE,OAAI,CAAC,MAAO;AACZ,QAAK,MAAM,QAAQ,OAAO;IACxB,MAAM,SAAS,qBAAqB,MAAA,YAAkB,KAAK;AAC3D,UAAA,aAAmB,OAAO;AAC1B,QAAI,OAAO,SAAS,CAAC,OAAO,KAAK;AAC/B,aAAQ,KACN,8CACA,OAAO,MACR;AACD;;AAEF,UAAA,qBAA2B,OAAO,IAAI;;WAEjC,OAAO;AACd,WAAQ,MAAM,6BAA6B,MAAM;;;;;;CAQvD,sBAAsB,KAAuB;AAC3C,MAAI,CAAC,MAAA,cACH;AAIF,QAAA,cAAoB,UAAU,IAAI;;CAOpC,kBAAwB;AACtB,QAAA,eAAqB;EAErB,MAAM,WAAW,MAAA,QAAc,qBAAqB;AAEpD,QAAA,iBAAuB,kBAAkB;AACvC,OAAI,MAAA,QAAc,eAAe,YAAY,KAC3C,OAAA,OAAa,KAAK,OAAO;KAE1B,SAAS;;CAGd,iBAAuB;AACrB,MAAI,MAAA,gBAAsB;AACxB,iBAAc,MAAA,eAAqB;AACnC,SAAA,iBAAuB,KAAA;;;CAQ3B,wBAA8B;EAE5B,IAAI,qBAAqB;AAEzB,QAAA,OAAa,wBAAuB,eAAc;AAEhD,SAAA,QAAc,WAAW,gBAAgB,WAAW;GAEpD,MAAM,EAAE,MAAM,OAAO;AAGrB,OAAI,GAAG,WAAW,kBAAkB,GAAG,OACrC,OAAA,QAAc,WAAW,eAAe,GAAG,OAAO;AAIpD,OAAI,GAAG,WAAW,eAChB,OAAA,QAAc,WAAW,iBAAiB,GAAG,SAAS,GAAG,cAAc;AAIzE,OACE,uBACC,KAAK,WAAW,kBAAkB,KAAK,WAAW,kBAClD,GAAG,WAAW,eAAe,GAAG,WAAW,SAE5C,OAAA,QAAc,WAAW,iBAAiB;AAI5C,OAAI,GAAG,WAAW,SAAS;AACzB,UAAA,QAAc,WAAW,WAAW;AACpC,yBAAqB;;IAEvB;;;;;CAUJ,WAAiC;AAC/B,SAAO,MAAA,OAAa,UAAU;;;;;CAMhC,uBACE,UACY;AACZ,SAAO,MAAA,OAAa,uBAAuB,SAAS;;;;;CAMtD,aACE,WACA,SAC+B;AAC/B,SAAO,MAAA,OAAa,aAAa,WAAW,QAAQ;;;;;CAMtD,cACE,QACA,SAC+B;AAC/B,SAAO,MAAA,OAAa,cAAc,QAAQ,QAAQ;;;;;CAMpD,IAAI,UAAmB;AACrB,SAAO,MAAA,OAAa,UAAU,CAAC,WAAW;;CAO5C,WAAuC;EACrC,MAAM,cAAc,sBAAsB;AAE1C,QAAA,aAAmB,iBAAiB;AACpC,SAAO;GACL,eAAe,KAAK;GACpB,OAAO,QAAoB;IACzB,MAAM,SAAS,MAAA;AACf,QAAI,CAAC,UAAU,OAAO,eAAe,YAAY,KAC/C;IAGF,MAAM,EAAE,OAAO,SAAS,sBAAsB,MAAA,YAAkB,IAAI;AACpE,UAAA,aAAmB;AAEnB,2BACE,OACA,SAAQ,OAAO,KAAK,IAAI,WAAW,KAAK,CAAC,OAAO,EAChD,MAAA,mBACA,YACD;;GAEH,YAAY;GAKb;;CAGH,MAAM,UAAyB;AAC7B,MAAI,CAAC,KAAK,SACR,OAAM,IAAI,MACR,8DACD;AAEH,QAAA,SAAe,KAAK,SAAS;AAC7B,QAAA,OAAa,SAAS,EAAE,MAAM,SAAS,CAAC;;CAG1C,MAAM,SAAwB;AAC5B,QAAA,YAAkB,SAAS;AAC3B,QAAA,OAAa,SAAS,EAAE,MAAM,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;AA4B3C,SAAgB,sBACd,SACkB;AAClB,cAAa,IAAI,yBAAyB,QAAQ"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as WebSocketConstructor, d as WebsocketClientState, f as WebsocketClientStateTransition, t as DisconnectReason } from "./types-
|
|
1
|
+
import { c as WebSocketConstructor, d as WebsocketClientState, f as WebsocketClientStateTransition, t as DisconnectReason } from "./types-c1S_xIRG.js";
|
|
2
2
|
import { GeneratedChannel, PeerId, Transport, TransportFactory } from "@kyneta/transport";
|
|
3
3
|
import { TransitionListener as TransitionListener$1 } from "@kyneta/machine";
|
|
4
4
|
|
|
@@ -131,4 +131,4 @@ declare class WebsocketClientTransport extends Transport<void> {
|
|
|
131
131
|
declare function createWebsocketClient(options: WebsocketClientOptions): TransportFactory;
|
|
132
132
|
//#endregion
|
|
133
133
|
export { createWebsocketClient as a, WebsocketClientTransport as i, WebsocketClientLifecycleEvents as n, WebsocketClientOptions as r, DEFAULT_FRAGMENT_THRESHOLD as t };
|
|
134
|
-
//# sourceMappingURL=client-transport-
|
|
134
|
+
//# sourceMappingURL=client-transport-D3tYQYrS.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-transport-
|
|
1
|
+
{"version":3,"file":"client-transport-D3tYQYrS.d.ts","names":[],"sources":["../src/client-transport.ts"],"mappings":";;;;;;;;;cAkEa,0BAAA;;;;UAKI,sBAAA;EAoBL;EAlBV,GAAA,aAAgB,MAAA,EAAQ,MAAA;EAuCkB;;;;;;;EA9B1C,SAAA,EAAW,oBAAA;EASD;;;;;;;EAAV,OAAA,GAAU,MAAA;EAqBV;EAlBA,SAAA;IACE,OAAA;IACA,WAAA;IACA,SAAA;IACA,QAAA;EAAA;EAyBsC;EArBxC,iBAAA;EAkB6B;;;;;EAX7B,iBAAA;EAiBkB;EAdlB,SAAA,GAAY,8BAAA;AAAA;;;;UAMG,8BAAA;EAmCqB;EAjCpC,aAAA,IAAiB,UAAA,EAAY,8BAAA;EAsDR;EAnDrB,YAAA,IAAgB,MAAA,EAAQ,gBAAA;EA0YO;EAvY/B,cAAA,IAAkB,OAAA,UAAiB,aAAA;EAgZd;EA7YrB,aAAA;EA+YG;EA5YH,OAAA;AAAA;;;;;;;;;;;;;;;cAqBW,wBAAA,SAAiC,SAAA;EAAA;cAqBhC,OAAA,EAAS,sBAAA;EAuVnB;;;EARF,QAAA,CAAA,GAAY,oBAAA;EAiBV;;;EAVF,sBAAA,CACE,QAAA,EAAU,oBAAA,CAAmB,oBAAA;EAWpB;;;EAHX,YAAA,CACE,SAAA,GAAY,KAAA,EAAO,oBAAA,cACnB,OAAA;IAAY,SAAA;EAAA,IACX,OAAA,CAAQ,oBAAA;EAUR;;;EAHH,aAAA,CACE,MAAA,EAAQ,oBAAA,YACR,OAAA;IAAY,SAAA;EAAA,IACX,OAAA,CAAQ,oBAAA;EA6CM;;;EAAA,IAtCb,OAAA,CAAA;EAAA,UAQM,QAAA,CAAA,GAAY,gBAAA;EA8BhB,OAAA,CAAA,GAAW,OAAA;EAUX,MAAA,CAAA,GAAU,OAAA;AAAA;;;;;;;;;;;;;;;;;;;;;iBA8BF,qBAAA,CACd,OAAA,EAAS,sBAAA,GACR,gBAAA"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { _ as wrapStandardWebsocket, a as SocketReadyState, c as WebSocketConstructor, g as wrapNodeWebsocket, h as WebsocketConnectionResult, i as Socket, l as WebSocketLike, m as WebsocketConnectionOptions, n as NodeWebsocketLike, p as WebsocketConnectionHandle, r as READY_STATE, s as WebSocketCloseEvent, t as DisconnectReason, u as WebSocketMessageEvent } from "./types-
|
|
2
|
-
import { r as WebsocketClientOptions } from "./client-transport-
|
|
1
|
+
import { _ as wrapStandardWebsocket, a as SocketReadyState, c as WebSocketConstructor, g as wrapNodeWebsocket, h as WebsocketConnectionResult, i as Socket, l as WebSocketLike, m as WebsocketConnectionOptions, n as NodeWebsocketLike, p as WebsocketConnectionHandle, r as READY_STATE, s as WebSocketCloseEvent, t as DisconnectReason, u as WebSocketMessageEvent } from "./types-c1S_xIRG.js";
|
|
2
|
+
import { r as WebsocketClientOptions } from "./client-transport-D3tYQYrS.js";
|
|
3
3
|
import { Channel, ChannelMsg, GeneratedChannel, PeerId, Transport, TransportFactory } from "@kyneta/transport";
|
|
4
4
|
|
|
5
5
|
//#region src/connection.d.ts
|
|
@@ -50,7 +50,8 @@ declare class WebsocketConnection {
|
|
|
50
50
|
/**
|
|
51
51
|
* Send a ChannelMsg through the Websocket.
|
|
52
52
|
*
|
|
53
|
-
*
|
|
53
|
+
* Pipeline: alias transformer → wire encode → frame → fragment if needed
|
|
54
|
+
* → socket.send().
|
|
54
55
|
*/
|
|
55
56
|
send(msg: ChannelMsg): void;
|
|
56
57
|
/**
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","names":[],"sources":["../src/connection.ts","../src/server-transport.ts","../src/service-client.ts"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"server.d.ts","names":[],"sources":["../src/connection.ts","../src/server-transport.ts","../src/service-client.ts"],"mappings":";;;;;;;;AA8BA;;cAAa,0BAAA;;;AAKb;UAAiB,yBAAA;;;;AAkBjB;;EAZE,iBAAA;AAAA;;;;;;;;;;cAYW,mBAAA;EAAA;WACF,MAAA,EAAQ,MAAA;EAAA,SACR,SAAA;cAgBP,MAAA,EAAQ,MAAA,EACR,SAAA,UACA,MAAA,EAAQ,MAAA,EACR,MAAA,GAAS,yBAAA;EAFT;;;;;EA4BF,WAAA,CAAY,OAAA,EAAS,OAAA;EAAA;;;;;;EAcrB,KAAA,CAAA;EAkDA;;;;;;EAjCA,IAAA,CAAK,GAAA,EAAK,UAAA;;AClFZ;;;;;AAiCA;EDwEE,SAAA,CAAA;;;;EAUA,KAAA,CAAM,IAAA,WAAe,MAAA;AAAA;;;;AAtIvB;;UCmBiB,+BAAA;EDnBmC;;AAKpD;;;ECoBE,iBAAA;AAAA;ADFF;;;;;;;;;;;;;;;;;;;;AAAA,cC6Ba,wBAAA,SAAiC,SAAA,CAAU,MAAA;EAAA;cAI1C,OAAA,GAAU,+BAAA;EAAA,UAUZ,QAAA,CAAS,MAAA,EAAQ,MAAA,GAAS,gBAAA;EAe9B,OAAA,CAAA,GAAW,OAAA;EAIX,MAAA,CAAA,GAAU,OAAA;EDDhB;;;;;;;;;;;;;ACjEF;;;;;AAiCA;;;;;;;;;;EAyEE,gBAAA,CACE,OAAA,EAAS,0BAAA,GACR,yBAAA;EA+DmB;;;EAAtB,aAAA,CAAc,MAAA,EAAQ,MAAA,GAAS,mBAAA;EAqBF;;;EAd7B,iBAAA,CAAA,GAAqB,mBAAA;EAjJgC;;;EAwJrD,WAAA,CAAY,MAAA,EAAQ,MAAA;;;;EAOpB,oBAAA,CAAqB,MAAA,EAAQ,MAAA;EAjJF;;;EA4J3B,SAAA,CAAU,GAAA,EAAK,UAAA;EA7IE;;;EAAA,IAsJb,eAAA,CAAA;AAAA;;;;;;ADvON;;;;;KETY,6BAAA,GAAgC,sBAAA;;;;;AFgC5C;;;;;;;;;;;;;;;;;;;;iBENgB,4BAAA,CACd,OAAA,EAAS,6BAAA,GACR,gBAAA"}
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { a as wrapNodeWebsocket, i as READY_STATE, n as WebsocketClientTransport, o as wrapStandardWebsocket } from "./client-transport-
|
|
1
|
+
import { a as wrapNodeWebsocket, i as READY_STATE, n as WebsocketClientTransport, o as wrapStandardWebsocket } from "./client-transport-B2V7s2VP.js";
|
|
2
2
|
import { Transport } from "@kyneta/transport";
|
|
3
|
-
import { FragmentReassembler,
|
|
3
|
+
import { FragmentReassembler, applyInboundAliasing, applyOutboundAliasing, createFrameIdCounter, decodeBinaryWires, emptyAliasState, encodeWireFrameAndSend } from "@kyneta/wire";
|
|
4
|
+
import { randomPeerId } from "@kyneta/random";
|
|
4
5
|
//#region src/connection.ts
|
|
5
6
|
/**
|
|
6
7
|
* Default fragment threshold in bytes.
|
|
@@ -25,6 +26,8 @@ var WebsocketConnection = class {
|
|
|
25
26
|
#started = false;
|
|
26
27
|
#fragmentThreshold;
|
|
27
28
|
#reassembler;
|
|
29
|
+
#nextFrameId = createFrameIdCounter();
|
|
30
|
+
#aliasState = emptyAliasState();
|
|
28
31
|
constructor(peerId, channelId, socket, config) {
|
|
29
32
|
this.peerId = peerId;
|
|
30
33
|
this.channelId = channelId;
|
|
@@ -61,11 +64,14 @@ var WebsocketConnection = class {
|
|
|
61
64
|
/**
|
|
62
65
|
* Send a ChannelMsg through the Websocket.
|
|
63
66
|
*
|
|
64
|
-
*
|
|
67
|
+
* Pipeline: alias transformer → wire encode → frame → fragment if needed
|
|
68
|
+
* → socket.send().
|
|
65
69
|
*/
|
|
66
70
|
send(msg) {
|
|
67
71
|
if (this.#socket.readyState !== "open") return;
|
|
68
|
-
|
|
72
|
+
const { state, wire } = applyOutboundAliasing(this.#aliasState, msg);
|
|
73
|
+
this.#aliasState = state;
|
|
74
|
+
encodeWireFrameAndSend(wire, (data) => this.#socket.send(data), this.#fragmentThreshold, this.#nextFrameId);
|
|
69
75
|
}
|
|
70
76
|
/**
|
|
71
77
|
* Send a "ready" signal to the client.
|
|
@@ -94,8 +100,17 @@ var WebsocketConnection = class {
|
|
|
94
100
|
return;
|
|
95
101
|
}
|
|
96
102
|
try {
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
103
|
+
const wires = decodeBinaryWires(data, this.#reassembler);
|
|
104
|
+
if (!wires) return;
|
|
105
|
+
for (const wire of wires) {
|
|
106
|
+
const result = applyInboundAliasing(this.#aliasState, wire);
|
|
107
|
+
this.#aliasState = result.state;
|
|
108
|
+
if (result.error || !result.msg) {
|
|
109
|
+
console.warn("[WebsocketConnection] alias resolution failed:", result.error);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
this.#handleChannelMessage(result.msg);
|
|
113
|
+
}
|
|
99
114
|
} catch (error) {
|
|
100
115
|
console.error("Failed to decode wire message:", error);
|
|
101
116
|
}
|
|
@@ -124,15 +139,6 @@ var WebsocketConnection = class {
|
|
|
124
139
|
//#endregion
|
|
125
140
|
//#region src/server-transport.ts
|
|
126
141
|
/**
|
|
127
|
-
* Generate a random peer ID for connections that don't provide one.
|
|
128
|
-
*/
|
|
129
|
-
function generatePeerId() {
|
|
130
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
131
|
-
let result = "ws-";
|
|
132
|
-
for (let i = 0; i < 12; i++) result += chars.charAt(Math.floor(Math.random() * 62));
|
|
133
|
-
return result;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
142
|
* Websocket server network adapter.
|
|
137
143
|
*
|
|
138
144
|
* Framework-agnostic — works with any Websocket library through the
|
|
@@ -206,7 +212,7 @@ var WebsocketServerTransport = class extends Transport {
|
|
|
206
212
|
*/
|
|
207
213
|
handleConnection(options) {
|
|
208
214
|
const { socket, peerId: providedPeerId } = options;
|
|
209
|
-
const peerId = providedPeerId ??
|
|
215
|
+
const peerId = providedPeerId ?? `ws-${randomPeerId()}`;
|
|
210
216
|
const existingConnection = this.#connections.get(peerId);
|
|
211
217
|
if (existingConnection) {
|
|
212
218
|
existingConnection.close(1e3, "Replaced by new connection");
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":["#fragmentThreshold","#reassembler","#socket","#channel","#started","#handleMessage","#handleKeepalive","#handleChannelMessage","#fragmentThreshold","#connections"],"sources":["../src/connection.ts","../src/server-transport.ts","../src/service-client.ts"],"sourcesContent":["// connection — WebsocketConnection for server-side peer connections.\n//\n// Wraps a Socket + CBOR codec + FragmentReassembler to provide\n// send/receive for ChannelMsg over a single Websocket connection.\n//\n// Used by WebsocketServerTransport to manage individual client connections.\n// The client adapter handles its own encoding/decoding inline since it\n// manages a single socket with reconnection logic.\n//\n// Ported from @loro-extended/adapter-websocket's WsConnection with\n// kyneta naming conventions and the kyneta wire format.\n\nimport type { Channel, ChannelMsg, PeerId } from \"@kyneta/transport\"\nimport {\n decodeBinaryMessages,\n encodeBinaryAndSend,\n FragmentReassembler,\n} from \"@kyneta/wire\"\nimport type { Socket } from \"./types.js\"\n\n/**\n * Default fragment threshold in bytes.\n * Messages larger than this are fragmented for cloud infrastructure compatibility.\n * AWS API Gateway has a 128KB limit, so 100KB provides a safe margin.\n */\nexport const DEFAULT_FRAGMENT_THRESHOLD = 100 * 1024\n\n/**\n * Configuration for creating a WebsocketConnection.\n */\nexport interface WebsocketConnectionConfig {\n /**\n * Fragment threshold in bytes. Messages larger than this are fragmented.\n * Set to 0 to disable fragmentation (not recommended for cloud deployments).\n * Default: 100KB (safe for AWS API Gateway's 128KB limit)\n */\n fragmentThreshold?: number\n}\n\n/**\n * Represents a single Websocket connection to a peer (server-side).\n *\n * Manages encoding, framing, fragmentation, and reassembly for one\n * connected client. Created by `WebsocketServerTransport.handleConnection()`.\n *\n * The connection uses the CBOR codec for binary transport — this is\n * the natural choice for Websocket's binary frame support.\n */\nexport class WebsocketConnection {\n readonly peerId: PeerId\n readonly channelId: number\n\n #socket: Socket\n #channel: Channel | null = null\n #started = false\n\n // Fragmentation support\n readonly #fragmentThreshold: number\n readonly #reassembler: FragmentReassembler\n\n constructor(\n peerId: PeerId,\n channelId: number,\n socket: Socket,\n config?: WebsocketConnectionConfig,\n ) {\n this.peerId = peerId\n this.channelId = channelId\n this.#socket = socket\n this.#fragmentThreshold =\n config?.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD\n this.#reassembler = new FragmentReassembler({\n timeoutMs: 10_000,\n onTimeout: (frameId: string) => {\n console.warn(\n `[WebsocketConnection] Fragment batch timed out: ${frameId}`,\n )\n },\n })\n }\n\n // ==========================================================================\n // INTERNAL API — for adapter use\n // ==========================================================================\n\n /**\n * Set the channel reference.\n * Called by the adapter when the channel is created.\n * @internal\n */\n _setChannel(channel: Channel): void {\n this.#channel = channel\n }\n\n // ==========================================================================\n // PUBLIC API\n // ==========================================================================\n\n /**\n * Start processing messages on this connection.\n *\n * Sets up the message handler on the socket. Must be called after\n * the connection is fully set up (channel assigned, stored in adapter).\n */\n start(): void {\n if (this.#started) {\n return\n }\n this.#started = true\n\n this.#socket.onMessage(data => {\n this.#handleMessage(data)\n })\n }\n\n /**\n * Send a ChannelMsg through the Websocket.\n *\n * Encodes via CBOR codec → frame → fragment if needed → socket.send().\n */\n send(msg: ChannelMsg): void {\n if (this.#socket.readyState !== \"open\") {\n return\n }\n\n encodeBinaryAndSend(msg, this.#fragmentThreshold, data =>\n this.#socket.send(data),\n )\n }\n\n /**\n * Send a \"ready\" signal to the client.\n *\n * This is a transport-level text message that tells the client the\n * server is ready to receive protocol messages. The client creates\n * its channel and sends establish after receiving this.\n */\n sendReady(): void {\n if (this.#socket.readyState !== \"open\") {\n return\n }\n this.#socket.send(\"ready\")\n }\n\n /**\n * Close the connection and clean up resources.\n */\n close(code?: number, reason?: string): void {\n this.#reassembler.dispose()\n this.#socket.close(code, reason)\n }\n\n // ==========================================================================\n // INTERNAL — message handling\n // ==========================================================================\n\n /**\n * Handle an incoming message from the Websocket.\n */\n #handleMessage(data: Uint8Array | string): void {\n // Handle keepalive ping/pong (text frames)\n if (typeof data === \"string\") {\n this.#handleKeepalive(data)\n return\n }\n\n // Handle binary protocol messages through shared decode pipeline\n try {\n const messages = decodeBinaryMessages(data, this.#reassembler)\n if (messages) {\n for (const msg of messages) {\n this.#handleChannelMessage(msg)\n }\n }\n } catch (error) {\n console.error(\"Failed to decode wire message:\", error)\n }\n }\n\n /**\n * Handle a decoded channel message.\n *\n * Delivers messages synchronously. The Synchronizer's receive queue\n * handles recursion prevention by queuing messages and processing\n * them iteratively.\n */\n #handleChannelMessage(msg: ChannelMsg): void {\n if (!this.#channel) {\n console.error(\"Cannot handle message: channel not set\")\n return\n }\n\n // Deliver synchronously — the Synchronizer's receive queue prevents recursion\n this.#channel.onReceive(msg)\n }\n\n /**\n * Handle keepalive ping/pong messages.\n */\n #handleKeepalive(text: string): void {\n if (text === \"ping\") {\n this.#socket.send(\"pong\")\n }\n // Ignore \"pong\" and \"ready\" responses\n }\n}\n","// server-adapter — Websocket server adapter for @kyneta/exchange.\n//\n// Manages Websocket connections from clients, encoding/decoding via the\n// kyneta wire format. Framework-agnostic — works with any Websocket\n// library through the Socket interface.\n//\n// Usage with Bun:\n// import { WebsocketServerTransport } from \"@kyneta/websocket-transport/server\"\n// import { createBunWebsocketHandlers } from \"@kyneta/websocket-transport/bun\"\n//\n// const serverAdapter = new WebsocketServerTransport()\n// Bun.serve({\n// websocket: createBunWebsocketHandlers(serverAdapter),\n// fetch(req, server) { server.upgrade(req); return new Response(\"\", { status: 101 }) },\n// })\n//\n// Usage with Node.js `ws`:\n// import { WebsocketServerTransport, wrapNodeWebsocket } from \"@kyneta/websocket-transport/server\"\n// import { WebSocketServer } from \"ws\"\n//\n// const serverAdapter = new WebsocketServerTransport()\n// const wss = new WebSocketServer({ server })\n// wss.on(\"connection\", (ws) => {\n// const { start } = serverAdapter.handleConnection({ socket: wrapNodeWebsocket(ws) })\n// start()\n// })\n//\n// Ported from @loro-extended/adapter-websocket's WsServerNetworkAdapter with\n// kyneta naming conventions and the kyneta 5-message protocol.\n\nimport type { ChannelMsg, GeneratedChannel, PeerId } from \"@kyneta/transport\"\nimport { Transport } from \"@kyneta/transport\"\nimport {\n DEFAULT_FRAGMENT_THRESHOLD,\n WebsocketConnection,\n} from \"./connection.js\"\nimport type {\n WebsocketConnectionOptions,\n WebsocketConnectionResult,\n} from \"./types.js\"\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\n/**\n * Options for the Websocket server adapter.\n */\nexport interface WebsocketServerTransportOptions {\n /**\n * Fragment threshold in bytes. Messages larger than this are fragmented.\n * Set to 0 to disable fragmentation (not recommended for cloud deployments).\n * Default: 100KB (safe for AWS API Gateway's 128KB limit)\n */\n fragmentThreshold?: number\n}\n\n// ---------------------------------------------------------------------------\n// Peer ID generation\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a random peer ID for connections that don't provide one.\n */\nfunction generatePeerId(): PeerId {\n const chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\"\n let result = \"ws-\"\n for (let i = 0; i < 12; i++) {\n result += chars.charAt(Math.floor(Math.random() * chars.length))\n }\n return result\n}\n\n// ---------------------------------------------------------------------------\n// WebsocketServerTransport\n// ---------------------------------------------------------------------------\n\n/**\n * Websocket server network adapter.\n *\n * Framework-agnostic — works with any Websocket library through the\n * `Socket` interface. Use `handleConnection()` to integrate with your\n * framework's Websocket upgrade handler.\n *\n * Each client connection is tracked as a `WebsocketConnection` keyed\n * by peer ID. The adapter creates a channel per connection and routes\n * outbound messages through the connection's send method.\n *\n * The connection handshake follows a two-phase protocol:\n * 1. Server sends text `\"ready\"` signal (transport-level)\n * 2. Client sends `establish` (protocol-level)\n * 3. Server upgrades channel and sends present (handled by Synchronizer)\n *\n * The server does NOT call `establishChannel()` — it waits for the\n * client's establish to avoid a race condition where the binary\n * establish could arrive before the client has processed \"ready\".\n */\nexport class WebsocketServerTransport extends Transport<PeerId> {\n #connections = new Map<PeerId, WebsocketConnection>()\n readonly #fragmentThreshold: number\n\n constructor(options?: WebsocketServerTransportOptions) {\n super({ transportType: \"websocket-server\" })\n this.#fragmentThreshold =\n options?.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD\n }\n\n // ==========================================================================\n // Adapter abstract method implementations\n // ==========================================================================\n\n protected generate(peerId: PeerId): GeneratedChannel {\n return {\n transportType: this.transportType,\n send: (msg: ChannelMsg) => {\n const connection = this.#connections.get(peerId)\n if (connection) {\n connection.send(msg)\n }\n },\n stop: () => {\n this.unregisterConnection(peerId)\n },\n }\n }\n\n async onStart(): Promise<void> {\n // Server adapter starts passively — connections arrive via handleConnection()\n }\n\n async onStop(): Promise<void> {\n // Disconnect all active connections\n for (const connection of this.#connections.values()) {\n connection.close(1001, \"Server shutting down\")\n }\n this.#connections.clear()\n }\n\n // ==========================================================================\n // Connection management\n // ==========================================================================\n\n /**\n * Handle a new Websocket connection.\n *\n * Call this from your framework's Websocket upgrade handler.\n * Returns a connection handle and a `start()` function that begins\n * message processing and sends the \"ready\" signal.\n *\n * @param options - Connection options including the Socket and optional peer ID\n * @returns A connection handle and start function\n *\n * @example Bun\n * ```typescript\n * const { start } = serverAdapter.handleConnection({\n * socket: wrapBunWebsocket(ws),\n * })\n * start()\n * ```\n *\n * @example Node.js ws\n * ```typescript\n * wss.on(\"connection\", (ws) => {\n * const { start } = serverAdapter.handleConnection({\n * socket: wrapNodeWebsocket(ws),\n * })\n * start()\n * })\n * ```\n */\n handleConnection(\n options: WebsocketConnectionOptions,\n ): WebsocketConnectionResult {\n const { socket, peerId: providedPeerId } = options\n\n // Generate peer ID if not provided\n const peerId = providedPeerId ?? generatePeerId()\n\n // Check for existing connection with same peer ID\n const existingConnection = this.#connections.get(peerId)\n if (existingConnection) {\n existingConnection.close(1000, \"Replaced by new connection\")\n this.unregisterConnection(peerId)\n }\n\n // Create channel for this peer\n const channel = this.addChannel(peerId)\n\n // Create connection object with fragmentation config\n const connection = new WebsocketConnection(\n peerId,\n channel.channelId,\n socket,\n {\n fragmentThreshold: this.#fragmentThreshold,\n },\n )\n connection._setChannel(channel)\n\n // Store connection\n this.#connections.set(peerId, connection)\n\n // Set up close handler\n socket.onClose((_code, _reason) => {\n this.unregisterConnection(peerId)\n })\n\n socket.onError(_error => {\n this.unregisterConnection(peerId)\n })\n\n return {\n connection,\n start: () => {\n connection.start()\n\n // Send ready signal to client so it knows the server is ready\n // This is a transport-level signal, separate from protocol-level establishment\n connection.sendReady()\n\n // NOTE: We do NOT call establishChannel() here.\n // The client will send establish after receiving \"ready\".\n // Our channel gets established when the Synchronizer receives\n // and processes that establish message.\n //\n // This prevents a race condition where our binary establish\n // could arrive before the client has processed \"ready\" and created\n // its channel.\n },\n }\n }\n\n /**\n * Get an active connection by peer ID.\n */\n getConnection(peerId: PeerId): WebsocketConnection | undefined {\n return this.#connections.get(peerId)\n }\n\n /**\n * Get all active connections.\n */\n getAllConnections(): WebsocketConnection[] {\n return Array.from(this.#connections.values())\n }\n\n /**\n * Check if a peer is connected.\n */\n isConnected(peerId: PeerId): boolean {\n return this.#connections.has(peerId)\n }\n\n /**\n * Unregister a connection, removing its channel and cleaning up state.\n */\n unregisterConnection(peerId: PeerId): void {\n const connection = this.#connections.get(peerId)\n if (connection) {\n this.removeChannel(connection.channelId)\n this.#connections.delete(peerId)\n }\n }\n\n /**\n * Broadcast a message to all connected peers.\n */\n broadcast(msg: ChannelMsg): void {\n for (const connection of this.#connections.values()) {\n connection.send(msg)\n }\n }\n\n /**\n * Get the number of connected peers.\n */\n get connectionCount(): number {\n return this.#connections.size\n }\n}\n","// service-client — service-to-service WebSocket client factory.\n//\n// Extracted from client-transport.ts so that the service client factory\n// lives in the `./server` entry point (where it belongs) rather than\n// the `./browser` entry point. Backend code imports from `./server`;\n// browser code imports from `./browser`.\n\nimport type { TransportFactory } from \"@kyneta/transport\"\nimport {\n type WebsocketClientOptions,\n WebsocketClientTransport,\n} from \"./client-transport.js\"\n\n/**\n * Options for service-to-service Websocket connections.\n *\n * Identical to `WebsocketClientOptions` — the `headers` field is always\n * available on the base options. This alias exists for API clarity:\n * importing `ServiceWebsocketClientOptions` from `./server` signals\n * intent and pairs with `createServiceWebsocketClient`.\n */\nexport type ServiceWebsocketClientOptions = WebsocketClientOptions\n\n/**\n * Create a Websocket client transport for service-to-service connections.\n *\n * This factory is for backend environments (Bun, Node.js) where you need\n * to pass authentication headers during the Websocket upgrade.\n *\n * Note: Headers are a Bun/Node-specific extension. The browser WebSocket API\n * does not support custom headers. For browser clients, use\n * `createWebsocketClient()` and authenticate via URL query parameters.\n *\n * @example\n * ```typescript\n * import { createServiceWebsocketClient } from \"@kyneta/websocket-transport/server\"\n *\n * const exchange = new Exchange({\n * transports: [createServiceWebsocketClient({\n * url: \"ws://primary-server:3000/ws\",\n * WebSocket,\n * headers: { Authorization: \"Bearer token\" },\n * reconnect: { enabled: true },\n * })],\n * })\n * ```\n */\nexport function createServiceWebsocketClient(\n options: ServiceWebsocketClientOptions,\n): TransportFactory {\n return () => new WebsocketClientTransport(options)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.js","names":["#fragmentThreshold","#reassembler","#socket","#channel","#started","#handleMessage","#aliasState","#nextFrameId","#handleKeepalive","#handleChannelMessage","#fragmentThreshold","#connections"],"sources":["../src/connection.ts","../src/server-transport.ts","../src/service-client.ts"],"sourcesContent":["// connection — WebsocketConnection for server-side peer connections.\n//\n// Wraps a Socket + CBOR codec + FragmentReassembler to provide\n// send/receive for ChannelMsg over a single Websocket connection.\n//\n// Used by WebsocketServerTransport to manage individual client connections.\n// The client adapter handles its own encoding/decoding inline since it\n// manages a single socket with reconnection logic.\n//\n// Ported from @loro-extended/adapter-websocket's WsConnection with\n// kyneta naming conventions and the kyneta wire format.\n\nimport type { Channel, ChannelMsg, PeerId } from \"@kyneta/transport\"\nimport {\n type AliasState,\n applyInboundAliasing,\n applyOutboundAliasing,\n createFrameIdCounter,\n decodeBinaryWires,\n emptyAliasState,\n encodeWireFrameAndSend,\n FragmentReassembler,\n} from \"@kyneta/wire\"\nimport type { Socket } from \"./types.js\"\n\n/**\n * Default fragment threshold in bytes.\n * Messages larger than this are fragmented for cloud infrastructure compatibility.\n * AWS API Gateway has a 128KB limit, so 100KB provides a safe margin.\n */\nexport const DEFAULT_FRAGMENT_THRESHOLD = 100 * 1024\n\n/**\n * Configuration for creating a WebsocketConnection.\n */\nexport interface WebsocketConnectionConfig {\n /**\n * Fragment threshold in bytes. Messages larger than this are fragmented.\n * Set to 0 to disable fragmentation (not recommended for cloud deployments).\n * Default: 100KB (safe for AWS API Gateway's 128KB limit)\n */\n fragmentThreshold?: number\n}\n\n/**\n * Represents a single Websocket connection to a peer (server-side).\n *\n * Manages encoding, framing, fragmentation, and reassembly for one\n * connected client. Created by `WebsocketServerTransport.handleConnection()`.\n *\n * The connection uses the CBOR codec for binary transport — this is\n * the natural choice for Websocket's binary frame support.\n */\nexport class WebsocketConnection {\n readonly peerId: PeerId\n readonly channelId: number\n\n #socket: Socket\n #channel: Channel | null = null\n #started = false\n\n // Fragmentation support\n readonly #fragmentThreshold: number\n readonly #reassembler: FragmentReassembler\n #nextFrameId = createFrameIdCounter()\n\n // Per-channel alias state (Phase 4). Captures features from establish\n // messages flowing through; gates dx/shx emissions on mutualAlias.\n #aliasState: AliasState = emptyAliasState()\n\n constructor(\n peerId: PeerId,\n channelId: number,\n socket: Socket,\n config?: WebsocketConnectionConfig,\n ) {\n this.peerId = peerId\n this.channelId = channelId\n this.#socket = socket\n this.#fragmentThreshold =\n config?.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD\n this.#reassembler = new FragmentReassembler({\n timeoutMs: 10_000,\n onTimeout: (frameId: number) => {\n console.warn(\n `[WebsocketConnection] Fragment batch timed out: ${frameId}`,\n )\n },\n })\n }\n\n // ==========================================================================\n // INTERNAL API — for adapter use\n // ==========================================================================\n\n /**\n * Set the channel reference.\n * Called by the adapter when the channel is created.\n * @internal\n */\n _setChannel(channel: Channel): void {\n this.#channel = channel\n }\n\n // ==========================================================================\n // PUBLIC API\n // ==========================================================================\n\n /**\n * Start processing messages on this connection.\n *\n * Sets up the message handler on the socket. Must be called after\n * the connection is fully set up (channel assigned, stored in adapter).\n */\n start(): void {\n if (this.#started) {\n return\n }\n this.#started = true\n\n this.#socket.onMessage(data => {\n this.#handleMessage(data)\n })\n }\n\n /**\n * Send a ChannelMsg through the Websocket.\n *\n * Pipeline: alias transformer → wire encode → frame → fragment if needed\n * → socket.send().\n */\n send(msg: ChannelMsg): void {\n if (this.#socket.readyState !== \"open\") {\n return\n }\n\n const { state, wire } = applyOutboundAliasing(this.#aliasState, msg)\n this.#aliasState = state\n\n encodeWireFrameAndSend(\n wire,\n data => this.#socket.send(data),\n this.#fragmentThreshold,\n this.#nextFrameId,\n )\n }\n\n /**\n * Send a \"ready\" signal to the client.\n *\n * This is a transport-level text message that tells the client the\n * server is ready to receive protocol messages. The client creates\n * its channel and sends establish after receiving this.\n */\n sendReady(): void {\n if (this.#socket.readyState !== \"open\") {\n return\n }\n this.#socket.send(\"ready\")\n }\n\n /**\n * Close the connection and clean up resources.\n */\n close(code?: number, reason?: string): void {\n this.#reassembler.dispose()\n this.#socket.close(code, reason)\n }\n\n // ==========================================================================\n // INTERNAL — message handling\n // ==========================================================================\n\n /**\n * Handle an incoming message from the Websocket.\n */\n #handleMessage(data: Uint8Array<ArrayBuffer> | string): void {\n // Handle keepalive ping/pong (text frames)\n if (typeof data === \"string\") {\n this.#handleKeepalive(data)\n return\n }\n\n // Binary path: reassemble → wire → alias transformer → ChannelMsg.\n try {\n const wires = decodeBinaryWires(data, this.#reassembler)\n if (!wires) return\n for (const wire of wires) {\n const result = applyInboundAliasing(this.#aliasState, wire)\n this.#aliasState = result.state\n if (result.error || !result.msg) {\n console.warn(\n \"[WebsocketConnection] alias resolution failed:\",\n result.error,\n )\n continue\n }\n this.#handleChannelMessage(result.msg)\n }\n } catch (error) {\n console.error(\"Failed to decode wire message:\", error)\n }\n }\n\n /**\n * Handle a decoded channel message.\n *\n * Delivers messages synchronously. The Synchronizer's receive queue\n * handles recursion prevention by queuing messages and processing\n * them iteratively.\n */\n #handleChannelMessage(msg: ChannelMsg): void {\n if (!this.#channel) {\n console.error(\"Cannot handle message: channel not set\")\n return\n }\n\n // Deliver synchronously — the Synchronizer's receive queue prevents recursion\n this.#channel.onReceive(msg)\n }\n\n /**\n * Handle keepalive ping/pong messages.\n */\n #handleKeepalive(text: string): void {\n if (text === \"ping\") {\n this.#socket.send(\"pong\")\n }\n // Ignore \"pong\" and \"ready\" responses\n }\n}\n","// server-adapter — Websocket server adapter for @kyneta/exchange.\n//\n// Manages Websocket connections from clients, encoding/decoding via the\n// kyneta wire format. Framework-agnostic — works with any Websocket\n// library through the Socket interface.\n//\n// Usage with Bun:\n// import { WebsocketServerTransport } from \"@kyneta/websocket-transport/server\"\n// import { createBunWebsocketHandlers } from \"@kyneta/websocket-transport/bun\"\n//\n// const serverAdapter = new WebsocketServerTransport()\n// Bun.serve({\n// websocket: createBunWebsocketHandlers(serverAdapter),\n// fetch(req, server) { server.upgrade(req); return new Response(\"\", { status: 101 }) },\n// })\n//\n// Usage with Node.js `ws`:\n// import { WebsocketServerTransport, wrapNodeWebsocket } from \"@kyneta/websocket-transport/server\"\n// import { WebSocketServer } from \"ws\"\n//\n// const serverAdapter = new WebsocketServerTransport()\n// const wss = new WebSocketServer({ server })\n// wss.on(\"connection\", (ws) => {\n// const { start } = serverAdapter.handleConnection({ socket: wrapNodeWebsocket(ws) })\n// start()\n// })\n//\n// Ported from @loro-extended/adapter-websocket's WsServerNetworkAdapter with\n// kyneta naming conventions and the kyneta 5-message protocol.\n\nimport { randomPeerId } from \"@kyneta/random\"\nimport type { ChannelMsg, GeneratedChannel, PeerId } from \"@kyneta/transport\"\nimport { Transport } from \"@kyneta/transport\"\nimport {\n DEFAULT_FRAGMENT_THRESHOLD,\n WebsocketConnection,\n} from \"./connection.js\"\nimport type {\n WebsocketConnectionOptions,\n WebsocketConnectionResult,\n} from \"./types.js\"\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\n/**\n * Options for the Websocket server adapter.\n */\nexport interface WebsocketServerTransportOptions {\n /**\n * Fragment threshold in bytes. Messages larger than this are fragmented.\n * Set to 0 to disable fragmentation (not recommended for cloud deployments).\n * Default: 100KB (safe for AWS API Gateway's 128KB limit)\n */\n fragmentThreshold?: number\n}\n\n// ---------------------------------------------------------------------------\n// WebsocketServerTransport\n// ---------------------------------------------------------------------------\n\n/**\n * Websocket server network adapter.\n *\n * Framework-agnostic — works with any Websocket library through the\n * `Socket` interface. Use `handleConnection()` to integrate with your\n * framework's Websocket upgrade handler.\n *\n * Each client connection is tracked as a `WebsocketConnection` keyed\n * by peer ID. The adapter creates a channel per connection and routes\n * outbound messages through the connection's send method.\n *\n * The connection handshake follows a two-phase protocol:\n * 1. Server sends text `\"ready\"` signal (transport-level)\n * 2. Client sends `establish` (protocol-level)\n * 3. Server upgrades channel and sends present (handled by Synchronizer)\n *\n * The server does NOT call `establishChannel()` — it waits for the\n * client's establish to avoid a race condition where the binary\n * establish could arrive before the client has processed \"ready\".\n */\nexport class WebsocketServerTransport extends Transport<PeerId> {\n #connections = new Map<PeerId, WebsocketConnection>()\n readonly #fragmentThreshold: number\n\n constructor(options?: WebsocketServerTransportOptions) {\n super({ transportType: \"websocket-server\" })\n this.#fragmentThreshold =\n options?.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD\n }\n\n // ==========================================================================\n // Adapter abstract method implementations\n // ==========================================================================\n\n protected generate(peerId: PeerId): GeneratedChannel {\n return {\n transportType: this.transportType,\n send: (msg: ChannelMsg) => {\n const connection = this.#connections.get(peerId)\n if (connection) {\n connection.send(msg)\n }\n },\n stop: () => {\n this.unregisterConnection(peerId)\n },\n }\n }\n\n async onStart(): Promise<void> {\n // Server adapter starts passively — connections arrive via handleConnection()\n }\n\n async onStop(): Promise<void> {\n // Disconnect all active connections\n for (const connection of this.#connections.values()) {\n connection.close(1001, \"Server shutting down\")\n }\n this.#connections.clear()\n }\n\n // ==========================================================================\n // Connection management\n // ==========================================================================\n\n /**\n * Handle a new Websocket connection.\n *\n * Call this from your framework's Websocket upgrade handler.\n * Returns a connection handle and a `start()` function that begins\n * message processing and sends the \"ready\" signal.\n *\n * @param options - Connection options including the Socket and optional peer ID\n * @returns A connection handle and start function\n *\n * @example Bun\n * ```typescript\n * const { start } = serverAdapter.handleConnection({\n * socket: wrapBunWebsocket(ws),\n * })\n * start()\n * ```\n *\n * @example Node.js ws\n * ```typescript\n * wss.on(\"connection\", (ws) => {\n * const { start } = serverAdapter.handleConnection({\n * socket: wrapNodeWebsocket(ws),\n * })\n * start()\n * })\n * ```\n */\n handleConnection(\n options: WebsocketConnectionOptions,\n ): WebsocketConnectionResult {\n const { socket, peerId: providedPeerId } = options\n\n // Generate peer ID if not provided\n const peerId = providedPeerId ?? (`ws-${randomPeerId()}` as PeerId)\n\n // Check for existing connection with same peer ID\n const existingConnection = this.#connections.get(peerId)\n if (existingConnection) {\n existingConnection.close(1000, \"Replaced by new connection\")\n this.unregisterConnection(peerId)\n }\n\n // Create channel for this peer\n const channel = this.addChannel(peerId)\n\n // Create connection object with fragmentation config\n const connection = new WebsocketConnection(\n peerId,\n channel.channelId,\n socket,\n {\n fragmentThreshold: this.#fragmentThreshold,\n },\n )\n connection._setChannel(channel)\n\n // Store connection\n this.#connections.set(peerId, connection)\n\n // Set up close handler\n socket.onClose((_code, _reason) => {\n this.unregisterConnection(peerId)\n })\n\n socket.onError(_error => {\n this.unregisterConnection(peerId)\n })\n\n return {\n connection,\n start: () => {\n connection.start()\n\n // Send ready signal to client so it knows the server is ready\n // This is a transport-level signal, separate from protocol-level establishment\n connection.sendReady()\n\n // NOTE: We do NOT call establishChannel() here.\n // The client will send establish after receiving \"ready\".\n // Our channel gets established when the Synchronizer receives\n // and processes that establish message.\n //\n // This prevents a race condition where our binary establish\n // could arrive before the client has processed \"ready\" and created\n // its channel.\n },\n }\n }\n\n /**\n * Get an active connection by peer ID.\n */\n getConnection(peerId: PeerId): WebsocketConnection | undefined {\n return this.#connections.get(peerId)\n }\n\n /**\n * Get all active connections.\n */\n getAllConnections(): WebsocketConnection[] {\n return Array.from(this.#connections.values())\n }\n\n /**\n * Check if a peer is connected.\n */\n isConnected(peerId: PeerId): boolean {\n return this.#connections.has(peerId)\n }\n\n /**\n * Unregister a connection, removing its channel and cleaning up state.\n */\n unregisterConnection(peerId: PeerId): void {\n const connection = this.#connections.get(peerId)\n if (connection) {\n this.removeChannel(connection.channelId)\n this.#connections.delete(peerId)\n }\n }\n\n /**\n * Broadcast a message to all connected peers.\n */\n broadcast(msg: ChannelMsg): void {\n for (const connection of this.#connections.values()) {\n connection.send(msg)\n }\n }\n\n /**\n * Get the number of connected peers.\n */\n get connectionCount(): number {\n return this.#connections.size\n }\n}\n","// service-client — service-to-service WebSocket client factory.\n//\n// Extracted from client-transport.ts so that the service client factory\n// lives in the `./server` entry point (where it belongs) rather than\n// the `./browser` entry point. Backend code imports from `./server`;\n// browser code imports from `./browser`.\n\nimport type { TransportFactory } from \"@kyneta/transport\"\nimport {\n type WebsocketClientOptions,\n WebsocketClientTransport,\n} from \"./client-transport.js\"\n\n/**\n * Options for service-to-service Websocket connections.\n *\n * Identical to `WebsocketClientOptions` — the `headers` field is always\n * available on the base options. This alias exists for API clarity:\n * importing `ServiceWebsocketClientOptions` from `./server` signals\n * intent and pairs with `createServiceWebsocketClient`.\n */\nexport type ServiceWebsocketClientOptions = WebsocketClientOptions\n\n/**\n * Create a Websocket client transport for service-to-service connections.\n *\n * This factory is for backend environments (Bun, Node.js) where you need\n * to pass authentication headers during the Websocket upgrade.\n *\n * Note: Headers are a Bun/Node-specific extension. The browser WebSocket API\n * does not support custom headers. For browser clients, use\n * `createWebsocketClient()` and authenticate via URL query parameters.\n *\n * @example\n * ```typescript\n * import { createServiceWebsocketClient } from \"@kyneta/websocket-transport/server\"\n *\n * const exchange = new Exchange({\n * transports: [createServiceWebsocketClient({\n * url: \"ws://primary-server:3000/ws\",\n * WebSocket,\n * headers: { Authorization: \"Bearer token\" },\n * reconnect: { enabled: true },\n * })],\n * })\n * ```\n */\nexport function createServiceWebsocketClient(\n options: ServiceWebsocketClientOptions,\n): TransportFactory {\n return () => new WebsocketClientTransport(options)\n}\n"],"mappings":"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"}
|
|
@@ -66,12 +66,19 @@ type SocketReadyState = "connecting" | "open" | "closing" | "closed";
|
|
|
66
66
|
* adapter needs are exposed.
|
|
67
67
|
*/
|
|
68
68
|
interface Socket {
|
|
69
|
-
/**
|
|
70
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Narrowed to `Uint8Array<ArrayBuffer>` because the strictest downstream
|
|
71
|
+
* runtimes reject `SharedArrayBuffer`-backed views: Bun's `BufferSource`
|
|
72
|
+
* resolves to `ArrayBufferView<ArrayBuffer> | ArrayBuffer`, and Hono's
|
|
73
|
+
* `WSContext.send` takes `Uint8Array<ArrayBuffer>` directly. The wire
|
|
74
|
+
* pipeline allocates with `new Uint8Array(n)`, so producers satisfy this
|
|
75
|
+
* without changes.
|
|
76
|
+
*/
|
|
77
|
+
send(data: Uint8Array<ArrayBuffer> | string): void;
|
|
71
78
|
/** Close the Websocket connection. */
|
|
72
79
|
close(code?: number, reason?: string): void;
|
|
73
80
|
/** Register a handler for incoming messages (binary or text). */
|
|
74
|
-
onMessage(handler: (data: Uint8Array | string) => void): void;
|
|
81
|
+
onMessage(handler: (data: Uint8Array<ArrayBuffer> | string) => void): void;
|
|
75
82
|
/** Register a handler for connection close. */
|
|
76
83
|
onClose(handler: (code: number, reason: string) => void): void;
|
|
77
84
|
/** Register a handler for errors. */
|
|
@@ -181,7 +188,7 @@ declare function wrapStandardWebsocket(ws: WebSocket): Socket;
|
|
|
181
188
|
* the actual `ws` instance, we just need these methods.
|
|
182
189
|
*/
|
|
183
190
|
interface NodeWebsocketLike {
|
|
184
|
-
send(data: Uint8Array | string): void;
|
|
191
|
+
send(data: Uint8Array<ArrayBuffer> | string): void;
|
|
185
192
|
close(code?: number, reason?: string): void;
|
|
186
193
|
on(event: "message", handler: (data: Buffer | ArrayBuffer | string, isBinary: boolean) => void): void;
|
|
187
194
|
on(event: "close", handler: (code: number, reason: Buffer) => void): void;
|
|
@@ -196,4 +203,4 @@ interface NodeWebsocketLike {
|
|
|
196
203
|
declare function wrapNodeWebsocket(ws: NodeWebsocketLike): Socket;
|
|
197
204
|
//#endregion
|
|
198
205
|
export { wrapStandardWebsocket as _, SocketReadyState as a, WebSocketConstructor as c, WebsocketClientState as d, WebsocketClientStateTransition as f, wrapNodeWebsocket as g, WebsocketConnectionResult as h, Socket as i, WebSocketLike as l, WebsocketConnectionOptions as m, NodeWebsocketLike as n, TransitionListener$1 as o, WebsocketConnectionHandle as p, READY_STATE as r, WebSocketCloseEvent as s, DisconnectReason as t, WebSocketMessageEvent as u };
|
|
199
|
-
//# sourceMappingURL=types-
|
|
206
|
+
//# sourceMappingURL=types-c1S_xIRG.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-c1S_xIRG.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;AAyBA;;;cAAa,WAAA;EAAA;;;;;;UAYI,qBAAA;EAAA,SACN,IAAA,WAAe,WAAA;AAAA;;UAIT,mBAAA;EAAA,SACN,IAAA;EAAA,SACA,MAAA;AAAA;;;AAkBX;;;;;;;;;UAAiB,aAAA;EAAA,SACN,UAAA;EACT,UAAA;EACA,IAAA,CAAK,IAAA,WAAe,WAAA;EACpB,KAAA,CAAM,IAAA,WAAe,MAAA;EACrB,gBAAA,CAAiB,IAAA,UAAc,QAAA,GAAW,KAAA;EAC1C,mBAAA,CAAoB,IAAA,UAAc,QAAA,GAAW,KAAA;AAAA;;;;;;AAW/C;;;KAAY,oBAAA,QACV,GAAA,aACG,IAAA,YACA,aAAA;;;;;KAUO,gBAAA;AAAZ;;;;;AAiBA;;;;;;AAjBA,UAiBiB,MAAA;EAqBU;;;;;;;;EAZzB,IAAA,CAAK,IAAA,EAAM,UAAA,CAAW,WAAA;EAGhB;EAAN,KAAA,CAAM,IAAA,WAAe,MAAA;EAGrB;EAAA,SAAA,CAAU,OAAA,GAAU,IAAA,EAAM,UAAA,CAAW,WAAA;EAAA;EAGrC,OAAA,CAAQ,OAAA,GAAU,IAAA,UAAc,MAAA;EAHtB;EAMV,OAAA,CAAQ,OAAA,GAAU,KAAA,EAAO,KAAA;EAHP;EAAA,SAMT,UAAA,EAAY,gBAAA;AAAA;;;;UAUN,0BAAA;EAVN;EAYT,MAAA,EAAQ,MAAA;EAZ6B;EAerC,MAAA,GAAS,MAAA;EALM;EAQf,SAAA;AAAA;;;;UAMe,yBAAA;EATN;EAAA,SAWA,MAAA,EAAQ,MAAA;EARR;EAAA,SAWA,SAAA;EALM;EAQf,KAAA,CAAM,IAAA,WAAe,MAAA;AAAA;;;;UAMN,yBAAA;EANf;EAQA,UAAA,EAAY,yBAAA;EARS;EAWrB,KAAA;AAAA;AALF;;;AAAA,KAeY,gBAAA;EACN,IAAA;AAAA;EACA,IAAA;EAAe,KAAA,EAAO,KAAA;AAAA;EACtB,IAAA;EAAgB,IAAA;EAAc,MAAA;AAAA;EAC9B,IAAA;EAA8B,QAAA;AAAA;EAC9B,IAAA;AAAA;;;;;;;;AAoBN;;;;;;;KAAY,oBAAA;EACN,MAAA;EAAwB,MAAA,GAAS,gBAAA;AAAA;EACjC,MAAA;EAAsB,OAAA;AAAA;EACtB,MAAA;AAAA;EACA,MAAA;AAAA;EACA,MAAA;EAAwB,OAAA;EAAiB,aAAA;AAAA;;;;;KAMnC,8BAAA,GACV,eAAA,CAAgB,oBAAA;;;;;KAMN,oBAAA,GAAqB,kBAAA,CAA0B,oBAAA;;;;AA4E3D;;;iBAhEgB,qBAAA,CAAsB,EAAA,EAAI,SAAA,GAAY,MAAA;;;;;;;UAgErC,iBAAA;EACf,IAAA,CAAK,IAAA,EAAM,UAAA,CAAW,WAAA;EACtB,KAAA,CAAM,IAAA,WAAe,MAAA;EACrB,EAAA,CACE,KAAA,aACA,OAAA,GAAU,IAAA,EAAM,MAAA,GAAS,WAAA,WAAsB,QAAA;EAEjD,EAAA,CAAG,KAAA,WAAgB,OAAA,GAAU,IAAA,UAAc,MAAA,EAAQ,MAAA;EACnD,EAAA,CAAG,KAAA,WAAgB,OAAA,GAAU,KAAA,EAAO,KAAA;EACpC,UAAA;AAAA;;;;;;iBAQc,iBAAA,CAAkB,EAAA,EAAI,iBAAA,GAAoB,MAAA"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kyneta/websocket-transport",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Websocket network adapter for @kyneta/exchange — browser, server, and Bun integration",
|
|
5
5
|
"author": "Duane Johnson",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/halecraft/kyneta",
|
|
10
|
-
"directory": "packages/exchange/
|
|
10
|
+
"directory": "packages/exchange/transports/websocket"
|
|
11
11
|
},
|
|
12
12
|
"publishConfig": {
|
|
13
13
|
"access": "public"
|
|
@@ -33,9 +33,10 @@
|
|
|
33
33
|
"./src/*": "./src/*"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@kyneta/machine": "^1.
|
|
37
|
-
"@kyneta/
|
|
38
|
-
"@kyneta/
|
|
36
|
+
"@kyneta/machine": "^1.5.0",
|
|
37
|
+
"@kyneta/random": "^1.5.0",
|
|
38
|
+
"@kyneta/transport": "^1.5.0",
|
|
39
|
+
"@kyneta/wire": "^1.5.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@types/node": "^22",
|
|
@@ -43,11 +44,11 @@
|
|
|
43
44
|
"tsdown": "^0.21.9",
|
|
44
45
|
"typescript": "^5.9.2",
|
|
45
46
|
"vitest": "^4.0.17",
|
|
46
|
-
"@kyneta/exchange": "^1.
|
|
47
|
-
"@kyneta/
|
|
48
|
-
"@kyneta/
|
|
49
|
-
"@kyneta/
|
|
50
|
-
"@kyneta/
|
|
47
|
+
"@kyneta/exchange": "^1.5.0",
|
|
48
|
+
"@kyneta/machine": "^1.5.0",
|
|
49
|
+
"@kyneta/schema": "^1.5.0",
|
|
50
|
+
"@kyneta/transport": "^1.5.0",
|
|
51
|
+
"@kyneta/wire": "^1.5.0"
|
|
51
52
|
},
|
|
52
53
|
"scripts": {
|
|
53
54
|
"build": "tsdown",
|
|
@@ -94,7 +94,7 @@ describe("WebsocketClientTransport — constructor injection", () => {
|
|
|
94
94
|
await startTransport(transport)
|
|
95
95
|
|
|
96
96
|
expect(calls).toHaveLength(1)
|
|
97
|
-
expect(calls[0]
|
|
97
|
+
expect(calls[0]?.url).toBe("ws://localhost:9999/ws")
|
|
98
98
|
})
|
|
99
99
|
|
|
100
100
|
it("resolves URL function with peerId before passing to constructor", async () => {
|
|
@@ -109,7 +109,7 @@ describe("WebsocketClientTransport — constructor injection", () => {
|
|
|
109
109
|
await startTransport(transport)
|
|
110
110
|
|
|
111
111
|
expect(calls).toHaveLength(1)
|
|
112
|
-
expect(calls[0]
|
|
112
|
+
expect(calls[0]?.url).toBe(`ws://localhost:9999/ws/${testIdentity.peerId}`)
|
|
113
113
|
})
|
|
114
114
|
})
|
|
115
115
|
|
|
@@ -131,7 +131,7 @@ describe("WebsocketClientTransport — header passing", () => {
|
|
|
131
131
|
await startTransport(transport)
|
|
132
132
|
|
|
133
133
|
expect(calls).toHaveLength(1)
|
|
134
|
-
expect(calls[0]
|
|
134
|
+
expect(calls[0]?.rest).toEqual([{ headers }])
|
|
135
135
|
})
|
|
136
136
|
|
|
137
137
|
it("omits second arg when headers are not provided", async () => {
|
|
@@ -146,7 +146,7 @@ describe("WebsocketClientTransport — header passing", () => {
|
|
|
146
146
|
await startTransport(transport)
|
|
147
147
|
|
|
148
148
|
expect(calls).toHaveLength(1)
|
|
149
|
-
expect(calls[0]
|
|
149
|
+
expect(calls[0]?.rest).toEqual([])
|
|
150
150
|
})
|
|
151
151
|
|
|
152
152
|
it("omits second arg when headers is an empty object", async () => {
|
|
@@ -162,6 +162,6 @@ describe("WebsocketClientTransport — header passing", () => {
|
|
|
162
162
|
await startTransport(transport)
|
|
163
163
|
|
|
164
164
|
expect(calls).toHaveLength(1)
|
|
165
|
-
expect(calls[0]
|
|
165
|
+
expect(calls[0]?.rest).toEqual([])
|
|
166
166
|
})
|
|
167
167
|
})
|
package/src/bun-websocket.ts
CHANGED
|
@@ -31,7 +31,7 @@ import type { Socket, SocketReadyState } from "./types.js"
|
|
|
31
31
|
*/
|
|
32
32
|
export type BunWebsocketData = {
|
|
33
33
|
handlers: {
|
|
34
|
-
onMessage?: (data: Uint8Array | string) => void
|
|
34
|
+
onMessage?: (data: Uint8Array<ArrayBuffer> | string) => void
|
|
35
35
|
onClose?: (code: number, reason: string) => void
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -78,7 +78,7 @@ export function wrapBunWebsocket(
|
|
|
78
78
|
ws.data = { handlers: {} }
|
|
79
79
|
|
|
80
80
|
return {
|
|
81
|
-
send(data: Uint8Array | string): void {
|
|
81
|
+
send(data: Uint8Array<ArrayBuffer> | string): void {
|
|
82
82
|
ws.send(data)
|
|
83
83
|
},
|
|
84
84
|
|
|
@@ -86,7 +86,7 @@ export function wrapBunWebsocket(
|
|
|
86
86
|
ws.close(code, reason)
|
|
87
87
|
},
|
|
88
88
|
|
|
89
|
-
onMessage(handler: (data: Uint8Array | string) => void): void {
|
|
89
|
+
onMessage(handler: (data: Uint8Array<ArrayBuffer> | string) => void): void {
|
|
90
90
|
ws.data.handlers.onMessage = handler
|
|
91
91
|
},
|
|
92
92
|
|
package/src/client-transport.ts
CHANGED
|
@@ -22,8 +22,13 @@ import type {
|
|
|
22
22
|
} from "@kyneta/transport"
|
|
23
23
|
import { Transport } from "@kyneta/transport"
|
|
24
24
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
type AliasState,
|
|
26
|
+
applyInboundAliasing,
|
|
27
|
+
applyOutboundAliasing,
|
|
28
|
+
createFrameIdCounter,
|
|
29
|
+
decodeBinaryWires,
|
|
30
|
+
emptyAliasState,
|
|
31
|
+
encodeWireFrameAndSend,
|
|
27
32
|
FragmentReassembler,
|
|
28
33
|
} from "@kyneta/wire"
|
|
29
34
|
import {
|
|
@@ -164,6 +169,9 @@ export class WebsocketClientTransport extends Transport<void> {
|
|
|
164
169
|
readonly #fragmentThreshold: number
|
|
165
170
|
readonly #reassembler: FragmentReassembler
|
|
166
171
|
|
|
172
|
+
// Per-channel alias state (Phase 4). Single channel per client.
|
|
173
|
+
#aliasState: AliasState = emptyAliasState()
|
|
174
|
+
|
|
167
175
|
constructor(options: WebsocketClientOptions) {
|
|
168
176
|
super({ transportType: "websocket-client" })
|
|
169
177
|
this.#options = options
|
|
@@ -215,6 +223,10 @@ export class WebsocketClientTransport extends Transport<void> {
|
|
|
215
223
|
this.#serverChannel = undefined
|
|
216
224
|
}
|
|
217
225
|
|
|
226
|
+
// Fresh reassembler for the new connection — stale fragments from
|
|
227
|
+
// the old connection must not collide with new fragments.
|
|
228
|
+
this.#reassembler.reset()
|
|
229
|
+
|
|
218
230
|
this.#serverChannel = this.addChannel()
|
|
219
231
|
|
|
220
232
|
// Establish immediately — the server already signaled ready
|
|
@@ -392,14 +404,19 @@ export class WebsocketClientTransport extends Transport<void> {
|
|
|
392
404
|
// Handle binary messages through shared decode pipeline
|
|
393
405
|
if (data instanceof ArrayBuffer) {
|
|
394
406
|
try {
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
407
|
+
const wires = decodeBinaryWires(new Uint8Array(data), this.#reassembler)
|
|
408
|
+
if (!wires) return
|
|
409
|
+
for (const wire of wires) {
|
|
410
|
+
const result = applyInboundAliasing(this.#aliasState, wire)
|
|
411
|
+
this.#aliasState = result.state
|
|
412
|
+
if (result.error || !result.msg) {
|
|
413
|
+
console.warn(
|
|
414
|
+
"[WebsocketClient] alias resolution failed:",
|
|
415
|
+
result.error,
|
|
416
|
+
)
|
|
417
|
+
continue
|
|
402
418
|
}
|
|
419
|
+
this.#handleChannelMessage(result.msg)
|
|
403
420
|
}
|
|
404
421
|
} catch (error) {
|
|
405
422
|
console.error("Failed to decode message:", error)
|
|
@@ -535,6 +552,9 @@ export class WebsocketClientTransport extends Transport<void> {
|
|
|
535
552
|
// ==========================================================================
|
|
536
553
|
|
|
537
554
|
protected generate(): GeneratedChannel {
|
|
555
|
+
const nextFrameId = createFrameIdCounter()
|
|
556
|
+
// New channel = fresh alias state; reset on (re)connect.
|
|
557
|
+
this.#aliasState = emptyAliasState()
|
|
538
558
|
return {
|
|
539
559
|
transportType: this.transportType,
|
|
540
560
|
send: (msg: ChannelMsg) => {
|
|
@@ -543,8 +563,14 @@ export class WebsocketClientTransport extends Transport<void> {
|
|
|
543
563
|
return
|
|
544
564
|
}
|
|
545
565
|
|
|
546
|
-
|
|
547
|
-
|
|
566
|
+
const { state, wire } = applyOutboundAliasing(this.#aliasState, msg)
|
|
567
|
+
this.#aliasState = state
|
|
568
|
+
|
|
569
|
+
encodeWireFrameAndSend(
|
|
570
|
+
wire,
|
|
571
|
+
data => socket.send(new Uint8Array(data).buffer),
|
|
572
|
+
this.#fragmentThreshold,
|
|
573
|
+
nextFrameId,
|
|
548
574
|
)
|
|
549
575
|
},
|
|
550
576
|
stop: () => {
|
package/src/connection.ts
CHANGED
|
@@ -12,8 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
import type { Channel, ChannelMsg, PeerId } from "@kyneta/transport"
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
type AliasState,
|
|
16
|
+
applyInboundAliasing,
|
|
17
|
+
applyOutboundAliasing,
|
|
18
|
+
createFrameIdCounter,
|
|
19
|
+
decodeBinaryWires,
|
|
20
|
+
emptyAliasState,
|
|
21
|
+
encodeWireFrameAndSend,
|
|
17
22
|
FragmentReassembler,
|
|
18
23
|
} from "@kyneta/wire"
|
|
19
24
|
import type { Socket } from "./types.js"
|
|
@@ -57,6 +62,11 @@ export class WebsocketConnection {
|
|
|
57
62
|
// Fragmentation support
|
|
58
63
|
readonly #fragmentThreshold: number
|
|
59
64
|
readonly #reassembler: FragmentReassembler
|
|
65
|
+
#nextFrameId = createFrameIdCounter()
|
|
66
|
+
|
|
67
|
+
// Per-channel alias state (Phase 4). Captures features from establish
|
|
68
|
+
// messages flowing through; gates dx/shx emissions on mutualAlias.
|
|
69
|
+
#aliasState: AliasState = emptyAliasState()
|
|
60
70
|
|
|
61
71
|
constructor(
|
|
62
72
|
peerId: PeerId,
|
|
@@ -71,7 +81,7 @@ export class WebsocketConnection {
|
|
|
71
81
|
config?.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD
|
|
72
82
|
this.#reassembler = new FragmentReassembler({
|
|
73
83
|
timeoutMs: 10_000,
|
|
74
|
-
onTimeout: (frameId:
|
|
84
|
+
onTimeout: (frameId: number) => {
|
|
75
85
|
console.warn(
|
|
76
86
|
`[WebsocketConnection] Fragment batch timed out: ${frameId}`,
|
|
77
87
|
)
|
|
@@ -116,15 +126,22 @@ export class WebsocketConnection {
|
|
|
116
126
|
/**
|
|
117
127
|
* Send a ChannelMsg through the Websocket.
|
|
118
128
|
*
|
|
119
|
-
*
|
|
129
|
+
* Pipeline: alias transformer → wire encode → frame → fragment if needed
|
|
130
|
+
* → socket.send().
|
|
120
131
|
*/
|
|
121
132
|
send(msg: ChannelMsg): void {
|
|
122
133
|
if (this.#socket.readyState !== "open") {
|
|
123
134
|
return
|
|
124
135
|
}
|
|
125
136
|
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
const { state, wire } = applyOutboundAliasing(this.#aliasState, msg)
|
|
138
|
+
this.#aliasState = state
|
|
139
|
+
|
|
140
|
+
encodeWireFrameAndSend(
|
|
141
|
+
wire,
|
|
142
|
+
data => this.#socket.send(data),
|
|
143
|
+
this.#fragmentThreshold,
|
|
144
|
+
this.#nextFrameId,
|
|
128
145
|
)
|
|
129
146
|
}
|
|
130
147
|
|
|
@@ -157,20 +174,28 @@ export class WebsocketConnection {
|
|
|
157
174
|
/**
|
|
158
175
|
* Handle an incoming message from the Websocket.
|
|
159
176
|
*/
|
|
160
|
-
#handleMessage(data: Uint8Array | string): void {
|
|
177
|
+
#handleMessage(data: Uint8Array<ArrayBuffer> | string): void {
|
|
161
178
|
// Handle keepalive ping/pong (text frames)
|
|
162
179
|
if (typeof data === "string") {
|
|
163
180
|
this.#handleKeepalive(data)
|
|
164
181
|
return
|
|
165
182
|
}
|
|
166
183
|
|
|
167
|
-
//
|
|
184
|
+
// Binary path: reassemble → wire → alias transformer → ChannelMsg.
|
|
168
185
|
try {
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
186
|
+
const wires = decodeBinaryWires(data, this.#reassembler)
|
|
187
|
+
if (!wires) return
|
|
188
|
+
for (const wire of wires) {
|
|
189
|
+
const result = applyInboundAliasing(this.#aliasState, wire)
|
|
190
|
+
this.#aliasState = result.state
|
|
191
|
+
if (result.error || !result.msg) {
|
|
192
|
+
console.warn(
|
|
193
|
+
"[WebsocketConnection] alias resolution failed:",
|
|
194
|
+
result.error,
|
|
195
|
+
)
|
|
196
|
+
continue
|
|
173
197
|
}
|
|
198
|
+
this.#handleChannelMessage(result.msg)
|
|
174
199
|
}
|
|
175
200
|
} catch (error) {
|
|
176
201
|
console.error("Failed to decode wire message:", error)
|
package/src/server-transport.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// Ported from @loro-extended/adapter-websocket's WsServerNetworkAdapter with
|
|
29
29
|
// kyneta naming conventions and the kyneta 5-message protocol.
|
|
30
30
|
|
|
31
|
+
import { randomPeerId } from "@kyneta/random"
|
|
31
32
|
import type { ChannelMsg, GeneratedChannel, PeerId } from "@kyneta/transport"
|
|
32
33
|
import { Transport } from "@kyneta/transport"
|
|
33
34
|
import {
|
|
@@ -55,22 +56,6 @@ export interface WebsocketServerTransportOptions {
|
|
|
55
56
|
fragmentThreshold?: number
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// Peer ID generation
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Generate a random peer ID for connections that don't provide one.
|
|
64
|
-
*/
|
|
65
|
-
function generatePeerId(): PeerId {
|
|
66
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
67
|
-
let result = "ws-"
|
|
68
|
-
for (let i = 0; i < 12; i++) {
|
|
69
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
70
|
-
}
|
|
71
|
-
return result
|
|
72
|
-
}
|
|
73
|
-
|
|
74
59
|
// ---------------------------------------------------------------------------
|
|
75
60
|
// WebsocketServerTransport
|
|
76
61
|
// ---------------------------------------------------------------------------
|
|
@@ -174,7 +159,7 @@ export class WebsocketServerTransport extends Transport<PeerId> {
|
|
|
174
159
|
const { socket, peerId: providedPeerId } = options
|
|
175
160
|
|
|
176
161
|
// Generate peer ID if not provided
|
|
177
|
-
const peerId = providedPeerId ??
|
|
162
|
+
const peerId = providedPeerId ?? (`ws-${randomPeerId()}` as PeerId)
|
|
178
163
|
|
|
179
164
|
// Check for existing connection with same peer ID
|
|
180
165
|
const existingConnection = this.#connections.get(peerId)
|
package/src/types.ts
CHANGED
|
@@ -108,14 +108,21 @@ export type SocketReadyState = "connecting" | "open" | "closing" | "closed"
|
|
|
108
108
|
* adapter needs are exposed.
|
|
109
109
|
*/
|
|
110
110
|
export interface Socket {
|
|
111
|
-
/**
|
|
112
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Narrowed to `Uint8Array<ArrayBuffer>` because the strictest downstream
|
|
113
|
+
* runtimes reject `SharedArrayBuffer`-backed views: Bun's `BufferSource`
|
|
114
|
+
* resolves to `ArrayBufferView<ArrayBuffer> | ArrayBuffer`, and Hono's
|
|
115
|
+
* `WSContext.send` takes `Uint8Array<ArrayBuffer>` directly. The wire
|
|
116
|
+
* pipeline allocates with `new Uint8Array(n)`, so producers satisfy this
|
|
117
|
+
* without changes.
|
|
118
|
+
*/
|
|
119
|
+
send(data: Uint8Array<ArrayBuffer> | string): void
|
|
113
120
|
|
|
114
121
|
/** Close the Websocket connection. */
|
|
115
122
|
close(code?: number, reason?: string): void
|
|
116
123
|
|
|
117
124
|
/** Register a handler for incoming messages (binary or text). */
|
|
118
|
-
onMessage(handler: (data: Uint8Array | string) => void): void
|
|
125
|
+
onMessage(handler: (data: Uint8Array<ArrayBuffer> | string) => void): void
|
|
119
126
|
|
|
120
127
|
/** Register a handler for connection close. */
|
|
121
128
|
onClose(handler: (code: number, reason: string) => void): void
|
|
@@ -234,17 +241,15 @@ export type TransitionListener = GenericTransitionListener<WebsocketClientState>
|
|
|
234
241
|
*/
|
|
235
242
|
export function wrapStandardWebsocket(ws: WebSocket): Socket {
|
|
236
243
|
return {
|
|
237
|
-
send(data: Uint8Array | string): void {
|
|
238
|
-
ws.send(
|
|
239
|
-
typeof data === "string" ? data : (data as Uint8Array<ArrayBuffer>),
|
|
240
|
-
)
|
|
244
|
+
send(data: Uint8Array<ArrayBuffer> | string): void {
|
|
245
|
+
ws.send(data)
|
|
241
246
|
},
|
|
242
247
|
|
|
243
248
|
close(code?: number, reason?: string): void {
|
|
244
249
|
ws.close(code, reason)
|
|
245
250
|
},
|
|
246
251
|
|
|
247
|
-
onMessage(handler: (data: Uint8Array | string) => void): void {
|
|
252
|
+
onMessage(handler: (data: Uint8Array<ArrayBuffer> | string) => void): void {
|
|
248
253
|
ws.addEventListener("message", event => {
|
|
249
254
|
if (event.data instanceof ArrayBuffer) {
|
|
250
255
|
handler(new Uint8Array(event.data))
|
|
@@ -299,7 +304,7 @@ export function wrapStandardWebsocket(ws: WebSocket): Socket {
|
|
|
299
304
|
* the actual `ws` instance, we just need these methods.
|
|
300
305
|
*/
|
|
301
306
|
export interface NodeWebsocketLike {
|
|
302
|
-
send(data: Uint8Array | string): void
|
|
307
|
+
send(data: Uint8Array<ArrayBuffer> | string): void
|
|
303
308
|
close(code?: number, reason?: string): void
|
|
304
309
|
on(
|
|
305
310
|
event: "message",
|
|
@@ -321,7 +326,7 @@ export function wrapNodeWebsocket(ws: NodeWebsocketLike): Socket {
|
|
|
321
326
|
const CLOSING = 2
|
|
322
327
|
|
|
323
328
|
return {
|
|
324
|
-
send(data: Uint8Array | string): void {
|
|
329
|
+
send(data: Uint8Array<ArrayBuffer> | string): void {
|
|
325
330
|
ws.send(data)
|
|
326
331
|
},
|
|
327
332
|
|
|
@@ -329,7 +334,7 @@ export function wrapNodeWebsocket(ws: NodeWebsocketLike): Socket {
|
|
|
329
334
|
ws.close(code, reason)
|
|
330
335
|
},
|
|
331
336
|
|
|
332
|
-
onMessage(handler: (data: Uint8Array | string) => void): void {
|
|
337
|
+
onMessage(handler: (data: Uint8Array<ArrayBuffer> | string) => void): void {
|
|
333
338
|
ws.on(
|
|
334
339
|
"message",
|
|
335
340
|
(data: Buffer | ArrayBuffer | string, isBinary: boolean) => {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client-transport-DIZ-LJxs.js","names":["#fragmentThreshold","#reassembler","#options","#WebSocketImpl","#handle","#executeEffect","#setupLifecycleEvents","#doCreateWebsocket","#socket","#serverChannel","#reconnectTimer","#startKeepalive","#stopKeepalive","#peerId","#handleMessage","#handleChannelMessage","#keepaliveTimer"],"sources":["../src/client-program.ts","../src/types.ts","../src/client-transport.ts"],"sourcesContent":["// client-program — pure Mealy machine for websocket client connection lifecycle.\n//\n// The client program encodes every state transition and effect as data.\n// The imperative shell (client-transport.ts) interprets effects as I/O.\n// Tests assert on data — no sockets, no timing, never flaky.\n//\n// Algebra: Program<WsClientMsg, WebsocketClientState, WsClientEffect>\n// Interpreter: client-transport.ts executeClientEffect()\n//\n// The websocket client has a 5-state lifecycle with an extra \"ready\" state\n// compared to the unix socket client. The server sends a text \"ready\" signal\n// after the connection opens, and only then does the client create a channel\n// and start the establishment handshake.\n//\n// Race condition: the server may send \"ready\" before the client's open event\n// fires (server-ready while connecting). The program handles this by\n// transitioning directly to ready, skipping the connected state.\n\nimport type { Program } from \"@kyneta/machine\"\nimport type { ReconnectOptions } from \"@kyneta/transport\"\nimport { computeBackoffDelay, DEFAULT_RECONNECT } from \"@kyneta/transport\"\n\nimport type { DisconnectReason, WebsocketClientState } from \"./types.js\"\n\n// ---------------------------------------------------------------------------\n// Messages\n// ---------------------------------------------------------------------------\n\nexport type WsClientMsg =\n | { type: \"start\" }\n | { type: \"socket-opened\" }\n | { type: \"server-ready\" }\n | { type: \"socket-closed\"; code: number; reason: string }\n | { type: \"socket-error\"; error: Error }\n | { type: \"reconnect-timer-fired\" }\n | { type: \"stop\" }\n\n// ---------------------------------------------------------------------------\n// Effects (data — interpreted by the imperative shell)\n// ---------------------------------------------------------------------------\n\nexport type WsClientEffect =\n | { type: \"create-websocket\"; attempt: number }\n | { type: \"close-websocket\" }\n | { type: \"add-channel-and-establish\" }\n | { type: \"remove-channel\" }\n | { type: \"start-reconnect-timer\"; delayMs: number }\n | { type: \"cancel-reconnect-timer\" }\n | { type: \"start-keepalive\" }\n | { type: \"stop-keepalive\" }\n\n// ---------------------------------------------------------------------------\n// Program factory\n// ---------------------------------------------------------------------------\n\nexport interface WsClientProgramOptions {\n reconnect?: Partial<ReconnectOptions>\n /** Inject jitter source for deterministic testing. Default: () => Math.random() * 1000 */\n jitterFn?: () => number\n}\n\n/**\n * Create the websocket client connection lifecycle program — a pure Mealy machine.\n *\n * The returned `Program<WsClientMsg, WebsocketClientState, WsClientEffect>`\n * encodes every state transition and effect as inspectable data. The imperative\n * shell interprets `WsClientEffect` as actual I/O.\n */\nexport function createWsClientProgram(\n options: WsClientProgramOptions = {},\n): Program<WsClientMsg, WebsocketClientState, WsClientEffect> {\n const { jitterFn = () => Math.random() * 1000 } = options\n const reconnect: ReconnectOptions = {\n ...DEFAULT_RECONNECT,\n ...options.reconnect,\n }\n\n /**\n * Attempt to transition into reconnecting, or give up and disconnect.\n *\n * Pure — computes the next state and effects from the current attempt\n * count and reconnect configuration. Returns a tuple suitable for\n * spreading into an `update` return.\n */\n function tryReconnect(\n currentAttempt: number,\n reason: DisconnectReason,\n ...extraEffects: WsClientEffect[]\n ): [WebsocketClientState, ...WsClientEffect[]] {\n if (!reconnect.enabled) {\n return [{ status: \"disconnected\", reason }, ...extraEffects]\n }\n\n if (currentAttempt >= reconnect.maxAttempts) {\n return [\n {\n status: \"disconnected\",\n reason: { type: \"max-retries-exceeded\", attempts: currentAttempt },\n },\n ...extraEffects,\n ]\n }\n\n const delay = computeBackoffDelay(\n currentAttempt + 1,\n reconnect.baseDelay,\n reconnect.maxDelay,\n jitterFn(),\n )\n\n return [\n {\n status: \"reconnecting\",\n attempt: currentAttempt + 1,\n nextAttemptMs: delay,\n },\n ...extraEffects,\n { type: \"start-reconnect-timer\", delayMs: delay },\n ]\n }\n\n return {\n init: [{ status: \"disconnected\" }],\n\n update(msg, model): [WebsocketClientState, ...WsClientEffect[]] {\n switch (msg.type) {\n // -----------------------------------------------------------------\n // start\n // -----------------------------------------------------------------\n case \"start\": {\n if (model.status !== \"disconnected\") return [model]\n return [\n { status: \"connecting\", attempt: 1 },\n { type: \"create-websocket\", attempt: 1 },\n ]\n }\n\n // -----------------------------------------------------------------\n // socket-opened\n // -----------------------------------------------------------------\n case \"socket-opened\": {\n if (model.status !== \"connecting\") return [model]\n return [{ status: \"connected\" }, { type: \"start-keepalive\" }]\n }\n\n // -----------------------------------------------------------------\n // server-ready\n // -----------------------------------------------------------------\n case \"server-ready\": {\n // Already ready — ignore duplicate\n if (model.status === \"ready\") return [model]\n\n // Normal path: connected → ready\n if (model.status === \"connected\") {\n return [{ status: \"ready\" }, { type: \"add-channel-and-establish\" }]\n }\n\n // Race condition: server sent \"ready\" before client's open event fired.\n // Skip connected, go directly to ready with both keepalive and channel effects.\n if (model.status === \"connecting\") {\n return [\n { status: \"ready\" },\n { type: \"start-keepalive\" },\n { type: \"add-channel-and-establish\" },\n ]\n }\n\n return [model]\n }\n\n // -----------------------------------------------------------------\n // socket-closed\n // -----------------------------------------------------------------\n case \"socket-closed\": {\n const reason: DisconnectReason = {\n type: \"closed\",\n code: msg.code,\n reason: msg.reason,\n }\n\n if (model.status === \"connected\") {\n return tryReconnect(0, reason, { type: \"stop-keepalive\" })\n }\n\n if (model.status === \"ready\") {\n return tryReconnect(\n 0,\n reason,\n { type: \"stop-keepalive\" },\n { type: \"remove-channel\" },\n )\n }\n\n return [model]\n }\n\n // -----------------------------------------------------------------\n // socket-error\n // -----------------------------------------------------------------\n case \"socket-error\": {\n const reason: DisconnectReason = {\n type: \"error\",\n error: msg.error,\n }\n\n if (model.status === \"connecting\") {\n return tryReconnect(model.attempt, reason)\n }\n\n if (model.status === \"connected\") {\n return tryReconnect(0, reason, { type: \"stop-keepalive\" })\n }\n\n if (model.status === \"ready\") {\n return tryReconnect(\n 0,\n reason,\n { type: \"stop-keepalive\" },\n { type: \"remove-channel\" },\n )\n }\n\n return [model]\n }\n\n // -----------------------------------------------------------------\n // reconnect-timer-fired\n // -----------------------------------------------------------------\n case \"reconnect-timer-fired\": {\n if (model.status !== \"reconnecting\") return [model]\n return [\n { status: \"connecting\", attempt: model.attempt },\n { type: \"create-websocket\", attempt: model.attempt },\n ]\n }\n\n // -----------------------------------------------------------------\n // stop\n // -----------------------------------------------------------------\n case \"stop\": {\n if (model.status === \"disconnected\") return [model]\n\n const effects: WsClientEffect[] = [{ type: \"cancel-reconnect-timer\" }]\n\n if (model.status === \"connecting\") {\n effects.push({ type: \"close-websocket\" })\n }\n\n if (model.status === \"connected\") {\n effects.push(\n { type: \"close-websocket\" },\n { type: \"stop-keepalive\" },\n )\n }\n\n if (model.status === \"ready\") {\n effects.push(\n { type: \"close-websocket\" },\n { type: \"stop-keepalive\" },\n { type: \"remove-channel\" },\n )\n }\n\n return [\n { status: \"disconnected\", reason: { type: \"intentional\" } },\n ...effects,\n ]\n }\n }\n },\n }\n}\n","// types — framework-agnostic Websocket abstractions for @kyneta/websocket-transport.\n//\n// The `Socket` interface decouples the adapter from any specific Websocket\n// library (browser WebSocket, Node `ws`, Bun ServerWebSocket). Platform-\n// specific wrappers (`wrapStandardWebsocket`, `wrapNodeWebsocket`,\n// `wrapBunWebsocket`) adapt concrete implementations to this interface.\n//\n// Ported from @loro-extended/adapter-websocket's WsSocket with kyneta\n// naming conventions applied.\n\nimport type {\n TransitionListener as GenericTransitionListener,\n PeerId,\n StateTransition,\n} from \"@kyneta/transport\"\n\n// ---------------------------------------------------------------------------\n// WebSocket readyState constants (spec values, no global dependency)\n// ---------------------------------------------------------------------------\n\n/**\n * WebSocket readyState constants per the WHATWG WebSocket spec.\n * Replaces references to `WebSocket.CONNECTING`, `WebSocket.OPEN`, etc.\n * so that shared code never depends on the browser global.\n */\nexport const READY_STATE = {\n CONNECTING: 0,\n OPEN: 1,\n CLOSING: 2,\n CLOSED: 3,\n} as const\n\n// ---------------------------------------------------------------------------\n// Structural event types (replace DOM MessageEvent / CloseEvent)\n// ---------------------------------------------------------------------------\n\n/** Minimal message event — only the fields the transport accesses. */\nexport interface WebSocketMessageEvent {\n readonly data: string | ArrayBuffer\n}\n\n/** Minimal close event — only the fields the transport accesses. */\nexport interface WebSocketCloseEvent {\n readonly code: number\n readonly reason: string\n}\n\n// ---------------------------------------------------------------------------\n// WebSocket instance and constructor structural types\n// ---------------------------------------------------------------------------\n\n/**\n * Structural type for a constructed WebSocket instance.\n *\n * Covers the browser's `WebSocket`, the `ws` library's `WebSocket`,\n * and Bun's client `WebSocket` — all satisfy this interface without casting.\n *\n * The client transport uses `addEventListener`/`removeEventListener` for\n * one-shot connection handlers with explicit cleanup during the connect\n * phase. This is why `WebSocketLike` exists alongside the server-side\n * `Socket` interface (which uses single-callback registration).\n */\nexport interface WebSocketLike {\n readonly readyState: number\n binaryType: string\n send(data: string | ArrayBuffer): void\n close(code?: number, reason?: string): void\n addEventListener(type: string, listener: (event: any) => void): void\n removeEventListener(type: string, listener: (event: any) => void): void\n}\n\n/**\n * Structural type for a WebSocket constructor.\n *\n * Type safety for constructor arguments is intentionally at the options\n * layer (`WebsocketClientOptions.headers`), not here. The `...rest: any[]`\n * absorbs both the browser's `protocols` arg and backend's `{ headers }`\n * arg without requiring the transport to know which runtime it's in.\n */\nexport type WebSocketConstructor = new (\n url: string,\n ...rest: any[]\n) => WebSocketLike\n\n// ---------------------------------------------------------------------------\n// Socket ready states (string enum for server-side Socket interface)\n// ---------------------------------------------------------------------------\n\n/**\n * Websocket ready states — mirrors the standard WebSocket readyState\n * values as human-readable strings.\n */\nexport type SocketReadyState = \"connecting\" | \"open\" | \"closing\" | \"closed\"\n\n// ---------------------------------------------------------------------------\n// Socket interface\n// ---------------------------------------------------------------------------\n\n/**\n * Framework-agnostic Websocket interface.\n *\n * This allows the adapter to work with any Websocket library:\n * - Browser `WebSocket` via `wrapStandardWebsocket()`\n * - Node.js `ws` library via `wrapNodeWebsocket()`\n * - Bun `ServerWebSocket` via `wrapBunWebsocket()`\n *\n * The interface is intentionally minimal — only the operations the\n * adapter needs are exposed.\n */\nexport interface Socket {\n /** Send binary or text data through the Websocket. */\n send(data: Uint8Array | string): void\n\n /** Close the Websocket connection. */\n close(code?: number, reason?: string): void\n\n /** Register a handler for incoming messages (binary or text). */\n onMessage(handler: (data: Uint8Array | string) => void): void\n\n /** Register a handler for connection close. */\n onClose(handler: (code: number, reason: string) => void): void\n\n /** Register a handler for errors. */\n onError(handler: (error: Error) => void): void\n\n /** The current ready state of the Websocket. */\n readonly readyState: SocketReadyState\n}\n\n// ---------------------------------------------------------------------------\n// Connection types — used by server adapter\n// ---------------------------------------------------------------------------\n\n/**\n * Options for handling a new Websocket connection on the server.\n */\nexport interface WebsocketConnectionOptions {\n /** The Websocket instance, wrapped in the Socket interface. */\n socket: Socket\n\n /** Optional peer ID extracted from the upgrade request. */\n peerId?: PeerId\n\n /** Optional authentication token from the upgrade request. */\n authToken?: string\n}\n\n/**\n * Handle for an active Websocket connection.\n */\nexport interface WebsocketConnectionHandle {\n /** The peer ID for this connection. */\n readonly peerId: PeerId\n\n /** The channel ID for this connection. */\n readonly channelId: number\n\n /** Close the connection. */\n close(code?: number, reason?: string): void\n}\n\n/**\n * Result of handling a Websocket connection on the server.\n */\nexport interface WebsocketConnectionResult {\n /** The connection handle for managing this peer. */\n connection: WebsocketConnectionHandle\n\n /** Call this to start processing messages. */\n start(): void\n}\n\n// ---------------------------------------------------------------------------\n// Disconnect reason\n// ---------------------------------------------------------------------------\n\n/**\n * Discriminated union describing why a Websocket connection was lost.\n */\nexport type DisconnectReason =\n | { type: \"intentional\" }\n | { type: \"error\"; error: Error }\n | { type: \"closed\"; code: number; reason: string }\n | { type: \"max-retries-exceeded\"; attempts: number }\n | { type: \"not-started\" }\n\n// ---------------------------------------------------------------------------\n// Connection state (for client adapter observability)\n// ---------------------------------------------------------------------------\n\n/**\n * All possible states of the Websocket client.\n *\n * State machine transitions:\n * ```\n * disconnected → connecting → connected → ready\n * ↓ ↓ ↓\n * reconnecting ← ─ ┴ ─ ─ ─ ─ ┘\n * ↓\n * connecting (retry)\n * ↓\n * disconnected (max retries)\n * ```\n */\nexport type WebsocketClientState =\n | { status: \"disconnected\"; reason?: DisconnectReason }\n | { status: \"connecting\"; attempt: number }\n | { status: \"connected\" }\n | { status: \"ready\" }\n | { status: \"reconnecting\"; attempt: number; nextAttemptMs: number }\n\n/**\n * A state transition event for websocket client states.\n * Specialized from the generic `StateTransition<S>`.\n */\nexport type WebsocketClientStateTransition =\n StateTransition<WebsocketClientState>\n\n/**\n * Listener for websocket client state transitions.\n * Specialized from the generic `TransitionListener<S>`.\n */\nexport type TransitionListener = GenericTransitionListener<WebsocketClientState>\n\n// ---------------------------------------------------------------------------\n// Socket wrapper — standard WebSocket API (browser + Node ws)\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a standard `WebSocket` (browser or Node.js `ws` via `ws` package\n * in `WebSocket`-compatible mode) into the `Socket` interface.\n *\n * Handles `ArrayBuffer`, `Blob`, and string messages.\n */\nexport function wrapStandardWebsocket(ws: WebSocket): Socket {\n return {\n send(data: Uint8Array | string): void {\n ws.send(\n typeof data === \"string\" ? data : (data as Uint8Array<ArrayBuffer>),\n )\n },\n\n close(code?: number, reason?: string): void {\n ws.close(code, reason)\n },\n\n onMessage(handler: (data: Uint8Array | string) => void): void {\n ws.addEventListener(\"message\", event => {\n if (event.data instanceof ArrayBuffer) {\n handler(new Uint8Array(event.data))\n } else if (typeof Blob !== \"undefined\" && event.data instanceof Blob) {\n // Handle Blob data (browser)\n event.data.arrayBuffer().then(buffer => {\n handler(new Uint8Array(buffer))\n })\n } else {\n handler(event.data as string)\n }\n })\n },\n\n onClose(handler: (code: number, reason: string) => void): void {\n ws.addEventListener(\"close\", event => {\n handler(event.code, event.reason)\n })\n },\n\n onError(handler: (error: Error) => void): void {\n ws.addEventListener(\"error\", _event => {\n handler(new Error(\"WebSocket error\"))\n })\n },\n\n get readyState(): SocketReadyState {\n switch (ws.readyState) {\n case READY_STATE.CONNECTING:\n return \"connecting\"\n case READY_STATE.OPEN:\n return \"open\"\n case READY_STATE.CLOSING:\n return \"closing\"\n case READY_STATE.CLOSED:\n return \"closed\"\n default:\n return \"closed\"\n }\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Socket wrapper — Node.js `ws` library (raw API, not WebSocket-compat)\n// ---------------------------------------------------------------------------\n\n/**\n * The minimal interface we need from the Node.js `ws` library's `WebSocket`.\n *\n * Using a structural type rather than importing `ws` — consumers provide\n * the actual `ws` instance, we just need these methods.\n */\nexport interface NodeWebsocketLike {\n send(data: Uint8Array | string): void\n close(code?: number, reason?: string): void\n on(\n event: \"message\",\n handler: (data: Buffer | ArrayBuffer | string, isBinary: boolean) => void,\n ): void\n on(event: \"close\", handler: (code: number, reason: Buffer) => void): void\n on(event: \"error\", handler: (error: Error) => void): void\n readyState: number\n}\n\n/**\n * Wrap a Node.js `ws` library WebSocket into the `Socket` interface.\n *\n * Handles `Buffer` → `Uint8Array` conversion for binary messages.\n */\nexport function wrapNodeWebsocket(ws: NodeWebsocketLike): Socket {\n const CONNECTING = 0\n const OPEN = 1\n const CLOSING = 2\n\n return {\n send(data: Uint8Array | string): void {\n ws.send(data)\n },\n\n close(code?: number, reason?: string): void {\n ws.close(code, reason)\n },\n\n onMessage(handler: (data: Uint8Array | string) => void): void {\n ws.on(\n \"message\",\n (data: Buffer | ArrayBuffer | string, isBinary: boolean) => {\n if (isBinary) {\n if (data instanceof ArrayBuffer) {\n handler(new Uint8Array(data))\n } else if (typeof Buffer !== \"undefined\" && Buffer.isBuffer(data)) {\n handler(new Uint8Array(data))\n } else {\n handler(new Uint8Array(data as unknown as ArrayBuffer))\n }\n } else {\n if (typeof Buffer !== \"undefined\" && Buffer.isBuffer(data)) {\n handler(data.toString(\"utf-8\"))\n } else {\n handler(data as string)\n }\n }\n },\n )\n },\n\n onClose(handler: (code: number, reason: string) => void): void {\n ws.on(\"close\", (code: number, reason: Buffer) => {\n handler(code, reason.toString())\n })\n },\n\n onError(handler: (error: Error) => void): void {\n ws.on(\"error\", handler)\n },\n\n get readyState(): SocketReadyState {\n switch (ws.readyState) {\n case CONNECTING:\n return \"connecting\"\n case OPEN:\n return \"open\"\n case CLOSING:\n return \"closing\"\n default:\n return \"closed\"\n }\n },\n }\n}\n","// client-transport — Websocket client transport for @kyneta/exchange.\n//\n// Thin imperative shell around the pure client program (client-program.ts).\n// The program produces data effects; this module interprets them as I/O.\n//\n// FC/IS design:\n// - client-program.ts: pure Mealy machine (functional core)\n// - client-transport.ts: effect executor (imperative shell)\n//\n// Uses the kyneta wire format (CBOR codec + framing + fragmentation)\n// for binary messages. Text frames carry the \"ready\" handshake and\n// keepalive ping/pong.\n\nimport type { ObservableHandle, TransitionListener } from \"@kyneta/machine\"\nimport { createObservableProgram } from \"@kyneta/machine\"\nimport type {\n Channel,\n ChannelMsg,\n GeneratedChannel,\n PeerId,\n TransportFactory,\n} from \"@kyneta/transport\"\nimport { Transport } from \"@kyneta/transport\"\nimport {\n decodeBinaryMessages,\n encodeBinaryAndSend,\n FragmentReassembler,\n} from \"@kyneta/wire\"\nimport {\n createWsClientProgram,\n type WsClientEffect,\n type WsClientMsg,\n} from \"./client-program.js\"\nimport type {\n DisconnectReason,\n WebsocketClientState,\n WebsocketClientStateTransition,\n} from \"./types.js\"\nimport {\n READY_STATE,\n type WebSocketCloseEvent,\n type WebSocketConstructor,\n type WebSocketLike,\n type WebSocketMessageEvent,\n} from \"./types.js\"\n\n// Re-export state types for convenience\nexport type {\n DisconnectReason,\n WebsocketClientState,\n WebsocketClientStateTransition,\n}\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\n/**\n * Default fragment threshold in bytes.\n * AWS API Gateway has a 128KB limit, so 100KB provides a safe margin.\n */\nexport const DEFAULT_FRAGMENT_THRESHOLD = 100 * 1024\n\n/**\n * Options for the Websocket client transport (browser connections).\n */\nexport interface WebsocketClientOptions {\n /** Websocket URL to connect to. Can be a string or a function of peerId. */\n url: string | ((peerId: PeerId) => string)\n\n /**\n * WebSocket constructor — caller must provide explicitly.\n *\n * In browsers, pass the global `WebSocket`. In Node.js, pass `ws`'s\n * `WebSocket`. In Bun, pass `globalThis.WebSocket`. The transport\n * never probes `globalThis` on its own.\n */\n WebSocket: WebSocketConstructor\n\n /**\n * Headers to send during Websocket upgrade.\n * Used for authentication in service-to-service communication.\n *\n * Note: Headers are a Bun/Node-specific extension. The browser WebSocket\n * API does not support custom headers per the WHATWG spec.\n */\n headers?: Record<string, string>\n\n /** Reconnection options. */\n reconnect?: {\n enabled?: boolean\n maxAttempts?: number\n baseDelay?: number\n maxDelay?: number\n }\n\n /** Keepalive interval in ms (default: 30000). */\n keepaliveInterval?: number\n\n /**\n * Fragment threshold in bytes. Messages larger than this are fragmented.\n * Set to 0 to disable fragmentation (not recommended for cloud deployments).\n * Default: 100KB\n */\n fragmentThreshold?: number\n\n /** Lifecycle event callbacks. */\n lifecycle?: WebsocketClientLifecycleEvents\n}\n\n/**\n * Lifecycle event callbacks for the Websocket client.\n */\nexport interface WebsocketClientLifecycleEvents {\n /** Called on every state transition (delivered async via microtask). */\n onStateChange?: (transition: WebsocketClientStateTransition) => void\n\n /** Called when the connection is lost. */\n onDisconnect?: (reason: DisconnectReason) => void\n\n /** Called when a reconnection attempt is scheduled. */\n onReconnecting?: (attempt: number, nextAttemptMs: number) => void\n\n /** Called when reconnection succeeds after a previous connection. */\n onReconnected?: () => void\n\n /** Called when the server sends the \"ready\" signal. */\n onReady?: () => void\n}\n\n// ---------------------------------------------------------------------------\n// WebsocketClientTransport\n// ---------------------------------------------------------------------------\n\n/**\n * Websocket client network transport for @kyneta/exchange.\n *\n * Connects to a Websocket server, sends and receives ChannelMsg via\n * the kyneta wire format (CBOR codec + framing + fragmentation).\n *\n * Internally, the connection lifecycle is a `Program<Msg, Model, Fx>` —\n * a pure Mealy machine whose transitions are deterministically testable.\n * This class is the imperative shell that interprets data effects as I/O.\n *\n * Prefer the factory functions for construction:\n * - `createWebsocketClient()` — browser-to-server (from `./browser`)\n * - `createServiceWebsocketClient()` — service-to-service with headers (from `./server`)\n */\nexport class WebsocketClientTransport extends Transport<void> {\n #peerId?: PeerId\n #options: WebsocketClientOptions\n #WebSocketImpl: WebSocketConstructor\n\n // Observable program handle — created in constructor, drives all state\n #handle: ObservableHandle<WsClientMsg, WebsocketClientState>\n\n // Executor-local I/O state — not in the program model\n #socket?: WebSocketLike\n #serverChannel?: Channel\n #keepaliveTimer?: ReturnType<typeof setInterval>\n #reconnectTimer?: ReturnType<typeof setTimeout>\n\n // Fragmentation\n readonly #fragmentThreshold: number\n readonly #reassembler: FragmentReassembler\n\n constructor(options: WebsocketClientOptions) {\n super({ transportType: \"websocket-client\" })\n this.#options = options\n this.#WebSocketImpl = options.WebSocket\n this.#fragmentThreshold =\n options.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD\n this.#reassembler = new FragmentReassembler({\n timeoutMs: 10_000,\n })\n\n const program = createWsClientProgram({\n reconnect: options.reconnect,\n })\n\n this.#handle = createObservableProgram(program, (effect, dispatch) => {\n this.#executeEffect(effect, dispatch)\n })\n\n // Set up lifecycle event forwarding\n this.#setupLifecycleEvents()\n }\n\n // ==========================================================================\n // Effect executor — interprets data effects as I/O\n // ==========================================================================\n\n #executeEffect(\n effect: WsClientEffect,\n dispatch: (msg: WsClientMsg) => void,\n ): void {\n switch (effect.type) {\n case \"create-websocket\": {\n this.#doCreateWebsocket(dispatch)\n break\n }\n\n case \"close-websocket\": {\n if (this.#socket) {\n this.#socket.close(1000, \"Client disconnecting\")\n this.#socket = undefined\n }\n break\n }\n\n case \"add-channel-and-establish\": {\n // Clean up previous channel if it exists (e.g. after reconnect)\n if (this.#serverChannel) {\n this.removeChannel(this.#serverChannel.channelId)\n this.#serverChannel = undefined\n }\n\n this.#serverChannel = this.addChannel()\n\n // Establish immediately — the server already signaled ready\n this.establishChannel(this.#serverChannel.channelId)\n break\n }\n\n case \"remove-channel\": {\n if (this.#serverChannel) {\n this.removeChannel(this.#serverChannel.channelId)\n this.#serverChannel = undefined\n }\n break\n }\n\n case \"start-reconnect-timer\": {\n this.#reconnectTimer = setTimeout(() => {\n this.#reconnectTimer = undefined\n dispatch({ type: \"reconnect-timer-fired\" })\n }, effect.delayMs)\n break\n }\n\n case \"cancel-reconnect-timer\": {\n if (this.#reconnectTimer !== undefined) {\n clearTimeout(this.#reconnectTimer)\n this.#reconnectTimer = undefined\n }\n break\n }\n\n case \"start-keepalive\": {\n this.#startKeepalive()\n break\n }\n\n case \"stop-keepalive\": {\n this.#stopKeepalive()\n break\n }\n }\n }\n\n // ==========================================================================\n // WebSocket creation — the core I/O operation\n // ==========================================================================\n\n /**\n * Create a WebSocket and wire up event handlers to dispatch messages.\n *\n * The message handler is set up IMMEDIATELY after creation (before\n * the open event) to handle the race condition where the server sends\n * \"ready\" before the client's open promise resolves.\n */\n #doCreateWebsocket(dispatch: (msg: WsClientMsg) => void): void {\n const peerId = this.#peerId\n if (!peerId) {\n dispatch({\n type: \"socket-error\",\n error: new Error(\"Cannot connect: peerId not set\"),\n })\n return\n }\n\n // Resolve URL\n const url =\n typeof this.#options.url === \"function\"\n ? this.#options.url(peerId)\n : this.#options.url\n\n try {\n // Create WebSocket — pass headers as second arg when present.\n // The structural WebSocketConstructor's `...rest: any[]` absorbs\n // both the browser's protocols arg and backend's { headers } arg.\n if (\n this.#options.headers &&\n Object.keys(this.#options.headers).length > 0\n ) {\n this.#socket = new this.#WebSocketImpl(url, {\n headers: this.#options.headers,\n })\n } else {\n this.#socket = new this.#WebSocketImpl(url)\n }\n this.#socket.binaryType = \"arraybuffer\"\n\n const socket = this.#socket\n\n // Set up message handler IMMEDIATELY to handle the \"ready\" race condition.\n // The server may send \"ready\" before the open event fires.\n socket.addEventListener(\"message\", (event: WebSocketMessageEvent) => {\n this.#handleMessage(event, dispatch)\n })\n\n // Track whether we've dispatched a terminal event for this connection attempt\n let settled = false\n\n const onOpen = () => {\n cleanup()\n settled = true\n dispatch({ type: \"socket-opened\" })\n\n // After open, set up permanent close handler for post-connection closes\n socket.addEventListener(\"close\", (event: WebSocketCloseEvent) => {\n dispatch({\n type: \"socket-closed\",\n code: event.code,\n reason: event.reason,\n })\n })\n }\n\n const onError = () => {\n if (settled) return\n cleanup()\n settled = true\n dispatch({\n type: \"socket-error\",\n error: new Error(\"WebSocket connection failed\"),\n })\n }\n\n const onClose = () => {\n if (settled) return\n cleanup()\n settled = true\n dispatch({\n type: \"socket-error\",\n error: new Error(\"WebSocket closed during connection\"),\n })\n }\n\n const cleanup = () => {\n socket.removeEventListener(\"open\", onOpen)\n socket.removeEventListener(\"error\", onError)\n socket.removeEventListener(\"close\", onClose)\n }\n\n socket.addEventListener(\"open\", onOpen)\n socket.addEventListener(\"error\", onError)\n socket.addEventListener(\"close\", onClose)\n } catch (error) {\n dispatch({\n type: \"socket-error\",\n error: error instanceof Error ? error : new Error(String(error)),\n })\n }\n }\n\n // ==========================================================================\n // Message handling — I/O parsing logic\n // ==========================================================================\n\n /**\n * Handle incoming Websocket messages.\n *\n * Text frames carry the \"ready\" handshake and keepalive pong.\n * Binary frames carry CBOR-encoded ChannelMsg.\n */\n #handleMessage(\n event: WebSocketMessageEvent,\n dispatch: (msg: WsClientMsg) => void,\n ): void {\n const data = event.data\n\n // Handle text messages (keepalive and ready signal)\n if (typeof data === \"string\") {\n if (data === \"ready\") {\n dispatch({ type: \"server-ready\" })\n }\n // Ignore pong responses and other text\n return\n }\n\n // Handle binary messages through shared decode pipeline\n if (data instanceof ArrayBuffer) {\n try {\n const messages = decodeBinaryMessages(\n new Uint8Array(data),\n this.#reassembler,\n )\n if (messages) {\n for (const msg of messages) {\n this.#handleChannelMessage(msg)\n }\n }\n } catch (error) {\n console.error(\"Failed to decode message:\", error)\n }\n }\n }\n\n /**\n * Handle a decoded channel message.\n */\n #handleChannelMessage(msg: ChannelMsg): void {\n if (!this.#serverChannel) {\n return\n }\n\n // Deliver synchronously — the Synchronizer's receive queue prevents recursion\n this.#serverChannel.onReceive(msg)\n }\n\n // ==========================================================================\n // Keepalive\n // ==========================================================================\n\n #startKeepalive(): void {\n this.#stopKeepalive()\n\n const interval = this.#options.keepaliveInterval ?? 30_000\n\n this.#keepaliveTimer = setInterval(() => {\n if (this.#socket?.readyState === READY_STATE.OPEN) {\n this.#socket.send(\"ping\")\n }\n }, interval)\n }\n\n #stopKeepalive(): void {\n if (this.#keepaliveTimer) {\n clearInterval(this.#keepaliveTimer)\n this.#keepaliveTimer = undefined\n }\n }\n\n // ==========================================================================\n // Lifecycle event forwarding\n // ==========================================================================\n\n #setupLifecycleEvents(): void {\n // wasConnectedBefore is observer-local state, not in the program model\n let wasConnectedBefore = false\n\n this.#handle.subscribeToTransitions(transition => {\n // Forward to onStateChange callback\n this.#options.lifecycle?.onStateChange?.(transition)\n\n const { from, to } = transition\n\n // onDisconnect: transitioning TO disconnected\n if (to.status === \"disconnected\" && to.reason) {\n this.#options.lifecycle?.onDisconnect?.(to.reason)\n }\n\n // onReconnecting: transitioning TO reconnecting\n if (to.status === \"reconnecting\") {\n this.#options.lifecycle?.onReconnecting?.(to.attempt, to.nextAttemptMs)\n }\n\n // onReconnected: from reconnecting/connecting TO connected/ready (after prior connection)\n if (\n wasConnectedBefore &&\n (from.status === \"reconnecting\" || from.status === \"connecting\") &&\n (to.status === \"connected\" || to.status === \"ready\")\n ) {\n this.#options.lifecycle?.onReconnected?.()\n }\n\n // onReady: transitioning TO ready\n if (to.status === \"ready\") {\n this.#options.lifecycle?.onReady?.()\n wasConnectedBefore = true\n }\n })\n }\n\n // ==========================================================================\n // State observation — delegated to the observable handle\n // ==========================================================================\n\n /**\n * Get the current connection state.\n */\n getState(): WebsocketClientState {\n return this.#handle.getState()\n }\n\n /**\n * Subscribe to state transitions.\n */\n subscribeToTransitions(\n listener: TransitionListener<WebsocketClientState>,\n ): () => void {\n return this.#handle.subscribeToTransitions(listener)\n }\n\n /**\n * Wait for a specific state.\n */\n waitForState(\n predicate: (state: WebsocketClientState) => boolean,\n options?: { timeoutMs?: number },\n ): Promise<WebsocketClientState> {\n return this.#handle.waitForState(predicate, options)\n }\n\n /**\n * Wait for a specific status.\n */\n waitForStatus(\n status: WebsocketClientState[\"status\"],\n options?: { timeoutMs?: number },\n ): Promise<WebsocketClientState> {\n return this.#handle.waitForStatus(status, options)\n }\n\n /**\n * Whether the client is ready (server ready signal received).\n */\n get isReady(): boolean {\n return this.#handle.getState().status === \"ready\"\n }\n\n // ==========================================================================\n // Transport abstract method implementations\n // ==========================================================================\n\n protected generate(): GeneratedChannel {\n return {\n transportType: this.transportType,\n send: (msg: ChannelMsg) => {\n const socket = this.#socket\n if (!socket || socket.readyState !== READY_STATE.OPEN) {\n return\n }\n\n encodeBinaryAndSend(msg, this.#fragmentThreshold, data =>\n socket.send(new Uint8Array(data).buffer),\n )\n },\n stop: () => {\n // Don't call disconnect here — channel.stop() is called when\n // the channel is removed, which can happen during effect execution.\n // The actual disconnect is handled by onStop() or the program.\n },\n }\n }\n\n async onStart(): Promise<void> {\n if (!this.identity) {\n throw new Error(\n \"Transport not properly initialized — identity not available\",\n )\n }\n this.#peerId = this.identity.peerId\n this.#handle.dispatch({ type: \"start\" })\n }\n\n async onStop(): Promise<void> {\n this.#reassembler.dispose()\n this.#handle.dispatch({ type: \"stop\" })\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory functions\n// ---------------------------------------------------------------------------\n\n/**\n * Create a Websocket client transport factory for browser-to-server\n * connections.\n *\n * Returns an `TransportFactory` — a closure that creates a fresh transport\n * instance when called. Pass directly to `Exchange({ transports: [...] })`.\n *\n * @example\n * ```typescript\n * import { createWebsocketClient } from \"@kyneta/websocket-transport/browser\"\n *\n * const exchange = new Exchange({\n * transports: [createWebsocketClient({\n * url: \"ws://localhost:3000/ws\",\n * WebSocket,\n * reconnect: { enabled: true },\n * })],\n * })\n * ```\n */\nexport function createWebsocketClient(\n options: WebsocketClientOptions,\n): TransportFactory {\n return () => new WebsocketClientTransport(options)\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types-CJAcr1Df.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;AAyBA;;;cAAa,WAAA;EAAA;;;;;;UAYI,qBAAA;EAAA,SACN,IAAA,WAAe,WAAA;AAAA;;UAIT,mBAAA;EAAA,SACN,IAAA;EAAA,SACA,MAAA;AAAA;;;AAkBX;;;;;;;;;UAAiB,aAAA;EAAA,SACN,UAAA;EACT,UAAA;EACA,IAAA,CAAK,IAAA,WAAe,WAAA;EACpB,KAAA,CAAM,IAAA,WAAe,MAAA;EACrB,gBAAA,CAAiB,IAAA,UAAc,QAAA,GAAW,KAAA;EAC1C,mBAAA,CAAoB,IAAA,UAAc,QAAA,GAAW,KAAA;AAAA;;;;;;AAW/C;;;KAAY,oBAAA,QACV,GAAA,aACG,IAAA,YACA,aAAA;;;;;KAUO,gBAAA;AAAZ;;;;;AAiBA;;;;;;AAjBA,UAiBiB,MAAA;EAiBsB;EAfrC,IAAA,CAAK,IAAA,EAAM,UAAA;EAAX;EAGA,KAAA,CAAM,IAAA,WAAe,MAAA;EAHhB;EAML,SAAA,CAAU,OAAA,GAAU,IAAA,EAAM,UAAA;EAHpB;EAMN,OAAA,CAAQ,OAAA,GAAU,IAAA,UAAc,MAAA;EAHhC;EAMA,OAAA,CAAQ,OAAA,GAAU,KAAA,EAAO,KAAA;EANL;EAAA,SASX,UAAA,EAAY,gBAAA;AAAA;;;;UAUN,0BAAA;EAbU;EAezB,MAAA,EAAQ,MAAA;EAfA;EAkBR,MAAA,GAAS,MAAA;EAfY;EAkBrB,SAAA;AAAA;AARF;;;AAAA,UAciB,yBAAA;EAZf;EAAA,SAcS,MAAA,EAAQ,MAAA;EAXjB;EAAA,SAcS,SAAA;EAXT;EAcA,KAAA,CAAM,IAAA,WAAe,MAAA;AAAA;AARvB;;;AAAA,UAciB,yBAAA;EAZN;EAcT,UAAA,EAAY,yBAAA;EAXH;EAcT,KAAA;AAAA;;;;KAUU,gBAAA;EACN,IAAA;AAAA;EACA,IAAA;EAAe,KAAA,EAAO,KAAA;AAAA;EACtB,IAAA;EAAgB,IAAA;EAAc,MAAA;AAAA;EAC9B,IAAA;EAA8B,QAAA;AAAA;EAC9B,IAAA;AAAA;;;;;;;;;;;AAoBN;;;;KAAY,oBAAA;EACN,MAAA;EAAwB,MAAA,GAAS,gBAAA;AAAA;EACjC,MAAA;EAAsB,OAAA;AAAA;EACtB,MAAA;AAAA;EACA,MAAA;AAAA;EACA,MAAA;EAAwB,OAAA;EAAiB,aAAA;AAAA;;;AAa/C;;KAPY,8BAAA,GACV,eAAA,CAAgB,oBAAA;;;AAkBlB;;KAZY,oBAAA,GAAqB,kBAAA,CAA0B,oBAAA;;;;;;;iBAY3C,qBAAA,CAAsB,EAAA,EAAI,SAAA,GAAY,MAAA;;;;;;;UAkErC,iBAAA;EACf,IAAA,CAAK,IAAA,EAAM,UAAA;EACX,KAAA,CAAM,IAAA,WAAe,MAAA;EACrB,EAAA,CACE,KAAA,aACA,OAAA,GAAU,IAAA,EAAM,MAAA,GAAS,WAAA,WAAsB,QAAA;EAEjD,EAAA,CAAG,KAAA,WAAgB,OAAA,GAAU,IAAA,UAAc,MAAA,EAAQ,MAAA;EACnD,EAAA,CAAG,KAAA,WAAgB,OAAA,GAAU,KAAA,EAAO,KAAA;EACpC,UAAA;AAAA;;;;;;iBAQc,iBAAA,CAAkB,EAAA,EAAI,iBAAA,GAAoB,MAAA"}
|