@wooksjs/event-ws 0.7.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/index.mjs ADDED
@@ -0,0 +1,677 @@
1
+ import { EventContext, current, defineEventKind, defineWook, key, routeParamsKey, run, slot, useLogger, useRouteParams, useRouteParams as useRouteParams$1 } from "@wooksjs/event-core";
2
+ import http from "http";
3
+ import { WooksAdapterBase } from "wooks";
4
+ import { WebSocketServer } from "ws";
5
+
6
+ //#region packages/event-ws/src/ws-kind.ts
7
+ /** Event kind for WebSocket connections (long-lived, one per client). */
8
+ const wsConnectionKind = defineEventKind("ws:connection", {
9
+ id: slot(),
10
+ ws: slot()
11
+ });
12
+ /** Event kind for WebSocket messages (short-lived, one per incoming message). */
13
+ const wsMessageKind = defineEventKind("ws:message", {
14
+ data: slot(),
15
+ rawMessage: slot(),
16
+ messageId: slot(),
17
+ messagePath: slot(),
18
+ messageEvent: slot()
19
+ });
20
+
21
+ //#endregion
22
+ //#region packages/event-ws/src/composables/state.ts
23
+ let state;
24
+ /** Called by the WooksWs adapter to expose state to composables. */
25
+ function setAdapterState(s) {
26
+ state = s;
27
+ }
28
+ /** Called by composables to access adapter state. Throws if adapter not initialized. */
29
+ function getAdapterState() {
30
+ if (!state) throw new Error("[event-ws] No active WooksWs adapter");
31
+ return state;
32
+ }
33
+
34
+ //#endregion
35
+ //#region packages/event-ws/src/composables/connection.ts
36
+ /**
37
+ * Returns the connection `EventContext` from either connection-level or message-level handlers.
38
+ * Mirrors `current()` from `@wooksjs/event-core`.
39
+ *
40
+ * - In `onConnect` / `onDisconnect`: returns `current()` directly.
41
+ * - In `onMessage`: returns `current().parent` (the connection context).
42
+ */
43
+ function currentConnection(ctx) {
44
+ const c = ctx ?? current();
45
+ return c.parent ?? c;
46
+ }
47
+ /**
48
+ * Provides access to the current WebSocket connection (id, send, close).
49
+ * Works in both connection and message contexts via parent chain traversal.
50
+ */
51
+ const useWsConnection = defineWook((ctx) => {
52
+ const id = ctx.get(wsConnectionKind.keys.id);
53
+ const ws = ctx.get(wsConnectionKind.keys.ws);
54
+ const state$1 = getAdapterState();
55
+ const connection = state$1.connections.get(id);
56
+ return {
57
+ id,
58
+ send(event, path, data, params) {
59
+ connection.send(event, path, data, params);
60
+ },
61
+ close(code, reason) {
62
+ ws.close(code, reason);
63
+ },
64
+ context: currentConnection(ctx)
65
+ };
66
+ });
67
+
68
+ //#endregion
69
+ //#region packages/event-ws/src/composables/message.ts
70
+ /**
71
+ * Provides access to the current WebSocket message data.
72
+ * Only available in message context (inside `onMessage` handlers).
73
+ */
74
+ function useWsMessage(ctx) {
75
+ return useWsMessageInternal(ctx);
76
+ }
77
+ const useWsMessageInternal = defineWook((ctx) => {
78
+ return {
79
+ data: ctx.get(wsMessageKind.keys.data),
80
+ raw: ctx.get(wsMessageKind.keys.rawMessage),
81
+ id: ctx.get(wsMessageKind.keys.messageId),
82
+ path: ctx.get(wsMessageKind.keys.messagePath),
83
+ event: ctx.get(wsMessageKind.keys.messageEvent)
84
+ };
85
+ });
86
+
87
+ //#endregion
88
+ //#region packages/event-ws/src/composables/rooms.ts
89
+ /**
90
+ * Provides room management for the current connection.
91
+ * All methods default to the current message path as the room.
92
+ * Only available in message context (inside `onMessage` handlers).
93
+ */
94
+ const useWsRooms = defineWook((ctx) => {
95
+ const messagePath = ctx.get(wsMessageKind.keys.messagePath);
96
+ const connectionId = ctx.get(wsConnectionKind.keys.id);
97
+ const state$1 = getAdapterState();
98
+ const connection = state$1.connections.get(connectionId);
99
+ return {
100
+ join(room) {
101
+ state$1.roomManager.join(connection, room ?? messagePath);
102
+ },
103
+ leave(room) {
104
+ state$1.roomManager.leave(connection, room ?? messagePath);
105
+ },
106
+ broadcast(event, data, options) {
107
+ const room = options?.room ?? messagePath;
108
+ const excludeSelf = options?.excludeSelf ?? true;
109
+ let params;
110
+ try {
111
+ const rp = useRouteParams$1(ctx);
112
+ const p = rp.params;
113
+ if (Object.keys(p).length > 0) params = p;
114
+ } catch {}
115
+ state$1.roomManager.broadcast(room, event, room, data, params, excludeSelf ? connection : void 0);
116
+ },
117
+ rooms() {
118
+ return [...connection.rooms];
119
+ }
120
+ };
121
+ });
122
+
123
+ //#endregion
124
+ //#region packages/event-ws/src/composables/server.ts
125
+ /**
126
+ * Provides server-wide operations. Available in any context.
127
+ * Not a `defineWook` — reads directly from the adapter state.
128
+ */
129
+ function useWsServer() {
130
+ const state$1 = getAdapterState();
131
+ return {
132
+ connections() {
133
+ return state$1.connections;
134
+ },
135
+ broadcast(event, path, data, params) {
136
+ for (const conn of state$1.connections.values()) conn.send(event, path, data, params);
137
+ },
138
+ getConnection(id) {
139
+ return state$1.connections.get(id);
140
+ },
141
+ roomConnections(room) {
142
+ return state$1.roomManager.connections(room);
143
+ }
144
+ };
145
+ }
146
+
147
+ //#endregion
148
+ //#region packages/event-ws/src/adapters/ws-adapter-default.ts
149
+ /** Creates the default WsServerAdapter wrapping the `ws` package (peer dependency). */
150
+ function createDefaultWsServerAdapter() {
151
+ return { create() {
152
+ const wss = new WebSocketServer({ noServer: true });
153
+ return {
154
+ handleUpgrade: (req, socket, head, cb) => {
155
+ wss.handleUpgrade(req, socket, head, (ws) => {
156
+ cb(ws);
157
+ });
158
+ },
159
+ close: () => {
160
+ wss.close();
161
+ }
162
+ };
163
+ } };
164
+ }
165
+
166
+ //#endregion
167
+ //#region packages/event-ws/src/ws-connection.ts
168
+ /** Internal class representing a connected WebSocket client. */
169
+ var WsConnection = class {
170
+ rooms = /* @__PURE__ */ new Set();
171
+ alive = true;
172
+ constructor(id, ws, ctx, serializer) {
173
+ this.id = id;
174
+ this.ws = ws;
175
+ this.ctx = ctx;
176
+ this.serializer = serializer;
177
+ }
178
+ /** Send a push message to this connection. */
179
+ send(event, path, data, params) {
180
+ if (this.ws.readyState !== 1) return;
181
+ const msg = {
182
+ event,
183
+ path
184
+ };
185
+ if (params) msg.params = params;
186
+ if (data !== void 0) msg.data = data;
187
+ this.ws.send(this.serializer(msg));
188
+ }
189
+ /** Send a reply to a client request. */
190
+ reply(id, data) {
191
+ if (this.ws.readyState !== 1) return;
192
+ const msg = { id };
193
+ if (data !== void 0) msg.data = data;
194
+ this.ws.send(this.serializer(msg));
195
+ }
196
+ /** Send an error reply to a client request. */
197
+ replyError(id, code, message) {
198
+ if (this.ws.readyState !== 1) return;
199
+ const msg = {
200
+ id,
201
+ error: {
202
+ code,
203
+ message
204
+ }
205
+ };
206
+ this.ws.send(this.serializer(msg));
207
+ }
208
+ /** Close the connection. */
209
+ close(code, reason) {
210
+ this.ws.close(code, reason);
211
+ }
212
+ };
213
+
214
+ //#endregion
215
+ //#region packages/event-ws/src/ws-error.ts
216
+ /** WebSocket error with a numeric code following HTTP conventions (401, 403, 404, 500, etc.). */
217
+ var WsError = class extends Error {
218
+ constructor(code, message) {
219
+ super(message ?? `WsError ${code}`);
220
+ this.code = code;
221
+ this.name = "WsError";
222
+ }
223
+ };
224
+
225
+ //#endregion
226
+ //#region packages/event-ws/src/ws-room-manager.ts
227
+ /** Manages room → connections mapping with optional distributed broadcast transport. */
228
+ var WsRoomManager = class {
229
+ rooms = /* @__PURE__ */ new Map();
230
+ constructor(transport) {
231
+ this.transport = transport;
232
+ if (transport) {}
233
+ }
234
+ /** Add a connection to a room. */
235
+ join(connection, room) {
236
+ connection.rooms.add(room);
237
+ let set = this.rooms.get(room);
238
+ if (!set) {
239
+ set = /* @__PURE__ */ new Set();
240
+ this.rooms.set(room, set);
241
+ if (this.transport) this.transport.subscribe(`ws:room:${room}`, (payload) => {
242
+ this.onTransportMessage(room, payload);
243
+ });
244
+ }
245
+ set.add(connection);
246
+ }
247
+ /** Remove a connection from a room. */
248
+ leave(connection, room) {
249
+ connection.rooms.delete(room);
250
+ const set = this.rooms.get(room);
251
+ if (!set) return;
252
+ set.delete(connection);
253
+ if (set.size === 0) {
254
+ this.rooms.delete(room);
255
+ if (this.transport) this.transport.unsubscribe(`ws:room:${room}`);
256
+ }
257
+ }
258
+ /** Remove a connection from ALL rooms (called on disconnect). */
259
+ leaveAll(connection) {
260
+ for (const room of connection.rooms) {
261
+ const set = this.rooms.get(room);
262
+ if (set) {
263
+ set.delete(connection);
264
+ if (set.size === 0) {
265
+ this.rooms.delete(room);
266
+ if (this.transport) this.transport.unsubscribe(`ws:room:${room}`);
267
+ }
268
+ }
269
+ }
270
+ connection.rooms.clear();
271
+ }
272
+ /** Get all connections in a room. */
273
+ connections(room) {
274
+ return this.rooms.get(room) ?? /* @__PURE__ */ new Set();
275
+ }
276
+ /** Broadcast to all connections in a room. */
277
+ broadcast(room, event, path, data, params, exclude) {
278
+ const set = this.rooms.get(room);
279
+ if (set) {
280
+ for (const conn of set) if (conn !== exclude) conn.send(event, path, data, params);
281
+ }
282
+ if (this.transport) {
283
+ const payload = JSON.stringify({
284
+ event,
285
+ path,
286
+ data,
287
+ params,
288
+ excludeId: exclude?.id
289
+ });
290
+ this.transport.publish(`ws:room:${room}`, payload);
291
+ }
292
+ }
293
+ /** Handle inbound message from broadcast transport (other instances). */
294
+ onTransportMessage(room, payload) {
295
+ const set = this.rooms.get(room);
296
+ if (!set || set.size === 0) return;
297
+ try {
298
+ const { event, path, data, params, excludeId } = JSON.parse(payload);
299
+ for (const conn of set) if (conn.id !== excludeId) conn.send(event, path, data, params);
300
+ } catch {}
301
+ }
302
+ };
303
+
304
+ //#endregion
305
+ //#region packages/event-ws/src/ws-adapter.ts
306
+ const DEFAULT_HEARTBEAT_INTERVAL = 3e4;
307
+ const DEFAULT_MAX_MESSAGE_SIZE = 1024 * 1024;
308
+ const upgradeReqKey = key("ws:upgrade-req");
309
+ const upgradeSocketKey = key("ws:upgrade-socket");
310
+ const upgradeHeadKey = key("ws:upgrade-head");
311
+ /** WebSocket adapter for Wooks. Implements WooksUpgradeHandler for HTTP integration. */
312
+ var WooksWs = class extends WooksAdapterBase {
313
+ logger;
314
+ eventContextOptions;
315
+ connections = /* @__PURE__ */ new Map();
316
+ roomManager;
317
+ wsServer;
318
+ opts;
319
+ serializer;
320
+ parser;
321
+ onConnectHandler;
322
+ onDisconnectHandler;
323
+ heartbeatTimer;
324
+ server;
325
+ reqKey = upgradeReqKey;
326
+ socketKey = upgradeSocketKey;
327
+ headKey = upgradeHeadKey;
328
+ constructor(wooksOrOpts, opts) {
329
+ const isWooks = wooksOrOpts instanceof WooksAdapterBase || wooksOrOpts && "getRouter" in wooksOrOpts;
330
+ const wooks = isWooks ? wooksOrOpts : void 0;
331
+ const resolvedOpts = isWooks ? opts ?? {} : wooksOrOpts ?? {};
332
+ super(wooks, resolvedOpts.logger, void 0);
333
+ if (wooksOrOpts && typeof wooksOrOpts.ws === "function") wooksOrOpts.ws(this);
334
+ this.opts = resolvedOpts;
335
+ this.logger = resolvedOpts.logger || this.getLogger(`[wooks-ws]`);
336
+ this.eventContextOptions = this.getEventContextOptions();
337
+ this.serializer = resolvedOpts.messageSerializer ?? JSON.stringify;
338
+ this.parser = resolvedOpts.messageParser ?? defaultParser;
339
+ this.roomManager = new WsRoomManager(resolvedOpts.broadcastTransport);
340
+ const adapter = resolvedOpts.wsServerAdapter ?? createDefaultWsServerAdapter();
341
+ this.wsServer = adapter.create();
342
+ setAdapterState({
343
+ connections: this.connections,
344
+ roomManager: this.roomManager,
345
+ serializer: this.serializer,
346
+ wooks: this.wooks
347
+ });
348
+ }
349
+ /** Register a handler that runs when a new WebSocket connection is established. */
350
+ onConnect(handler) {
351
+ this.onConnectHandler = handler;
352
+ }
353
+ /** Register a handler that runs when a WebSocket connection closes. */
354
+ onDisconnect(handler) {
355
+ this.onDisconnectHandler = handler;
356
+ }
357
+ /** Register a routed message handler. Uses the standard Wooks router internally. */
358
+ onMessage(event, path, handler) {
359
+ return this.on(event, path, handler);
360
+ }
361
+ /**
362
+ * Complete the WebSocket handshake from inside an UPGRADE route handler.
363
+ * Reads req/socket/head from the current HTTP context (set by the HTTP adapter).
364
+ * The HTTP context becomes the parent of the WS connection context.
365
+ */
366
+ upgrade() {
367
+ const httpCtx = current();
368
+ const req = httpCtx.get(upgradeReqKey);
369
+ const socket = httpCtx.get(upgradeSocketKey);
370
+ const head = httpCtx.get(upgradeHeadKey);
371
+ this.doUpgrade(req, socket, head, httpCtx);
372
+ }
373
+ /**
374
+ * Fallback: called by the HTTP adapter when no UPGRADE route matches.
375
+ * Also used internally for standalone mode.
376
+ */
377
+ handleUpgrade(req, socket, head) {
378
+ this.doUpgrade(req, socket, head);
379
+ }
380
+ /** Start a standalone server (without event-http). */
381
+ async listen(port, hostname) {
382
+ const server = this.server = http.createServer();
383
+ server.on("upgrade", (req, socket, head) => {
384
+ this.doUpgrade(req, socket, head);
385
+ });
386
+ this.startHeartbeat();
387
+ return new Promise((resolve, reject) => {
388
+ server.once("listening", resolve);
389
+ server.once("error", reject);
390
+ if (hostname) server.listen(port, hostname);
391
+ else server.listen(port);
392
+ });
393
+ }
394
+ /** Stop the server and clean up. */
395
+ close() {
396
+ this.stopHeartbeat();
397
+ this.wsServer.close();
398
+ for (const conn of this.connections.values()) conn.close(1001, "Server shutting down");
399
+ this.connections.clear();
400
+ }
401
+ /** Returns the underlying HTTP server (if any). */
402
+ getServer() {
403
+ return this.server;
404
+ }
405
+ doUpgrade(req, socket, head, parentCtx) {
406
+ this.wsServer.handleUpgrade(req, socket, head, (ws) => {
407
+ const id = crypto.randomUUID();
408
+ const ctxOptions = {
409
+ ...this.eventContextOptions,
410
+ ...parentCtx ? { parent: parentCtx } : {}
411
+ };
412
+ const connectionCtx = new EventContext(ctxOptions);
413
+ const connection = new WsConnection(id, ws, connectionCtx, this.serializer);
414
+ connectionCtx.seed(wsConnectionKind, {
415
+ id,
416
+ ws
417
+ });
418
+ try {
419
+ if (this.onConnectHandler) {
420
+ const result = run(connectionCtx, this.onConnectHandler);
421
+ if (result !== null && result !== void 0 && typeof result.then === "function") {
422
+ result.then(() => this.acceptConnection(connection)).catch((error) => this.rejectConnection(connection, error));
423
+ return;
424
+ }
425
+ }
426
+ this.acceptConnection(connection);
427
+ } catch (error) {
428
+ this.rejectConnection(connection, error);
429
+ }
430
+ });
431
+ }
432
+ acceptConnection(connection) {
433
+ this.connections.set(connection.id, connection);
434
+ this.logger.debug(`WS connected: ${connection.id}`);
435
+ const ws = connection.ws;
436
+ ws.on("message", (raw) => {
437
+ this.handleMessage(connection, raw);
438
+ });
439
+ ws.on("close", () => {
440
+ this.handleClose(connection);
441
+ });
442
+ ws.on("error", (err) => {
443
+ this.logger.error(`WS error [${connection.id}]:`, err);
444
+ });
445
+ ws.on("pong", () => {
446
+ connection.alive = true;
447
+ });
448
+ }
449
+ rejectConnection(connection, error) {
450
+ const code = error instanceof WsError ? error.code : 500;
451
+ const message = error instanceof Error ? error.message : "Connection rejected";
452
+ this.logger.debug(`WS rejected: ${message}`);
453
+ const wsCloseCode = code === 401 || code === 403 ? 1008 : 1011;
454
+ connection.close(wsCloseCode, message);
455
+ }
456
+ handleMessage(connection, raw) {
457
+ const maxSize = this.opts.maxMessageSize ?? DEFAULT_MAX_MESSAGE_SIZE;
458
+ const size = typeof raw === "string" ? Buffer.byteLength(raw) : raw.length;
459
+ if (size > maxSize) {
460
+ this.logger.debug(`WS message too large (${size} > ${maxSize}), dropping`);
461
+ return;
462
+ }
463
+ let msg;
464
+ try {
465
+ msg = this.parser(raw);
466
+ } catch {
467
+ this.logger.debug("WS message parse failed, dropping");
468
+ return;
469
+ }
470
+ const { event, path, data, id: messageId } = msg;
471
+ const msgCtx = new EventContext({
472
+ ...this.eventContextOptions,
473
+ parent: connection.ctx
474
+ });
475
+ msgCtx.seed(wsMessageKind, {
476
+ data,
477
+ rawMessage: raw,
478
+ messageId,
479
+ messagePath: path,
480
+ messageEvent: event
481
+ });
482
+ run(msgCtx, () => {
483
+ const handlers = this.wooks.lookupHandlers(event, path, msgCtx);
484
+ if (!handlers) {
485
+ if (messageId !== void 0) connection.replyError(messageId, 404, "Not found");
486
+ return;
487
+ }
488
+ const result = this.processHandlers(handlers, connection, messageId);
489
+ if (result !== null && result !== void 0 && typeof result.then === "function") result.catch((error) => {
490
+ this.handleHandlerError(error, connection, messageId);
491
+ });
492
+ });
493
+ }
494
+ processHandlers(handlers, connection, messageId) {
495
+ for (let i = 0; i < handlers.length; i++) {
496
+ const handler = handlers[i];
497
+ const isLast = i === handlers.length - 1;
498
+ try {
499
+ const result = handler();
500
+ if (result !== null && result !== void 0 && typeof result.then === "function") return this.processAsyncResult(result, handlers, i, connection, messageId);
501
+ this.sendReply(connection, messageId, result);
502
+ return;
503
+ } catch (error) {
504
+ if (isLast) {
505
+ this.handleHandlerError(error, connection, messageId);
506
+ return;
507
+ }
508
+ }
509
+ }
510
+ }
511
+ async processAsyncResult(promise, handlers, startIndex, connection, messageId) {
512
+ try {
513
+ const result = await promise;
514
+ this.sendReply(connection, messageId, result);
515
+ return;
516
+ } catch (error) {
517
+ if (startIndex === handlers.length - 1) {
518
+ this.handleHandlerError(error, connection, messageId);
519
+ return;
520
+ }
521
+ }
522
+ for (let i = startIndex + 1; i < handlers.length; i++) {
523
+ const isLast = i === handlers.length - 1;
524
+ try {
525
+ const result = await handlers[i]();
526
+ this.sendReply(connection, messageId, result);
527
+ return;
528
+ } catch (error) {
529
+ if (isLast) {
530
+ this.handleHandlerError(error, connection, messageId);
531
+ return;
532
+ }
533
+ }
534
+ }
535
+ }
536
+ sendReply(connection, messageId, data) {
537
+ if (messageId === void 0) return;
538
+ connection.reply(messageId, data ?? null);
539
+ }
540
+ handleHandlerError(error, connection, messageId) {
541
+ const code = error instanceof WsError ? error.code : 500;
542
+ const message = error instanceof WsError ? error.message : "Internal Error";
543
+ if (messageId !== void 0) connection.replyError(messageId, code, message);
544
+ if (!(error instanceof WsError)) this.logger.error("Uncaught WS handler error:", error);
545
+ }
546
+ handleClose(connection) {
547
+ this.logger.debug(`WS disconnected: ${connection.id}`);
548
+ if (this.onDisconnectHandler) try {
549
+ const result = run(connection.ctx, this.onDisconnectHandler);
550
+ if (result !== null && result !== void 0 && typeof result.then === "function") result.catch((error) => {
551
+ this.logger.error("onDisconnect error:", error);
552
+ });
553
+ } catch (error) {
554
+ this.logger.error("onDisconnect error:", error);
555
+ }
556
+ this.roomManager.leaveAll(connection);
557
+ this.connections.delete(connection.id);
558
+ }
559
+ startHeartbeat() {
560
+ const interval = this.opts.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
561
+ if (interval === 0) return;
562
+ this.heartbeatTimer = setInterval(() => {
563
+ for (const conn of this.connections.values()) {
564
+ if (!conn.alive) {
565
+ this.logger.debug(`WS heartbeat timeout: ${conn.id}`);
566
+ conn.ws.close(1001, "Heartbeat timeout");
567
+ continue;
568
+ }
569
+ conn.alive = false;
570
+ conn.ws.ping();
571
+ }
572
+ }, interval);
573
+ if (this.heartbeatTimer.unref) this.heartbeatTimer.unref();
574
+ }
575
+ stopHeartbeat() {
576
+ if (this.heartbeatTimer) {
577
+ clearInterval(this.heartbeatTimer);
578
+ this.heartbeatTimer = void 0;
579
+ }
580
+ }
581
+ };
582
+ function defaultParser(raw) {
583
+ const str = typeof raw === "string" ? raw : raw.toString("utf8");
584
+ const parsed = JSON.parse(str);
585
+ if (typeof parsed.event !== "string" || typeof parsed.path !== "string") throw new TypeError("Invalid WS message: missing event or path");
586
+ return parsed;
587
+ }
588
+ /**
589
+ * Creates a new WooksWs WebSocket adapter.
590
+ *
591
+ * @example Integrated with HTTP (recommended):
592
+ * ```ts
593
+ * const http = createHttpApp()
594
+ * const ws = createWsApp(http) // auto-registers upgrade contract
595
+ * http.upgrade('/ws', () => ws.upgrade())
596
+ * http.listen(3000)
597
+ * ```
598
+ *
599
+ * @example Standalone:
600
+ * ```ts
601
+ * const ws = createWsApp({ heartbeatInterval: 30_000 })
602
+ * ws.listen(3000)
603
+ * ```
604
+ */
605
+ function createWsApp(wooksOrOpts, opts) {
606
+ return new WooksWs(wooksOrOpts, opts);
607
+ }
608
+
609
+ //#endregion
610
+ //#region packages/event-ws/src/testing.ts
611
+ /** Creates a mock WsSocket for testing. */
612
+ function createMockWsSocket() {
613
+ return {
614
+ send: () => {},
615
+ close: () => {},
616
+ on: () => {},
617
+ ping: () => {},
618
+ readyState: 1
619
+ };
620
+ }
621
+ /**
622
+ * Creates a fully initialized WS connection context for testing.
623
+ * Returns a runner function that executes callbacks inside the context scope.
624
+ */
625
+ function prepareTestWsConnectionContext(options) {
626
+ const id = options?.id ?? "test-conn-id";
627
+ const ws = createMockWsSocket();
628
+ const ctx = new EventContext({
629
+ logger: console,
630
+ ...options?.parentCtx ? { parent: options.parentCtx } : {}
631
+ });
632
+ ctx.seed(wsConnectionKind, {
633
+ id,
634
+ ws
635
+ });
636
+ if (options?.params) ctx.set(routeParamsKey, options.params);
637
+ return (cb) => run(ctx, cb);
638
+ }
639
+ /**
640
+ * Creates a fully initialized WS message context for testing.
641
+ * Sets up both connection context (parent) and message context (child).
642
+ * Returns a runner function that executes callbacks inside the message context scope.
643
+ */
644
+ function prepareTestWsMessageContext(options) {
645
+ const id = options.id ?? "test-conn-id";
646
+ const ws = createMockWsSocket();
647
+ const connectionCtx = new EventContext({
648
+ logger: console,
649
+ ...options.parentCtx ? { parent: options.parentCtx } : {}
650
+ });
651
+ connectionCtx.seed(wsConnectionKind, {
652
+ id,
653
+ ws
654
+ });
655
+ const messageCtx = new EventContext({
656
+ logger: console,
657
+ parent: connectionCtx
658
+ });
659
+ const rawMessage = options.rawMessage ?? JSON.stringify({
660
+ event: options.event,
661
+ path: options.path,
662
+ data: options.data,
663
+ id: options.messageId
664
+ });
665
+ messageCtx.seed(wsMessageKind, {
666
+ data: options.data,
667
+ rawMessage,
668
+ messageId: options.messageId,
669
+ messagePath: options.path,
670
+ messageEvent: options.event
671
+ });
672
+ if (options.params) messageCtx.set(routeParamsKey, options.params);
673
+ return (cb) => run(messageCtx, cb);
674
+ }
675
+
676
+ //#endregion
677
+ export { WooksWs, WsConnection, WsError, WsRoomManager, createWsApp, currentConnection, prepareTestWsConnectionContext, prepareTestWsMessageContext, useLogger, useRouteParams, useWsConnection, useWsMessage, useWsRooms, useWsServer, wsConnectionKind, wsMessageKind };