Skip to content

Commit c27686a

Browse files
committed
Support timeout to auto clean up zombie/stale lobbies
1 parent 08f0aa9 commit c27686a

2 files changed

Lines changed: 94 additions & 0 deletions

File tree

src/events/events.gateway.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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;

src/types/models.types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ export interface Lobby {
8282
spectators: Record<SocketId, Spectator>;
8383

8484
songInfo?: SongInfo;
85+
86+
// Timestamp of the last activity in this lobby (for inactivity cleanup)
87+
lastUpdate: number;
8588
}
8689

8790
export interface LobbyInfo {

0 commit comments

Comments
 (0)