@forinda/kickjs-ws 3.0.8 → 3.1.1

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.d.mts CHANGED
@@ -1,7 +1,31 @@
1
1
 
2
2
  import { WebSocket, WebSocketServer } from "ws";
3
+ import * as _$_forinda_kickjs0 from "@forinda/kickjs";
3
4
  import { AdapterContext, AppAdapter, Ref } from "@forinda/kickjs";
5
+ import { IncomingMessage } from "node:http";
4
6
 
7
+ //#region src/room-manager.d.ts
8
+ /**
9
+ * Manages WebSocket room membership and broadcasting.
10
+ * Standalone from ws/socket.io — can be swapped for socket.io's built-in rooms.
11
+ */
12
+ declare class RoomManager {
13
+ /** socketId → set of room names */
14
+ private socketRooms;
15
+ /** room name → set of { socketId, socket } */
16
+ private roomSockets;
17
+ join(socketId: string, socket: WebSocket, room: string): void;
18
+ leave(socketId: string, room: string): void;
19
+ /** Remove socket from all rooms (called on disconnect) */
20
+ leaveAll(socketId: string): void;
21
+ getRooms(socketId: string): string[];
22
+ getSockets(room: string): Map<string, WebSocket>;
23
+ /** Get all rooms with their member counts */
24
+ getAllRooms(): Record<string, number>;
25
+ /** Broadcast to all sockets in a room, optionally excluding one */
26
+ broadcast(room: string, event: string, data: any, excludeId?: string): void;
27
+ }
28
+ //#endregion
5
29
  //#region src/interfaces.d.ts
6
30
  declare const WS_METADATA: {
7
31
  readonly WS_CONTROLLER: symbol;
@@ -15,6 +39,33 @@ interface WsHandlerDefinition {
15
39
  /** Method name on the controller class */
16
40
  handlerName: string;
17
41
  }
42
+ /**
43
+ * Resolved principal returned from {@link WsAuthConfig.resolveUser}. Only `id`
44
+ * is required — everything else is free-form metadata stashed on the
45
+ * `WsContext` under `user:<field>` keys for handler access.
46
+ */
47
+ interface WsAuthenticatedUser {
48
+ id: string;
49
+ [key: string]: unknown;
50
+ }
51
+ interface WsAuthConfig {
52
+ /**
53
+ * Resolve a user from the upgrade request. Called once per socket during
54
+ * handshake, before any `@OnConnect` handler fires. Return `null` or throw
55
+ * to reject the socket with HTTP 401.
56
+ */
57
+ resolveUser: (request: IncomingMessage) => Promise<WsAuthenticatedUser | null> | WsAuthenticatedUser | null;
58
+ /**
59
+ * When true, each authenticated socket auto-joins `user:<id>` immediately
60
+ * after `resolveUser` resolves. Pairs with {@link WsUserBroadcaster}.
61
+ */
62
+ autoJoinUserRoom?: boolean;
63
+ /**
64
+ * Room name prefix for per-user broadcasting (default: `'user:'`).
65
+ * Must match what `@forinda/kickjs-ws`'s `WsUserBroadcaster` targets.
66
+ */
67
+ userRoomPrefix?: string;
68
+ }
18
69
  interface WsAdapterOptions {
19
70
  /** Base path for WebSocket upgrade (default: '/ws') */
20
71
  path?: string;
@@ -22,7 +73,31 @@ interface WsAdapterOptions {
22
73
  heartbeatInterval?: number;
23
74
  /** Maximum message payload size in bytes */
24
75
  maxPayload?: number;
76
+ /** Optional authenticated-handshake configuration. */
77
+ auth?: WsAuthConfig;
25
78
  }
79
+ /**
80
+ * Per-user broadcasting across all WS namespaces. Registered on the DI
81
+ * container when {@link WsAdapterOptions.auth} is configured, but the
82
+ * underlying room (`user:<id>`) can also be joined manually by any
83
+ * controller — the helper works either way.
84
+ */
85
+ interface WsUserBroadcaster {
86
+ /** Send a single event to every socket bound to this user. */
87
+ toUser(userId: string): {
88
+ send(event: string, data: unknown): void;
89
+ };
90
+ /** Convenience — `toUser(id).send(event, data)` in one call. */
91
+ broadcastToUser(userId: string, event: string, data: unknown): void;
92
+ /** Room name for a given user (respects `userRoomPrefix`). */
93
+ roomFor(userId: string): string;
94
+ }
95
+ /** DI token for the live {@link WsAdapter} instance. */
96
+ declare const WS_ADAPTER: _$_forinda_kickjs0.InjectionToken<unknown>;
97
+ /** DI token for the shared {@link RoomManager}. */
98
+ declare const WS_ROOM_MANAGER: _$_forinda_kickjs0.InjectionToken<RoomManager>;
99
+ /** DI token for the per-user broadcaster helper. */
100
+ declare const WS_USER_BROADCASTER: _$_forinda_kickjs0.InjectionToken<WsUserBroadcaster>;
26
101
  //#endregion
27
102
  //#region src/ws-adapter.d.ts
28
103
  /**
@@ -49,6 +124,8 @@ declare class WsAdapter implements AppAdapter {
49
124
  private basePath;
50
125
  private heartbeatInterval;
51
126
  private maxPayload;
127
+ private auth?;
128
+ private userRoomPrefix;
52
129
  private wss;
53
130
  private container;
54
131
  private namespaces;
@@ -78,6 +155,11 @@ declare class WsAdapter implements AppAdapter {
78
155
  }>;
79
156
  rooms: Record<string, number>;
80
157
  };
158
+ /** Room name used for per-user broadcasting. */
159
+ userRoom(userId: string): string;
160
+ /** Broadcast a single event to every socket in `user:<id>` (across namespaces). */
161
+ broadcastToUser(userId: string, event: string, data: unknown): void;
162
+ private buildUserBroadcaster;
81
163
  beforeStart({
82
164
  container
83
165
  }: AdapterContext): void;
@@ -86,32 +168,17 @@ declare class WsAdapter implements AppAdapter {
86
168
  }: AdapterContext): void;
87
169
  shutdown(): void;
88
170
  private handleConnection;
171
+ /**
172
+ * Runs the configured auth hook against the upgrade request. Stashes the
173
+ * resolved user on the context (as `user`, plus mirrored keys) and, when
174
+ * `autoJoinUserRoom` is enabled, joins the socket to `user:<id>`.
175
+ * Returns `false` (and closes the socket with code 4401) on failure.
176
+ */
177
+ private authenticate;
89
178
  private invokeHandlers;
90
179
  private safeInvoke;
91
180
  }
92
181
  //#endregion
93
- //#region src/room-manager.d.ts
94
- /**
95
- * Manages WebSocket room membership and broadcasting.
96
- * Standalone from ws/socket.io — can be swapped for socket.io's built-in rooms.
97
- */
98
- declare class RoomManager {
99
- /** socketId → set of room names */
100
- private socketRooms;
101
- /** room name → set of { socketId, socket } */
102
- private roomSockets;
103
- join(socketId: string, socket: WebSocket, room: string): void;
104
- leave(socketId: string, room: string): void;
105
- /** Remove socket from all rooms (called on disconnect) */
106
- leaveAll(socketId: string): void;
107
- getRooms(socketId: string): string[];
108
- getSockets(room: string): Map<string, WebSocket>;
109
- /** Get all rooms with their member counts */
110
- getAllRooms(): Record<string, number>;
111
- /** Broadcast to all sockets in a room, optionally excluding one */
112
- broadcast(room: string, event: string, data: any, excludeId?: string): void;
113
- }
114
- //#endregion
115
182
  //#region src/ws-context.d.ts
116
183
  /**
117
184
  * Context object passed to WebSocket handler methods.
@@ -142,8 +209,13 @@ declare class WsContext {
142
209
  event: string;
143
210
  /** The namespace this connection belongs to */
144
211
  readonly namespace: string;
212
+ /** The HTTP upgrade request — available from @OnConnect onward. Use to read
213
+ * cookies, headers, query string, and client IP for authenticated handshakes. */
214
+ readonly request: IncomingMessage;
145
215
  private metadata;
146
- constructor(socket: WebSocket, server: WebSocketServer, roomManager: RoomManager, namespaceSockets: Map<string, WebSocket>, id: string, namespace: string);
216
+ constructor(socket: WebSocket, server: WebSocketServer, roomManager: RoomManager, namespaceSockets: Map<string, WebSocket>, id: string, namespace: string, request: IncomingMessage);
217
+ /** Parsed cookies from the upgrade request (raw `Cookie` header parse). */
218
+ get cookies(): Record<string, string>;
147
219
  /** Get a metadata value */
