@@ -53,6 +53,13 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
5353 >
5454 > ;
5555
56+ /** Cleanup interval in milliseconds */
57+ private readonly CLEANUP_INTERVAL = 30000 ; // 30 seconds
58+ /** Lobby inactivity timeout in milliseconds */
59+ private readonly LOBBY_INACTIVITY_TIMEOUT = 30 * 60 * 1000 ; // 30 minutes
60+
61+ private cleanupIntervalId : NodeJS . Timeout | null = null ;
62+
5663 constructor ( private readonly clients : ClientService ) { }
5764
5865 afterInit ( ) {
@@ -66,6 +73,80 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
6673 lobbyState : this . lobbyState ,
6774 selectSong : this . selectSong ,
6875 } ;
76+
77+ // Start the cleanup interval to remove stale lobbies
78+ this . startCleanupInterval ( ) ;
79+ }
80+
81+ /**
82+ * Updates a lobby's lastUpdate timestamp to track activity.
83+ * This is used for inactivity-based cleanup of zombie lobbies.
84+ * @param code The lobby code to update
85+ */
86+ private updateLobbyActivity ( code : LobbyCode ) : void {
87+ const lobby = LOBBYMAN . lobbies [ code ] ;
88+ if ( lobby ) {
89+ lobby . lastUpdate = Date . now ( ) ;
90+ }
91+ }
92+
93+ /**
94+ * Starts the periodic cleanup interval to remove stale lobbies.
95+ * Lobbies that haven't been updated for LOBBY_INACTIVITY_TIMEOUT are deleted.
96+ */
97+ private startCleanupInterval ( ) : void {
98+ if ( this . cleanupIntervalId ) {
99+ clearInterval ( this . cleanupIntervalId ) ;
100+ }
101+ this . cleanupIntervalId = setInterval ( ( ) => {
102+ this . cleanupStaleLobbies ( ) ;
103+ } , this . CLEANUP_INTERVAL ) ;
104+ }
105+
106+ /**
107+ * Cleans up lobbies that haven't been updated within LOBBY_INACTIVITY_TIMEOUT.
108+ * This prevents zombie lobbies from accumulating in memory.
109+ */
110+ private cleanupStaleLobbies ( ) : void {
111+ const now = Date . now ( ) ;
112+ const lobbyCodes = Object . keys ( LOBBYMAN . lobbies ) ;
113+
114+ for ( const code of lobbyCodes ) {
115+ const lobby = LOBBYMAN . lobbies [ code ] ;
116+ if ( ! lobby ) {
117+ continue ;
118+ }
119+
120+ const timeSinceLastUpdate = now - lobby . lastUpdate ;
121+ if ( timeSinceLastUpdate > this . LOBBY_INACTIVITY_TIMEOUT ) {
122+ console . log (
123+ `Cleaning up stale lobby ${ code } (inactive for ${ Math . round (
124+ timeSinceLastUpdate / 1000 / 60 ,
125+ ) } minutes)`,
126+ ) ;
127+
128+ // Disconnect all spectators
129+ for ( const spectator of Object . values ( lobby . spectators ) ) {
130+ if ( spectator . socketId ) {
131+ ROOMMAN . leave ( spectator . socketId , code ) ;
132+ this . clients . disconnect ( spectator . socketId ) ;
133+ delete LOBBYMAN . spectatorConnections [ spectator . socketId ] ;
134+ }
135+ }
136+
137+ // Disconnect all machines
138+ for ( const machine of Object . values ( lobby . machines ) ) {
139+ if ( machine . socketId ) {
140+ ROOMMAN . leave ( machine . socketId , code ) ;
141+ delete LOBBYMAN . machineConnections [ machine . socketId ] ;
142+ }
143+ }
144+
145+ // Delete the lobby and its room
146+ delete ROOMMAN . rooms [ code ] ;
147+ delete LOBBYMAN . lobbies [ code ] ;
148+ }
149+ }
69150 }
70151
71152 /**
@@ -168,11 +249,13 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
168249 } ,
169250 } ,
170251 spectators : { } ,
252+ lastUpdate : Date . now ( ) ,
171253 } ;
172254 console . log ( 'Created lobby' , { code } ) ;
173255
174256 ROOMMAN . join ( socketId , code ) ;
175257 LOBBYMAN . machineConnections [ socketId ] = code ;
258+ this . updateLobbyActivity ( code ) ;
176259
177260 this . broadcastLobbyState ( code ) ;
178261 return undefined ;
@@ -238,6 +321,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
238321 ROOMMAN . join ( socketId , normalizedCode ) ;
239322 LOBBYMAN . machineConnections [ socketId ] = normalizedCode ;
240323
324+ this . updateLobbyActivity ( normalizedCode ) ;
241325 this . broadcastLobbyState ( normalizedCode ) ;
242326
243327 return undefined ;
@@ -279,6 +363,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
279363 } ) ;
280364 }
281365
366+ this . updateLobbyActivity ( lobby . code ) ;
282367 this . broadcastLobbyState ( lobby . code ) ;
283368 return undefined ;
284369 }
@@ -315,6 +400,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
315400 }
316401 lobby . songInfo = songInfo ;
317402
403+ this . updateLobbyActivity ( code ) ;
318404 this . broadcastLobbyState ( code ) ;
319405
320406 return undefined ;
@@ -329,8 +415,12 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
329415 socketId : SocketId ,
330416 { } ,
331417 ) : Promise < EventMessage < LobbyLeftPayload > > {
418+ const code = LOBBYMAN . machineConnections [ socketId ] ;
332419 let left = false ;
333420 left = this . disconnectMachine ( socketId ) ;
421+ if ( code ) {
422+ this . updateLobbyActivity ( code ) ;
423+ }
334424 return { event : 'lobbyLeft' , data : { left } } ;
335425 }
336426
@@ -374,6 +464,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
374464
375465 // Broadcasts an updated spectator count to all machines
376466 // and the initial lobby state for the newly-added spectator
467+ this . updateLobbyActivity ( code . toUpperCase ( ) ) ;
377468 this . broadcastLobbyState ( code ) ;
378469
379470 return undefined ;
0 commit comments