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