@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/src/connection.ts CHANGED
@@ -12,8 +12,13 @@
12
12
 
13
13
  import type { Channel, ChannelMsg, PeerId } from "@kyneta/transport"
14
14
  import {
15
- decodeBinaryMessages,
16
- encodeBinaryAndSend,
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: string) => {
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
- * Encodes via CBOR codec → frame → fragment if needed → socket.send().
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
- encodeBinaryAndSend(msg, this.#fragmentThreshold, data =>
127
- this.#socket.send(data),
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-request after receiving this.
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
- // Handle binary protocol messages through shared decode pipeline
184
+ // Binary path: reassemble wire alias transformer → ChannelMsg.
168
185
  try {
169
- const messages = decodeBinaryMessages(data, this.#reassembler)
170
- if (messages) {
171
- for (const msg of messages) {
172
- this.#handleChannelMessage(msg)
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)
@@ -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-request` (protocol-level)
92
- * 3. Server responds with `establish-response` (handled by Synchronizer)
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-request to avoid a race condition where the binary
96
- * establish-request could arrive before the client has processed "ready".
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 ?? generatePeerId()
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-request after receiving "ready".
207
+ // The client will send establish after receiving "ready".
223
208
  // Our channel gets established when the Synchronizer receives
224
- // and processes that establish-request.
209
+ // and processes that establish message.
225
210
  //
226
- // This prevents a race condition where our binary establish-request
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
@@ -50,4 +50,8 @@ export type {
50
50
  WebsocketConnectionResult,
51
51
  } from "./types.js"
52
52
 
53
- export { READY_STATE, wrapNodeWebsocket, wrapStandardWebsocket } from "./types.js"
53
+ export {
54
+ READY_STATE,
55
+ wrapNodeWebsocket,
56
+ wrapStandardWebsocket,
57
+ } from "./types.js"
@@ -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 | ArrayBufferLike | Uint8Array): void
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
- /** Send binary or text data through the Websocket. */
112
- send(data: Uint8Array | string): void
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) => {
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}