@kyneta/websocket-transport 1.3.0 → 1.3.1
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/README.md +28 -15
- package/dist/browser.d.ts +58 -0
- package/dist/browser.js +15 -0
- package/dist/browser.js.map +1 -0
- package/dist/bun.d.ts +6 -6
- package/dist/bun.js.map +1 -1
- package/dist/{client.js → chunk-YZQF5RLV.js} +123 -17
- package/dist/chunk-YZQF5RLV.js.map +1 -0
- package/dist/{client.d.ts → client-transport-DUAFjVbh.d.ts} +25 -100
- package/dist/server.d.ts +41 -4
- package/dist/server.js +10 -1
- package/dist/server.js.map +1 -1
- package/dist/{types-DdNb8cAz.d.ts → types-D0lbeevu.d.ts} +50 -2
- package/package.json +13 -13
- package/src/__tests__/client-transport.test.ts +168 -0
- package/src/{client.ts → browser.ts} +15 -10
- package/src/bun-websocket.ts +5 -5
- package/src/bun.ts +1 -1
- package/src/client-transport.ts +42 -66
- package/src/server-transport.ts +3 -3
- package/src/server.ts +18 -4
- package/src/service-client.ts +52 -0
- package/src/types.ts +74 -6
- package/dist/chunk-PSG3LLT5.js +0 -111
- package/dist/chunk-PSG3LLT5.js.map +0 -1
- package/dist/client.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
# @kyneta/websocket-
|
|
1
|
+
# @kyneta/websocket-transport
|
|
2
2
|
|
|
3
|
-
Websocket
|
|
3
|
+
Websocket transport for `@kyneta/exchange` — browser, server, and Bun integration. Provides bidirectional real-time sync over Websockets using the `@kyneta/wire` binary protocol (CBOR codec + framing + fragmentation).
|
|
4
4
|
|
|
5
5
|
## Subpath Exports
|
|
6
6
|
|
|
7
7
|
| Export | Entry point | Environment |
|
|
8
8
|
|--------|-------------|-------------|
|
|
9
|
-
| `@kyneta/websocket-
|
|
10
|
-
| `@kyneta/websocket-
|
|
11
|
-
| `@kyneta/websocket-
|
|
9
|
+
| `@kyneta/websocket-transport/browser` | `./dist/browser.js` | Browser, Bun, Node.js |
|
|
10
|
+
| `@kyneta/websocket-transport/server` | `./dist/server.js` | Bun, Node.js |
|
|
11
|
+
| `@kyneta/websocket-transport/bun` | `./dist/bun.js` | Bun only |
|
|
12
12
|
|
|
13
13
|
## Server Setup
|
|
14
14
|
|
|
@@ -18,8 +18,8 @@ Use `createBunWebsocketHandlers` for zero-boilerplate integration with `Bun.serv
|
|
|
18
18
|
|
|
19
19
|
```/dev/null/bun-server.ts#L1-18
|
|
20
20
|
import { Exchange } from "@kyneta/exchange"
|
|
21
|
-
import { WebsocketServerAdapter } from "@kyneta/websocket-
|
|
22
|
-
import { createBunWebsocketHandlers, type BunWebsocketData } from "@kyneta/websocket-
|
|
21
|
+
import { WebsocketServerAdapter } from "@kyneta/websocket-transport/server"
|
|
22
|
+
import { createBunWebsocketHandlers, type BunWebsocketData } from "@kyneta/websocket-transport/bun"
|
|
23
23
|
|
|
24
24
|
const serverAdapter = new WebsocketServerAdapter()
|
|
25
25
|
|
|
@@ -40,7 +40,7 @@ Bun.serve<BunWebsocketData>({
|
|
|
40
40
|
For more control, use `wrapBunWebsocket` directly:
|
|
41
41
|
|
|
42
42
|
```/dev/null/bun-server-manual.ts#L1-17
|
|
43
|
-
import { wrapBunWebsocket, type BunWebsocketData } from "@kyneta/websocket-
|
|
43
|
+
import { wrapBunWebsocket, type BunWebsocketData } from "@kyneta/websocket-transport/bun"
|
|
44
44
|
|
|
45
45
|
Bun.serve<BunWebsocketData>({
|
|
46
46
|
fetch(req, server) {
|
|
@@ -69,7 +69,7 @@ Use `wrapNodeWebsocket` to adapt the `ws` library's `WebSocket` to the framework
|
|
|
69
69
|
|
|
70
70
|
```/dev/null/node-server.ts#L1-16
|
|
71
71
|
import { WebSocketServer } from "ws"
|
|
72
|
-
import { WebsocketServerAdapter, wrapNodeWebsocket } from "@kyneta/websocket-
|
|
72
|
+
import { WebsocketServerAdapter, wrapNodeWebsocket } from "@kyneta/websocket-transport/server"
|
|
73
73
|
|
|
74
74
|
const serverAdapter = new WebsocketServerAdapter()
|
|
75
75
|
|
|
@@ -92,12 +92,13 @@ wss.on("connection", (ws) => {
|
|
|
92
92
|
|
|
93
93
|
Use `createWebsocketClient` for browser-to-server connections:
|
|
94
94
|
|
|
95
|
-
```/dev/null/browser-client.ts#L1-
|
|
95
|
+
```/dev/null/browser-client.ts#L1-13
|
|
96
96
|
import { Exchange } from "@kyneta/exchange"
|
|
97
|
-
import { createWebsocketClient } from "@kyneta/websocket-
|
|
97
|
+
import { createWebsocketClient } from "@kyneta/websocket-transport/browser"
|
|
98
98
|
|
|
99
99
|
const adapter = createWebsocketClient({
|
|
100
100
|
url: "ws://localhost:3000/ws",
|
|
101
|
+
WebSocket,
|
|
101
102
|
reconnect: { enabled: true },
|
|
102
103
|
})
|
|
103
104
|
|
|
@@ -112,7 +113,7 @@ const exchange = new Exchange({
|
|
|
112
113
|
Use `createServiceWebsocketClient` for backend connections that need authentication headers during the Websocket upgrade. Headers are a Bun/Node-specific extension — the browser `WebSocket` API does not support custom headers.
|
|
113
114
|
|
|
114
115
|
```/dev/null/service-client.ts#L1-13
|
|
115
|
-
import { createServiceWebsocketClient } from "@kyneta/websocket-
|
|
116
|
+
import { createServiceWebsocketClient } from "@kyneta/websocket-transport/server"
|
|
116
117
|
|
|
117
118
|
const adapter = createServiceWebsocketClient({
|
|
118
119
|
url: "ws://primary-server:3000/ws",
|
|
@@ -180,7 +181,7 @@ The server may send `"ready"` before the client's `open` event fires (server-rea
|
|
|
180
181
|
The public observation API is powered by `createObservableProgram` from `@kyneta/machine`:
|
|
181
182
|
|
|
182
183
|
```/dev/null/observe-state.ts#L1-18
|
|
183
|
-
import { createWebsocketClient } from "@kyneta/websocket-
|
|
184
|
+
import { createWebsocketClient } from "@kyneta/websocket-transport/browser"
|
|
184
185
|
|
|
185
186
|
const adapter = createWebsocketClient({
|
|
186
187
|
url: "ws://localhost:3000/ws",
|
|
@@ -207,7 +208,7 @@ await adapter.waitForStatus("ready", { timeoutMs: 5000 })
|
|
|
207
208
|
|
|
208
209
|
| Wrapper | Input | Export |
|
|
209
210
|
|---------|-------|--------|
|
|
210
|
-
| `wrapStandardWebsocket(ws)` | Browser `WebSocket` | `./
|
|
211
|
+
| `wrapStandardWebsocket(ws)` | Browser `WebSocket` | `./server` |
|
|
211
212
|
| `wrapNodeWebsocket(ws)` | Node.js `ws` library | `./server` |
|
|
212
213
|
| `wrapBunWebsocket(ws)` | Bun `ServerWebSocket` | `./bun` |
|
|
213
214
|
|
|
@@ -229,7 +230,7 @@ interface Socket {
|
|
|
229
230
|
| Option | Default | Description |
|
|
230
231
|
|--------|---------|-------------|
|
|
231
232
|
| `url` | — | Websocket URL. String or `(peerId) => string` function. |
|
|
232
|
-
| `WebSocket` |
|
|
233
|
+
| `WebSocket` | — | WebSocket constructor (**required**). In browsers, pass `WebSocket`. In Node.js, pass `ws`'s `WebSocket`. |
|
|
233
234
|
| `reconnect.enabled` | `true` | Enable automatic reconnection. |
|
|
234
235
|
| `reconnect.maxAttempts` | `10` | Maximum reconnection attempts before giving up. |
|
|
235
236
|
| `reconnect.baseDelay` | `1000` | Base delay in ms for exponential backoff. |
|
|
@@ -256,6 +257,18 @@ interface Socket {
|
|
|
256
257
|
|
|
257
258
|
The client sends text `"ping"` frames at the configured interval. The server responds with text `"pong"`. This keeps connections alive through proxies and load balancers that terminate idle connections.
|
|
258
259
|
|
|
260
|
+
## Runtime Agnosticism
|
|
261
|
+
|
|
262
|
+
Every entry point (`./browser`, `./server`, `./bun`) is safe to import in any JavaScript runtime — no top-level side effects, no `globalThis` probes. The transport package has zero runtime dependencies; callers provide all implementations:
|
|
263
|
+
|
|
264
|
+
| Entry Point | Purpose | Caller Provides |
|
|
265
|
+
|-------------|---------|----------------|
|
|
266
|
+
| `./browser` | Browser-to-server connections | `WebSocket` constructor (the global `WebSocket` in browsers) |
|
|
267
|
+
| `./server` | Server transport + service-to-service client | `Socket` wrapper via `wrapNodeWebsocket()` or `wrapStandardWebsocket()` |
|
|
268
|
+
| `./bun` | Bun-optimized server handlers | `ServerWebSocket` from Bun's callback API |
|
|
269
|
+
|
|
270
|
+
This follows the same structural-typing principle used throughout: `wrapNodeWebsocket` takes a `NodeWebsocketLike` — it never imports `ws`. `wrapBunWebsocket` takes a Bun `ServerWebSocket` — it never imports Bun APIs at the top level.
|
|
271
|
+
|
|
259
272
|
## Peer Dependencies
|
|
260
273
|
|
|
261
274
|
```/dev/null/package.json#L1-4
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Program } from '@kyneta/machine';
|
|
2
|
+
import { ReconnectOptions } from '@kyneta/transport';
|
|
3
|
+
import { W as WebsocketClientState } from './types-D0lbeevu.js';
|
|
4
|
+
export { D as DisconnectReason, R as READY_STATE, S as Socket, a as SocketReadyState, T as TransitionListener, b as WebSocketCloseEvent, c as WebSocketConstructor, d as WebSocketLike, e as WebSocketMessageEvent, f as WebsocketClientStateTransition } from './types-D0lbeevu.js';
|
|
5
|
+
export { D as DEFAULT_FRAGMENT_THRESHOLD, W as WebsocketClientLifecycleEvents, a as WebsocketClientOptions, b as WebsocketClientTransport, c as createWebsocketClient } from './client-transport-DUAFjVbh.js';
|
|
6
|
+
|
|
7
|
+
type WsClientMsg = {
|
|
8
|
+
type: "start";
|
|
9
|
+
} | {
|
|
10
|
+
type: "socket-opened";
|
|
11
|
+
} | {
|
|
12
|
+
type: "server-ready";
|
|
13
|
+
} | {
|
|
14
|
+
type: "socket-closed";
|
|
15
|
+
code: number;
|
|
16
|
+
reason: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: "socket-error";
|
|
19
|
+
error: Error;
|
|
20
|
+
} | {
|
|
21
|
+
type: "reconnect-timer-fired";
|
|
22
|
+
} | {
|
|
23
|
+
type: "stop";
|
|
24
|
+
};
|
|
25
|
+
type WsClientEffect = {
|
|
26
|
+
type: "create-websocket";
|
|
27
|
+
attempt: number;
|
|
28
|
+
} | {
|
|
29
|
+
type: "close-websocket";
|
|
30
|
+
} | {
|
|
31
|
+
type: "add-channel-and-establish";
|
|
32
|
+
} | {
|
|
33
|
+
type: "remove-channel";
|
|
34
|
+
} | {
|
|
35
|
+
type: "start-reconnect-timer";
|
|
36
|
+
delayMs: number;
|
|
37
|
+
} | {
|
|
38
|
+
type: "cancel-reconnect-timer";
|
|
39
|
+
} | {
|
|
40
|
+
type: "start-keepalive";
|
|
41
|
+
} | {
|
|
42
|
+
type: "stop-keepalive";
|
|
43
|
+
};
|
|
44
|
+
interface WsClientProgramOptions {
|
|
45
|
+
reconnect?: Partial<ReconnectOptions>;
|
|
46
|
+
/** Inject jitter source for deterministic testing. Default: () => Math.random() * 1000 */
|
|
47
|
+
jitterFn?: () => number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Create the websocket client connection lifecycle program — a pure Mealy machine.
|
|
51
|
+
*
|
|
52
|
+
* The returned `Program<WsClientMsg, WebsocketClientState, WsClientEffect>`
|
|
53
|
+
* encodes every state transition and effect as inspectable data. The imperative
|
|
54
|
+
* shell interprets `WsClientEffect` as actual I/O.
|
|
55
|
+
*/
|
|
56
|
+
declare function createWsClientProgram(options?: WsClientProgramOptions): Program<WsClientMsg, WebsocketClientState, WsClientEffect>;
|
|
57
|
+
|
|
58
|
+
export { WebsocketClientState, type WsClientEffect, type WsClientMsg, type WsClientProgramOptions, createWsClientProgram };
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_FRAGMENT_THRESHOLD,
|
|
3
|
+
READY_STATE,
|
|
4
|
+
WebsocketClientTransport,
|
|
5
|
+
createWebsocketClient,
|
|
6
|
+
createWsClientProgram
|
|
7
|
+
} from "./chunk-YZQF5RLV.js";
|
|
8
|
+
export {
|
|
9
|
+
DEFAULT_FRAGMENT_THRESHOLD,
|
|
10
|
+
READY_STATE,
|
|
11
|
+
WebsocketClientTransport,
|
|
12
|
+
createWebsocketClient,
|
|
13
|
+
createWsClientProgram
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/bun.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ServerWebSocket } from 'bun';
|
|
2
|
-
import { S as Socket } from './types-
|
|
3
|
-
export { D as DisconnectReason, a as SocketReadyState } from './types-
|
|
2
|
+
import { S as Socket } from './types-D0lbeevu.js';
|
|
3
|
+
export { D as DisconnectReason, a as SocketReadyState } from './types-D0lbeevu.js';
|
|
4
4
|
import '@kyneta/transport';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -30,8 +30,8 @@ type BunWebsocketData = {
|
|
|
30
30
|
*
|
|
31
31
|
* @example
|
|
32
32
|
* ```typescript
|
|
33
|
-
* import { WebsocketServerTransport } from "@kyneta/websocket-
|
|
34
|
-
* import { wrapBunWebsocket, type BunWebsocketData } from "@kyneta/websocket-
|
|
33
|
+
* import { WebsocketServerTransport } from "@kyneta/websocket-transport/server"
|
|
34
|
+
* import { wrapBunWebsocket, type BunWebsocketData } from "@kyneta/websocket-transport/bun"
|
|
35
35
|
*
|
|
36
36
|
* const serverAdapter = new WebsocketServerTransport()
|
|
37
37
|
*
|
|
@@ -62,8 +62,8 @@ declare function wrapBunWebsocket(ws: ServerWebSocket<BunWebsocketData>): Socket
|
|
|
62
62
|
*
|
|
63
63
|
* @example
|
|
64
64
|
* ```typescript
|
|
65
|
-
* import { WebsocketServerTransport } from "@kyneta/websocket-
|
|
66
|
-
* import { createBunWebsocketHandlers, type BunWebsocketData } from "@kyneta/websocket-
|
|
65
|
+
* import { WebsocketServerTransport } from "@kyneta/websocket-transport/server"
|
|
66
|
+
* import { createBunWebsocketHandlers, type BunWebsocketData } from "@kyneta/websocket-transport/bun"
|
|
67
67
|
*
|
|
68
68
|
* const serverAdapter = new WebsocketServerTransport()
|
|
69
69
|
*
|
package/dist/bun.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/bun-websocket.ts"],"sourcesContent":["// bun-websocket — Bun-specific Websocket wrapper for @kyneta/websocket-
|
|
1
|
+
{"version":3,"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":";AA0EO,SAAS,iBACd,IACQ;AACR,KAAG,OAAO,EAAE,UAAU,CAAC,EAAE;AAEzB,SAAO;AAAA,IACL,KAAK,MAAiC;AACpC,SAAG,KAAK,IAAI;AAAA,IACd;AAAA,IAEA,MAAM,MAAe,QAAuB;AAC1C,SAAG,MAAM,MAAM,MAAM;AAAA,IACvB;AAAA,IAEA,UAAU,SAAoD;AAC5D,SAAG,KAAK,SAAS,YAAY;AAAA,IAC/B;AAAA,IAEA,QAAQ,SAAuD;AAC7D,SAAG,KAAK,SAAS,UAAU;AAAA,IAC7B;AAAA,IAEA,QAAQ,UAAwC;AAAA,IAEhD;AAAA,IAEA,IAAI,aAA+B;AACjC,YAAM,SAA6B;AAAA,QACjC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,aAAO,OAAO,GAAG,UAAU,KAAK;AAAA,IAClC;AAAA,EACF;AACF;AA6BO,SAAS,2BAA2B,WAExC;AACD,SAAO;AAAA,IACL,KAAK,IAAuC;AAC1C,gBAAU,iBAAiB,EAAE,QAAQ,iBAAiB,EAAE,EAAE,CAAC,EAAE,MAAM;AAAA,IACrE;AAAA,IACA,QACE,IACA,KACA;AACA,YAAM,OACJ,eAAe,cACX,IAAI,WAAW,GAAG,IAClB,OAAO,SAAS,GAAG,IACjB,IAAI,WAAW,GAAG,IAClB;AACR,SAAG,KAAK,SAAS,YAAY,IAAI;AAAA,IACnC;AAAA,IACA,MAAM,IAAuC,MAAc,QAAgB;AACzE,SAAG,KAAK,SAAS,UAAU,MAAM,MAAM;AAAA,IACzC;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
wrapStandardWebsocket
|
|
3
|
-
} from "./chunk-PSG3LLT5.js";
|
|
4
|
-
|
|
5
1
|
// src/client-program.ts
|
|
6
2
|
import { computeBackoffDelay, DEFAULT_RECONNECT } from "@kyneta/transport";
|
|
7
3
|
function createWsClientProgram(options = {}) {
|
|
@@ -165,6 +161,118 @@ function createWsClientProgram(options = {}) {
|
|
|
165
161
|
};
|
|
166
162
|
}
|
|
167
163
|
|
|
164
|
+
// src/types.ts
|
|
165
|
+
var READY_STATE = {
|
|
166
|
+
CONNECTING: 0,
|
|
167
|
+
OPEN: 1,
|
|
168
|
+
CLOSING: 2,
|
|
169
|
+
CLOSED: 3
|
|
170
|
+
};
|
|
171
|
+
function wrapStandardWebsocket(ws) {
|
|
172
|
+
return {
|
|
173
|
+
send(data) {
|
|
174
|
+
ws.send(
|
|
175
|
+
typeof data === "string" ? data : data
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
close(code, reason) {
|
|
179
|
+
ws.close(code, reason);
|
|
180
|
+
},
|
|
181
|
+
onMessage(handler) {
|
|
182
|
+
ws.addEventListener("message", (event) => {
|
|
183
|
+
if (event.data instanceof ArrayBuffer) {
|
|
184
|
+
handler(new Uint8Array(event.data));
|
|
185
|
+
} else if (typeof Blob !== "undefined" && event.data instanceof Blob) {
|
|
186
|
+
event.data.arrayBuffer().then((buffer) => {
|
|
187
|
+
handler(new Uint8Array(buffer));
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
handler(event.data);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
onClose(handler) {
|
|
195
|
+
ws.addEventListener("close", (event) => {
|
|
196
|
+
handler(event.code, event.reason);
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
onError(handler) {
|
|
200
|
+
ws.addEventListener("error", (_event) => {
|
|
201
|
+
handler(new Error("WebSocket error"));
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
get readyState() {
|
|
205
|
+
switch (ws.readyState) {
|
|
206
|
+
case READY_STATE.CONNECTING:
|
|
207
|
+
return "connecting";
|
|
208
|
+
case READY_STATE.OPEN:
|
|
209
|
+
return "open";
|
|
210
|
+
case READY_STATE.CLOSING:
|
|
211
|
+
return "closing";
|
|
212
|
+
case READY_STATE.CLOSED:
|
|
213
|
+
return "closed";
|
|
214
|
+
default:
|
|
215
|
+
return "closed";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function wrapNodeWebsocket(ws) {
|
|
221
|
+
const CONNECTING = 0;
|
|
222
|
+
const OPEN = 1;
|
|
223
|
+
const CLOSING = 2;
|
|
224
|
+
return {
|
|
225
|
+
send(data) {
|
|
226
|
+
ws.send(data);
|
|
227
|
+
},
|
|
228
|
+
close(code, reason) {
|
|
229
|
+
ws.close(code, reason);
|
|
230
|
+
},
|
|
231
|
+
onMessage(handler) {
|
|
232
|
+
ws.on(
|
|
233
|
+
"message",
|
|
234
|
+
(data, isBinary) => {
|
|
235
|
+
if (isBinary) {
|
|
236
|
+
if (data instanceof ArrayBuffer) {
|
|
237
|
+
handler(new Uint8Array(data));
|
|
238
|
+
} else if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
|
|
239
|
+
handler(new Uint8Array(data));
|
|
240
|
+
} else {
|
|
241
|
+
handler(new Uint8Array(data));
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
|
|
245
|
+
handler(data.toString("utf-8"));
|
|
246
|
+
} else {
|
|
247
|
+
handler(data);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
onClose(handler) {
|
|
254
|
+
ws.on("close", (code, reason) => {
|
|
255
|
+
handler(code, reason.toString());
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
onError(handler) {
|
|
259
|
+
ws.on("error", handler);
|
|
260
|
+
},
|
|
261
|
+
get readyState() {
|
|
262
|
+
switch (ws.readyState) {
|
|
263
|
+
case CONNECTING:
|
|
264
|
+
return "connecting";
|
|
265
|
+
case OPEN:
|
|
266
|
+
return "open";
|
|
267
|
+
case CLOSING:
|
|
268
|
+
return "closing";
|
|
269
|
+
default:
|
|
270
|
+
return "closed";
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
168
276
|
// src/client-transport.ts
|
|
169
277
|
import { createObservableProgram } from "@kyneta/machine";
|
|
170
278
|
import { Transport } from "@kyneta/transport";
|
|
@@ -191,7 +299,7 @@ var WebsocketClientTransport = class extends Transport {
|
|
|
191
299
|
constructor(options) {
|
|
192
300
|
super({ transportType: "websocket-client" });
|
|
193
301
|
this.#options = options;
|
|
194
|
-
this.#WebSocketImpl = options.WebSocket
|
|
302
|
+
this.#WebSocketImpl = options.WebSocket;
|
|
195
303
|
this.#fragmentThreshold = options.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD;
|
|
196
304
|
this.#reassembler = new FragmentReassembler({
|
|
197
305
|
timeoutMs: 1e4
|
|
@@ -282,8 +390,7 @@ var WebsocketClientTransport = class extends Transport {
|
|
|
282
390
|
const url = typeof this.#options.url === "function" ? this.#options.url(peerId) : this.#options.url;
|
|
283
391
|
try {
|
|
284
392
|
if (this.#options.headers && Object.keys(this.#options.headers).length > 0) {
|
|
285
|
-
|
|
286
|
-
this.#socket = new BunWebSocket(url, {
|
|
393
|
+
this.#socket = new this.#WebSocketImpl(url, {
|
|
287
394
|
headers: this.#options.headers
|
|
288
395
|
});
|
|
289
396
|
} else {
|
|
@@ -389,7 +496,7 @@ var WebsocketClientTransport = class extends Transport {
|
|
|
389
496
|
this.#stopKeepalive();
|
|
390
497
|
const interval = this.#options.keepaliveInterval ?? 3e4;
|
|
391
498
|
this.#keepaliveTimer = setInterval(() => {
|
|
392
|
-
if (this.#socket?.readyState ===
|
|
499
|
+
if (this.#socket?.readyState === READY_STATE.OPEN) {
|
|
393
500
|
this.#socket.send("ping");
|
|
394
501
|
}
|
|
395
502
|
}, interval);
|
|
@@ -464,7 +571,7 @@ var WebsocketClientTransport = class extends Transport {
|
|
|
464
571
|
transportType: this.transportType,
|
|
465
572
|
send: (msg) => {
|
|
466
573
|
const socket = this.#socket;
|
|
467
|
-
if (!socket || socket.readyState !==
|
|
574
|
+
if (!socket || socket.readyState !== READY_STATE.OPEN) {
|
|
468
575
|
return;
|
|
469
576
|
}
|
|
470
577
|
encodeBinaryAndSend(
|
|
@@ -494,15 +601,14 @@ var WebsocketClientTransport = class extends Transport {
|
|
|
494
601
|
function createWebsocketClient(options) {
|
|
495
602
|
return () => new WebsocketClientTransport(options);
|
|
496
603
|
}
|
|
497
|
-
|
|
498
|
-
return () => new WebsocketClientTransport(options);
|
|
499
|
-
}
|
|
604
|
+
|
|
500
605
|
export {
|
|
606
|
+
createWsClientProgram,
|
|
607
|
+
READY_STATE,
|
|
608
|
+
wrapStandardWebsocket,
|
|
609
|
+
wrapNodeWebsocket,
|
|
501
610
|
DEFAULT_FRAGMENT_THRESHOLD,
|
|
502
611
|
WebsocketClientTransport,
|
|
503
|
-
|
|
504
|
-
createWebsocketClient,
|
|
505
|
-
createWsClientProgram,
|
|
506
|
-
wrapStandardWebsocket
|
|
612
|
+
createWebsocketClient
|
|
507
613
|
};
|
|
508
|
-
//# sourceMappingURL=
|
|
614
|
+
//# sourceMappingURL=chunk-YZQF5RLV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"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 | ArrayBufferLike | Uint8Array): 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 {\n READY_STATE,\n type WebSocketCloseEvent,\n type WebSocketConstructor,\n type WebSocketLike,\n type WebSocketMessageEvent,\n} from \"./types.js\"\nimport type {\n DisconnectReason,\n WebsocketClientState,\n WebsocketClientStateTransition,\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\n"],"mappings":"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","names":[]}
|