148
220
  get<T = any>(key: string): T | undefined;
149
221
  /** Set a metadata value (persists for the lifetime of the connection) */
@@ -241,5 +313,5 @@ declare const OnError: () => MethodDecorator;
241
313
  */
242
314
  declare function OnMessage(event: string): MethodDecorator;
243
315
  //#endregion
244
- export { OnConnect, OnDisconnect, OnError, OnMessage, RoomManager, WS_METADATA, WsAdapter, type WsAdapterOptions, WsContext, WsController, type WsHandlerDefinition, type WsHandlerType };
316
+ export { OnConnect, OnDisconnect, OnError, OnMessage, RoomManager, WS_ADAPTER, WS_METADATA, WS_ROOM_MANAGER, WS_USER_BROADCASTER, WsAdapter, type WsAdapterOptions, type WsAuthConfig, type WsAuthenticatedUser, WsContext, WsController, type WsHandlerDefinition, type WsHandlerType, type WsUserBroadcaster };
245
317
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/interfaces.ts","../src/ws-adapter.ts","../src/room-manager.ts","../src/ws-context.ts","../src/decorators.ts"],"mappings":";;;;;cAEa,WAAA;EAAA,SAGH,aAAA;EAAA,SAAA,WAAA;AAAA;AAAA,KAEE,aAAA;AAAA,UAEK,mBAAA;EACf,IAAA,EAAM,aAAA;;EAEN,KAAA;EALuB;EAOvB,WAAA;AAAA;AAAA,UAGe,gBAAA;EARA;EAUf,IAAA;;EAEA,iBAAA;EAXA;EAaA,UAAA;AAAA;;;;;AArBF;;;;;AAKA;;;;;AAEA;;;;;;;cC2Ca,SAAA,YAAqB,UAAA;EAAA,SACvB,IAAA;EAAA,QAED,QAAA;EAAA,QACA,iBAAA;EAAA,QACA,UAAA;EAAA,QACA,GAAA;EAAA,QACA,SAAA;EAAA,QACA,UAAA;EAAA,QACA,WAAA;EAAA,QACA,cAAA;EDvCE;EAAA,SC2CD,gBAAA,EAAkB,GAAA;;WAElB,iBAAA,EAAmB,GAAA;;WAEnB,gBAAA,EAAkB,GAAA;EAlBN;EAAA,SAoBZ,YAAA,EAAc,GAAA;EANI;EAAA,SAQlB,QAAA,EAAU,GAAA;cAEP,OAAA,GAAS,gBAAA;EAJE;EAiBvB,QAAA,CAAA;;;;;;;;;;;;EAmBA,WAAA,CAAA;IAAc;EAAA,GAAa,cAAA;EA4B3B,UAAA,CAAA;IAAa;EAAA,GAAU,cAAA;EAiDvB,QAAA,CAAA;EAAA,QAmBQ,gBAAA;EAAA,QAgFA,cAAA;EAAA,QAaA,UAAA;AAAA;;;;;;ADvSV;cEIa,WAAA;;UAEH,WAAA;;UAEA,WAAA;EAER,IAAA,CAAK,QAAA,UAAkB,MAAA,EAAQ,SAAA,EAAW,IAAA;EAY1C,KAAA,CAAM,QAAA,UAAkB,IAAA;EFjBD;EE4BvB,QAAA,CAAS,QAAA;EAaT,QAAA,CAAS,QAAA;EAIT,UAAA,CAAW,IAAA,WAAe,GAAA,SAAY,SAAA;;EAKtC,WAAA,CAAA,GAAe,MAAA;EF/Cf;EEwDA,SAAA,CAAU,IAAA,UAAc,KAAA,UAAe,IAAA,OAAW,SAAA;AAAA;;;;;AFhEpD;;;;;AAKA;;;;;AAEA;;;;cGUa,SAAA;EAAA,SAaA,MAAA,EAAQ,SAAA;EAAA,SACR,MAAA,EAAQ,eAAA;EAAA,iBACA,WAAA;EAAA,iBACA,gBAAA;EHrBR;EAAA,SGOF,EAAA;EHJsB;EGM/B,IAAA;EHN+B;EGQ/B,KAAA;EHJA;EAAA,SGMS,SAAA;EAAA,QAED,QAAA;cAGG,MAAA,EAAQ,SAAA,EACR,MAAA,EAAQ,eAAA,EACA,WAAA,EAAa,WAAA,EACb,gBAAA,EAAkB,GAAA,SAAY,SAAA,GAC/C,EAAA,UACA,SAAA;;EASF,GAAA,SAAA,CAAa,GAAA,WAAc,CAAA;;EAK3B,GAAA,CAAI,GAAA,UAAa,KAAA;EFCI;EEIrB,IAAA,CAAK,KAAA,UAAe,IAAA;EFUO;EEH3B,SAAA,CAAU,KAAA,UAAe,IAAA;EFOE;EEG3B,YAAA,CAAa,KAAA,UAAe,IAAA;EFCT;EESnB,IAAA,CAAK,IAAA;;EAKL,KAAA,CAAM,IAAA;EFoBQ;EEfd,KAAA,CAAA;EF2Ca;EEtCb,EAAA,CAAG,IAAA;IAAiB,IAAA,CAAK,KAAA,UAAe,IAAA;EAAA;AAAA;;;;;;;AHhG1C;;;;;AAKA;;;;;AAEA;;iBIUgB,YAAA,CAAa,SAAA,YAAqB,cAAA;;;;;;;;AJFlD;;;;cIiCa,SAAA,QAtBA,eAAA;;;;;;;;AHwBb;;;;cGWa,YAAA,QAnCA,eAAA;;;;;;;;;;;;cAgDA,OAAA,QAhDA,eAAA;;;;;;;;;;;;;;;;;;;;iBAqEG,SAAA,CAAU,KAAA,WAAgB,eAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/room-manager.ts","../src/interfaces.ts","../src/ws-adapter.ts","../src/ws-context.ts","../src/decorators.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-ws v3.0.8
2
+ * @forinda/kickjs-ws v3.1.1
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -10,12 +10,18 @@
10
10
  */
11
11
  import { randomUUID } from "node:crypto";
12
12
  import { WebSocketServer } from "ws";
13
- import { Service, createLogger, getClassMeta, getClassMetaOrUndefined, pushClassMeta, ref, setClassMeta } from "@forinda/kickjs";
13
+ import { Service, createLogger, createToken, getClassMeta, getClassMetaOrUndefined, pushClassMeta, ref, setClassMeta } from "@forinda/kickjs";
14
14
  //#region src/interfaces.ts
15
15
  const WS_METADATA = {
16
16
  WS_CONTROLLER: Symbol("kick:ws:controller"),
17
17
  WS_HANDLERS: Symbol("kick:ws:handlers")
18
18
  };
19
+ /** DI token for the live {@link WsAdapter} instance. */
20
+ const WS_ADAPTER = createToken("WsAdapter");
21
+ /** DI token for the shared {@link RoomManager}. */
22
+ const WS_ROOM_MANAGER = createToken("WsRoomManager");
23
+ /** DI token for the per-user broadcaster helper. */
24
+ const WS_USER_BROADCASTER = createToken("WsUserBroadcaster");
19
25
  /** Registry of all @WsController classes — populated at decorator time */
20
26
  const wsControllerRegistry = /* @__PURE__ */ new Set();
21
27
  //#endregion
