@kyneta/websocket-transport 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Duane Johnson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,255 @@
1
+ # @kyneta/websocket-network-adapter
2
+
3
+ Websocket adapter for `@kyneta/exchange` — client, server, and Bun integration. Provides bidirectional real-time sync over Websockets using the `@kyneta/wire` binary protocol (CBOR codec + framing + fragmentation).
4
+
5
+ ## Subpath Exports
6
+
7
+ | Export | Entry point | Environment |
8
+ |--------|-------------|-------------|
9
+ | `@kyneta/websocket-network-adapter/client` | `./dist/client.js` | Browser, Bun, Node.js |
10
+ | `@kyneta/websocket-network-adapter/server` | `./dist/server.js` | Bun, Node.js |
11
+ | `@kyneta/websocket-network-adapter/bun` | `./dist/bun.js` | Bun only |
12
+
13
+ ## Server Setup
14
+
15
+ ### Bun (recommended)
16
+
17
+ Use `createBunWebsocketHandlers` for zero-boilerplate integration with `Bun.serve()`:
18
+
19
+ ```/dev/null/bun-server.ts#L1-18
20
+ import { Exchange } from "@kyneta/exchange"
21
+ import { WebsocketServerAdapter } from "@kyneta/websocket-network-adapter/server"
22
+ import { createBunWebsocketHandlers, type BunWebsocketData } from "@kyneta/websocket-network-adapter/bun"
23
+
24
+ const serverAdapter = new WebsocketServerAdapter()
25
+
26
+ const exchange = new Exchange({
27
+ identity: { peerId: "server", name: "server", type: "service" },
28
+ adapters: [serverAdapter],
29
+ })
30
+
31
+ Bun.serve<BunWebsocketData>({
32
+ fetch(req, server) {
33
+ server.upgrade(req)
34
+ return new Response("upgrade failed", { status: 400 })
35
+ },
36
+ websocket: createBunWebsocketHandlers(serverAdapter),
37
+ })
38
+ ```
39
+
40
+ For more control, use `wrapBunWebsocket` directly:
41
+
42
+ ```/dev/null/bun-server-manual.ts#L1-17
43
+ import { wrapBunWebsocket, type BunWebsocketData } from "@kyneta/websocket-network-adapter/bun"
44
+
45
+ Bun.serve<BunWebsocketData>({
46
+ fetch(req, server) {
47
+ server.upgrade(req)
48
+ return new Response("upgrade failed", { status: 400 })
49
+ },
50
+ websocket: {
51
+ open(ws) {
52
+ const socket = wrapBunWebsocket(ws)
53
+ serverAdapter.handleConnection({ socket }).start()
54
+ },
55
+ message(ws, msg) {
56
+ const data = msg instanceof ArrayBuffer ? new Uint8Array(msg) : msg
57
+ ws.data?.handlers?.onMessage?.(data)
58
+ },
59
+ close(ws, code, reason) {
60
+ ws.data?.handlers?.onClose?.(code, reason)
61
+ },
62
+ },
63
+ })
64
+ ```
65
+
66
+ ### Node.js (`ws` library)
67
+
68
+ Use `wrapNodeWebsocket` to adapt the `ws` library's `WebSocket` to the framework-agnostic `Socket` interface:
69
+
70
+ ```/dev/null/node-server.ts#L1-16
71
+ import { WebSocketServer } from "ws"
72
+ import { WebsocketServerAdapter, wrapNodeWebsocket } from "@kyneta/websocket-network-adapter/server"
73
+
74
+ const serverAdapter = new WebsocketServerAdapter()
75
+
76
+ const exchange = new Exchange({
77
+ identity: { peerId: "server", name: "server", type: "service" },
78
+ adapters: [serverAdapter],
79
+ })
80
+
81
+ const wss = new WebSocketServer({ port: 3000 })
82
+
83
+ wss.on("connection", (ws) => {
84
+ const { start } = serverAdapter.handleConnection({ socket: wrapNodeWebsocket(ws) })
85
+ start()
86
+ })
87
+ ```
88
+
89
+ ## Client Setup
90
+
91
+ ### Browser
92
+
93
+ Use `createWebsocketClient` for browser-to-server connections:
94
+
95
+ ```/dev/null/browser-client.ts#L1-12
96
+ import { Exchange } from "@kyneta/exchange"
97
+ import { createWebsocketClient } from "@kyneta/websocket-network-adapter/client"
98
+
99
+ const adapter = createWebsocketClient({
100
+ url: "ws://localhost:3000/ws",
101
+ reconnect: { enabled: true },
102
+ })
103
+
104
+ const exchange = new Exchange({
105
+ identity: { peerId: "browser-client", name: "Alice", type: "user" },
106
+ adapters: [adapter],
107
+ })
108
+ ```
109
+
110
+ ### Service-to-Service
111
+
112
+ 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
+ ```/dev/null/service-client.ts#L1-13
115
+ import { createServiceWebsocketClient } from "@kyneta/websocket-network-adapter/client"
116
+
117
+ const adapter = createServiceWebsocketClient({
118
+ url: "ws://primary-server:3000/ws",
119
+ headers: {
120
+ Authorization: "Bearer <token>",
121
+ },
122
+ reconnect: { enabled: true },
123
+ })
124
+
125
+ const exchange = new Exchange({
126
+ identity: { peerId: "worker-1", name: "worker-1", type: "service" },
127
+ adapters: [adapter],
128
+ })
129
+ ```
130
+
131
+ > For browser clients, authenticate via URL query parameters instead of headers.
132
+
133
+ ## Connection Lifecycle
134
+
135
+ The client adapter manages connection state through a validated state machine with async-observable transitions:
136
+
137
+ ```/dev/null/state-machine.txt#L1-8
138
+ disconnected → connecting → connected → ready
139
+ ↓ ↓ ↓
140
+ reconnecting ← ─ ┴ ─ ─ ─ ─ ┘
141
+
142
+ connecting (retry)
143
+
144
+ disconnected (max retries)
145
+ ```
146
+
147
+ | State | Description |
148
+ |-------|-------------|
149
+ | `disconnected` | No active connection. Optional `reason` field describes why. |
150
+ | `connecting` | Websocket handshake in progress. Tracks `attempt` number. |
151
+ | `connected` | TCP connection open, waiting for server "ready" signal. |
152
+ | `ready` | Server sent `"ready"` text frame — protocol messages can flow. |
153
+ | `reconnecting` | Connection lost, scheduling next attempt. Tracks `attempt` and `nextAttemptMs`. |
154
+
155
+ ### Connection Handshake
156
+
157
+ 1. Client opens Websocket, transitions to `connecting`
158
+ 2. Websocket `open` event fires, transitions to `connected`
159
+ 3. Server sends text `"ready"` frame, client transitions to `ready`
160
+ 4. Client creates its channel, calls `establishChannel()`
161
+ 5. Synchronizer exchanges `establish-request` / `establish-response`
162
+
163
+ ### Observing State
164
+
165
+ ```/dev/null/observe-state.ts#L1-18
166
+ import { createWebsocketClient } from "@kyneta/websocket-network-adapter/client"
167
+
168
+ const adapter = createWebsocketClient({
169
+ url: "ws://localhost:3000/ws",
170
+ lifecycle: {
171
+ onStateChange: ({ from, to }) => console.log(`${from.status} → ${to.status}`),
172
+ onDisconnect: (reason) => console.log("disconnected:", reason.type),
173
+ onReconnecting: (attempt, nextMs) => console.log(`retry #${attempt} in ${nextMs}ms`),
174
+ onReconnected: () => console.log("reconnected"),
175
+ onReady: () => console.log("ready"),
176
+ },
177
+ })
178
+
179
+ // Or subscribe to transitions programmatically
180
+ const unsub = adapter.subscribeToTransitions(({ from, to }) => {
181
+ console.log(`${from.status} → ${to.status}`)
182
+ })
183
+
184
+ await adapter.waitForStatus("ready", { timeoutMs: 5000 })
185
+ ```
186
+
187
+ ## The Socket Interface
188
+
189
+ `Socket` is the framework-agnostic abstraction that decouples the adapter from any specific Websocket library. Platform-specific wrappers adapt concrete implementations to this interface:
190
+
191
+ | Wrapper | Input | Export |
192
+ |---------|-------|--------|
193
+ | `wrapStandardWebsocket(ws)` | Browser `WebSocket` | `./client`, `./server` |
194
+ | `wrapNodeWebsocket(ws)` | Node.js `ws` library | `./server` |
195
+ | `wrapBunWebsocket(ws)` | Bun `ServerWebSocket` | `./bun` |
196
+
197
+ ```/dev/null/socket-interface.ts#L1-8
198
+ interface Socket {
199
+ send(data: Uint8Array | string): void
200
+ close(code?: number, reason?: string): void
201
+ onMessage(handler: (data: Uint8Array | string) => void): void
202
+ onClose(handler: (code: number, reason: string) => void): void
203
+ onError(handler: (error: Error) => void): void
204
+ readonly readyState: "connecting" | "open" | "closing" | "closed"
205
+ }
206
+ ```
207
+
208
+ ## Configuration
209
+
210
+ ### Client Options
211
+
212
+ | Option | Default | Description |
213
+ |--------|---------|-------------|
214
+ | `url` | — | Websocket URL. String or `(peerId) => string` function. |
215
+ | `WebSocket` | `globalThis.WebSocket` | Custom WebSocket constructor (for Node.js or testing). |
216
+ | `reconnect.enabled` | `true` | Enable automatic reconnection. |
217
+ | `reconnect.maxAttempts` | `10` | Maximum reconnection attempts before giving up. |
218
+ | `reconnect.baseDelay` | `1000` | Base delay in ms for exponential backoff. |
219
+ | `reconnect.maxDelay` | `30000` | Maximum delay cap in ms. |
220
+ | `keepaliveInterval` | `30000` | Interval in ms for keepalive ping frames. |
221
+ | `fragmentThreshold` | `102400` | Payload size threshold for fragmentation (bytes). |
222
+ | `headers` | — | Upgrade headers (`ServiceWebsocketClientOptions` only). |
223
+
224
+ ### Server Options
225
+
226
+ | Option | Default | Description |
227
+ |--------|---------|-------------|
228
+ | `fragmentThreshold` | `102400` | Payload size threshold for fragmentation (bytes). |
229
+
230
+ ### Fragment Thresholds by Environment
231
+
232
+ | Environment | Recommended threshold | Reason |
233
+ |-------------|----------------------|--------|
234
+ | AWS API Gateway | `100KB` (default) | 128KB frame limit |
235
+ | Cloudflare | `500KB` | 1MB frame limit |
236
+ | Self-hosted | `0` (disabled) | No infrastructure limits |
237
+
238
+ ### Keepalive
239
+
240
+ 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.
241
+
242
+ ## Peer Dependencies
243
+
244
+ ```/dev/null/package.json#L1-4
245
+ {
246
+ "peerDependencies": {
247
+ "@kyneta/exchange": ">=0.0.1",
248
+ "@kyneta/wire": ">=0.0.1"
249
+ }
250
+ }
251
+ ```
252
+
253
+ ## License
254
+
255
+ MIT
package/dist/bun.d.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { ServerWebSocket } from 'bun';
2
+ import { S as Socket } from './types-DG_89zA4.js';
3
+ export { D as DisconnectReason, a as SocketReadyState } from './types-DG_89zA4.js';
4
+ import '@kyneta/exchange';
5
+
6
+ /**
7
+ * Data structure stored in `ws.data` for handler callbacks.
8
+ * Use this type when defining your `Bun.serve()` generic.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * Bun.serve<BunWebsocketData>({
13
+ * websocket: { ... }
14
+ * })
15
+ * ```
16
+ */
17
+ type BunWebsocketData = {
18
+ handlers: {
19
+ onMessage?: (data: Uint8Array | string) => void;
20
+ onClose?: (code: number, reason: string) => void;
21
+ };
22
+ };
23
+ /**
24
+ * Wrap Bun's `ServerWebSocket` to match the `Socket` interface.
25
+ *
26
+ * Bun's WebSocket API uses server-level callbacks (`websocket: { message, close }`)
27
+ * rather than per-socket event handlers. This wrapper bridges that gap by
28
+ * storing handlers in `ws.data` and having the server-level callbacks delegate
29
+ * to them.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * import { WebsocketServerTransport } from "@kyneta/websocket-network-adapter/server"
34
+ * import { wrapBunWebsocket, type BunWebsocketData } from "@kyneta/websocket-network-adapter/bun"
35
+ *
36
+ * const serverAdapter = new WebsocketServerTransport()
37
+ *
38
+ * Bun.serve<BunWebsocketData>({
39
+ * websocket: {
40
+ * open(ws) {
41
+ * const socket = wrapBunWebsocket(ws)
42
+ * serverAdapter.handleConnection({ socket }).start()
43
+ * },
44
+ * message(ws, msg) {
45
+ * const data = msg instanceof ArrayBuffer ? new Uint8Array(msg) : msg
46
+ * ws.data?.handlers?.onMessage?.(data)
47
+ * },
48
+ * close(ws, code, reason) {
49
+ * ws.data?.handlers?.onClose?.(code, reason)
50
+ * },
51
+ * },
52
+ * })
53
+ * ```
54
+ */
55
+ declare function wrapBunWebsocket(ws: ServerWebSocket<BunWebsocketData>): Socket;
56
+ /**
57
+ * Create Bun Websocket handlers that integrate with `WebsocketServerTransport`.
58
+ *
59
+ * This helper eliminates boilerplate by providing pre-configured handlers
60
+ * for `open`, `message`, and `close` events that automatically wire up
61
+ * to the adapter's `handleConnection()` method.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * import { WebsocketServerTransport } from "@kyneta/websocket-network-adapter/server"
66
+ * import { createBunWebsocketHandlers, type BunWebsocketData } from "@kyneta/websocket-network-adapter/bun"
67
+ *
68
+ * const serverAdapter = new WebsocketServerTransport()
69
+ *
70
+ * Bun.serve<BunWebsocketData>({
71
+ * fetch(req, server) {
72
+ * server.upgrade(req)
73
+ * return new Response("upgrade failed", { status: 400 })
74
+ * },
75
+ * websocket: createBunWebsocketHandlers(serverAdapter),
76
+ * })
77
+ * ```
78
+ */
79
+ declare function createBunWebsocketHandlers(wsAdapter: {
80
+ handleConnection: (opts: {
81
+ socket: Socket;
82
+ }) => {
83
+ start: () => void;
84
+ };
85
+ }): {
86
+ open(ws: ServerWebSocket<BunWebsocketData>): void;
87
+ message(ws: ServerWebSocket<BunWebsocketData>, msg: string | ArrayBuffer | Buffer): void;
88
+ close(ws: ServerWebSocket<BunWebsocketData>, code: number, reason: string): void;
89
+ };
90
+
91
+ export { type BunWebsocketData, Socket, createBunWebsocketHandlers, wrapBunWebsocket };
package/dist/bun.js ADDED
@@ -0,0 +1,48 @@
1
+ // src/bun-websocket.ts
2
+ function wrapBunWebsocket(ws) {
3
+ ws.data = { handlers: {} };
4
+ return {
5
+ send(data) {
6
+ ws.send(data);
7
+ },
8
+ close(code, reason) {
9
+ ws.close(code, reason);
10
+ },
11
+ onMessage(handler) {
12
+ ws.data.handlers.onMessage = handler;
13
+ },
14
+ onClose(handler) {
15
+ ws.data.handlers.onClose = handler;
16
+ },
17
+ onError(_handler) {
18
+ },
19
+ get readyState() {
20
+ const states = [
21
+ "connecting",
22
+ "open",
23
+ "closing",
24
+ "closed"
25
+ ];
26
+ return states[ws.readyState] ?? "closed";
27
+ }
28
+ };
29
+ }
30
+ function createBunWebsocketHandlers(wsAdapter) {
31
+ return {
32
+ open(ws) {
33
+ wsAdapter.handleConnection({ socket: wrapBunWebsocket(ws) }).start();
34
+ },
35
+ message(ws, msg) {
36
+ const data = msg instanceof ArrayBuffer ? new Uint8Array(msg) : Buffer.isBuffer(msg) ? new Uint8Array(msg) : msg;
37
+ ws.data.handlers.onMessage?.(data);
38
+ },
39
+ close(ws, code, reason) {
40
+ ws.data.handlers.onClose?.(code, reason);
41
+ }
42
+ };
43
+ }
44
+ export {
45
+ createBunWebsocketHandlers,
46
+ wrapBunWebsocket
47
+ };
48
+ //# sourceMappingURL=bun.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/bun-websocket.ts"],"sourcesContent":["// bun-websocket — Bun-specific Websocket wrapper for @kyneta/websocket-network-adapter.\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-network-adapter/server\"\n * import { wrapBunWebsocket, type BunWebsocketData } from \"@kyneta/websocket-network-adapter/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-network-adapter/server\"\n * import { createBunWebsocketHandlers, type BunWebsocketData } from \"@kyneta/websocket-network-adapter/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":[]}
@@ -0,0 +1,109 @@
1
+ // src/types.ts
2
+ function wrapStandardWebsocket(ws) {
3
+ return {
4
+ send(data) {
5
+ ws.send(data);
6
+ },
7
+ close(code, reason) {
8
+ ws.close(code, reason);
9
+ },
10
+ onMessage(handler) {
11
+ ws.addEventListener("message", (event) => {
12
+ if (event.data instanceof ArrayBuffer) {
13
+ handler(new Uint8Array(event.data));
14
+ } else if (typeof Blob !== "undefined" && event.data instanceof Blob) {
15
+ event.data.arrayBuffer().then((buffer) => {
16
+ handler(new Uint8Array(buffer));
17
+ });
18
+ } else {
19
+ handler(event.data);
20
+ }
21
+ });
22
+ },
23
+ onClose(handler) {
24
+ ws.addEventListener("close", (event) => {
25
+ handler(event.code, event.reason);
26
+ });
27
+ },
28
+ onError(handler) {
29
+ ws.addEventListener("error", (_event) => {
30
+ handler(new Error("WebSocket error"));
31
+ });
32
+ },
33
+ get readyState() {
34
+ switch (ws.readyState) {
35
+ case WebSocket.CONNECTING:
36
+ return "connecting";
37
+ case WebSocket.OPEN:
38
+ return "open";
39
+ case WebSocket.CLOSING:
40
+ return "closing";
41
+ case WebSocket.CLOSED:
42
+ return "closed";
43
+ default:
44
+ return "closed";
45
+ }
46
+ }
47
+ };
48
+ }
49
+ function wrapNodeWebsocket(ws) {
50
+ const CONNECTING = 0;
51
+ const OPEN = 1;
52
+ const CLOSING = 2;
53
+ return {
54
+ send(data) {
55
+ ws.send(data);
56
+ },
57
+ close(code, reason) {
58
+ ws.close(code, reason);
59
+ },
60
+ onMessage(handler) {
61
+ ws.on(
62
+ "message",
63
+ (data, isBinary) => {
64
+ if (isBinary) {
65
+ if (data instanceof ArrayBuffer) {
66
+ handler(new Uint8Array(data));
67
+ } else if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
68
+ handler(new Uint8Array(data));
69
+ } else {
70
+ handler(new Uint8Array(data));
71
+ }
72
+ } else {
73
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
74
+ handler(data.toString("utf-8"));
75
+ } else {
76
+ handler(data);
77
+ }
78
+ }
79
+ }
80
+ );
81
+ },
82
+ onClose(handler) {
83
+ ws.on("close", (code, reason) => {
84
+ handler(code, reason.toString());
85
+ });
86
+ },
87
+ onError(handler) {
88
+ ws.on("error", handler);
89
+ },
90
+ get readyState() {
91
+ switch (ws.readyState) {
92
+ case CONNECTING:
93
+ return "connecting";
94
+ case OPEN:
95
+ return "open";
96
+ case CLOSING:
97
+ return "closing";
98
+ default:
99
+ return "closed";
100
+ }
101
+ }
102
+ };
103
+ }
104
+
105
+ export {
106
+ wrapStandardWebsocket,
107
+ wrapNodeWebsocket
108
+ };
109
+ //# sourceMappingURL=chunk-5FHT54WT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["// types — framework-agnostic Websocket abstractions for @kyneta/websocket-network-adapter.\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/exchange\"\n\n// ---------------------------------------------------------------------------\n// Socket ready states\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(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.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 WebSocket.CONNECTING:\n return \"connecting\"\n case WebSocket.OPEN:\n return \"open\"\n case WebSocket.CLOSING:\n return \"closing\"\n case WebSocket.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"],"mappings":";AAsKO,SAAS,sBAAsB,IAAuB;AAC3D,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,iBAAiB,WAAW,WAAS;AACtC,YAAI,MAAM,gBAAgB,aAAa;AACrC,kBAAQ,IAAI,WAAW,MAAM,IAAI,CAAC;AAAA,QACpC,WAAW,OAAO,SAAS,eAAe,MAAM,gBAAgB,MAAM;AAEpE,gBAAM,KAAK,YAAY,EAAE,KAAK,YAAU;AACtC,oBAAQ,IAAI,WAAW,MAAM,CAAC;AAAA,UAChC,CAAC;AAAA,QACH,OAAO;AACL,kBAAQ,MAAM,IAAc;AAAA,QAC9B;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IAEA,QAAQ,SAAuD;AAC7D,SAAG,iBAAiB,SAAS,WAAS;AACpC,gBAAQ,MAAM,MAAM,MAAM,MAAM;AAAA,MAClC,CAAC;AAAA,IACH;AAAA,IAEA,QAAQ,SAAuC;AAC7C,SAAG,iBAAiB,SAAS,YAAU;AACrC,gBAAQ,IAAI,MAAM,iBAAiB,CAAC;AAAA,MACtC,CAAC;AAAA,IACH;AAAA,IAEA,IAAI,aAA+B;AACjC,cAAQ,GAAG,YAAY;AAAA,QACrB,KAAK,UAAU;AACb,iBAAO;AAAA,QACT,KAAK,UAAU;AACb,iBAAO;AAAA,QACT,KAAK,UAAU;AACb,iBAAO;AAAA,QACT,KAAK,UAAU;AACb,iBAAO;AAAA,QACT;AACE,iBAAO;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACF;AA6BO,SAAS,kBAAkB,IAA+B;AAC/D,QAAM,aAAa;AACnB,QAAM,OAAO;AACb,QAAM,UAAU;AAEhB,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;AAAA,QACD;AAAA,QACA,CAAC,MAAqC,aAAsB;AAC1D,cAAI,UAAU;AACZ,gBAAI,gBAAgB,aAAa;AAC/B,sBAAQ,IAAI,WAAW,IAAI,CAAC;AAAA,YAC9B,WAAW,OAAO,WAAW,eAAe,OAAO,SAAS,IAAI,GAAG;AACjE,sBAAQ,IAAI,WAAW,IAAI,CAAC;AAAA,YAC9B,OAAO;AACL,sBAAQ,IAAI,WAAW,IAA8B,CAAC;AAAA,YACxD;AAAA,UACF,OAAO;AACL,gBAAI,OAAO,WAAW,eAAe,OAAO,SAAS,IAAI,GAAG;AAC1D,sBAAQ,KAAK,SAAS,OAAO,CAAC;AAAA,YAChC,OAAO;AACL,sBAAQ,IAAc;AAAA,YACxB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IAEA,QAAQ,SAAuD;AAC7D,SAAG,GAAG,SAAS,CAAC,MAAc,WAAmB;AAC/C,gBAAQ,MAAM,OAAO,SAAS,CAAC;AAAA,MACjC,CAAC;AAAA,IACH;AAAA,IAEA,QAAQ,SAAuC;AAC7C,SAAG,GAAG,SAAS,OAAO;AAAA,IACxB;AAAA,IAEA,IAAI,aAA+B;AACjC,cAAQ,GAAG,YAAY;AAAA,QACrB,KAAK;AACH,iBAAO;AAAA,QACT,KAAK;AACH,iBAAO;AAAA,QACT,KAAK;AACH,iBAAO;AAAA,QACT;AACE,iBAAO;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACF;","names":[]}