@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/src/types.ts ADDED
@@ -0,0 +1,308 @@
1
+ // types — framework-agnostic Websocket abstractions for @kyneta/websocket-network-adapter.
2
+ //
3
+ // The `Socket` interface decouples the adapter from any specific Websocket
4
+ // library (browser WebSocket, Node `ws`, Bun ServerWebSocket). Platform-
5
+ // specific wrappers (`wrapStandardWebsocket`, `wrapNodeWebsocket`,
6
+ // `wrapBunWebsocket`) adapt concrete implementations to this interface.
7
+ //
8
+ // Ported from @loro-extended/adapter-websocket's WsSocket with kyneta
9
+ // naming conventions applied.
10
+
11
+ import type {
12
+ TransitionListener as GenericTransitionListener,
13
+ PeerId,
14
+ StateTransition,
15
+ } from "@kyneta/exchange"
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Socket ready states
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Websocket ready states — mirrors the standard WebSocket readyState
23
+ * values as human-readable strings.
24
+ */
25
+ export type SocketReadyState = "connecting" | "open" | "closing" | "closed"
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Socket interface
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /**
32
+ * Framework-agnostic Websocket interface.
33
+ *
34
+ * This allows the adapter to work with any Websocket library:
35
+ * - Browser `WebSocket` via `wrapStandardWebsocket()`
36
+ * - Node.js `ws` library via `wrapNodeWebsocket()`
37
+ * - Bun `ServerWebSocket` via `wrapBunWebsocket()`
38
+ *
39
+ * The interface is intentionally minimal — only the operations the
40
+ * adapter needs are exposed.
41
+ */
42
+ export interface Socket {
43
+ /** Send binary or text data through the Websocket. */
44
+ send(data: Uint8Array | string): void
45
+
46
+ /** Close the Websocket connection. */
47
+ close(code?: number, reason?: string): void
48
+
49
+ /** Register a handler for incoming messages (binary or text). */
50
+ onMessage(handler: (data: Uint8Array | string) => void): void
51
+
52
+ /** Register a handler for connection close. */
53
+ onClose(handler: (code: number, reason: string) => void): void
54
+
55
+ /** Register a handler for errors. */
56
+ onError(handler: (error: Error) => void): void
57
+
58
+ /** The current ready state of the Websocket. */
59
+ readonly readyState: SocketReadyState
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Connection types — used by server adapter
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /**
67
+ * Options for handling a new Websocket connection on the server.
68
+ */
69
+ export interface WebsocketConnectionOptions {
70
+ /** The Websocket instance, wrapped in the Socket interface. */
71
+ socket: Socket
72
+
73
+ /** Optional peer ID extracted from the upgrade request. */
74
+ peerId?: PeerId
75
+
76
+ /** Optional authentication token from the upgrade request. */
77
+ authToken?: string
78
+ }
79
+
80
+ /**
81
+ * Handle for an active Websocket connection.
82
+ */
83
+ export interface WebsocketConnectionHandle {
84
+ /** The peer ID for this connection. */
85
+ readonly peerId: PeerId
86
+
87
+ /** The channel ID for this connection. */
88
+ readonly channelId: number
89
+
90
+ /** Close the connection. */
91
+ close(code?: number, reason?: string): void
92
+ }
93
+
94
+ /**
95
+ * Result of handling a Websocket connection on the server.
96
+ */
97
+ export interface WebsocketConnectionResult {
98
+ /** The connection handle for managing this peer. */
99
+ connection: WebsocketConnectionHandle
100
+
101
+ /** Call this to start processing messages. */
102
+ start(): void
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Disconnect reason
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /**
110
+ * Discriminated union describing why a Websocket connection was lost.
111
+ */
112
+ export type DisconnectReason =
113
+ | { type: "intentional" }
114
+ | { type: "error"; error: Error }
115
+ | { type: "closed"; code: number; reason: string }
116
+ | { type: "max-retries-exceeded"; attempts: number }
117
+ | { type: "not-started" }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Connection state (for client adapter observability)
121
+ // ---------------------------------------------------------------------------
122
+
123
+ /**
124
+ * All possible states of the Websocket client.
125
+ *
126
+ * State machine transitions:
127
+ * ```
128
+ * disconnected → connecting → connected → ready
129
+ * ↓ ↓ ↓
130
+ * reconnecting ← ─ ┴ ─ ─ ─ ─ ┘
131
+ * ↓
132
+ * connecting (retry)
133
+ * ↓
134
+ * disconnected (max retries)
135
+ * ```
136
+ */
137
+ export type WebsocketClientState =
138
+ | { status: "disconnected"; reason?: DisconnectReason }
139
+ | { status: "connecting"; attempt: number }
140
+ | { status: "connected" }
141
+ | { status: "ready" }
142
+ | { status: "reconnecting"; attempt: number; nextAttemptMs: number }
143
+
144
+ /**
145
+ * A state transition event for websocket client states.
146
+ * Specialized from the generic `StateTransition<S>`.
147
+ */
148
+ export type WebsocketClientStateTransition =
149
+ StateTransition<WebsocketClientState>
150
+
151
+ /**
152
+ * Listener for websocket client state transitions.
153
+ * Specialized from the generic `TransitionListener<S>`.
154
+ */
155
+ export type TransitionListener = GenericTransitionListener<WebsocketClientState>
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Socket wrapper — standard WebSocket API (browser + Node ws)
159
+ // ---------------------------------------------------------------------------
160
+
161
+ /**
162
+ * Wrap a standard `WebSocket` (browser or Node.js `ws` via `ws` package
163
+ * in `WebSocket`-compatible mode) into the `Socket` interface.
164
+ *
165
+ * Handles `ArrayBuffer`, `Blob`, and string messages.
166
+ */
167
+ export function wrapStandardWebsocket(ws: WebSocket): Socket {
168
+ return {
169
+ send(data: Uint8Array | string): void {
170
+ ws.send(data)
171
+ },
172
+
173
+ close(code?: number, reason?: string): void {
174
+ ws.close(code, reason)
175
+ },
176
+
177
+ onMessage(handler: (data: Uint8Array | string) => void): void {
178
+ ws.addEventListener("message", event => {
179
+ if (event.data instanceof ArrayBuffer) {
180
+ handler(new Uint8Array(event.data))
181
+ } else if (typeof Blob !== "undefined" && event.data instanceof Blob) {
182
+ // Handle Blob data (browser)
183
+ event.data.arrayBuffer().then(buffer => {
184
+ handler(new Uint8Array(buffer))
185
+ })
186
+ } else {
187
+ handler(event.data as string)
188
+ }
189
+ })
190
+ },
191
+
192
+ onClose(handler: (code: number, reason: string) => void): void {
193
+ ws.addEventListener("close", event => {
194
+ handler(event.code, event.reason)
195
+ })
196
+ },
197
+
198
+ onError(handler: (error: Error) => void): void {
199
+ ws.addEventListener("error", _event => {
200
+ handler(new Error("WebSocket error"))
201
+ })
202
+ },
203
+
204
+ get readyState(): SocketReadyState {
205
+ switch (ws.readyState) {
206
+ case WebSocket.CONNECTING:
207
+ return "connecting"
208
+ case WebSocket.OPEN:
209
+ return "open"
210
+ case WebSocket.CLOSING:
211
+ return "closing"
212
+ case WebSocket.CLOSED:
213
+ return "closed"
214
+ default:
215
+ return "closed"
216
+ }
217
+ },
218
+ }
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Socket wrapper — Node.js `ws` library (raw API, not WebSocket-compat)
223
+ // ---------------------------------------------------------------------------
224
+
225
+ /**
226
+ * The minimal interface we need from the Node.js `ws` library's `WebSocket`.
227
+ *
228
+ * Using a structural type rather than importing `ws` — consumers provide
229
+ * the actual `ws` instance, we just need these methods.
230
+ */
231
+ export interface NodeWebsocketLike {
232
+ send(data: Uint8Array | string): void
233
+ close(code?: number, reason?: string): void
234
+ on(
235
+ event: "message",
236
+ handler: (data: Buffer | ArrayBuffer | string, isBinary: boolean) => void,
237
+ ): void
238
+ on(event: "close", handler: (code: number, reason: Buffer) => void): void
239
+ on(event: "error", handler: (error: Error) => void): void
240
+ readyState: number
241
+ }
242
+
243
+ /**
244
+ * Wrap a Node.js `ws` library WebSocket into the `Socket` interface.
245
+ *
246
+ * Handles `Buffer` → `Uint8Array` conversion for binary messages.
247
+ */
248
+ export function wrapNodeWebsocket(ws: NodeWebsocketLike): Socket {
249
+ const CONNECTING = 0
250
+ const OPEN = 1
251
+ const CLOSING = 2
252
+
253
+ return {
254
+ send(data: Uint8Array | string): void {
255
+ ws.send(data)
256
+ },
257
+
258
+ close(code?: number, reason?: string): void {
259
+ ws.close(code, reason)
260
+ },
261
+
262
+ onMessage(handler: (data: Uint8Array | string) => void): void {
263
+ ws.on(
264
+ "message",
265
+ (data: Buffer | ArrayBuffer | string, isBinary: boolean) => {
266
+ if (isBinary) {
267
+ if (data instanceof ArrayBuffer) {
268
+ handler(new Uint8Array(data))
269
+ } else if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
270
+ handler(new Uint8Array(data))
271
+ } else {
272
+ handler(new Uint8Array(data as unknown as ArrayBuffer))
273
+ }
274
+ } else {
275
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
276
+ handler(data.toString("utf-8"))
277
+ } else {
278
+ handler(data as string)
279
+ }
280
+ }
281
+ },
282
+ )
283
+ },
284
+
285
+ onClose(handler: (code: number, reason: string) => void): void {
286
+ ws.on("close", (code: number, reason: Buffer) => {
287
+ handler(code, reason.toString())
288
+ })
289
+ },
290
+
291
+ onError(handler: (error: Error) => void): void {
292
+ ws.on("error", handler)
293
+ },
294
+
295
+ get readyState(): SocketReadyState {
296
+ switch (ws.readyState) {
297
+ case CONNECTING:
298
+ return "connecting"
299
+ case OPEN:
300
+ return "open"
301
+ case CLOSING:
302
+ return "closing"
303
+ default:
304
+ return "closed"
305
+ }
306
+ },
307
+ }
308
+ }