@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 +21 -0
- package/README.md +255 -0
- package/dist/bun.d.ts +91 -0
- package/dist/bun.js +48 -0
- package/dist/bun.js.map +1 -0
- package/dist/chunk-5FHT54WT.js +109 -0
- package/dist/chunk-5FHT54WT.js.map +1 -0
- package/dist/client.d.ts +185 -0
- package/dist/client.js +418 -0
- package/dist/client.js.map +1 -0
- package/dist/server.d.ts +161 -0
- package/dist/server.js +315 -0
- package/dist/server.js.map +1 -0
- package/dist/types-DG_89zA4.d.ts +149 -0
- package/package.json +54 -0
- package/src/__tests__/client-state-machine.test.ts +472 -0
- package/src/bun-websocket.ts +163 -0
- package/src/bun.ts +24 -0
- package/src/client-state-machine.ts +78 -0
- package/src/client-transport.ts +711 -0
- package/src/client.ts +39 -0
- package/src/connection.ts +224 -0
- package/src/server-transport.ts +282 -0
- package/src/server.ts +39 -0
- package/src/types.ts +308 -0
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
|
+
}
|