@@ -36,7 +36,7 @@ import {
3636 RoomRegistration ,
3737 WebSocketRequestContext ,
3838} from './websocket/room-policy' ;
39- import { InPosGuard , GlobalGuard } from './websocket/event-guards' ;
39+ import { InPosGuard , ForUserGuard } from './websocket/event-guards' ;
4040import { EventRegistry , ResolvedRoom } from './websocket/event-registry' ;
4141import { getPointOfSaleRelation } from './websocket/pos-relation-helper' ;
4242import RoleManager from '../rbac/role-manager' ;
@@ -79,6 +79,8 @@ export default class WebSocketService {
7979
8080 private readonly eventRegistry : EventRegistry = new EventRegistry ( ) ;
8181
82+ private connectionHandlerRegistered : boolean = false ;
83+
8284 /**
8385 * Creates a new WebSocketService instance.
8486 * @param options - The service options.
@@ -185,7 +187,14 @@ export default class WebSocketService {
185187 entityId : transaction . pointOfSale . id ,
186188 } ) ;
187189 }
188-
190+
191+ if ( transaction . from ?. id ) {
192+ rooms . push ( {
193+ roomName : `user:${ transaction . from . id } :transactions` ,
194+ entityId : transaction . from . id ,
195+ } ) ;
196+ }
197+
189198 rooms . push ( {
190199 roomName : 'transactions:all' ,
191200 entityId : null ,
@@ -194,15 +203,16 @@ export default class WebSocketService {
194203 return rooms ;
195204 } ,
196205 guard : async ( transaction , roomContext ) => {
197- if ( roomContext . isGlobal ) {
198- return GlobalGuard ( transaction , roomContext ) ;
206+ if ( roomContext . isGlobal ) return true ;
207+
208+ switch ( roomContext . entityType ) {
209+ case 'pos' :
210+ return InPosGuard ( transaction , roomContext ) ;
211+ case 'user' :
212+ return ForUserGuard ( transaction , roomContext ) ;
213+ default :
214+ return false ;
199215 }
200-
201- if ( roomContext . entityType === 'pos' ) {
202- return InPosGuard ( transaction , roomContext ) ;
203- }
204-
205- return false ;
206216 } ,
207217 } ) ;
208218 }
@@ -226,20 +236,41 @@ export default class WebSocketService {
226236 * Initializes the WebSocket server and sets up connection handlers.
227237 */
228238 public initiateWebSocket ( ) : void {
239+ // Prevent multiple initializations
240+ if ( this . connectionHandlerRegistered ) {
241+ this . logger . trace ( 'WebSocket connection handler already registered, skipping initialization' ) ;
242+ return ;
243+ }
244+
229245 if ( process . env . NODE_ENV == 'production' ) {
230246 this . setupAdapter ( ) ;
231247 } else {
232248 const port = process . env . WEBSOCKET_PORT ? parseInt ( process . env . WEBSOCKET_PORT , 10 ) : 8080 ;
233249
234- this . server . listen ( port , ( ) => {
235- this . logger . info ( `WebSocket opened on port ${ port } .` ) ;
236- } ) ;
250+ // Only start listening if not already listening
251+ if ( ! this . server . listening ) {
252+ this . server . listen ( port , ( ) => {
253+ this . logger . info ( `WebSocket opened on port ${ port } .` ) ;
254+ } ) ;
255+ // Handle EADDRINUSE error gracefully (e.g., in tests where port might already be in use)
256+ this . server . on ( 'error' , ( error : NodeJS . ErrnoException ) => {
257+ if ( error . code === 'EADDRINUSE' ) {
258+ this . logger . warn ( `Port ${ port } is already in use. WebSocket server may already be running.` ) ;
259+ } else {
260+ this . logger . error ( 'WebSocket server error:' , error ) ;
261+ }
262+ } ) ;
263+ } else {
264+ this . logger . trace ( `WebSocket server already listening on port ${ port } , skipping listen call` ) ;
265+ }
237266 }
238267
268+ // Register connection handler only once
239269 this . io . on ( 'connection' , async ( client : Socket ) => {
240- await this . handleAuthentication ( client ) ;
241270 this . setupConnectionHandlers ( client ) ;
271+ await this . handleAuthentication ( client ) ;
242272 } ) ;
273+ this . connectionHandlerRegistered = true ;
243274 }
244275
245276 /**
@@ -483,4 +514,50 @@ export default class WebSocketService {
483514 public static async emitTransactionCreated ( transaction : TransactionResponse ) : Promise < void > {
484515 await this . getInstance ( ) . emitTransactionCreated ( transaction ) ;
485516 }
517+
518+ /**
519+ * Closes the WebSocket server and cleans up resources.
520+ * @returns Promise that resolves when the server is closed.
521+ */
522+ public async close ( ) : Promise < void > {
523+ return new Promise < void > ( ( resolve ) => {
524+ if ( ! this . server . listening ) {
525+ resolve ( ) ;
526+ return ;
527+ }
528+
529+ this . io . close ( ( ) => {
530+ // Socket.IO's close() already closes the underlying HTTP server,
531+ // but we check if it's still listening before trying to close it again
532+ if ( this . server . listening ) {
533+ this . server . close ( ( err ) => {
534+ if ( err ) {
535+ const nodeErr = err as NodeJS . ErrnoException ;
536+ if ( nodeErr . code !== 'ERR_SERVER_NOT_RUNNING' ) {
537+ this . logger . error ( 'Error closing WebSocket server:' , err ) ;
538+ } else {
539+ this . logger . info ( 'WebSocket server closed' ) ;
540+ }
541+ } else {
542+ this . logger . info ( 'WebSocket server closed' ) ;
543+ }
544+ resolve ( ) ;
545+ } ) ;
546+ } else {
547+ // Server was already closed by io.close()
548+ this . logger . info ( 'WebSocket server closed' ) ;
549+ resolve ( ) ;
550+ }
551+ } ) ;
552+ } ) ;
553+ }
554+
555+ /**
556+ * Static method for backward compatibility.
557+ * Delegates to the singleton instance.
558+ * @throws Error if WebSocketService has not been initialized.
559+ */
560+ public static async close ( ) : Promise < void > {
561+ await this . getInstance ( ) . close ( ) ;
562+ }
486563}
0 commit comments