@kyneta/websocket-transport 1.3.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.ts +31 -30
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +2 -15
- package/dist/bun.d.ts +18 -18
- package/dist/bun.d.ts.map +1 -0
- package/dist/bun.js +95 -43
- package/dist/bun.js.map +1 -1
- package/dist/client-transport-B2V7s2VP.js +525 -0
- package/dist/client-transport-B2V7s2VP.js.map +1 -0
- package/dist/client-transport-D3tYQYrS.d.ts +134 -0
- package/dist/client-transport-D3tYQYrS.d.ts.map +1 -0
- package/dist/server.d.ts +121 -118
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +304 -305
- package/dist/server.js.map +1 -1
- package/dist/{types-D0lbeevu.d.ts → types-c1S_xIRG.d.ts} +78 -69
- package/dist/types-c1S_xIRG.d.ts.map +1 -0
- package/package.json +13 -12
- package/src/__tests__/client-transport.test.ts +8 -9
- package/src/browser.ts +3 -3
- package/src/bun-websocket.ts +3 -3
- package/src/client-transport.ts +42 -17
- package/src/connection.ts +38 -13
- package/src/server-transport.ts +9 -24
- package/src/server.ts +5 -1
- package/src/service-client.ts +2 -2
- package/src/types.ts +17 -12
- package/dist/browser.js.map +0 -1
- package/dist/chunk-YZQF5RLV.js +0 -614
- package/dist/chunk-YZQF5RLV.js.map +0 -1
- package/dist/client-transport-DUAFjVbh.d.ts +0 -132
package/src/connection.ts
CHANGED
|
@@ -12,8 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
import type { Channel, ChannelMsg, PeerId } from "@kyneta/transport"
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
type AliasState,
|
|
16
|
+
applyInboundAliasing,
|
|
17
|
+
applyOutboundAliasing,
|
|
18
|
+
createFrameIdCounter,
|
|
19
|
+
decodeBinaryWires,
|
|
20
|
+
emptyAliasState,
|
|
21
|
+
encodeWireFrameAndSend,
|
|
17
22
|
FragmentReassembler,
|
|
18
23
|
} from "@kyneta/wire"
|
|
19
24
|
import type { Socket } from "./types.js"
|
|
@@ -57,6 +62,11 @@ export class WebsocketConnection {
|
|
|
57
62
|
// Fragmentation support
|
|
58
63
|
readonly #fragmentThreshold: number
|
|
59
64
|
readonly #reassembler: FragmentReassembler
|
|
65
|
+
#nextFrameId = createFrameIdCounter()
|
|
66
|
+
|
|
67
|
+
// Per-channel alias state (Phase 4). Captures features from establish
|
|
68
|
+
// messages flowing through; gates dx/shx emissions on mutualAlias.
|
|
69
|
+
#aliasState: AliasState = emptyAliasState()
|
|
60
70
|
|
|
61
71
|
constructor(
|
|
62
72
|
peerId: PeerId,
|
|
@@ -71,7 +81,7 @@ export class WebsocketConnection {
|
|
|
71
81
|
config?.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD
|
|
72
82
|
this.#reassembler = new FragmentReassembler({
|
|
73
83
|
timeoutMs: 10_000,
|
|
74
|
-
onTimeout: (frameId:
|
|
84
|
+
onTimeout: (frameId: number) => {
|
|
75
85
|
console.warn(
|
|
76
86
|
`[WebsocketConnection] Fragment batch timed out: ${frameId}`,
|
|
77
87
|
)
|
|
@@ -116,15 +126,22 @@ export class WebsocketConnection {
|
|
|
116
126
|
/**
|
|
117
127
|
* Send a ChannelMsg through the Websocket.
|
|
118
128
|
*
|
|
119
|
-
*
|
|
129
|
+
* Pipeline: alias transformer → wire encode → frame → fragment if needed
|
|
130
|
+
* → socket.send().
|
|
120
131
|
*/
|
|
121
132
|
send(msg: ChannelMsg): void {
|
|
122
133
|
if (this.#socket.readyState !== "open") {
|
|
123
134
|
return
|
|
124
135
|
}
|
|
125
136
|
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
const { state, wire } = applyOutboundAliasing(this.#aliasState, msg)
|
|
138
|
+
this.#aliasState = state
|
|
139
|
+
|
|
140
|
+
encodeWireFrameAndSend(
|
|
141
|
+
wire,
|
|
142
|
+
data => this.#socket.send(data),
|
|
143
|
+
this.#fragmentThreshold,
|
|
144
|
+
this.#nextFrameId,
|
|
128
145
|
)
|
|
129
146
|
}
|
|
130
147
|
|
|
@@ -133,7 +150,7 @@ export class WebsocketConnection {
|
|
|
133
150
|
*
|
|
134
151
|
* This is a transport-level text message that tells the client the
|
|
135
152
|
* server is ready to receive protocol messages. The client creates
|
|
136
|
-
* its channel and sends establish
|
|
153
|
+
* its channel and sends establish after receiving this.
|
|
137
154
|
*/
|
|
138
155
|
sendReady(): void {
|
|
139
156
|
if (this.#socket.readyState !== "open") {
|
|
@@ -157,20 +174,28 @@ export class WebsocketConnection {
|
|
|
157
174
|
/**
|
|
158
175
|
* Handle an incoming message from the Websocket.
|
|
159
176
|
*/
|
|
160
|
-
#handleMessage(data: Uint8Array | string): void {
|
|
177
|
+
#handleMessage(data: Uint8Array<ArrayBuffer> | string): void {
|
|
161
178
|
// Handle keepalive ping/pong (text frames)
|
|
162
179
|
if (typeof data === "string") {
|
|
163
180
|
this.#handleKeepalive(data)
|
|
164
181
|
return
|
|
165
182
|
}
|
|
166
183
|
|
|
167
|
-
//
|
|
184
|
+
// Binary path: reassemble → wire → alias transformer → ChannelMsg.
|
|
168
185
|
try {
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
186
|
+
const wires = decodeBinaryWires(data, this.#reassembler)
|
|
187
|
+
if (!wires) return
|
|
188
|
+
for (const wire of wires) {
|
|
189
|
+
const result = applyInboundAliasing(this.#aliasState, wire)
|
|
190
|
+
this.#aliasState = result.state
|
|
191
|
+
if (result.error || !result.msg) {
|
|
192
|
+
console.warn(
|
|
193
|
+
"[WebsocketConnection] alias resolution failed:",
|
|
194
|
+
result.error,
|
|
195
|
+
)
|
|
196
|
+
continue
|
|
173
197
|
}
|
|
198
|
+
this.#handleChannelMessage(result.msg)
|
|
174
199
|
}
|
|
175
200
|
} catch (error) {
|
|
176
201
|
console.error("Failed to decode wire message:", error)
|
package/src/server-transport.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// Ported from @loro-extended/adapter-websocket's WsServerNetworkAdapter with
|
|
29
29
|
// kyneta naming conventions and the kyneta 5-message protocol.
|
|
30
30
|
|
|
31
|
+
import { randomPeerId } from "@kyneta/random"
|
|
31
32
|
import type { ChannelMsg, GeneratedChannel, PeerId } from "@kyneta/transport"
|
|
32
33
|
import { Transport } from "@kyneta/transport"
|
|
33
34
|
import {
|
|
@@ -55,22 +56,6 @@ export interface WebsocketServerTransportOptions {
|
|
|
55
56
|
fragmentThreshold?: number
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// Peer ID generation
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Generate a random peer ID for connections that don't provide one.
|
|
64
|
-
*/
|
|
65
|
-
function generatePeerId(): PeerId {
|
|
66
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
67
|
-
let result = "ws-"
|
|
68
|
-
for (let i = 0; i < 12; i++) {
|
|
69
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
70
|
-
}
|
|
71
|
-
return result
|
|
72
|
-
}
|
|
73
|
-
|
|
74
59
|
// ---------------------------------------------------------------------------
|
|
75
60
|
// WebsocketServerTransport
|
|
76
61
|
// ---------------------------------------------------------------------------
|
|
@@ -88,12 +73,12 @@ function generatePeerId(): PeerId {
|
|
|
88
73
|
*
|
|
89
74
|
* The connection handshake follows a two-phase protocol:
|
|
90
75
|
* 1. Server sends text `"ready"` signal (transport-level)
|
|
91
|
-
* 2. Client sends `establish
|
|
92
|
-
* 3. Server
|
|
76
|
+
* 2. Client sends `establish` (protocol-level)
|
|
77
|
+
* 3. Server upgrades channel and sends present (handled by Synchronizer)
|
|
93
78
|
*
|
|
94
79
|
* The server does NOT call `establishChannel()` — it waits for the
|
|
95
|
-
* client's establish
|
|
96
|
-
* establish
|
|
80
|
+
* client's establish to avoid a race condition where the binary
|
|
81
|
+
* establish could arrive before the client has processed "ready".
|
|
97
82
|
*/
|
|
98
83
|
export class WebsocketServerTransport extends Transport<PeerId> {
|
|
99
84
|
#connections = new Map<PeerId, WebsocketConnection>()
|
|
@@ -174,7 +159,7 @@ export class WebsocketServerTransport extends Transport<PeerId> {
|
|
|
174
159
|
const { socket, peerId: providedPeerId } = options
|
|
175
160
|
|
|
176
161
|
// Generate peer ID if not provided
|
|
177
|
-
const peerId = providedPeerId ??
|
|
162
|
+
const peerId = providedPeerId ?? (`ws-${randomPeerId()}` as PeerId)
|
|
178
163
|
|
|
179
164
|
// Check for existing connection with same peer ID
|
|
180
165
|
const existingConnection = this.#connections.get(peerId)
|
|
@@ -219,11 +204,11 @@ export class WebsocketServerTransport extends Transport<PeerId> {
|
|
|
219
204
|
connection.sendReady()
|
|
220
205
|
|
|
221
206
|
// NOTE: We do NOT call establishChannel() here.
|
|
222
|
-
// The client will send establish
|
|
207
|
+
// The client will send establish after receiving "ready".
|
|
223
208
|
// Our channel gets established when the Synchronizer receives
|
|
224
|
-
// and processes that establish
|
|
209
|
+
// and processes that establish message.
|
|
225
210
|
//
|
|
226
|
-
// This prevents a race condition where our binary establish
|
|
211
|
+
// This prevents a race condition where our binary establish
|
|
227
212
|
// could arrive before the client has processed "ready" and created
|
|
228
213
|
// its channel.
|
|
229
214
|
},
|
package/src/server.ts
CHANGED
package/src/service-client.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import type { TransportFactory } from "@kyneta/transport"
|
|
9
9
|
import {
|
|
10
|
-
WebsocketClientTransport,
|
|
11
10
|
type WebsocketClientOptions,
|
|
11
|
+
WebsocketClientTransport,
|
|
12
12
|
} from "./client-transport.js"
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -49,4 +49,4 @@ export function createServiceWebsocketClient(
|
|
|
49
49
|
options: ServiceWebsocketClientOptions,
|
|
50
50
|
): TransportFactory {
|
|
51
51
|
return () => new WebsocketClientTransport(options)
|
|
52
|
-
}
|
|
52
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -63,7 +63,7 @@ export interface WebSocketCloseEvent {
|
|
|
63
63
|
export interface WebSocketLike {
|
|
64
64
|
readonly readyState: number
|
|
65
65
|
binaryType: string
|
|
66
|
-
send(data: string |
|
|
66
|
+
send(data: string | ArrayBuffer): void
|
|
67
67
|
close(code?: number, reason?: string): void
|
|
68
68
|
addEventListener(type: string, listener: (event: any) => void): void
|
|
69
69
|
removeEventListener(type: string, listener: (event: any) => void): void
|
|
@@ -108,14 +108,21 @@ export type SocketReadyState = "connecting" | "open" | "closing" | "closed"
|
|
|
108
108
|
* adapter needs are exposed.
|
|
109
109
|
*/
|
|
110
110
|
export interface Socket {
|
|
111
|
-
/**
|
|
112
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Narrowed to `Uint8Array<ArrayBuffer>` because the strictest downstream
|
|
113
|
+
* runtimes reject `SharedArrayBuffer`-backed views: Bun's `BufferSource`
|
|
114
|
+
* resolves to `ArrayBufferView<ArrayBuffer> | ArrayBuffer`, and Hono's
|
|
115
|
+
* `WSContext.send` takes `Uint8Array<ArrayBuffer>` directly. The wire
|
|
116
|
+
* pipeline allocates with `new Uint8Array(n)`, so producers satisfy this
|
|
117
|
+
* without changes.
|
|
118
|
+
*/
|
|
119
|
+
send(data: Uint8Array<ArrayBuffer> | string): void
|
|
113
120
|
|
|
114
121
|
/** Close the Websocket connection. */
|
|
115
122
|
close(code?: number, reason?: string): void
|
|
116
123
|
|
|
117
124
|
/** Register a handler for incoming messages (binary or text). */
|
|
118
|
-
onMessage(handler: (data: Uint8Array | string) => void): void
|
|
125
|
+
onMessage(handler: (data: Uint8Array<ArrayBuffer> | string) => void): void
|
|
119
126
|
|
|
120
127
|
/** Register a handler for connection close. */
|
|
121
128
|
onClose(handler: (code: number, reason: string) => void): void
|
|
@@ -234,17 +241,15 @@ export type TransitionListener = GenericTransitionListener<WebsocketClientState>
|
|
|
234
241
|
*/
|
|
235
242
|
export function wrapStandardWebsocket(ws: WebSocket): Socket {
|
|
236
243
|
return {
|
|
237
|
-
send(data: Uint8Array | string): void {
|
|
238
|
-
ws.send(
|
|
239
|
-
typeof data === "string" ? data : (data as Uint8Array<ArrayBuffer>),
|
|
240
|
-
)
|
|
244
|
+
send(data: Uint8Array<ArrayBuffer> | string): void {
|
|
245
|
+
ws.send(data)
|
|
241
246
|
},
|
|
242
247
|
|
|
243
248
|
close(code?: number, reason?: string): void {
|
|
244
249
|
ws.close(code, reason)
|
|
245
250
|
},
|
|
246
251
|
|
|
247
|
-
onMessage(handler: (data: Uint8Array | string) => void): void {
|
|
252
|
+
onMessage(handler: (data: Uint8Array<ArrayBuffer> | string) => void): void {
|
|
248
253
|
ws.addEventListener("message", event => {
|
|
249
254
|
if (event.data instanceof ArrayBuffer) {
|
|
250
255
|
handler(new Uint8Array(event.data))
|
|
@@ -299,7 +304,7 @@ export function wrapStandardWebsocket(ws: WebSocket): Socket {
|
|
|
299
304
|
* the actual `ws` instance, we just need these methods.
|
|
300
305
|
*/
|
|
301
306
|
export interface NodeWebsocketLike {
|
|
302
|
-
send(data: Uint8Array | string): void
|
|
307
|
+
send(data: Uint8Array<ArrayBuffer> | string): void
|
|
303
308
|
close(code?: number, reason?: string): void
|
|
304
309
|
on(
|
|
305
310
|
event: "message",
|
|
@@ -321,7 +326,7 @@ export function wrapNodeWebsocket(ws: NodeWebsocketLike): Socket {
|
|
|
321
326
|
const CLOSING = 2
|
|
322
327
|
|
|
323
328
|
return {
|
|
324
|
-
send(data: Uint8Array | string): void {
|
|
329
|
+
send(data: Uint8Array<ArrayBuffer> | string): void {
|
|
325
330
|
ws.send(data)
|
|
326
331
|
},
|
|
327
332
|
|
|
@@ -329,7 +334,7 @@ export function wrapNodeWebsocket(ws: NodeWebsocketLike): Socket {
|
|
|
329
334
|
ws.close(code, reason)
|
|
330
335
|
},
|
|
331
336
|
|
|
332
|
-
onMessage(handler: (data: Uint8Array | string) => void): void {
|
|
337
|
+
onMessage(handler: (data: Uint8Array<ArrayBuffer> | string) => void): void {
|
|
333
338
|
ws.on(
|
|
334
339
|
"message",
|
|
335
340
|
(data: Buffer | ArrayBuffer | string, isBinary: boolean) => {
|
package/dist/browser.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|