@@ -45,17 +51,35 @@ var WsContext = class {
45
51
  event;
46
52
  /** The namespace this connection belongs to */
47
53
  namespace;
54
+ /** The HTTP upgrade request — available from @OnConnect onward. Use to read
55
+ * cookies, headers, query string, and client IP for authenticated handshakes. */
56
+ request;
48
57
  metadata = /* @__PURE__ */ new Map();
49
- constructor(socket, server, roomManager, namespaceSockets, id, namespace) {
58
+ constructor(socket, server, roomManager, namespaceSockets, id, namespace, request) {
50
59
  this.socket = socket;
51
60
  this.server = server;
52
61
  this.roomManager = roomManager;
53
62
  this.namespaceSockets = namespaceSockets;
54
63
  this.id = id;
55
64
  this.namespace = namespace;
65
+ this.request = request;
56
66
  this.data = null;
57
67
  this.event = "";
58
68
  }
69
+ /** Parsed cookies from the upgrade request (raw `Cookie` header parse). */
70
+ get cookies() {
71
+ const header = this.request.headers.cookie;
72
+ if (!header) return {};
73
+ const out = {};
74
+ for (const part of header.split(";")) {
75
+ const idx = part.indexOf("=");
76
+ if (idx === -1) continue;
77
+ const k = part.slice(0, idx).trim();
78
+ const v = part.slice(idx + 1).trim();
79
+ if (k) out[k] = decodeURIComponent(v);
80
+ }
81
+ return out;
82
+ }
59
83
  /** Get a metadata value */
60
84
  get(key) {
61
85
  return this.metadata.get(key);
@@ -187,6 +211,8 @@ var WsAdapter = class {
187
211
  basePath;
188
212
  heartbeatInterval;
189
213
  maxPayload;
214
+ auth;
215
+ userRoomPrefix;
190
216
  wss = null;
191
217
  container = null;
192
218
  namespaces = /* @__PURE__ */ new Map();
@@ -206,6 +232,8 @@ var WsAdapter = class {
206
232
  this.basePath = options.path ?? "/ws";
207
233
  this.heartbeatInterval = options.heartbeatInterval ?? 3e4;
208
234
  this.maxPayload = options.maxPayload;
235
+ this.auth = options.auth;
236
+ this.userRoomPrefix = options.auth?.userRoomPrefix ?? "user:";
209
237
  this.totalConnections = ref(0);
210
238
  this.activeConnections = ref(0);
211
239
  this.messagesReceived = ref(0);
@@ -229,8 +257,27 @@ var WsAdapter = class {
229
257
  rooms: this.roomManager.getAllRooms()
230
258
  };
231
259
  }
260
+ /** Room name used for per-user broadcasting. */
261
+ userRoom(userId) {
262
+ return this.userRoomPrefix + userId;
263
+ }
264
+ /** Broadcast a single event to every socket in `user:<id>` (across namespaces). */
265
+ broadcastToUser(userId, event, data) {
266
+ this.roomManager.broadcast(this.userRoom(userId), event, data);
267
+ }
268
+ buildUserBroadcaster() {
269
+ const self = this;
270
+ return {
271
+ roomFor: (id) => self.userRoom(id),
272
+ broadcastToUser: (id, event, data) => self.broadcastToUser(id, event, data),
273
+ toUser: (id) => ({ send: (event, data) => self.broadcastToUser(id, event, data) })
274
+ };
275
+ }
232
276
  beforeStart({ container }) {
233
277
  this.container = container;
278
+ container.registerInstance(WS_ADAPTER, this);
279
+ container.registerInstance(WS_ROOM_MANAGER, this.roomManager);
280
+ container.registerInstance(WS_USER_BROADCASTER, this.buildUserBroadcaster());
234
281
  for (const controllerClass of wsControllerRegistry) {
235
282
  const namespace = getClassMetaOrUndefined(WS_METADATA.WS_CONTROLLER, controllerClass);
236
283
  if (namespace === void 0) continue;
@@ -261,7 +308,7 @@ var WsAdapter = class {
261
308
  return;
262
309
  }
263
310
  this.wss.handleUpgrade(request, socket, head, (ws) => {
264
- this.handleConnection(ws, entry);
311
+ this.handleConnection(ws, entry, request);
265
312
  });
266
313
  });
267
314
  if (this.heartbeatInterval > 0) this.heartbeatTimer = setInterval(() => {
@@ -286,20 +333,26 @@ var WsAdapter = class {
286
333
  }
287
334
  this.wss?.close();
288
335
  }
289
- handleConnection(ws, entry) {
336
+ handleConnection(ws, entry, request) {
290
337
  const socketId = randomUUID();
291
338
  ws.__alive = true;
292
339
  entry.sockets.set(socketId, ws);
293
340
  this.totalConnections.value++;
294
341
  this.activeConnections.value++;
295
- const ctx = new WsContext(ws, this.wss, this.roomManager, entry.sockets, socketId, entry.namespace);
342
+ const ctx = new WsContext(ws, this.wss, this.roomManager, entry.sockets, socketId, entry.namespace, request);
296
343
  entry.contexts.set(socketId, ctx);
297
344
  const controller = this.container.resolve(entry.controllerClass);
298
345
  ws.on("pong", () => {
299
346
  ws.__alive = true;
300
347
  });
301
- this.invokeHandlers(controller, entry.handlers, "connect", ctx);
348
+ let authed = this.auth === void 0;
349
+ (this.auth ? this.authenticate(ctx) : Promise.resolve(true)).then((ok) => {
350
+ if (!ok) return;
351
+ authed = true;
352
+ this.invokeHandlers(controller, entry.handlers, "connect", ctx);
353
+ });
302
354
  ws.on("message", (raw) => {
355
+ if (!authed) return;
303
356
  this.messagesReceived.value++;
304
357
  try {
305
358
  const parsed = JSON.parse(raw.toString());
@@ -338,6 +391,31 @@ var WsAdapter = class {
338
391
  this.invokeHandlers(controller, entry.handlers, "error", ctx);
339
392
  });
340
393
  }
394
+ /**
395
+ * Runs the configured auth hook against the upgrade request. Stashes the
396
+ * resolved user on the context (as `user`, plus mirrored keys) and, when
397
+ * `autoJoinUserRoom` is enabled, joins the socket to `user:<id>`.
398
+ * Returns `false` (and closes the socket with code 4401) on failure.
399
+ */
400
+ async authenticate(ctx) {
401
+ const cfg = this.auth;
402
+ if (!cfg) return true;
403
+ try {
404
+ const user = await cfg.resolveUser(ctx.request);
405
+ if (!user || !user.id) {
406
+ ctx.socket.close(4401, "Unauthorized");
407
+ return false;
408
+ }
409
+ ctx.set("user", user);
410
+ ctx.set("userId", user.id);
411
+ if (cfg.autoJoinUserRoom !== false) ctx.join(this.userRoom(user.id));
412
+ return true;
413
+ } catch (err) {
414
+ log.warn("WS auth failed", err);
415
+ ctx.socket.close(4401, "Unauthorized");
416
+ return false;
417
+ }
418
+ }
341
419
  invokeHandlers(controller, handlers, type, ctx) {
342
420
  for (const handler of handlers) if (handler.type === type) this.safeInvoke(controller, handler.handlerName, ctx);
343
421
  }
@@ -453,6 +531,6 @@ function OnMessage(event) {
453
531
  };
454
532
  }
455
533
  //#endregion
456
- export { OnConnect, OnDisconnect, OnError, OnMessage, RoomManager, WS_METADATA, WsAdapter, WsContext, WsController };
534
+ export { OnConnect, OnDisconnect, OnError, OnMessage, RoomManager, WS_ADAPTER, WS_METADATA, WS_ROOM_MANAGER, WS_USER_BROADCASTER, WsAdapter, WsContext, WsController };
457
535
 
458
536
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/interfaces.ts","../src/ws-context.ts","../src/room-manager.ts","../src/ws-adapter.ts","../src/decorators.ts"],"sourcesContent":["type Constructor = new (...args: any[]) => any\n\nexport const WS_METADATA = {\n WS_CONTROLLER: Symbol('kick:ws:controller'),\n WS_HANDLERS: Symbol('kick:ws:handlers'),\n} as const\n\nexport type WsHandlerType = 'connect' | 'disconnect' | 'message' | 'error'\n\nexport interface WsHandlerDefinition {\n type: WsHandlerType\n /** Event name — only for 'message' type */\n event?: string\n /** Method name on the controller class */\n handlerName: string\n}\n\nexport interface WsAdapterOptions {\n /** Base path for WebSocket upgrade (default: '/ws') */\n path?: string\n /** Heartbeat ping interval in ms (default: 30000). Set to 0 to disable. */\n heartbeatInterval?: number\n /** Maximum message payload size in bytes */\n maxPayload?: number\n}\n\n/** Registry of all @WsController classes — populated at decorator time */\nexport const wsControllerRegistry = new Set<Constructor>()\n","import type { WebSocket, WebSocketServer } from 'ws'\nimport type { RoomManager } from './room-manager'\n\n/**\n * Context object passed to WebSocket handler methods.\n * Analogous to RequestContext for HTTP controllers.\n *\n * @example\n * ```ts\n * @OnMessage('chat:send')\n * handleSend(ctx: WsContext) {\n * console.log(ctx.data) // parsed message payload\n * ctx.send('chat:ack', { ok: true })\n * ctx.broadcast('chat:receive', ctx.data)\n * ctx.join('room-1')\n * ctx.to('room-1').send('chat:receive', ctx.data)\n * }\n * ```\n */\nexport class WsContext {\n /** Unique connection ID */\n readonly id: string\n /** Parsed message payload (set for @OnMessage handlers) */\n data: any\n /** Event name from the message envelope (set for @OnMessage handlers) */\n event: string\n /** The namespace this connection belongs to */\n readonly namespace: string\n\n private metadata = new Map<string, any>()\n\n constructor(\n readonly socket: WebSocket,\n readonly server: WebSocketServer,\n private readonly roomManager: RoomManager,\n private readonly namespaceSockets: Map<string, WebSocket>,\n id: string,\n namespace: string,\n ) {\n this.id = id\n this.namespace = namespace\n this.data = null\n this.event = ''\n }\n\n /** Get a metadata value */\n get<T = any>(key: string): T | undefined {\n return this.metadata.get(key)\n }\n\n /** Set a metadata value (persists for the lifetime of the connection) */\n set(key: string, value: any): void {\n this.metadata.set(key, value)\n }\n\n /** Send a message to this socket */\n send(event: string, data: any): void {\n if (this.socket.readyState === this.socket.OPEN) {\n this.socket.send(JSON.stringify({ event, data }))\n }\n }\n\n /** Send to all sockets in the same namespace except this one */\n broadcast(event: string, data: any): void {\n const message = JSON.stringify({ event, data })\n for (const [id, socket] of this.namespaceSockets) {\n if (id !== this.id && socket.readyState === socket.OPEN) {\n socket.send(message)\n }\n }\n }\n\n /** Send to all sockets in the same namespace including this one */\n broadcastAll(event: string, data: any): void {\n const message = JSON.stringify({ event, data })\n for (const [, socket] of this.namespaceSockets) {\n if (socket.readyState === socket.OPEN) {\n socket.send(message)\n }\n }\n }\n\n /** Join a room */\n join(room: string): void {\n this.roomManager.join(this.id, this.socket, room)\n }\n\n /** Leave a room */\n leave(room: string): void {\n this.roomManager.leave(this.id, room)\n }\n\n /** Get all rooms this socket is in */\n rooms(): string[] {\n return this.roomManager.getRooms(this.id)\n }\n\n /** Send to all sockets in a room */\n to(room: string): { send(event: string, data: any): void } {\n return {\n send: (event: string, data: any) => {\n this.roomManager.broadcast(room, event, data)\n },\n }\n }\n}\n","import type { WebSocket } from 'ws'\n\n/**\n * Manages WebSocket room membership and broadcasting.\n * Standalone from ws/socket.io — can be swapped for socket.io's built-in rooms.\n */\nexport class RoomManager {\n /** socketId → set of room names */\n private socketRooms = new Map<string, Set<string>>()\n /** room name → set of { socketId, socket } */\n private roomSockets = new Map<string, Map<string, WebSocket>>()\n\n join(socketId: string, socket: WebSocket, room: string): void {\n if (!this.socketRooms.has(socketId)) {\n this.socketRooms.set(socketId, new Set())\n }\n this.socketRooms.get(socketId)!.add(room)\n\n if (!this.roomSockets.has(room)) {\n this.roomSockets.set(room, new Map())\n }\n this.roomSockets.get(room)!.set(socketId, socket)\n }\n\n leave(socketId: string, room: string): void {\n this.socketRooms.get(socketId)?.delete(room)\n this.roomSockets.get(room)?.delete(socketId)\n\n // Clean up empty rooms\n if (this.roomSockets.get(room)?.size === 0) {\n this.roomSockets.delete(room)\n }\n }\n\n /** Remove socket from all rooms (called on disconnect) */\n leaveAll(socketId: string): void {\n const rooms = this.socketRooms.get(socketId)\n if (rooms) {\n for (const room of rooms) {\n this.roomSockets.get(room)?.delete(socketId)\n if (this.roomSockets.get(room)?.size === 0) {\n this.roomSockets.delete(room)\n }\n }\n }\n this.socketRooms.delete(socketId)\n }\n\n getRooms(socketId: string): string[] {\n return Array.from(this.socketRooms.get(socketId) ?? [])\n }\n\n getSockets(room: string): Map<string, WebSocket> {\n return this.roomSockets.get(room) ?? new Map()\n }\n\n /** Get all rooms with their member counts */\n getAllRooms(): Record<string, number> {\n const result: Record<string, number> = {}\n for (const [room, sockets] of this.roomSockets) {\n result[room] = sockets.size\n }\n return result\n }\n\n /** Broadcast to all sockets in a room, optionally excluding one */\n broadcast(room: string, event: string, data: any, excludeId?: string): void {\n const sockets = this.roomSockets.get(room)\n if (!sockets) return\n\n const message = JSON.stringify({ event, data })\n for (const [id, socket] of sockets) {\n if (id !== excludeId && socket.readyState === socket.OPEN) {\n socket.send(message)\n }\n }\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport { WebSocketServer, type WebSocket } from 'ws'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport {\n type AppAdapter,\n type AdapterContext,\n type Container,\n createLogger,\n ref,\n type Ref,\n getClassMetaOrUndefined,\n getClassMeta,\n} from '@forinda/kickjs'\nimport {\n WS_METADATA,\n wsControllerRegistry,\n type WsAdapterOptions,\n type WsHandlerDefinition,\n} from './interfaces'\nimport { WsContext } from './ws-context'\nimport { RoomManager } from './room-manager'\n\nconst log = createLogger('WsAdapter')\n\ninterface NamespaceEntry {\n namespace: string\n controllerClass: any\n handlers: WsHandlerDefinition[]\n sockets: Map<string, WebSocket>\n contexts: Map<string, WsContext>\n}\n\n/**\n * WebSocket adapter for KickJS. Attaches to the HTTP server and routes\n * WebSocket connections to @WsController classes based on namespace paths.\n *\n * @example\n * ```ts\n * import { WsAdapter } from '@forinda/kickjs-ws'\n *\n * bootstrap({\n * modules: [ChatModule],\n * adapters: [\n * new WsAdapter({ path: '/ws' }),\n * ],\n * })\n * ```\n *\n * Clients connect to: `ws://localhost:3000/ws/chat`\n * Messages are JSON: `{ \"event\": \"send\", \"data\": { \"text\": \"hello\" } }`\n */\nexport class WsAdapter implements AppAdapter {\n readonly name = 'WsAdapter'\n\n private basePath: string\n private heartbeatInterval: number\n private maxPayload: number | undefined\n private wss: WebSocketServer | null = null\n private container: Container | null = null\n private namespaces = new Map<string, NamespaceEntry>()\n private roomManager = new RoomManager()\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null\n\n // ── Reactive Stats (exposed for DevToolsAdapter) ─────────────────────\n /** Total WebSocket connections ever opened */\n readonly totalConnections: Ref<number>\n /** Currently active connections */\n readonly activeConnections: Ref<number>\n /** Total messages received */\n readonly messagesReceived: Ref<number>\n /** Total messages sent */\n readonly messagesSent: Ref<number>\n /** Total errors */\n readonly wsErrors: Ref<number>\n\n constructor(options: WsAdapterOptions = {}) {\n this.basePath = options.path ?? '/ws'\n this.heartbeatInterval = options.heartbeatInterval ?? 30000\n this.maxPayload = options.maxPayload\n\n this.totalConnections = ref(0)\n this.activeConnections = ref(0)\n this.messagesReceived = ref(0)\n this.messagesSent = ref(0)\n this.wsErrors = ref(0)\n }\n\n /** Get a snapshot of WebSocket stats for DevTools */\n getStats() {\n const namespaceStats: Record<string, { connections: number; handlers: number }> = {}\n for (const [path, entry] of this.namespaces) {\n namespaceStats[path] = {\n connections: entry.sockets.size,\n handlers: entry.handlers.length,\n }\n }\n return {\n totalConnections: this.totalConnections.value,\n activeConnections: this.activeConnections.value,\n messagesReceived: this.messagesReceived.value,\n messagesSent: this.messagesSent.value,\n errors: this.wsErrors.value,\n namespaces: namespaceStats,\n rooms: this.roomManager.getAllRooms(),\n }\n }\n\n beforeStart({ container }: AdapterContext): void {\n this.container = container\n\n // Discover all @WsController classes and build routing table\n for (const controllerClass of wsControllerRegistry) {\n const namespace = getClassMetaOrUndefined<string>(WS_METADATA.WS_CONTROLLER, controllerClass)\n if (namespace === undefined) continue\n\n const handlers = getClassMeta<WsHandlerDefinition[]>(\n WS_METADATA.WS_HANDLERS,\n controllerClass,\n [],\n )\n\n const fullPath = this.basePath + (namespace === '/' ? '' : namespace)\n\n this.namespaces.set(fullPath, {\n namespace,\n controllerClass,\n handlers,\n sockets: new Map(),\n contexts: new Map(),\n })\n\n log.info(`Registered WS namespace: ${fullPath} (${controllerClass.name})`)\n }\n }\n\n afterStart({ server }: AdapterContext): void {\n if (!server) return\n\n this.wss = new WebSocketServer({\n noServer: true,\n maxPayload: this.maxPayload,\n })\n\n // Handle upgrade requests — route to correct namespace\n server.on('upgrade', (request: IncomingMessage, socket: Duplex, head: Buffer) => {\n const url = request.url || '/'\n // Parse pathname without relying on host header\n const pathname = url.split('?')[0]\n\n const entry = this.namespaces.get(pathname)\n if (!entry) {\n socket.write('HTTP/1.1 404 Not Found\\r\\n\\r\\n')\n socket.destroy()\n return\n }\n\n this.wss!.handleUpgrade(request, socket, head, (ws) => {\n this.handleConnection(ws, entry)\n })\n })\n\n // Heartbeat ping/pong\n if (this.heartbeatInterval > 0) {\n this.heartbeatTimer = setInterval(() => {\n for (const [, entry] of this.namespaces) {\n for (const [, socket] of entry.sockets) {\n if ((socket as any).__alive === false) {\n socket.terminate()\n continue\n }\n ;(socket as any).__alive = false\n socket.ping()\n }\n }\n }, this.heartbeatInterval)\n }\n\n const totalHandlers = Array.from(this.namespaces.values()).reduce(\n (sum, e) => sum + e.handlers.length,\n 0,\n )\n log.info(`WebSocket ready — ${this.namespaces.size} namespace(s), ${totalHandlers} handler(s)`)\n }\n\n shutdown(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer)\n }\n\n // Close all connections\n for (const [, entry] of this.namespaces) {\n for (const [, socket] of entry.sockets) {\n socket.close(1001, 'Server shutting down')\n }\n entry.sockets.clear()\n entry.contexts.clear()\n }\n\n this.wss?.close()\n }\n\n // ── Internal ───────────────────────────────────────────────────────────\n\n private handleConnection(ws: WebSocket, entry: NamespaceEntry): void {\n const socketId = randomUUID()\n ;(ws as any).__alive = true\n\n entry.sockets.set(socketId, ws)\n this.totalConnections.value++\n this.activeConnections.value++\n\n const ctx = new WsContext(\n ws,\n this.wss!,\n this.roomManager,\n entry.sockets,\n socketId,\n entry.namespace,\n )\n entry.contexts.set(socketId, ctx)\n\n // Resolve controller from DI\n const controller = this.container!.resolve(entry.controllerClass)\n\n // Pong handler for heartbeat\n ws.on('pong', () => {\n ;(ws as any).__alive = true\n })\n\n // @OnConnect handlers\n this.invokeHandlers(controller, entry.handlers, 'connect', ctx)\n\n // Message handler\n ws.on('message', (raw: Buffer | string) => {\n this.messagesReceived.value++\n try {\n const parsed = JSON.parse(raw.toString())\n const event = parsed.event as string\n const data = parsed.data\n\n if (!event || typeof event !== 'string') {\n ctx.send('error', { message: 'Invalid message format: missing \"event\" field' })\n return\n }\n\n ctx.event = event\n ctx.data = data\n\n // Find matching @OnMessage handler\n const handler = entry.handlers.find((h) => h.type === 'message' && h.event === event)\n\n if (handler) {\n this.safeInvoke(controller, handler.handlerName, ctx)\n } else {\n // Try catch-all @OnMessage('*')\n const catchAll = entry.handlers.find((h) => h.type === 'message' && h.event === '*')\n if (catchAll) {\n this.safeInvoke(controller, catchAll.handlerName, ctx)\n }\n }\n } catch {\n ctx.data = { message: 'Invalid JSON' }\n this.invokeHandlers(controller, entry.handlers, 'error', ctx)\n }\n })\n\n // Close handler\n ws.on('close', () => {\n this.activeConnections.value--\n this.invokeHandlers(controller, entry.handlers, 'disconnect', ctx)\n this.roomManager.leaveAll(socketId)\n entry.sockets.delete(socketId)\n entry.contexts.delete(socketId)\n })\n\n // Error handler\n ws.on('error', (err: Error) => {\n this.wsErrors.value++\n ctx.data = { message: err.message, name: err.name }\n this.invokeHandlers(controller, entry.handlers, 'error', ctx)\n })\n }\n\n private invokeHandlers(\n controller: any,\n handlers: WsHandlerDefinition[],\n type: WsHandlerDefinition['type'],\n ctx: WsContext,\n ): void {\n for (const handler of handlers) {\n if (handler.type === type) {\n this.safeInvoke(controller, handler.handlerName, ctx)\n }\n }\n }\n\n private safeInvoke(controller: any, method: string, ctx: WsContext): void {\n try {\n const result = controller[method](ctx)\n if (result instanceof Promise) {\n result.catch((err: Error) => {\n log.error({ err }, `WS handler error in ${method}`)\n })\n }\n } catch (err) {\n log.error({ err }, `WS handler error in ${method}`)\n }\n }\n}\n","import { Service, setClassMeta, pushClassMeta } from '@forinda/kickjs'\nimport { WS_METADATA, wsControllerRegistry, type WsHandlerDefinition } from './interfaces'\n\n/**\n * Mark a class as a WebSocket controller with a namespace path.\n * Registers the class in the DI container and the WS controller registry.\n *\n * @example\n * ```ts\n * @WsController('/chat')\n * export class ChatController {\n * @OnConnect()\n * handleConnect(ctx: WsContext) { }\n *\n * @OnMessage('send')\n * handleSend(ctx: WsContext) { }\n * }\n * ```\n */\nexport function WsController(namespace?: string): ClassDecorator {\n return (target: any) => {\n Service()(target)\n setClassMeta(WS_METADATA.WS_CONTROLLER, namespace || '/', target)\n wsControllerRegistry.add(target)\n }\n}\n\nfunction createWsHandlerDecorator(type: WsHandlerDefinition['type'], event?: string) {\n return (): MethodDecorator => {\n return (target, propertyKey) => {\n pushClassMeta<WsHandlerDefinition>(WS_METADATA.WS_HANDLERS, target.constructor, {\n type,\n event,\n handlerName: propertyKey as string,\n })\n }\n }\n}\n\n/**\n * Handle new WebSocket connections.\n *\n * @example\n * ```ts\n * @OnConnect()\n * handleConnect(ctx: WsContext) {\n * console.log(`Client ${ctx.id} connected`)\n * }\n * ```\n */\nexport const OnConnect = createWsHandlerDecorator('connect')\n\n/**\n * Handle WebSocket disconnections.\n *\n * @example\n * ```ts\n * @OnDisconnect()\n * handleDisconnect(ctx: WsContext) {\n * console.log(`Client ${ctx.id} disconnected`)\n * }\n * ```\n */\nexport const OnDisconnect = createWsHandlerDecorator('disconnect')\n\n/**\n * Handle WebSocket errors.\n *\n * @example\n * ```ts\n * @OnError()\n * handleError(ctx: WsContext) {\n * console.error('WS error:', ctx.data)\n * }\n * ```\n */\nexport const OnError = createWsHandlerDecorator('error')\n\n/**\n * Handle a specific WebSocket message event.\n * Use '*' as a catch-all for unmatched events.\n *\n * Messages must be JSON: `{ \"event\": \"chat:send\", \"data\": { ... } }`\n *\n * @example\n * ```ts\n * @OnMessage('chat:send')\n * handleChatSend(ctx: WsContext) {\n * ctx.broadcast('chat:receive', ctx.data)\n * }\n *\n * @OnMessage('*')\n * handleUnknown(ctx: WsContext) {\n * ctx.send('error', { message: `Unknown event: ${ctx.event}` })\n * }\n * ```\n */\nexport function OnMessage(event: string): MethodDecorator {\n return (target, propertyKey) => {\n pushClassMeta<WsHandlerDefinition>(WS_METADATA.WS_HANDLERS, target.constructor, {\n type: 'message',\n event,\n handlerName: propertyKey as string,\n })\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/interfaces.ts","../src/ws-context.ts","../src/room-manager.ts","../src/ws-adapter.ts","../src/decorators.ts"],"sourcesContent":["import type { IncomingMessage } from 'node:http'\nimport { createToken } from '@forinda/kickjs'\nimport type { RoomManager } from './room-manager'\n\ntype Constructor = new (...args: any[]) => any\n\nexport const WS_METADATA = {\n WS_CONTROLLER: Symbol('kick:ws:controller'),\n WS_HANDLERS: Symbol('kick:ws:handlers'),\n} as const\n\nexport type WsHandlerType = 'connect' | 'disconnect' | 'message' | 'error'\n\nexport interface WsHandlerDefinition {\n type: WsHandlerType\n /** Event name — only for 'message' type */\n event?: string\n /** Method name on the controller class */\n handlerName: string\n}\n\n/**\n * Resolved principal returned from {@link WsAuthConfig.resolveUser}. Only `id`\n * is required — everything else is free-form metadata stashed on the\n * `WsContext` under `user:<field>` keys for handler access.\n */\nexport interface WsAuthenticatedUser {\n id: string\n [key: string]: unknown\n}\n\nexport interface WsAuthConfig {\n /**\n * Resolve a user from the upgrade request. Called once per socket during\n * handshake, before any `@OnConnect` handler fires. Return `null` or throw\n * to reject the socket with HTTP 401.\n */\n resolveUser: (\n request: IncomingMessage,\n ) => Promise<WsAuthenticatedUser | null> | WsAuthenticatedUser | null\n /**\n * When true, each authenticated socket auto-joins `user:<id>` immediately\n * after `resolveUser` resolves. Pairs with {@link WsUserBroadcaster}.\n */\n autoJoinUserRoom?: boolean\n /**\n * Room name prefix for per-user broadcasting (default: `'user:'`).\n * Must match what `@forinda/kickjs-ws`'s `WsUserBroadcaster` targets.\n */\n userRoomPrefix?: string\n}\n\nexport interface WsAdapterOptions {\n /** Base path for WebSocket upgrade (default: '/ws') */\n path?: string\n /** Heartbeat ping interval in ms (default: 30000). Set to 0 to disable. */\n heartbeatInterval?: number\n /** Maximum message payload size in bytes */\n maxPayload?: number\n /** Optional authenticated-handshake configuration. */\n auth?: WsAuthConfig\n}\n\n/**\n * Per-user broadcasting across all WS namespaces. Registered on the DI\n * container when {@link WsAdapterOptions.auth} is configured, but the\n * underlying room (`user:<id>`) can also be joined manually by any\n * controller — the helper works either way.\n */\nexport interface WsUserBroadcaster {\n /** Send a single event to every socket bound to this user. */\n toUser(userId: string): { send(event: string, data: unknown): void }\n /** Convenience — `toUser(id).send(event, data)` in one call. */\n broadcastToUser(userId: string, event: string, data: unknown): void\n /** Room name for a given user (respects `userRoomPrefix`). */\n roomFor(userId: string): string\n}\n\n/** DI token for the live {@link WsAdapter} instance. */\nexport const WS_ADAPTER = createToken<unknown>('WsAdapter')\n/** DI token for the shared {@link RoomManager}. */\nexport const WS_ROOM_MANAGER = createToken<RoomManager>('WsRoomManager')\n/** DI token for the per-user broadcaster helper. */\nexport const WS_USER_BROADCASTER = createToken<WsUserBroadcaster>('WsUserBroadcaster')\n\n/** Registry of all @WsController classes — populated at decorator time */\nexport const wsControllerRegistry = new Set<Constructor>()\n","import type { IncomingMessage } from 'node:http'\nimport type { WebSocket, WebSocketServer } from 'ws'\nimport type { RoomManager } from './room-manager'\n\n/**\n * Context object passed to WebSocket handler methods.\n * Analogous to RequestContext for HTTP controllers.\n *\n * @example\n * ```ts\n * @OnMessage('chat:send')\n * handleSend(ctx: WsContext) {\n * console.log(ctx.data) // parsed message payload\n * ctx.send('chat:ack', { ok: true })\n * ctx.broadcast('chat:receive', ctx.data)\n * ctx.join('room-1')\n * ctx.to('room-1').send('chat:receive', ctx.data)\n * }\n * ```\n */\nexport class WsContext {\n /** Unique connection ID */\n readonly id: string\n /** Parsed message payload (set for @OnMessage handlers) */\n data: any\n /** Event name from the message envelope (set for @OnMessage handlers) */\n event: string\n /** The namespace this connection belongs to */\n readonly namespace: string\n /** The HTTP upgrade request — available from @OnConnect onward. Use to read\n * cookies, headers, query string, and client IP for authenticated handshakes. */\n readonly request: IncomingMessage\n\n private metadata = new Map<string, any>()\n\n constructor(\n readonly socket: WebSocket,\n readonly server: WebSocketServer,\n private readonly roomManager: RoomManager,\n private readonly namespaceSockets: Map<string, WebSocket>,\n id: string,\n namespace: string,\n request: IncomingMessage,\n ) {\n this.id = id\n this.namespace = namespace\n this.request = request\n this.data = null\n this.event = ''\n }\n\n /** Parsed cookies from the upgrade request (raw `Cookie` header parse). */\n get cookies(): Record<string, string> {\n const header = this.request.headers.cookie\n if (!header) return {}\n const out: Record<string, string> = {}\n for (const part of header.split(';')) {\n const idx = part.indexOf('=')\n if (idx === -1) continue\n const k = part.slice(0, idx).trim()\n const v = part.slice(idx + 1).trim()\n if (k) out[k] = decodeURIComponent(v)\n }\n return out\n }\n\n /** Get a metadata value */\n get<T = any>(key: string): T | undefined {\n return this.metadata.get(key)\n }\n\n /** Set a metadata value (persists for the lifetime of the connection) */\n set(key: string, value: any): void {\n this.metadata.set(key, value)\n }\n\n /** Send a message to this socket */\n send(event: string, data: any): void {\n if (this.socket.readyState === this.socket.OPEN) {\n this.socket.send(JSON.stringify({ event, data }))\n }\n }\n\n /** Send to all sockets in the same namespace except this one */\n broadcast(event: string, data: any): void {\n const message = JSON.stringify({ event, data })\n for (const [id, socket] of this.namespaceSockets) {\n if (id !== this.id && socket.readyState === socket.OPEN) {\n socket.send(message)\n }\n }\n }\n\n /** Send to all sockets in the same namespace including this one */\n broadcastAll(event: string, data: any): void {\n const message = JSON.stringify({ event, data })\n for (const [, socket] of this.namespaceSockets) {\n if (socket.readyState === socket.OPEN) {\n socket.send(message)\n }\n }\n }\n\n /** Join a room */\n join(room: string): void {\n this.roomManager.join(this.id, this.socket, room)\n }\n\n /** Leave a room */\n leave(room: string): void {\n this.roomManager.leave(this.id, room)\n }\n\n /** Get all rooms this socket is in */\n rooms(): string[] {\n return this.roomManager.getRooms(this.id)\n }\n\n /** Send to all sockets in a room */\n to(room: string): { send(event: string, data: any): void } {\n return {\n send: (event: string, data: any) => {\n this.roomManager.broadcast(room, event, data)\n },\n }\n }\n}\n","import type { WebSocket } from 'ws'\n\n/**\n * Manages WebSocket room membership and broadcasting.\n * Standalone from ws/socket.io — can be swapped for socket.io's built-in rooms.\n */\nexport class RoomManager {\n /** socketId → set of room names */\n private socketRooms = new Map<string, Set<string>>()\n /** room name → set of { socketId, socket } */\n private roomSockets = new Map<string, Map<string, WebSocket>>()\n\n join(socketId: string, socket: WebSocket, room: string): void {\n if (!this.socketRooms.has(socketId)) {\n this.socketRooms.set(socketId, new Set())\n }\n this.socketRooms.get(socketId)!.add(room)\n\n if (!this.roomSockets.has(room)) {\n this.roomSockets.set(room, new Map())\n }\n this.roomSockets.get(room)!.set(socketId, socket)\n }\n\n leave(socketId: string, room: string): void {\n this.socketRooms.get(socketId)?.delete(room)\n this.roomSockets.get(room)?.delete(socketId)\n\n // Clean up empty rooms\n if (this.roomSockets.get(room)?.size === 0) {\n this.roomSockets.delete(room)\n }\n }\n\n /** Remove socket from all rooms (called on disconnect) */\n leaveAll(socketId: string): void {\n const rooms = this.socketRooms.get(socketId)\n if (rooms) {\n for (const room of rooms) {\n this.roomSockets.get(room)?.delete(socketId)\n if (this.roomSockets.get(room)?.size === 0) {\n this.roomSockets.delete(room)\n }\n }\n }\n this.socketRooms.delete(socketId)\n }\n\n getRooms(socketId: string): string[] {\n return Array.from(this.socketRooms.get(socketId) ?? [])\n }\n\n getSockets(room: string): Map<string, WebSocket> {\n return this.roomSockets.get(room) ?? new Map()\n }\n\n /** Get all rooms with their member counts */\n getAllRooms(): Record<string, number> {\n const result: Record<string, number> = {}\n for (const [room, sockets] of this.roomSockets) {\n result[room] = sockets.size\n }\n return result\n }\n\n /** Broadcast to all sockets in a room, optionally excluding one */\n broadcast(room: string, event: string, data: any, excludeId?: string): void {\n const sockets = this.roomSockets.get(room)\n if (!sockets) return\n\n const message = JSON.stringify({ event, data })\n for (const [id, socket] of sockets) {\n if (id !== excludeId && socket.readyState === socket.OPEN) {\n socket.send(message)\n }\n }\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport { WebSocketServer, type WebSocket } from 'ws'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport {\n type AppAdapter,\n type AdapterContext,\n type Container,\n createLogger,\n ref,\n type Ref,\n getClassMetaOrUndefined,\n getClassMeta,\n} from '@forinda/kickjs'\nimport {\n WS_ADAPTER,\n WS_METADATA,\n WS_ROOM_MANAGER,\n WS_USER_BROADCASTER,\n wsControllerRegistry,\n type WsAdapterOptions,\n type WsAuthConfig,\n type WsHandlerDefinition,\n type WsUserBroadcaster,\n} from './interfaces'\nimport { WsContext } from './ws-context'\nimport { RoomManager } from './room-manager'\n\nconst log = createLogger('WsAdapter')\n\ninterface NamespaceEntry {\n namespace: string\n controllerClass: any\n handlers: WsHandlerDefinition[]\n sockets: Map<string, WebSocket>\n contexts: Map<string, WsContext>\n}\n\n/**\n * WebSocket adapter for KickJS. Attaches to the HTTP server and routes\n * WebSocket connections to @WsController classes based on namespace paths.\n *\n * @example\n * ```ts\n * import { WsAdapter } from '@forinda/kickjs-ws'\n *\n * bootstrap({\n * modules: [ChatModule],\n * adapters: [\n * new WsAdapter({ path: '/ws' }),\n * ],\n * })\n * ```\n *\n * Clients connect to: `ws://localhost:3000/ws/chat`\n * Messages are JSON: `{ \"event\": \"send\", \"data\": { \"text\": \"hello\" } }`\n */\nexport class WsAdapter implements AppAdapter {\n readonly name = 'WsAdapter'\n\n private basePath: string\n private heartbeatInterval: number\n private maxPayload: number | undefined\n private auth?: WsAuthConfig\n private userRoomPrefix: string\n private wss: WebSocketServer | null = null\n private container: Container | null = null\n private namespaces = new Map<string, NamespaceEntry>()\n private roomManager = new RoomManager()\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null\n\n // ── Reactive Stats (exposed for DevToolsAdapter) ─────────────────────\n /** Total WebSocket connections ever opened */\n readonly totalConnections: Ref<number>\n /** Currently active connections */\n readonly activeConnections: Ref<number>\n /** Total messages received */\n readonly messagesReceived: Ref<number>\n /** Total messages sent */\n readonly messagesSent: Ref<number>\n /** Total errors */\n readonly wsErrors: Ref<number>\n\n constructor(options: WsAdapterOptions = {}) {\n this.basePath = options.path ?? '/ws'\n this.heartbeatInterval = options.heartbeatInterval ?? 30000\n this.maxPayload = options.maxPayload\n this.auth = options.auth\n this.userRoomPrefix = options.auth?.userRoomPrefix ?? 'user:'\n\n this.totalConnections = ref(0)\n this.activeConnections = ref(0)\n this.messagesReceived = ref(0)\n this.messagesSent = ref(0)\n this.wsErrors = ref(0)\n }\n\n /** Get a snapshot of WebSocket stats for DevTools */\n getStats() {\n const namespaceStats: Record<string, { connections: number; handlers: number }> = {}\n for (const [path, entry] of this.namespaces) {\n namespaceStats[path] = {\n connections: entry.sockets.size,\n handlers: entry.handlers.length,\n }\n }\n return {\n totalConnections: this.totalConnections.value,\n activeConnections: this.activeConnections.value,\n messagesReceived: this.messagesReceived.value,\n messagesSent: this.messagesSent.value,\n errors: this.wsErrors.value,\n namespaces: namespaceStats,\n rooms: this.roomManager.getAllRooms(),\n }\n }\n\n /** Room name used for per-user broadcasting. */\n userRoom(userId: string): string {\n return this.userRoomPrefix + userId\n }\n\n /** Broadcast a single event to every socket in `user:<id>` (across namespaces). */\n broadcastToUser(userId: string, event: string, data: unknown): void {\n this.roomManager.broadcast(this.userRoom(userId), event, data)\n }\n\n private buildUserBroadcaster(): WsUserBroadcaster {\n const self = this\n return {\n roomFor: (id) => self.userRoom(id),\n broadcastToUser: (id, event, data) => self.broadcastToUser(id, event, data),\n toUser: (id) => ({\n send: (event, data) => self.broadcastToUser(id, event, data),\n }),\n }\n }\n\n beforeStart({ container }: AdapterContext): void {\n this.container = container\n\n container.registerInstance(WS_ADAPTER, this)\n container.registerInstance(WS_ROOM_MANAGER, this.roomManager)\n container.registerInstance(WS_USER_BROADCASTER, this.buildUserBroadcaster())\n\n // Discover all @WsController classes and build routing table\n for (const controllerClass of wsControllerRegistry) {\n const namespace = getClassMetaOrUndefined<string>(WS_METADATA.WS_CONTROLLER, controllerClass)\n if (namespace === undefined) continue\n\n const handlers = getClassMeta<WsHandlerDefinition[]>(\n WS_METADATA.WS_HANDLERS,\n controllerClass,\n [],\n )\n\n const fullPath = this.basePath + (namespace === '/' ? '' : namespace)\n\n this.namespaces.set(fullPath, {\n namespace,\n controllerClass,\n handlers,\n sockets: new Map(),\n contexts: new Map(),\n })\n\n log.info(`Registered WS namespace: ${fullPath} (${controllerClass.name})`)\n }\n }\n\n afterStart({ server }: AdapterContext): void {\n if (!server) return\n\n this.wss = new WebSocketServer({\n noServer: true,\n maxPayload: this.maxPayload,\n })\n\n // Handle upgrade requests — route to correct namespace\n server.on('upgrade', (request: IncomingMessage, socket: Duplex, head: Buffer) => {\n const url = request.url || '/'\n // Parse pathname without relying on host header\n const pathname = url.split('?')[0]\n\n const entry = this.namespaces.get(pathname)\n if (!entry) {\n socket.write('HTTP/1.1 404 Not Found\\r\\n\\r\\n')\n socket.destroy()\n return\n }\n\n this.wss!.handleUpgrade(request, socket, head, (ws) => {\n this.handleConnection(ws, entry, request)\n })\n })\n\n // Heartbeat ping/pong\n if (this.heartbeatInterval > 0) {\n this.heartbeatTimer = setInterval(() => {\n for (const [, entry] of this.namespaces) {\n for (const [, socket] of entry.sockets) {\n if ((socket as any).__alive === false) {\n socket.terminate()\n continue\n }\n ;(socket as any).__alive = false\n socket.ping()\n }\n }\n }, this.heartbeatInterval)\n }\n\n const totalHandlers = Array.from(this.namespaces.values()).reduce(\n (sum, e) => sum + e.handlers.length,\n 0,\n )\n log.info(`WebSocket ready — ${this.namespaces.size} namespace(s), ${totalHandlers} handler(s)`)\n }\n\n shutdown(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer)\n }\n\n // Close all connections\n for (const [, entry] of this.namespaces) {\n for (const [, socket] of entry.sockets) {\n socket.close(1001, 'Server shutting down')\n }\n entry.sockets.clear()\n entry.contexts.clear()\n }\n\n this.wss?.close()\n }\n\n // ── Internal ───────────────────────────────────────────────────────────\n\n private handleConnection(ws: WebSocket, entry: NamespaceEntry, request: IncomingMessage): void {\n const socketId = randomUUID()\n ;(ws as any).__alive = true\n\n entry.sockets.set(socketId, ws)\n this.totalConnections.value++\n this.activeConnections.value++\n\n const ctx = new WsContext(\n ws,\n this.wss!,\n this.roomManager,\n entry.sockets,\n socketId,\n entry.namespace,\n request,\n )\n entry.contexts.set(socketId, ctx)\n\n // Resolve controller from DI\n const controller = this.container!.resolve(entry.controllerClass)\n\n // Pong handler for heartbeat\n ws.on('pong', () => {\n ;(ws as any).__alive = true\n })\n\n // Authenticated handshake (optional). Messages received before auth\n // resolves are buffered on the socket; we gate dispatch on `authed`.\n let authed = this.auth === undefined\n const authPromise = this.auth ? this.authenticate(ctx) : Promise.resolve(true)\n\n authPromise.then((ok) => {\n if (!ok) return\n authed = true\n // @OnConnect handlers\n this.invokeHandlers(controller, entry.handlers, 'connect', ctx)\n })\n\n // Message handler\n ws.on('message', (raw: Buffer | string) => {\n if (!authed) return\n this.messagesReceived.value++\n try {\n const parsed = JSON.parse(raw.toString())\n const event = parsed.event as string\n const data = parsed.data\n\n if (!event || typeof event !== 'string') {\n ctx.send('error', { message: 'Invalid message format: missing \"event\" field' })\n return\n }\n\n ctx.event = event\n ctx.data = data\n\n // Find matching @OnMessage handler\n const handler = entry.handlers.find((h) => h.type === 'message' && h.event === event)\n\n if (handler) {\n this.safeInvoke(controller, handler.handlerName, ctx)\n } else {\n // Try catch-all @OnMessage('*')\n const catchAll = entry.handlers.find((h) => h.type === 'message' && h.event === '*')\n if (catchAll) {\n this.safeInvoke(controller, catchAll.handlerName, ctx)\n }\n }\n } catch {\n ctx.data = { message: 'Invalid JSON' }\n this.invokeHandlers(controller, entry.handlers, 'error', ctx)\n }\n })\n\n // Close handler\n ws.on('close', () => {\n this.activeConnections.value--\n this.invokeHandlers(controller, entry.handlers, 'disconnect', ctx)\n this.roomManager.leaveAll(socketId)\n entry.sockets.delete(socketId)\n entry.contexts.delete(socketId)\n })\n\n // Error handler\n ws.on('error', (err: Error) => {\n this.wsErrors.value++\n ctx.data = { message: err.message, name: err.name }\n this.invokeHandlers(controller, entry.handlers, 'error', ctx)\n })\n }\n\n /**\n * Runs the configured auth hook against the upgrade request. Stashes the\n * resolved user on the context (as `user`, plus mirrored keys) and, when\n * `autoJoinUserRoom` is enabled, joins the socket to `user:<id>`.\n * Returns `false` (and closes the socket with code 4401) on failure.\n */\n private async authenticate(ctx: WsContext): Promise<boolean> {\n const cfg = this.auth\n if (!cfg) return true\n try {\n const user = await cfg.resolveUser(ctx.request)\n if (!user || !user.id) {\n ctx.socket.close(4401, 'Unauthorized')\n return false\n }\n ctx.set('user', user)\n ctx.set('userId', user.id)\n if (cfg.autoJoinUserRoom !== false) {\n ctx.join(this.userRoom(user.id))\n }\n return true\n } catch (err) {\n log.warn('WS auth failed', err)\n ctx.socket.close(4401, 'Unauthorized')\n return false\n }\n }\n\n private invokeHandlers(\n controller: any,\n handlers: WsHandlerDefinition[],\n type: WsHandlerDefinition['type'],\n ctx: WsContext,\n ): void {\n for (const handler of handlers) {\n if (handler.type === type) {\n this.safeInvoke(controller, handler.handlerName, ctx)\n }\n }\n }\n\n private safeInvoke(controller: any, method: string, ctx: WsContext): void {\n try {\n const result = controller[method](ctx)\n if (result instanceof Promise) {\n result.catch((err: Error) => {\n log.error({ err }, `WS handler error in ${method}`)\n })\n }\n } catch (err) {\n log.error({ err }, `WS handler error in ${method}`)\n }\n }\n}\n","import { Service, setClassMeta, pushClassMeta } from '@forinda/kickjs'\nimport { WS_METADATA, wsControllerRegistry, type WsHandlerDefinition } from './interfaces'\n\n/**\n * Mark a class as a WebSocket controller with a namespace path.\n * Registers the class in the DI container and the WS controller registry.\n *\n * @example\n * ```ts\n * @WsController('/chat')\n * export class ChatController {\n * @OnConnect()\n * handleConnect(ctx: WsContext) { }\n *\n * @OnMessage('send')\n * handleSend(ctx: WsContext) { }\n * }\n * ```\n */\nexport function WsController(namespace?: string): ClassDecorator {\n return (target: any) => {\n Service()(target)\n setClassMeta(WS_METADATA.WS_CONTROLLER, namespace || '/', target)\n wsControllerRegistry.add(target)\n }\n}\n\nfunction createWsHandlerDecorator(type: WsHandlerDefinition['type'], event?: string) {\n return (): MethodDecorator => {\n return (target, propertyKey) => {\n pushClassMeta<WsHandlerDefinition>(WS_METADATA.WS_HANDLERS, target.constructor, {\n type,\n event,\n handlerName: propertyKey as string,\n })\n }\n }\n}\n\n/**\n * Handle new WebSocket connections.\n *\n * @example\n * ```ts\n * @OnConnect()\n * handleConnect(ctx: WsContext) {\n * console.log(`Client ${ctx.id} connected`)\n * }\n * ```\n */\nexport const OnConnect = createWsHandlerDecorator('connect')\n\n/**\n * Handle WebSocket disconnections.\n *\n * @example\n * ```ts\n * @OnDisconnect()\n * handleDisconnect(ctx: WsContext) {\n * console.log(`Client ${ctx.id} disconnected`)\n * }\n * ```\n */\nexport const OnDisconnect = createWsHandlerDecorator('disconnect')\n\n/**\n * Handle WebSocket errors.\n *\n * @example\n * ```ts\n * @OnError()\n * handleError(ctx: WsContext) {\n * console.error('WS error:', ctx.data)\n * }\n * ```\n */\nexport const OnError = createWsHandlerDecorator('error')\n\n/**\n * Handle a specific WebSocket message event.\n * Use '*' as a catch-all for unmatched events.\n *\n * Messages must be JSON: `{ \"event\": \"chat:send\", \"data\": { ... } }`\n *\n * @example\n * ```ts\n * @OnMessage('chat:send')\n * handleChatSend(ctx: WsContext) {\n * ctx.broadcast('chat:receive', ctx.data)\n * }\n *\n * @OnMessage('*')\n * handleUnknown(ctx: WsContext) {\n * ctx.send('error', { message: `Unknown event: ${ctx.event}` })\n * }\n * ```\n */\nexport function OnMessage(event: string): MethodDecorator {\n return (target, propertyKey) => {\n pushClassMeta<WsHandlerDefinition>(WS_METADATA.WS_HANDLERS, target.constructor, {\n type: 'message',\n event,\n handlerName: propertyKey as string,\n })\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs-ws",
3
- "version": "3.0.8",
3
+ "version": "3.1.1",
4
4
  "description": "WebSocket support with decorators, namespaces, rooms, and DI integration for KickJS",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -79,7 +79,7 @@
79
79
  "@types/ws": "^8.18.0",
80
80
  "typescript": "^5.9.2",
81
81
  "vitest": "^4.1.2",
82
- "@forinda/kickjs": "3.0.8"
82
+ "@forinda/kickjs": "3.1.1"
83
83
  },
84
84
  "publishConfig": {
85
85
  "access": "public"