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