@forinda/kickjs-ws 7.0.1 → 7.0.2

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/README.md CHANGED
@@ -18,7 +18,7 @@ import { WsController, OnConnect, OnMessage, WsContext } from '@forinda/kickjs-w
18
18
  export class ChatController {
19
19
  @OnConnect()
20
20
  onConnect(ctx: WsContext) {
21
- ctx.send('welcome', { id: ctx.socketId })
21
+ ctx.send('welcome', { id: ctx.id })
22
22
  }
23
23
 
24
24
  @OnMessage('say')
@@ -42,6 +42,12 @@ export const app = await bootstrap({
42
42
 
43
43
  Clients connect to `ws://localhost:3000/ws/chat`.
44
44
 
45
+ ## Limits
46
+
47
+ Runs in **one Node process** under `bootstrap()`. Rooms and per-user broadcasts
48
+ reach sockets in that process only, so a second instance is a second island. It
49
+ does not run on the `@forinda/kickjs/web` edge entry (Workers, Bun, Deno).
50
+
45
51
  ## Documentation
46
52
 
47
53
  [kickjs.app/guide/websockets](https://kickjs.app/guide/websockets)
package/dist/index.d.mts CHANGED
@@ -2,13 +2,23 @@
2
2
  import { WebSocket, WebSocketServer } from "ws";
3
3
  import { Ref } from "@forinda/kickjs";
4
4
  import { IncomingMessage } from "node:http";
5
-
6
5
  //#region src/room-manager.d.ts
7
6
  /**
8
7
  * Manages WebSocket room membership and broadcasting.
9
8
  * Standalone from ws/socket.io — can be swapped for socket.io's built-in rooms.
9
+ *
10
+ * Room names are GLOBAL, not per namespace: a room joined from `/ws/chat` and
11
+ * one joined from `/ws/admin` with the same name are the same room. That is
12
+ * what lets a service broadcast through `WS_ROOM_MANAGER` and lets `user:<id>`
13
+ * reach a user's sockets in every namespace — prefix names (`chat:lobby`) when
14
+ * namespaces must not overlap.
15
+ *
16
+ * Membership lives in this process only. A second instance has its own rooms.
10
17
  */
11
- declare class RoomManager {
18
+ export declare class RoomManager {
19
+ private readonly onSend?;
20
+ /** @param onSend Called with the number of frames each broadcast wrote. */
21
+ constructor(onSend?: ((count: number) => void) | undefined);
12
22
  /** socketId → set of room names */
13
23
  private socketRooms;
14
24
  /** room name → set of { socketId, socket } */
@@ -26,7 +36,7 @@ declare class RoomManager {
26
36
  }
27
37
  //#endregion
28
38
  //#region src/interfaces.d.ts
29
- declare const WS_METADATA: {
39
+ export declare const WS_METADATA: {
30
40
  readonly WS_CONTROLLER: 'kick/ws/controller';
31
41
  readonly WS_HANDLERS: 'kick/ws/handlers';
32
42
  };
@@ -40,8 +50,8 @@ interface WsHandlerDefinition {
40
50
  }
41
51
  /**
42
52
  * Resolved principal returned from {@link WsAuthConfig.resolveUser}. Only `id`
43
- * is required everything else is free-form metadata stashed on the
44
- * `WsContext` under `user:<field>` keys for handler access.
53
+ * is required. The whole object is stored on the `WsContext` as `user`, and
54
+ * `id` alone as `userId` read them with `ctx.get('user')` / `ctx.get('userId')`.
45
55
  */
46
56
  interface WsAuthenticatedUser {
47
57
  id: string;
@@ -49,14 +59,23 @@ interface WsAuthenticatedUser {
49
59
  }
50
60
  interface WsAuthConfig {
51
61
  /**
52
- * Resolve a user from the upgrade request. Called once per socket during
53
- * handshake, before any `@OnConnect` handler fires. Return `null` or throw
54
- * to reject the socket with HTTP 401.
62
+ * Resolve a user from the upgrade request. Called once per socket, before
63
+ * any `@OnConnect` handler fires. Return `null` or throw to reject.
64
+ *
65
+ * A rejected socket is accepted and then closed with code `4401` — a
66
+ * WebSocket close code, not an HTTP status. Browsers do not expose a failed
67
+ * handshake's status to script, so a close code is what a client can act on.
68
+ *
69
+ * Messages the client sends before this settles are held and delivered once
70
+ * `@OnConnect` has settled, up to 64 messages or 1 MiB; beyond either the socket is
71
+ * closed with `1008`. A socket that closes while this runs never reaches
72
+ * `@OnConnect` or its user room.
55
73
  */
56
74
  resolveUser: (request: IncomingMessage) => Promise<WsAuthenticatedUser | null> | WsAuthenticatedUser | null;
57
75
  /**
58
- * When true, each authenticated socket auto-joins `user:<id>` immediately
59
- * after `resolveUser` resolves. Pairs with {@link WsUserBroadcaster}.
76
+ * Join each authenticated socket to `user:<id>` as soon as `resolveUser`
77
+ * resolves (default: `true` — pass `false` to opt out). Pairs with
78
+ * {@link WsUserBroadcaster}.
60
79
  */
61
80
  autoJoinUserRoom?: boolean;
62
81
  /**
@@ -76,10 +95,12 @@ interface WsAdapterOptions {
76
95
  auth?: WsAuthConfig;
77
96
  }
78
97
  /**
79
- * Per-user broadcasting across all WS namespaces. Registered on the DI
80
- * container when {@link WsAdapterOptions.auth} is configured, but the
81
- * underlying room (`user:<id>`) can also be joined manually by any
82
- * controller — the helper works either way.
98
+ * Per-user broadcasting across all WS namespaces. Always registered on the DI
99
+ * container. With {@link WsAdapterOptions.auth} sockets join `user:<id>`
100
+ * automatically; without it a controller can join the room manually and the
101
+ * helper works the same.
102
+ *
103
+ * Reaches sockets in THIS process only.
83
104
  */
84
105
  interface WsUserBroadcaster {
85
106
  /** Send a single event to every socket bound to this user. */
@@ -92,11 +113,11 @@ interface WsUserBroadcaster {
92
113
  roomFor(userId: string): string;
93
114
  }
94
115
  /** DI token for the live {@link WsAdapter} instance. */
95
- declare const WS_ADAPTER: import("@forinda/kickjs").InjectionToken<unknown>;
116
+ export declare const WS_ADAPTER: import("@forinda/kickjs").InjectionToken<unknown>;
96
117
  /** DI token for the shared {@link RoomManager}. */
97
- declare const WS_ROOM_MANAGER: import("@forinda/kickjs").InjectionToken<RoomManager>;
118
+ export declare const WS_ROOM_MANAGER: import("@forinda/kickjs").InjectionToken<RoomManager>;
98
119
  /** DI token for the per-user broadcaster helper. */
99
- declare const WS_USER_BROADCASTER: import("@forinda/kickjs").InjectionToken<WsUserBroadcaster>;
120
+ export declare const WS_USER_BROADCASTER: import("@forinda/kickjs").InjectionToken<WsUserBroadcaster>;
100
121
  //#endregion
101
122
  //#region src/ws-adapter.d.ts
102
123
  /**
@@ -152,7 +173,7 @@ interface WsAdapterExtensions {
152
173
  * Clients connect to: `ws://localhost:3000/ws/chat`
153
174
  * Messages are JSON: `{ "event": "send", "data": { "text": "hello" } }`
154
175
  */
155
- declare const WsAdapter: import("@forinda/kickjs").AdapterFactory<WsAdapterOptions, WsAdapterExtensions>;
176
+ export declare const WsAdapter: import("@forinda/kickjs").AdapterFactory<WsAdapterOptions, WsAdapterExtensions>;
156
177
  //#endregion
157
178
  //#region src/ws-context.d.ts
158
179
  /**
@@ -171,11 +192,13 @@ declare const WsAdapter: import("@forinda/kickjs").AdapterFactory<WsAdapterOptio
171
192
  * }
172
193
  * ```
173
194
  */
174
- declare class WsContext {
195
+ export declare class WsContext {
175
196
  readonly socket: WebSocket;
176
197
  readonly server: WebSocketServer;
177
198
  private readonly roomManager;
178
199
  private readonly namespaceSockets;
200
+ /** Called with the number of frames each send wrote — feeds `messagesSent`. */
201
+ private readonly onSend?;
179
202
  /** Unique connection ID */
180
203
  readonly id: string;
181
204
  /** Parsed message payload (set for @OnMessage handlers) */
@@ -188,7 +211,9 @@ declare class WsContext {
188
211
  * cookies, headers, query string, and client IP for authenticated handshakes. */
189
212
  readonly request: IncomingMessage;
190
213
  private metadata;
191
- constructor(socket: WebSocket, server: WebSocketServer, roomManager: RoomManager, namespaceSockets: Map<string, WebSocket>, id: string, namespace: string, request: IncomingMessage);
214
+ constructor(socket: WebSocket, server: WebSocketServer, roomManager: RoomManager, namespaceSockets: Map<string, WebSocket>, id: string, namespace: string, request: IncomingMessage,
215
+ /** Called with the number of frames each send wrote — feeds `messagesSent`. */
216
+ onSend?: ((count: number) => void) | undefined);
192
217
  /** Parsed cookies from the upgrade request (raw `Cookie` header parse). */
193
218
  get cookies(): Record<string, string>;
194
219
  /** Get a metadata value */
@@ -201,7 +226,7 @@ declare class WsContext {
201
226
  broadcast(event: string, data: any): void;
202
227
  /** Send to all sockets in the same namespace including this one */
203
228
  broadcastAll(event: string, data: any): void;
204
- /** Join a room */
229
+ /** Join a room. Names are global across namespaces — see {@link RoomManager}. */
205
230
  join(room: string): void;
206
231
  /** Leave a room */
207
232
  leave(room: string): void;
@@ -230,7 +255,7 @@ declare class WsContext {
230
255
  * }
231
256
  * ```
232
257
  */
233
- declare function WsController(namespace?: string): ClassDecorator;
258
+ export declare function WsController(namespace?: string): ClassDecorator;
234
259
  /**
235
260
  * Handle new WebSocket connections.
236
261
  *
@@ -242,7 +267,7 @@ declare function WsController(namespace?: string): ClassDecorator;
242
267
  * }
243
268
  * ```
244
269
  */
245
- declare const OnConnect: () => MethodDecorator;
270
+ export declare const OnConnect: () => MethodDecorator;
246
271
  /**
247
272
  * Handle WebSocket disconnections.
248
273
  *
@@ -254,7 +279,7 @@ declare const OnConnect: () => MethodDecorator;
254
279
  * }
255
280
  * ```
256
281
  */
257
- declare const OnDisconnect: () => MethodDecorator;
282
+ export declare const OnDisconnect: () => MethodDecorator;
258
283
  /**
259
284
  * Handle WebSocket errors.
260
285
  *
@@ -266,7 +291,7 @@ declare const OnDisconnect: () => MethodDecorator;
266
291
  * }
267
292
  * ```
268
293
  */
269
- declare const OnError: () => MethodDecorator;
294
+ export declare const OnError: () => MethodDecorator;
270
295
  /**
271
296
  * Handle a specific WebSocket message event.
272
297
  * Use '*' as a catch-all for unmatched events.
@@ -286,7 +311,7 @@ declare const OnError: () => MethodDecorator;
286
311
  * }
287
312
  * ```
288
313
  */
289
- declare function OnMessage(event: string): MethodDecorator;
314
+ export declare function OnMessage(event: string): MethodDecorator;
290
315
  //#endregion
291
- 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 };
316
+ export type { WsAdapterOptions, WsAuthConfig, WsAuthenticatedUser, WsHandlerDefinition, WsHandlerType, WsUserBroadcaster };
292
317
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
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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"}
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":";;;;;;;;;;;;;;;;;qBAca;mBAEkB;;EAA7B,YAA6B,WAAU;;UAG/B;;UAEA;EAER,KAAK,kBAAkB,QAAQ,WAAW;EAY1C,MAAM,kBAAkB;;EAWxB,SAAS;EAaT,SAAS;EAIT,WAAW,eAAe,YAAY;;EAKtC,eAAe;;EASf,UAAU,cAAc,eAAe,WAAW;;;;qBCpEvC;WACX;WACA;;KAGU;UAEK;EACf,MAAM;;EAEN;;EAEA;;;;;;;UAQe;EACf;GACC;;UAGc;;;;;;;;;;;;;;EAcf,cACE,SAAS,oBACN,QAAQ,8BAA8B;;;;;;EAM3C;;;;;EAKA;;UAGe;;EAEf;;EAEA;;EAEA;;EAEA,OAAO;;;;;;;;;;UAWQ;;EAEf,OAAO;IAAmB,KAAK,eAAe;;;EAE9C,gBAAgB,gBAAgB,eAAe;;EAE/C,QAAQ;;;qBAIG,sCAAU;;qBAEV,2CAAe,eAAA;;qBAEf,+CAAmB,eAAA;;;;;;;;UCnDf;;EAEf;IACE;IACA;IACA;IACA;IACA;IACA,YAAY;MAAiB;MAAqB;;IAClD,OAAO,WAAW;;;EAGpB,SAAS;;EAET,gBAAgB,gBAAgB,eAAe;;WAEtC,kBAAkB;;WAElB,mBAAmB;;WAEnB,kBAAkB;;WAElB,cAAc;;WAEd,UAAU;;;;;;;;;;;;;;;;;;;;;qBAsBR,qCAAS,eAAA,kBAAA;;;;;;;;;;;;;;;;;;;qBCxET;WAgBA,QAAQ;WACR,QAAQ;mBACA;mBACA;;mBAKA;;WAtBV;;EAET;;EAEA;;WAES;;;WAGA,SAAS;UAEV;EAER,YACW,QAAQ,WACR,QAAQ,iBACA,aAAa,aACb,kBAAkB,YAAY,YAC/C,YACA,mBACA,SAAS,iBAEQ;;EAAA,WAAU;;MAUzB,WAAW;;EAef,IAAI,SAAS,cAAc;;EAK3B,IAAI,aAAa;;EAKjB,KAAK,eAAe;;EAQpB,UAAU,eAAe;;EAazB,aAAa,eAAe;;EAa5B,KAAK;;EAKL,MAAM;;EAKN;;EAKA,GAAG;IAAiB,KAAK,eAAe;;;;;;;;;;;;;;;;;;;;;wBC7G1B,aAAa,qBAAqB;;;;;;;;;;;;qBA+BrC,iBAtBA;;;;;;;;;;;;qBAmCA,oBAnCA;;;;;;;;;;;;qBAgDA,eAhDA;;;;;;;;;;;;;;;;;;;;wBAqEG,UAAU,gBAAgB"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-ws v7.0.1
2
+ * @forinda/kickjs-ws v7.0.2
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{randomUUID}from"node:crypto";import{WebSocketServer}from"ws";import{Service,createLogger,createToken,defineAdapter,getClassMeta,getClassMetaOrUndefined,pushClassMeta,ref,setClassMeta}from"@forinda/kickjs";const WS_METADATA={WS_CONTROLLER:`kick/ws/controller`,WS_HANDLERS:`kick/ws/handlers`},WS_ADAPTER=createToken(`kick/ws/Adapter`),WS_ROOM_MANAGER=createToken(`kick/ws/RoomManager`),WS_USER_BROADCASTER=createToken(`kick/ws/UserBroadcaster`),wsControllerRegistry=new Set;var WsContext=class{socket;server;roomManager;namespaceSockets;id;data;event;namespace;request;metadata=new Map;constructor(socket,server,roomManager,namespaceSockets,id,namespace,request){this.socket=socket,this.server=server,this.roomManager=roomManager,this.namespaceSockets=namespaceSockets,this.id=id,this.namespace=namespace,this.request=request,this.data=null,this.event=``}get cookies(){let header=this.request.headers.cookie;if(!header)return{};let out={};for(let part of header.split(`;`)){let idx=part.indexOf(`=`);if(idx===-1)continue;let k=part.slice(0,idx).trim(),v=part.slice(idx+1).trim();k&&(out[k]=decodeURIComponent(v))}return out}get(key){return this.metadata.get(key)}set(key,value){this.metadata.set(key,value)}send(event,data){this.socket.readyState===this.socket.OPEN&&this.socket.send(JSON.stringify({event,data}))}broadcast(event,data){let message=JSON.stringify({event,data});for(let[id,socket]of this.namespaceSockets)id!==this.id&&socket.readyState===socket.OPEN&&socket.send(message)}broadcastAll(event,data){let message=JSON.stringify({event,data});for(let[,socket]of this.namespaceSockets)socket.readyState===socket.OPEN&&socket.send(message)}join(room){this.roomManager.join(this.id,this.socket,room)}leave(room){this.roomManager.leave(this.id,room)}rooms(){return this.roomManager.getRooms(this.id)}to(room){return{send:(event,data)=>{this.roomManager.broadcast(room,event,data)}}}},RoomManager=class{socketRooms=new Map;roomSockets=new Map;join(socketId,socket,room){this.socketRooms.has(socketId)||this.socketRooms.set(socketId,new Set),this.socketRooms.get(socketId).add(room),this.roomSockets.has(room)||this.roomSockets.set(room,new Map),this.roomSockets.get(room).set(socketId,socket)}leave(socketId,room){this.socketRooms.get(socketId)?.delete(room),this.roomSockets.get(room)?.delete(socketId),this.roomSockets.get(room)?.size===0&&this.roomSockets.delete(room)}leaveAll(socketId){let rooms=this.socketRooms.get(socketId);if(rooms)for(let room of rooms)this.roomSockets.get(room)?.delete(socketId),this.roomSockets.get(room)?.size===0&&this.roomSockets.delete(room);this.socketRooms.delete(socketId)}getRooms(socketId){return Array.from(this.socketRooms.get(socketId)??[])}getSockets(room){return this.roomSockets.get(room)??new Map}getAllRooms(){let result={};for(let[room,sockets]of this.roomSockets)result[room]=sockets.size;return result}broadcast(room,event,data,excludeId){let sockets=this.roomSockets.get(room);if(!sockets)return;let message=JSON.stringify({event,data});for(let[id,socket]of sockets)id!==excludeId&&socket.readyState===socket.OPEN&&socket.send(message)}};const log=createLogger(`WsAdapter`),WsAdapter=defineAdapter({name:`WsAdapter`,defaults:{path:`/ws`,heartbeatInterval:3e4},build:options=>{let basePath=options.path,heartbeatInterval=options.heartbeatInterval,maxPayload=options.maxPayload,auth=options.auth,userRoomPrefix=options.auth?.userRoomPrefix??`user:`,wss=null,container=null,namespaces=new Map,roomManager=new RoomManager,heartbeatTimer=null,totalConnections=ref(0),activeConnections=ref(0),messagesReceived=ref(0),messagesSent=ref(0),wsErrors=ref(0),userRoom=userId=>userRoomPrefix+userId,broadcastToUser=(userId,event,data)=>{roomManager.broadcast(userRoom(userId),event,data)},buildUserBroadcaster=()=>({roomFor:id=>userRoom(id),broadcastToUser:(id,event,data)=>broadcastToUser(id,event,data),toUser:id=>({send:(event,data)=>broadcastToUser(id,event,data)})}),getStats=()=>{let namespaceStats={};for(let[path,entry]of namespaces)namespaceStats[path]={connections:entry.sockets.size,handlers:entry.handlers.length};return{totalConnections:totalConnections.value,activeConnections:activeConnections.value,messagesReceived:messagesReceived.value,messagesSent:messagesSent.value,errors:wsErrors.value,namespaces:namespaceStats,rooms:roomManager.getAllRooms()}},safeInvoke=(controller,method,ctx)=>{try{let result=controller[method](ctx);result instanceof Promise&&result.catch(err=>{log.error({err},`WS handler error in ${method}`)})}catch(err){log.error({err},`WS handler error in ${method}`)}},invokeHandlers=(controller,handlers,type,ctx)=>{for(let handler of handlers)handler.type===type&&safeInvoke(controller,handler.handlerName,ctx)},authenticate=async ctx=>{if(!auth)return!0;try{let user=await auth.resolveUser(ctx.request);return!user||!user.id?(ctx.socket.close(4401,`Unauthorized`),!1):(ctx.set(`user`,user),ctx.set(`userId`,user.id),auth.autoJoinUserRoom!==!1&&ctx.join(userRoom(user.id)),!0)}catch(err){return log.warn(`WS auth failed`,err),ctx.socket.close(4401,`Unauthorized`),!1}},handleConnection=(ws,entry,request)=>{let socketId=randomUUID();ws.__alive=!0,entry.sockets.set(socketId,ws),totalConnections.value++,activeConnections.value++;let ctx=new WsContext(ws,wss,roomManager,entry.sockets,socketId,entry.namespace,request);entry.contexts.set(socketId,ctx);let controller=container.resolve(entry.controllerClass);ws.on(`pong`,()=>{ws.__alive=!0});let authed=auth===void 0;(auth?authenticate(ctx):Promise.resolve(!0)).then(ok=>{ok&&(authed=!0,invokeHandlers(controller,entry.handlers,`connect`,ctx))}),ws.on(`message`,raw=>{if(authed){messagesReceived.value++;try{let parsed=JSON.parse(raw.toString()),event=parsed.event,data=parsed.data;if(!event||typeof event!=`string`){ctx.send(`error`,{message:`Invalid message format: missing "event" field`});return}ctx.event=event,ctx.data=data;let handler=entry.handlers.find(h=>h.type===`message`&&h.event===event);if(handler)safeInvoke(controller,handler.handlerName,ctx);else{let catchAll=entry.handlers.find(h=>h.type===`message`&&h.event===`*`);catchAll&&safeInvoke(controller,catchAll.handlerName,ctx)}}catch{ctx.data={message:`Invalid JSON`},invokeHandlers(controller,entry.handlers,`error`,ctx)}}}),ws.on(`close`,()=>{activeConnections.value--,invokeHandlers(controller,entry.handlers,`disconnect`,ctx),roomManager.leaveAll(socketId),entry.sockets.delete(socketId),entry.contexts.delete(socketId)}),ws.on(`error`,err=>{wsErrors.value++,ctx.data={message:err.message,name:err.name},invokeHandlers(controller,entry.handlers,`error`,ctx)})};return{getStats,userRoom,broadcastToUser,totalConnections,activeConnections,messagesReceived,messagesSent,wsErrors,beforeStart({container:containerArg}){container=containerArg,container.registerInstance(WS_ADAPTER,{getStats,userRoom,broadcastToUser,totalConnections,activeConnections,messagesReceived,messagesSent,wsErrors}),container.registerInstance(WS_ROOM_MANAGER,roomManager),container.registerInstance(WS_USER_BROADCASTER,buildUserBroadcaster());for(let controllerClass of wsControllerRegistry){let namespace=getClassMetaOrUndefined(WS_METADATA.WS_CONTROLLER,controllerClass);if(namespace===void 0)continue;let handlers=getClassMeta(WS_METADATA.WS_HANDLERS,controllerClass,[]),fullPath=basePath+(namespace===`/`?``:namespace);namespaces.set(fullPath,{namespace,controllerClass,handlers,sockets:new Map,contexts:new Map}),log.info(`Registered WS namespace: ${fullPath} (${controllerClass.name})`)}},afterStart({server}){if(!server)return;wss=new WebSocketServer({noServer:!0,maxPayload}),server.on(`upgrade`,(request,socket,head)=>{let pathname=(request.url||`/`).split(`?`)[0],entry=namespaces.get(pathname);if(!entry){socket.write(`HTTP/1.1 404 Not Found\r
11
+ import{randomUUID}from"node:crypto";import{WebSocketServer}from"ws";import{Service,createLogger,createToken,defineAdapter,getClassMeta,getClassMetaOrUndefined,pushClassMeta,ref,setClassMeta}from"@forinda/kickjs";const WS_METADATA={WS_CONTROLLER:`kick/ws/controller`,WS_HANDLERS:`kick/ws/handlers`},WS_ADAPTER=createToken(`kick/ws/Adapter`),WS_ROOM_MANAGER=createToken(`kick/ws/RoomManager`),WS_USER_BROADCASTER=createToken(`kick/ws/UserBroadcaster`),wsControllerRegistry=new Set;var WsContext=class{socket;server;roomManager;namespaceSockets;onSend;id;data;event;namespace;request;metadata=new Map;constructor(socket,server,roomManager,namespaceSockets,id,namespace,request,onSend){this.socket=socket,this.server=server,this.roomManager=roomManager,this.namespaceSockets=namespaceSockets,this.onSend=onSend,this.id=id,this.namespace=namespace,this.request=request,this.data=null,this.event=``}get cookies(){let header=this.request.headers.cookie;if(!header)return{};let out={};for(let part of header.split(`;`)){let idx=part.indexOf(`=`);if(idx===-1)continue;let k=part.slice(0,idx).trim(),v=part.slice(idx+1).trim();k&&(out[k]=decodeURIComponent(v))}return out}get(key){return this.metadata.get(key)}set(key,value){this.metadata.set(key,value)}send(event,data){this.socket.readyState===this.socket.OPEN&&(this.socket.send(JSON.stringify({event,data})),this.onSend?.(1))}broadcast(event,data){let message=JSON.stringify({event,data}),sent=0;for(let[id,socket]of this.namespaceSockets)id!==this.id&&socket.readyState===socket.OPEN&&(socket.send(message),sent++);sent&&this.onSend?.(sent)}broadcastAll(event,data){let message=JSON.stringify({event,data}),sent=0;for(let[,socket]of this.namespaceSockets)socket.readyState===socket.OPEN&&(socket.send(message),sent++);sent&&this.onSend?.(sent)}join(room){this.roomManager.join(this.id,this.socket,room)}leave(room){this.roomManager.leave(this.id,room)}rooms(){return this.roomManager.getRooms(this.id)}to(room){return{send:(event,data)=>{this.roomManager.broadcast(room,event,data)}}}},RoomManager=class{onSend;constructor(onSend){this.onSend=onSend}socketRooms=new Map;roomSockets=new Map;join(socketId,socket,room){this.socketRooms.has(socketId)||this.socketRooms.set(socketId,new Set),this.socketRooms.get(socketId).add(room),this.roomSockets.has(room)||this.roomSockets.set(room,new Map),this.roomSockets.get(room).set(socketId,socket)}leave(socketId,room){this.socketRooms.get(socketId)?.delete(room),this.roomSockets.get(room)?.delete(socketId),this.roomSockets.get(room)?.size===0&&this.roomSockets.delete(room)}leaveAll(socketId){let rooms=this.socketRooms.get(socketId);if(rooms)for(let room of rooms)this.roomSockets.get(room)?.delete(socketId),this.roomSockets.get(room)?.size===0&&this.roomSockets.delete(room);this.socketRooms.delete(socketId)}getRooms(socketId){return Array.from(this.socketRooms.get(socketId)??[])}getSockets(room){return this.roomSockets.get(room)??new Map}getAllRooms(){let result={};for(let[room,sockets]of this.roomSockets)result[room]=sockets.size;return result}broadcast(room,event,data,excludeId){let sockets=this.roomSockets.get(room);if(!sockets)return;let message=JSON.stringify({event,data}),sent=0;for(let[id,socket]of sockets)id!==excludeId&&socket.readyState===socket.OPEN&&(socket.send(message),sent++);sent&&this.onSend?.(sent)}};const log=createLogger(`WsAdapter`),WsAdapter=defineAdapter({name:`WsAdapter`,defaults:{path:`/ws`,heartbeatInterval:3e4},build:options=>{let basePath=options.path,heartbeatInterval=options.heartbeatInterval,maxPayload=options.maxPayload,auth=options.auth,userRoomPrefix=options.auth?.userRoomPrefix??`user:`,wss=null,container=null,namespaces=new Map,heartbeatTimer=null,totalConnections=ref(0),activeConnections=ref(0),messagesReceived=ref(0),messagesSent=ref(0),wsErrors=ref(0),countSent=n=>{messagesSent.value+=n},roomManager=new RoomManager(countSent),userRoom=userId=>userRoomPrefix+userId,broadcastToUser=(userId,event,data)=>{roomManager.broadcast(userRoom(userId),event,data)},buildUserBroadcaster=()=>({roomFor:id=>userRoom(id),broadcastToUser:(id,event,data)=>broadcastToUser(id,event,data),toUser:id=>({send:(event,data)=>broadcastToUser(id,event,data)})}),getStats=()=>{let namespaceStats={};for(let[path,entry]of namespaces)namespaceStats[path]={connections:entry.sockets.size,handlers:entry.handlers.length};return{totalConnections:totalConnections.value,activeConnections:activeConnections.value,messagesReceived:messagesReceived.value,messagesSent:messagesSent.value,errors:wsErrors.value,namespaces:namespaceStats,rooms:roomManager.getAllRooms()}},safeInvoke=async(controller,method,ctx)=>{try{await controller[method](ctx)}catch(err){log.error({err},`WS handler error in ${method}`)}},invokeHandlers=async(controller,handlers,type,ctx)=>{await Promise.all(handlers.filter(handler=>handler.type===type).map(handler=>safeInvoke(controller,handler.handlerName,ctx)))},authenticate=async ctx=>{if(!auth)return!0;try{let user=await auth.resolveUser(ctx.request);return ctx.socket.readyState===ctx.socket.OPEN?!user||!user.id?(ctx.socket.close(4401,`Unauthorized`),!1):(ctx.set(`user`,user),ctx.set(`userId`,user.id),auth.autoJoinUserRoom!==!1&&ctx.join(userRoom(user.id)),!0):!1}catch(err){return log.warn(`WS auth failed`,err),ctx.socket.close(4401,`Unauthorized`),!1}},handleConnection=(ws,entry,request)=>{let socketId=randomUUID();ws.__alive=!0,entry.sockets.set(socketId,ws),totalConnections.value++,activeConnections.value++;let ctx=new WsContext(ws,wss,roomManager,entry.sockets,socketId,entry.namespace,request,countSent);entry.contexts.set(socketId,ctx);let controller=container.resolve(entry.controllerClass);ws.on(`pong`,()=>{ws.__alive=!0});let handleMessage=raw=>{messagesReceived.value++;try{let parsed=JSON.parse(raw.toString()),event=parsed.event,data=parsed.data;if(!event||typeof event!=`string`){ctx.send(`error`,{message:`Invalid message format: missing "event" field`});return}ctx.event=event,ctx.data=data;let handler=entry.handlers.find(h=>h.type===`message`&&h.event===event);if(handler)safeInvoke(controller,handler.handlerName,ctx);else{let catchAll=entry.handlers.find(h=>h.type===`message`&&h.event===`*`);catchAll&&safeInvoke(controller,catchAll.handlerName,ctx)}}catch{ctx.data={message:`Invalid JSON`},invokeHandlers(controller,entry.handlers,`error`,ctx)}},ready=!1,pending=[],pendingBytes=0,dropPending=()=>{pending.length=0,pendingBytes=0};ws.on(`message`,raw=>{if(ready)return handleMessage(raw);let size=typeof raw==`string`?Buffer.byteLength(raw):raw.length;if(pending.length>=64||pendingBytes+size>1048576){dropPending(),ws.close(1008,`Too many messages before connect`);return}pending.push(raw),pendingBytes+=size}),(auth?authenticate(ctx):Promise.resolve(!0)).then(async ok=>{if(!ok||ws.readyState!==ws.OPEN||(await invokeHandlers(controller,entry.handlers,`connect`,ctx),ws.readyState!==ws.OPEN))return dropPending();ready=!0;for(let raw of pending.splice(0))handleMessage(raw);pendingBytes=0}),ws.on(`close`,()=>{dropPending(),activeConnections.value--,invokeHandlers(controller,entry.handlers,`disconnect`,ctx),roomManager.leaveAll(socketId),entry.sockets.delete(socketId),entry.contexts.delete(socketId)}),ws.on(`error`,err=>{wsErrors.value++,ctx.data={message:err.message,name:err.name},invokeHandlers(controller,entry.handlers,`error`,ctx)})};return{getStats,userRoom,broadcastToUser,totalConnections,activeConnections,messagesReceived,messagesSent,wsErrors,beforeStart({container:containerArg}){container=containerArg,container.registerInstance(WS_ADAPTER,{getStats,userRoom,broadcastToUser,totalConnections,activeConnections,messagesReceived,messagesSent,wsErrors}),container.registerInstance(WS_ROOM_MANAGER,roomManager),container.registerInstance(WS_USER_BROADCASTER,buildUserBroadcaster());for(let controllerClass of wsControllerRegistry){let namespace=getClassMetaOrUndefined(WS_METADATA.WS_CONTROLLER,controllerClass);if(namespace===void 0)continue;let handlers=getClassMeta(WS_METADATA.WS_HANDLERS,controllerClass,[]),fullPath=basePath+(namespace===`/`?``:namespace);namespaces.set(fullPath,{namespace,controllerClass,handlers,sockets:new Map,contexts:new Map}),log.info(`Registered WS namespace: ${fullPath} (${controllerClass.name})`)}},afterStart({server}){if(!server)return;wss=new WebSocketServer({noServer:!0,maxPayload}),server.on(`upgrade`,(request,socket,head)=>{let pathname=(request.url||`/`).split(`?`)[0],entry=namespaces.get(pathname);if(!entry){if(server.listenerCount(`upgrade`)>1)return;socket.write(`HTTP/1.1 404 Not Found\r
12
12
  \r
13
13
  `),socket.destroy();return}wss.handleUpgrade(request,socket,head,ws=>{handleConnection(ws,entry,request)})}),heartbeatInterval>0&&(heartbeatTimer=setInterval(()=>{for(let[,entry]of namespaces)for(let[,socket]of entry.sockets){if(socket.__alive===!1){socket.terminate();continue}socket.__alive=!1,socket.ping()}},heartbeatInterval));let totalHandlers=Array.from(namespaces.values()).reduce((sum,e)=>sum+e.handlers.length,0);log.info(`WebSocket ready — ${namespaces.size} namespace(s), ${totalHandlers} handler(s)`)},shutdown(){heartbeatTimer&&clearInterval(heartbeatTimer);for(let[,entry]of namespaces){for(let[,socket]of entry.sockets)socket.close(1001,`Server shutting down`);entry.sockets.clear(),entry.contexts.clear()}wss?.close()}}}});function WsController(namespace){return target=>{Service()(target),setClassMeta(WS_METADATA.WS_CONTROLLER,namespace||`/`,target),wsControllerRegistry.add(target)}}function createWsHandlerDecorator(type,event){return()=>(target,propertyKey)=>{pushClassMeta(WS_METADATA.WS_HANDLERS,target.constructor,{type,event,handlerName:propertyKey})}}const OnConnect=createWsHandlerDecorator(`connect`),OnDisconnect=createWsHandlerDecorator(`disconnect`),OnError=createWsHandlerDecorator(`error`);function OnMessage(event){return(target,propertyKey)=>{pushClassMeta(WS_METADATA.WS_HANDLERS,target.constructor,{type:`message`,event,handlerName:propertyKey})}}export{OnConnect,OnDisconnect,OnError,OnMessage,RoomManager,WS_ADAPTER,WS_METADATA,WS_ROOM_MANAGER,WS_USER_BROADCASTER,WsAdapter,WsContext,WsController};
14
14
  //# 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":["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\n// String metadata keys (post-Symbol migration). Slash-delimited under\n// `kick/ws/` for consistency with other framework decorators and to keep\n// Reflect.metadata storage collision-safe.\nexport const WS_METADATA = {\n WS_CONTROLLER: 'kick/ws/controller',\n WS_HANDLERS: '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>('kick/ws/Adapter')\n/** DI token for the shared {@link RoomManager}. */\nexport const WS_ROOM_MANAGER = createToken<RoomManager>('kick/ws/RoomManager')\n/** DI token for the per-user broadcaster helper. */\nexport const WS_USER_BROADCASTER = createToken<WsUserBroadcaster>('kick/ws/UserBroadcaster')\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 defineAdapter,\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 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 * Public extension methods exposed by a WsAdapter instance — broadcast\n * helpers, namespace stats, and reactive counters that DevTools and\n * other adapters consume directly.\n */\nexport interface WsAdapterExtensions {\n /** Snapshot of WebSocket stats — consumed by DevTools / Swagger ws-server discovery. */\n getStats(): {\n totalConnections: number\n activeConnections: number\n messagesReceived: number\n messagesSent: number\n errors: number\n namespaces: Record<string, { connections: number; handlers: number }>\n rooms: ReturnType<RoomManager['getAllRooms']>\n }\n /** Room name used for per-user broadcasting. */\n userRoom(userId: string): string\n /** Broadcast a single event to every socket in `user:<id>` (across namespaces). */\n broadcastToUser(userId: string, event: string, data: unknown): void\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\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 * 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 const WsAdapter = defineAdapter<WsAdapterOptions, WsAdapterExtensions>({\n name: 'WsAdapter',\n defaults: {\n path: '/ws',\n heartbeatInterval: 30000,\n },\n build: (options) => {\n const basePath = options.path!\n const heartbeatInterval = options.heartbeatInterval!\n const maxPayload = options.maxPayload\n const auth = options.auth\n const userRoomPrefix = options.auth?.userRoomPrefix ?? 'user:'\n\n let wss: WebSocketServer | null = null\n let container: Container | null = null\n const namespaces = new Map<string, NamespaceEntry>()\n const roomManager = new RoomManager()\n let heartbeatTimer: ReturnType<typeof setInterval> | null = null\n\n const totalConnections = ref(0)\n const activeConnections = ref(0)\n const messagesReceived = ref(0)\n const messagesSent = ref(0)\n const wsErrors = ref(0)\n\n const userRoom = (userId: string): string => userRoomPrefix + userId\n\n const broadcastToUser = (userId: string, event: string, data: unknown): void => {\n roomManager.broadcast(userRoom(userId), event, data)\n }\n\n const buildUserBroadcaster = (): WsUserBroadcaster => ({\n roomFor: (id) => userRoom(id),\n broadcastToUser: (id, event, data) => broadcastToUser(id, event, data),\n toUser: (id) => ({\n send: (event, data) => broadcastToUser(id, event, data),\n }),\n })\n\n const getStats = () => {\n const namespaceStats: Record<string, { connections: number; handlers: number }> = {}\n for (const [path, entry] of namespaces) {\n namespaceStats[path] = {\n connections: entry.sockets.size,\n handlers: entry.handlers.length,\n }\n }\n return {\n totalConnections: totalConnections.value,\n activeConnections: activeConnections.value,\n messagesReceived: messagesReceived.value,\n messagesSent: messagesSent.value,\n errors: wsErrors.value,\n namespaces: namespaceStats,\n rooms: roomManager.getAllRooms(),\n }\n }\n\n const 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 const 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 safeInvoke(controller, handler.handlerName, ctx)\n }\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 const authenticate = async (ctx: WsContext): Promise<boolean> => {\n if (!auth) return true\n try {\n const user = await auth.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 (auth.autoJoinUserRoom !== false) {\n ctx.join(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 const handleConnection = (\n ws: WebSocket,\n entry: NamespaceEntry,\n request: IncomingMessage,\n ): void => {\n const socketId = randomUUID()\n ;(ws as any).__alive = true\n\n entry.sockets.set(socketId, ws)\n totalConnections.value++\n activeConnections.value++\n\n const ctx = new WsContext(\n ws,\n wss!,\n roomManager,\n entry.sockets,\n socketId,\n entry.namespace,\n request,\n )\n entry.contexts.set(socketId, ctx)\n\n const controller = container!.resolve(entry.controllerClass)\n\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 = auth === undefined\n const authPromise = auth ? authenticate(ctx) : Promise.resolve(true)\n\n authPromise.then((ok) => {\n if (!ok) return\n authed = true\n invokeHandlers(controller, entry.handlers, 'connect', ctx)\n })\n\n ws.on('message', (raw: Buffer | string) => {\n if (!authed) return\n 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 const handler = entry.handlers.find((h) => h.type === 'message' && h.event === event)\n\n if (handler) {\n safeInvoke(controller, handler.handlerName, ctx)\n } else {\n const catchAll = entry.handlers.find((h) => h.type === 'message' && h.event === '*')\n if (catchAll) {\n safeInvoke(controller, catchAll.handlerName, ctx)\n }\n }\n } catch {\n ctx.data = { message: 'Invalid JSON' }\n invokeHandlers(controller, entry.handlers, 'error', ctx)\n }\n })\n\n ws.on('close', () => {\n activeConnections.value--\n invokeHandlers(controller, entry.handlers, 'disconnect', ctx)\n roomManager.leaveAll(socketId)\n entry.sockets.delete(socketId)\n entry.contexts.delete(socketId)\n })\n\n ws.on('error', (err: Error) => {\n wsErrors.value++\n ctx.data = { message: err.message, name: err.name }\n invokeHandlers(controller, entry.handlers, 'error', ctx)\n })\n }\n\n return {\n getStats,\n userRoom,\n broadcastToUser,\n totalConnections,\n activeConnections,\n messagesReceived,\n messagesSent,\n wsErrors,\n\n beforeStart({ container: containerArg }) {\n container = containerArg\n\n // The factory's mutate-name pattern means `this` inside lifecycle\n // hooks does not reach the returned adapter object; pass the\n // adapter itself via WS_ADAPTER for code that wants the full\n // surface (broadcast helpers, stats, refs).\n // We don't have a `this` reference for the WS_ADAPTER token, so\n // we rebuild the externally-visible surface inline here.\n container.registerInstance(WS_ADAPTER, {\n getStats,\n userRoom,\n broadcastToUser,\n totalConnections,\n activeConnections,\n messagesReceived,\n messagesSent,\n wsErrors,\n })\n container.registerInstance(WS_ROOM_MANAGER, roomManager)\n container.registerInstance(WS_USER_BROADCASTER, buildUserBroadcaster())\n\n // Discover all @WsController classes and build routing table\n for (const controllerClass of wsControllerRegistry) {\n const namespace = getClassMetaOrUndefined<string>(\n WS_METADATA.WS_CONTROLLER,\n controllerClass,\n )\n if (namespace === undefined) continue\n\n const handlers = getClassMeta<WsHandlerDefinition[]>(\n WS_METADATA.WS_HANDLERS,\n controllerClass,\n [],\n )\n\n const fullPath = basePath + (namespace === '/' ? '' : namespace)\n\n 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 }) {\n if (!server) return\n\n wss = new WebSocketServer({\n noServer: true,\n 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 = 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 wss!.handleUpgrade(request, socket, head, (ws) => {\n handleConnection(ws, entry, request)\n })\n })\n\n // Heartbeat ping/pong\n if (heartbeatInterval > 0) {\n heartbeatTimer = setInterval(() => {\n for (const [, entry] of 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 }, heartbeatInterval)\n }\n\n const totalHandlers = Array.from(namespaces.values()).reduce(\n (sum, e) => sum + e.handlers.length,\n 0,\n )\n log.info(`WebSocket ready — ${namespaces.size} namespace(s), ${totalHandlers} handler(s)`)\n },\n\n shutdown() {\n if (heartbeatTimer) {\n clearInterval(heartbeatTimer)\n }\n\n // Close all connections\n for (const [, entry] of 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 wss?.close()\n },\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\n// String metadata keys (post-Symbol migration). Slash-delimited under\n// `kick/ws/` for consistency with other framework decorators and to keep\n// Reflect.metadata storage collision-safe.\nexport const WS_METADATA = {\n WS_CONTROLLER: 'kick/ws/controller',\n WS_HANDLERS: '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. The whole object is stored on the `WsContext` as `user`, and\n * `id` alone as `userId` — read them with `ctx.get('user')` / `ctx.get('userId')`.\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, before\n * any `@OnConnect` handler fires. Return `null` or throw to reject.\n *\n * A rejected socket is accepted and then closed with code `4401` — a\n * WebSocket close code, not an HTTP status. Browsers do not expose a failed\n * handshake's status to script, so a close code is what a client can act on.\n *\n * Messages the client sends before this settles are held and delivered once\n * `@OnConnect` has settled, up to 64 messages or 1 MiB; beyond either the socket is\n * closed with `1008`. A socket that closes while this runs never reaches\n * `@OnConnect` or its user room.\n */\n resolveUser: (\n request: IncomingMessage,\n ) => Promise<WsAuthenticatedUser | null> | WsAuthenticatedUser | null\n /**\n * Join each authenticated socket to `user:<id>` as soon as `resolveUser`\n * resolves (default: `true` — pass `false` to opt out). Pairs with\n * {@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. Always registered on the DI\n * container. With {@link WsAdapterOptions.auth} sockets join `user:<id>`\n * automatically; without it a controller can join the room manually and the\n * helper works the same.\n *\n * Reaches sockets in THIS process only.\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>('kick/ws/Adapter')\n/** DI token for the shared {@link RoomManager}. */\nexport const WS_ROOM_MANAGER = createToken<RoomManager>('kick/ws/RoomManager')\n/** DI token for the per-user broadcaster helper. */\nexport const WS_USER_BROADCASTER = createToken<WsUserBroadcaster>('kick/ws/UserBroadcaster')\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 /** Called with the number of frames each send wrote — feeds `messagesSent`. */\n private readonly onSend?: (count: number) => void,\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 this.onSend?.(1)\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 let sent = 0\n for (const [id, socket] of this.namespaceSockets) {\n if (id !== this.id && socket.readyState === socket.OPEN) {\n socket.send(message)\n sent++\n }\n }\n if (sent) this.onSend?.(sent)\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 let sent = 0\n for (const [, socket] of this.namespaceSockets) {\n if (socket.readyState === socket.OPEN) {\n socket.send(message)\n sent++\n }\n }\n if (sent) this.onSend?.(sent)\n }\n\n /** Join a room. Names are global across namespaces — see {@link RoomManager}. */\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 *\n * Room names are GLOBAL, not per namespace: a room joined from `/ws/chat` and\n * one joined from `/ws/admin` with the same name are the same room. That is\n * what lets a service broadcast through `WS_ROOM_MANAGER` and lets `user:<id>`\n * reach a user's sockets in every namespace — prefix names (`chat:lobby`) when\n * namespaces must not overlap.\n *\n * Membership lives in this process only. A second instance has its own rooms.\n */\nexport class RoomManager {\n /** @param onSend Called with the number of frames each broadcast wrote. */\n constructor(private readonly onSend?: (count: number) => void) {}\n\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 let sent = 0\n for (const [id, socket] of sockets) {\n if (id !== excludeId && socket.readyState === socket.OPEN) {\n socket.send(message)\n sent++\n }\n }\n if (sent) this.onSend?.(sent)\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 defineAdapter,\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 WsHandlerDefinition,\n type WsUserBroadcaster,\n} from './interfaces'\nimport { WsContext } from './ws-context'\nimport { RoomManager } from './room-manager'\n\nconst log = createLogger('WsAdapter')\n\n/** Messages held from a socket whose `auth.resolveUser` has not settled yet. */\nconst MAX_PENDING_BEFORE_AUTH = 64\n/** Total bytes held before auth. `maxPayload` bounds one message, not the queue. */\nconst MAX_PENDING_BYTES_BEFORE_AUTH = 1024 * 1024\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 * Public extension methods exposed by a WsAdapter instance — broadcast\n * helpers, namespace stats, and reactive counters that DevTools and\n * other adapters consume directly.\n */\nexport interface WsAdapterExtensions {\n /** Snapshot of WebSocket stats — consumed by DevTools / Swagger ws-server discovery. */\n getStats(): {\n totalConnections: number\n activeConnections: number\n messagesReceived: number\n messagesSent: number\n errors: number\n namespaces: Record<string, { connections: number; handlers: number }>\n rooms: ReturnType<RoomManager['getAllRooms']>\n }\n /** Room name used for per-user broadcasting. */\n userRoom(userId: string): string\n /** Broadcast a single event to every socket in `user:<id>` (across namespaces). */\n broadcastToUser(userId: string, event: string, data: unknown): void\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\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 * 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 const WsAdapter = defineAdapter<WsAdapterOptions, WsAdapterExtensions>({\n name: 'WsAdapter',\n defaults: {\n path: '/ws',\n heartbeatInterval: 30000,\n },\n build: (options) => {\n const basePath = options.path!\n const heartbeatInterval = options.heartbeatInterval!\n const maxPayload = options.maxPayload\n const auth = options.auth\n const userRoomPrefix = options.auth?.userRoomPrefix ?? 'user:'\n\n let wss: WebSocketServer | null = null\n let container: Container | null = null\n const namespaces = new Map<string, NamespaceEntry>()\n let heartbeatTimer: ReturnType<typeof setInterval> | null = null\n\n const totalConnections = ref(0)\n const activeConnections = ref(0)\n const messagesReceived = ref(0)\n const messagesSent = ref(0)\n const wsErrors = ref(0)\n\n // Every frame written through a context or a room lands here. The counter\n // was declared and exposed (devtools reads it) but never incremented.\n const countSent = (n: number): void => {\n messagesSent.value += n\n }\n const roomManager = new RoomManager(countSent)\n\n const userRoom = (userId: string): string => userRoomPrefix + userId\n\n const broadcastToUser = (userId: string, event: string, data: unknown): void => {\n roomManager.broadcast(userRoom(userId), event, data)\n }\n\n const buildUserBroadcaster = (): WsUserBroadcaster => ({\n roomFor: (id) => userRoom(id),\n broadcastToUser: (id, event, data) => broadcastToUser(id, event, data),\n toUser: (id) => ({\n send: (event, data) => broadcastToUser(id, event, data),\n }),\n })\n\n const getStats = () => {\n const namespaceStats: Record<string, { connections: number; handlers: number }> = {}\n for (const [path, entry] of namespaces) {\n namespaceStats[path] = {\n connections: entry.sockets.size,\n handlers: entry.handlers.length,\n }\n }\n return {\n totalConnections: totalConnections.value,\n activeConnections: activeConnections.value,\n messagesReceived: messagesReceived.value,\n messagesSent: messagesSent.value,\n errors: wsErrors.value,\n namespaces: namespaceStats,\n rooms: roomManager.getAllRooms(),\n }\n }\n\n /** Never rejects: errors are logged. Resolves once an async handler settles. */\n const safeInvoke = async (controller: any, method: string, ctx: WsContext): Promise<void> => {\n try {\n await controller[method](ctx)\n } catch (err) {\n log.error({ err }, `WS handler error in ${method}`)\n }\n }\n\n /** Starts every handler of `type` in order; resolves when all have settled. */\n const invokeHandlers = async (\n controller: any,\n handlers: WsHandlerDefinition[],\n type: WsHandlerDefinition['type'],\n ctx: WsContext,\n ): Promise<void> => {\n await Promise.all(\n handlers\n .filter((handler) => handler.type === type)\n .map((handler) => safeInvoke(controller, handler.handlerName, 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 const authenticate = async (ctx: WsContext): Promise<boolean> => {\n if (!auth) return true\n try {\n const user = await auth.resolveUser(ctx.request)\n // Joining the user room after close would re-add a socket the close\n // handler already removed from every room.\n if (ctx.socket.readyState !== ctx.socket.OPEN) return false\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 (auth.autoJoinUserRoom !== false) {\n ctx.join(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 const handleConnection = (\n ws: WebSocket,\n entry: NamespaceEntry,\n request: IncomingMessage,\n ): void => {\n const socketId = randomUUID()\n ;(ws as any).__alive = true\n\n entry.sockets.set(socketId, ws)\n totalConnections.value++\n activeConnections.value++\n\n const ctx = new WsContext(\n ws,\n wss!,\n roomManager,\n entry.sockets,\n socketId,\n entry.namespace,\n request,\n countSent,\n )\n entry.contexts.set(socketId, ctx)\n\n const controller = container!.resolve(entry.controllerClass)\n\n ws.on('pong', () => {\n ;(ws as any).__alive = true\n })\n\n const handleMessage = (raw: Buffer | string): void => {\n 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 const handler = entry.handlers.find((h) => h.type === 'message' && h.event === event)\n\n if (handler) {\n safeInvoke(controller, handler.handlerName, ctx)\n } else {\n const catchAll = entry.handlers.find((h) => h.type === 'message' && h.event === '*')\n if (catchAll) {\n safeInvoke(controller, catchAll.handlerName, ctx)\n }\n }\n } catch {\n ctx.data = { message: 'Invalid JSON' }\n invokeHandlers(controller, entry.handlers, 'error', ctx)\n }\n }\n\n // A client usually sends as soon as the socket opens, which is before an\n // async `resolveUser` or `@OnConnect` settles — those messages were\n // silently dropped, or reached `@OnMessage` before connect setup ran.\n // They are held and replayed once every `@OnConnect` has settled, so\n // handlers see them in order. The hold is capped: the sender may not be\n // authenticated yet, so an unbounded queue would let anyone buffer memory\n // on the server before being rejected.\n let ready = false\n const pending: Array<Buffer | string> = []\n let pendingBytes = 0\n const dropPending = (): void => {\n pending.length = 0\n pendingBytes = 0\n }\n\n ws.on('message', (raw: Buffer | string) => {\n if (ready) return handleMessage(raw)\n const size = typeof raw === 'string' ? Buffer.byteLength(raw) : raw.length\n if (\n pending.length >= MAX_PENDING_BEFORE_AUTH ||\n pendingBytes + size > MAX_PENDING_BYTES_BEFORE_AUTH\n ) {\n dropPending()\n ws.close(1008, 'Too many messages before connect')\n return\n }\n pending.push(raw)\n pendingBytes += size\n })\n\n const authPromise = auth ? authenticate(ctx) : Promise.resolve(true)\n authPromise.then(async (ok) => {\n // Closed while resolveUser or @OnConnect ran (client left, or the\n // overflow above): going on would act on a dead socket after the close\n // handler already cleaned it up.\n if (!ok || ws.readyState !== ws.OPEN) return dropPending()\n await invokeHandlers(controller, entry.handlers, 'connect', ctx)\n if (ws.readyState !== ws.OPEN) return dropPending()\n ready = true\n for (const raw of pending.splice(0)) handleMessage(raw)\n pendingBytes = 0\n })\n\n ws.on('close', () => {\n dropPending()\n activeConnections.value--\n invokeHandlers(controller, entry.handlers, 'disconnect', ctx)\n roomManager.leaveAll(socketId)\n entry.sockets.delete(socketId)\n entry.contexts.delete(socketId)\n })\n\n ws.on('error', (err: Error) => {\n wsErrors.value++\n ctx.data = { message: err.message, name: err.name }\n invokeHandlers(controller, entry.handlers, 'error', ctx)\n })\n }\n\n return {\n getStats,\n userRoom,\n broadcastToUser,\n totalConnections,\n activeConnections,\n messagesReceived,\n messagesSent,\n wsErrors,\n\n beforeStart({ container: containerArg }) {\n container = containerArg\n\n // The factory's mutate-name pattern means `this` inside lifecycle\n // hooks does not reach the returned adapter object; pass the\n // adapter itself via WS_ADAPTER for code that wants the full\n // surface (broadcast helpers, stats, refs).\n // We don't have a `this` reference for the WS_ADAPTER token, so\n // we rebuild the externally-visible surface inline here.\n container.registerInstance(WS_ADAPTER, {\n getStats,\n userRoom,\n broadcastToUser,\n totalConnections,\n activeConnections,\n messagesReceived,\n messagesSent,\n wsErrors,\n })\n container.registerInstance(WS_ROOM_MANAGER, roomManager)\n container.registerInstance(WS_USER_BROADCASTER, buildUserBroadcaster())\n\n // Discover all @WsController classes and build routing table\n for (const controllerClass of wsControllerRegistry) {\n const namespace = getClassMetaOrUndefined<string>(\n WS_METADATA.WS_CONTROLLER,\n controllerClass,\n )\n if (namespace === undefined) continue\n\n const handlers = getClassMeta<WsHandlerDefinition[]>(\n WS_METADATA.WS_HANDLERS,\n controllerClass,\n [],\n )\n\n const fullPath = basePath + (namespace === '/' ? '' : namespace)\n\n 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 }) {\n if (!server) return\n\n wss = new WebSocketServer({\n noServer: true,\n 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 = namespaces.get(pathname)\n if (!entry) {\n // Node calls EVERY 'upgrade' listener. Answering 404 here destroyed\n // sockets owned by other listeners on the same server — the\n // devtools bus, a GraphQL subscription server, Vite's HMR socket in\n // dev. Only close the socket when nothing else could be handling it.\n if (server.listenerCount('upgrade') > 1) return\n socket.write('HTTP/1.1 404 Not Found\\r\\n\\r\\n')\n socket.destroy()\n return\n }\n\n wss!.handleUpgrade(request, socket, head, (ws) => {\n handleConnection(ws, entry, request)\n })\n })\n\n // Heartbeat ping/pong\n if (heartbeatInterval > 0) {\n heartbeatTimer = setInterval(() => {\n for (const [, entry] of 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 }, heartbeatInterval)\n }\n\n const totalHandlers = Array.from(namespaces.values()).reduce(\n (sum, e) => sum + e.handlers.length,\n 0,\n )\n log.info(`WebSocket ready — ${namespaces.size} namespace(s), ${totalHandlers} handler(s)`)\n },\n\n shutdown() {\n if (heartbeatTimer) {\n clearInterval(heartbeatTimer)\n }\n\n // Close all connections\n for (const [, entry] of 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 wss?.close()\n },\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": "7.0.1",
3
+ "version": "7.0.2",
4
4
  "description": "WebSocket support with decorators, namespaces, rooms, and DI integration for KickJS",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -31,13 +31,13 @@
31
31
  "reflect-metadata": "^0.2.2"
32
32
  },
33
33
  "devDependencies": {
34
- "@swc/core": "^1.15.40",
35
- "@types/node": "^25.9.2",
34
+ "@swc/core": "^1.16.1",
35
+ "@types/node": "^26.4.1",
36
36
  "@types/ws": "^8.18.0",
37
- "typescript": "^6.0.3",
38
- "vitest": "^4.1.8",
39
- "ws": "^8.21.0",
40
- "@forinda/kickjs": "6.1.1"
37
+ "typescript": "^7.0.2",
38
+ "vitest": "^5.0.0",
39
+ "ws": "^8.21.3",
40
+ "@forinda/kickjs": "8.3.2"
41
41
  },
42
42
  "publishConfig": {
43
43
  "access": "public"
@@ -66,7 +66,8 @@
66
66
  "dev": "tsdown --watch",
67
67
  "test": "vitest run",
68
68
  "test:watch": "vitest",
69
- "typecheck": "tsgo --noEmit",
69
+ "typecheck": "tsc --noEmit",
70
+ "bench": "node bench/run.mjs",
70
71
  "clean": "rm -rf dist .wireit"
71
72
  }
72
73
  }