@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/server.js CHANGED
@@ -1,313 +1,312 @@
1
- import {
2
- READY_STATE,
3
- WebsocketClientTransport,
4
- wrapNodeWebsocket,
5
- wrapStandardWebsocket
6
- } from "./chunk-YZQF5RLV.js";
7
-
8
- // src/server-transport.ts
1
+ import { a as wrapNodeWebsocket, i as READY_STATE, n as WebsocketClientTransport, o as wrapStandardWebsocket } from "./client-transport-B2V7s2VP.js";
9
2
  import { Transport } from "@kyneta/transport";
10
-
11
- // src/connection.ts
12
- import {
13
- decodeBinaryMessages,
14
- encodeBinaryAndSend,
15
- FragmentReassembler
16
- } from "@kyneta/wire";
17
- var DEFAULT_FRAGMENT_THRESHOLD = 100 * 1024;
3
+ import { FragmentReassembler, applyInboundAliasing, applyOutboundAliasing, createFrameIdCounter, decodeBinaryWires, emptyAliasState, encodeWireFrameAndSend } from "@kyneta/wire";
4
+ import { randomPeerId } from "@kyneta/random";
5
+ //#region src/connection.ts
6
+ /**
7
+ * Default fragment threshold in bytes.
8
+ * Messages larger than this are fragmented for cloud infrastructure compatibility.
9
+ * AWS API Gateway has a 128KB limit, so 100KB provides a safe margin.
10
+ */
11
+ const DEFAULT_FRAGMENT_THRESHOLD = 100 * 1024;
12
+ /**
13
+ * Represents a single Websocket connection to a peer (server-side).
14
+ *
15
+ * Manages encoding, framing, fragmentation, and reassembly for one
16
+ * connected client. Created by `WebsocketServerTransport.handleConnection()`.
17
+ *
18
+ * The connection uses the CBOR codec for binary transport — this is
19
+ * the natural choice for Websocket's binary frame support.
20
+ */
18
21
  var WebsocketConnection = class {
19
- peerId;
20
- channelId;
21
- #socket;
22
- #channel = null;
23
- #started = false;
24
- // Fragmentation support
25
- #fragmentThreshold;
26
- #reassembler;
27
- constructor(peerId, channelId, socket, config) {
28
- this.peerId = peerId;
29
- this.channelId = channelId;
30
- this.#socket = socket;
31
- this.#fragmentThreshold = config?.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD;
32
- this.#reassembler = new FragmentReassembler({
33
- timeoutMs: 1e4,
34
- onTimeout: (frameId) => {
35
- console.warn(
36
- `[WebsocketConnection] Fragment batch timed out: ${frameId}`
37
- );
38
- }
39
- });
40
- }
41
- // ==========================================================================
42
- // INTERNAL API for adapter use
43
- // ==========================================================================
44
- /**
45
- * Set the channel reference.
46
- * Called by the adapter when the channel is created.
47
- * @internal
48
- */
49
- _setChannel(channel) {
50
- this.#channel = channel;
51
- }
52
- // ==========================================================================
53
- // PUBLIC API
54
- // ==========================================================================
55
- /**
56
- * Start processing messages on this connection.
57
- *
58
- * Sets up the message handler on the socket. Must be called after
59
- * the connection is fully set up (channel assigned, stored in adapter).
60
- */
61
- start() {
62
- if (this.#started) {
63
- return;
64
- }
65
- this.#started = true;
66
- this.#socket.onMessage((data) => {
67
- this.#handleMessage(data);
68
- });
69
- }
70
- /**
71
- * Send a ChannelMsg through the Websocket.
72
- *
73
- * Encodes via CBOR codec → frame → fragment if needed → socket.send().
74
- */
75
- send(msg) {
76
- if (this.#socket.readyState !== "open") {
77
- return;
78
- }
79
- encodeBinaryAndSend(
80
- msg,
81
- this.#fragmentThreshold,
82
- (data) => this.#socket.send(data)
83
- );
84
- }
85
- /**
86
- * Send a "ready" signal to the client.
87
- *
88
- * This is a transport-level text message that tells the client the
89
- * server is ready to receive protocol messages. The client creates
90
- * its channel and sends establish-request after receiving this.
91
- */
92
- sendReady() {
93
- if (this.#socket.readyState !== "open") {
94
- return;
95
- }
96
- this.#socket.send("ready");
97
- }
98
- /**
99
- * Close the connection and clean up resources.
100
- */
101
- close(code, reason) {
102
- this.#reassembler.dispose();
103
- this.#socket.close(code, reason);
104
- }
105
- // ==========================================================================
106
- // INTERNAL message handling
107
- // ==========================================================================
108
- /**
109
- * Handle an incoming message from the Websocket.
110
- */
111
- #handleMessage(data) {
112
- if (typeof data === "string") {
113
- this.#handleKeepalive(data);
114
- return;
115
- }
116
- try {
117
- const messages = decodeBinaryMessages(data, this.#reassembler);
118
- if (messages) {
119
- for (const msg of messages) {
120
- this.#handleChannelMessage(msg);
121
- }
122
- }
123
- } catch (error) {
124
- console.error("Failed to decode wire message:", error);
125
- }
126
- }
127
- /**
128
- * Handle a decoded channel message.
129
- *
130
- * Delivers messages synchronously. The Synchronizer's receive queue
131
- * handles recursion prevention by queuing messages and processing
132
- * them iteratively.
133
- */
134
- #handleChannelMessage(msg) {
135
- if (!this.#channel) {
136
- console.error("Cannot handle message: channel not set");
137
- return;
138
- }
139
- this.#channel.onReceive(msg);
140
- }
141
- /**
142
- * Handle keepalive ping/pong messages.
143
- */
144
- #handleKeepalive(text) {
145
- if (text === "ping") {
146
- this.#socket.send("pong");
147
- }
148
- }
22
+ peerId;
23
+ channelId;
24
+ #socket;
25
+ #channel = null;
26
+ #started = false;
27
+ #fragmentThreshold;
28
+ #reassembler;
29
+ #nextFrameId = createFrameIdCounter();
30
+ #aliasState = emptyAliasState();
31
+ constructor(peerId, channelId, socket, config) {
32
+ this.peerId = peerId;
33
+ this.channelId = channelId;
34
+ this.#socket = socket;
35
+ this.#fragmentThreshold = config?.fragmentThreshold ?? 102400;
36
+ this.#reassembler = new FragmentReassembler({
37
+ timeoutMs: 1e4,
38
+ onTimeout: (frameId) => {
39
+ console.warn(`[WebsocketConnection] Fragment batch timed out: ${frameId}`);
40
+ }
41
+ });
42
+ }
43
+ /**
44
+ * Set the channel reference.
45
+ * Called by the adapter when the channel is created.
46
+ * @internal
47
+ */
48
+ _setChannel(channel) {
49
+ this.#channel = channel;
50
+ }
51
+ /**
52
+ * Start processing messages on this connection.
53
+ *
54
+ * Sets up the message handler on the socket. Must be called after
55
+ * the connection is fully set up (channel assigned, stored in adapter).
56
+ */
57
+ start() {
58
+ if (this.#started) return;
59
+ this.#started = true;
60
+ this.#socket.onMessage((data) => {
61
+ this.#handleMessage(data);
62
+ });
63
+ }
64
+ /**
65
+ * Send a ChannelMsg through the Websocket.
66
+ *
67
+ * Pipeline: alias transformer → wire encode → frame → fragment if needed
68
+ * socket.send().
69
+ */
70
+ send(msg) {
71
+ if (this.#socket.readyState !== "open") return;
72
+ const { state, wire } = applyOutboundAliasing(this.#aliasState, msg);
73
+ this.#aliasState = state;
74
+ encodeWireFrameAndSend(wire, (data) => this.#socket.send(data), this.#fragmentThreshold, this.#nextFrameId);
75
+ }
76
+ /**
77
+ * Send a "ready" signal to the client.
78
+ *
79
+ * This is a transport-level text message that tells the client the
80
+ * server is ready to receive protocol messages. The client creates
81
+ * its channel and sends establish after receiving this.
82
+ */
83
+ sendReady() {
84
+ if (this.#socket.readyState !== "open") return;
85
+ this.#socket.send("ready");
86
+ }
87
+ /**
88
+ * Close the connection and clean up resources.
89
+ */
90
+ close(code, reason) {
91
+ this.#reassembler.dispose();
92
+ this.#socket.close(code, reason);
93
+ }
94
+ /**
95
+ * Handle an incoming message from the Websocket.
96
+ */
97
+ #handleMessage(data) {
98
+ if (typeof data === "string") {
99
+ this.#handleKeepalive(data);
100
+ return;
101
+ }
102
+ try {
103
+ const wires = decodeBinaryWires(data, this.#reassembler);
104
+ if (!wires) return;
105
+ for (const wire of wires) {
106
+ const result = applyInboundAliasing(this.#aliasState, wire);
107
+ this.#aliasState = result.state;
108
+ if (result.error || !result.msg) {
109
+ console.warn("[WebsocketConnection] alias resolution failed:", result.error);
110
+ continue;
111
+ }
112
+ this.#handleChannelMessage(result.msg);
113
+ }
114
+ } catch (error) {
115
+ console.error("Failed to decode wire message:", error);
116
+ }
117
+ }
118
+ /**
119
+ * Handle a decoded channel message.
120
+ *
121
+ * Delivers messages synchronously. The Synchronizer's receive queue
122
+ * handles recursion prevention by queuing messages and processing
123
+ * them iteratively.
124
+ */
125
+ #handleChannelMessage(msg) {
126
+ if (!this.#channel) {
127
+ console.error("Cannot handle message: channel not set");
128
+ return;
129
+ }
130
+ this.#channel.onReceive(msg);
131
+ }
132
+ /**
133
+ * Handle keepalive ping/pong messages.
134
+ */
135
+ #handleKeepalive(text) {
136
+ if (text === "ping") this.#socket.send("pong");
137
+ }
149
138
  };
150
-
151
- // src/server-transport.ts
152
- function generatePeerId() {
153
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
154
- let result = "ws-";
155
- for (let i = 0; i < 12; i++) {
156
- result += chars.charAt(Math.floor(Math.random() * chars.length));
157
- }
158
- return result;
159
- }
139
+ //#endregion
140
+ //#region src/server-transport.ts
141
+ /**
142
+ * Websocket server network adapter.
143
+ *
144
+ * Framework-agnostic works with any Websocket library through the
145
+ * `Socket` interface. Use `handleConnection()` to integrate with your
146
+ * framework's Websocket upgrade handler.
147
+ *
148
+ * Each client connection is tracked as a `WebsocketConnection` keyed
149
+ * by peer ID. The adapter creates a channel per connection and routes
150
+ * outbound messages through the connection's send method.
151
+ *
152
+ * The connection handshake follows a two-phase protocol:
153
+ * 1. Server sends text `"ready"` signal (transport-level)
154
+ * 2. Client sends `establish` (protocol-level)
155
+ * 3. Server upgrades channel and sends present (handled by Synchronizer)
156
+ *
157
+ * The server does NOT call `establishChannel()` — it waits for the
158
+ * client's establish to avoid a race condition where the binary
159
+ * establish could arrive before the client has processed "ready".
160
+ */
160
161
  var WebsocketServerTransport = class extends Transport {
161
- #connections = /* @__PURE__ */ new Map();
162
- #fragmentThreshold;
163
- constructor(options) {
164
- super({ transportType: "websocket-server" });
165
- this.#fragmentThreshold = options?.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD;
166
- }
167
- // ==========================================================================
168
- // Adapter abstract method implementations
169
- // ==========================================================================
170
- generate(peerId) {
171
- return {
172
- transportType: this.transportType,
173
- send: (msg) => {
174
- const connection = this.#connections.get(peerId);
175
- if (connection) {
176
- connection.send(msg);
177
- }
178
- },
179
- stop: () => {
180
- this.unregisterConnection(peerId);
181
- }
182
- };
183
- }
184
- async onStart() {
185
- }
186
- async onStop() {
187
- for (const connection of this.#connections.values()) {
188
- connection.close(1001, "Server shutting down");
189
- }
190
- this.#connections.clear();
191
- }
192
- // ==========================================================================
193
- // Connection management
194
- // ==========================================================================
195
- /**
196
- * Handle a new Websocket connection.
197
- *
198
- * Call this from your framework's Websocket upgrade handler.
199
- * Returns a connection handle and a `start()` function that begins
200
- * message processing and sends the "ready" signal.
201
- *
202
- * @param options - Connection options including the Socket and optional peer ID
203
- * @returns A connection handle and start function
204
- *
205
- * @example Bun
206
- * ```typescript
207
- * const { start } = serverAdapter.handleConnection({
208
- * socket: wrapBunWebsocket(ws),
209
- * })
210
- * start()
211
- * ```
212
- *
213
- * @example Node.js ws
214
- * ```typescript
215
- * wss.on("connection", (ws) => {
216
- * const { start } = serverAdapter.handleConnection({
217
- * socket: wrapNodeWebsocket(ws),
218
- * })
219
- * start()
220
- * })
221
- * ```
222
- */
223
- handleConnection(options) {
224
- const { socket, peerId: providedPeerId } = options;
225
- const peerId = providedPeerId ?? generatePeerId();
226
- const existingConnection = this.#connections.get(peerId);
227
- if (existingConnection) {
228
- existingConnection.close(1e3, "Replaced by new connection");
229
- this.unregisterConnection(peerId);
230
- }
231
- const channel = this.addChannel(peerId);
232
- const connection = new WebsocketConnection(
233
- peerId,
234
- channel.channelId,
235
- socket,
236
- {
237
- fragmentThreshold: this.#fragmentThreshold
238
- }
239
- );
240
- connection._setChannel(channel);
241
- this.#connections.set(peerId, connection);
242
- socket.onClose((_code, _reason) => {
243
- this.unregisterConnection(peerId);
244
- });
245
- socket.onError((_error) => {
246
- this.unregisterConnection(peerId);
247
- });
248
- return {
249
- connection,
250
- start: () => {
251
- connection.start();
252
- connection.sendReady();
253
- }
254
- };
255
- }
256
- /**
257
- * Get an active connection by peer ID.
258
- */
259
- getConnection(peerId) {
260
- return this.#connections.get(peerId);
261
- }
262
- /**
263
- * Get all active connections.
264
- */
265
- getAllConnections() {
266
- return Array.from(this.#connections.values());
267
- }
268
- /**
269
- * Check if a peer is connected.
270
- */
271
- isConnected(peerId) {
272
- return this.#connections.has(peerId);
273
- }
274
- /**
275
- * Unregister a connection, removing its channel and cleaning up state.
276
- */
277
- unregisterConnection(peerId) {
278
- const connection = this.#connections.get(peerId);
279
- if (connection) {
280
- this.removeChannel(connection.channelId);
281
- this.#connections.delete(peerId);
282
- }
283
- }
284
- /**
285
- * Broadcast a message to all connected peers.
286
- */
287
- broadcast(msg) {
288
- for (const connection of this.#connections.values()) {
289
- connection.send(msg);
290
- }
291
- }
292
- /**
293
- * Get the number of connected peers.
294
- */
295
- get connectionCount() {
296
- return this.#connections.size;
297
- }
162
+ #connections = /* @__PURE__ */ new Map();
163
+ #fragmentThreshold;
164
+ constructor(options) {
165
+ super({ transportType: "websocket-server" });
166
+ this.#fragmentThreshold = options?.fragmentThreshold ?? 102400;
167
+ }
168
+ generate(peerId) {
169
+ return {
170
+ transportType: this.transportType,
171
+ send: (msg) => {
172
+ const connection = this.#connections.get(peerId);
173
+ if (connection) connection.send(msg);
174
+ },
175
+ stop: () => {
176
+ this.unregisterConnection(peerId);
177
+ }
178
+ };
179
+ }
180
+ async onStart() {}
181
+ async onStop() {
182
+ for (const connection of this.#connections.values()) connection.close(1001, "Server shutting down");
183
+ this.#connections.clear();
184
+ }
185
+ /**
186
+ * Handle a new Websocket connection.
187
+ *
188
+ * Call this from your framework's Websocket upgrade handler.
189
+ * Returns a connection handle and a `start()` function that begins
190
+ * message processing and sends the "ready" signal.
191
+ *
192
+ * @param options - Connection options including the Socket and optional peer ID
193
+ * @returns A connection handle and start function
194
+ *
195
+ * @example Bun
196
+ * ```typescript
197
+ * const { start } = serverAdapter.handleConnection({
198
+ * socket: wrapBunWebsocket(ws),
199
+ * })
200
+ * start()
201
+ * ```
202
+ *
203
+ * @example Node.js ws
204
+ * ```typescript
205
+ * wss.on("connection", (ws) => {
206
+ * const { start } = serverAdapter.handleConnection({
207
+ * socket: wrapNodeWebsocket(ws),
208
+ * })
209
+ * start()
210
+ * })
211
+ * ```
212
+ */
213
+ handleConnection(options) {
214
+ const { socket, peerId: providedPeerId } = options;
215
+ const peerId = providedPeerId ?? `ws-${randomPeerId()}`;
216
+ const existingConnection = this.#connections.get(peerId);
217
+ if (existingConnection) {
218
+ existingConnection.close(1e3, "Replaced by new connection");
219
+ this.unregisterConnection(peerId);
220
+ }
221
+ const channel = this.addChannel(peerId);
222
+ const connection = new WebsocketConnection(peerId, channel.channelId, socket, { fragmentThreshold: this.#fragmentThreshold });
223
+ connection._setChannel(channel);
224
+ this.#connections.set(peerId, connection);
225
+ socket.onClose((_code, _reason) => {
226
+ this.unregisterConnection(peerId);
227
+ });
228
+ socket.onError((_error) => {
229
+ this.unregisterConnection(peerId);
230
+ });
231
+ return {
232
+ connection,
233
+ start: () => {
234
+ connection.start();
235
+ connection.sendReady();
236
+ }
237
+ };
238
+ }
239
+ /**
240
+ * Get an active connection by peer ID.
241
+ */
242
+ getConnection(peerId) {
243
+ return this.#connections.get(peerId);
244
+ }
245
+ /**
246
+ * Get all active connections.
247
+ */
248
+ getAllConnections() {
249
+ return Array.from(this.#connections.values());
250
+ }
251
+ /**
252
+ * Check if a peer is connected.
253
+ */
254
+ isConnected(peerId) {
255
+ return this.#connections.has(peerId);
256
+ }
257
+ /**
258
+ * Unregister a connection, removing its channel and cleaning up state.
259
+ */
260
+ unregisterConnection(peerId) {
261
+ const connection = this.#connections.get(peerId);
262
+ if (connection) {
263
+ this.removeChannel(connection.channelId);
264
+ this.#connections.delete(peerId);
265
+ }
266
+ }
267
+ /**
268
+ * Broadcast a message to all connected peers.
269
+ */
270
+ broadcast(msg) {
271
+ for (const connection of this.#connections.values()) connection.send(msg);
272
+ }
273
+ /**
274
+ * Get the number of connected peers.
275
+ */
276
+ get connectionCount() {
277
+ return this.#connections.size;
278
+ }
298
279
  };
299
-
300
- // src/service-client.ts
280
+ //#endregion
281
+ //#region src/service-client.ts
282
+ /**
283
+ * Create a Websocket client transport for service-to-service connections.
284
+ *
285
+ * This factory is for backend environments (Bun, Node.js) where you need
286
+ * to pass authentication headers during the Websocket upgrade.
287
+ *
288
+ * Note: Headers are a Bun/Node-specific extension. The browser WebSocket API
289
+ * does not support custom headers. For browser clients, use
290
+ * `createWebsocketClient()` and authenticate via URL query parameters.
291
+ *
292
+ * @example
293
+ * ```typescript
294
+ * import { createServiceWebsocketClient } from "@kyneta/websocket-transport/server"
295
+ *
296
+ * const exchange = new Exchange({
297
+ * transports: [createServiceWebsocketClient({
298
+ * url: "ws://primary-server:3000/ws",
299
+ * WebSocket,
300
+ * headers: { Authorization: "Bearer token" },
301
+ * reconnect: { enabled: true },
302
+ * })],
303
+ * })
304
+ * ```
305
+ */
301
306
  function createServiceWebsocketClient(options) {
302
- return () => new WebsocketClientTransport(options);
307
+ return () => new WebsocketClientTransport(options);
303
308
  }
304
- export {
305
- DEFAULT_FRAGMENT_THRESHOLD,
306
- READY_STATE,
307
- WebsocketConnection,
308
- WebsocketServerTransport,
309
- createServiceWebsocketClient,
310
- wrapNodeWebsocket,
311
- wrapStandardWebsocket
312
- };
309
+ //#endregion
310
+ export { DEFAULT_FRAGMENT_THRESHOLD, READY_STATE, WebsocketConnection, WebsocketServerTransport, createServiceWebsocketClient, wrapNodeWebsocket, wrapStandardWebsocket };
311
+
313
312
  //# sourceMappingURL=server.js.map