From 511eb6a5d9c06be83c233cc0ad77d267d39d8320 Mon Sep 17 00:00:00 2001 From: Matteo Susca Date: Thu, 29 Jan 2026 10:53:12 +0100 Subject: [PATCH 1/2] PIX-41 Automatically unload empty lobbies from server memory and clean up client state on view unmount. --- server/src/services/canvas.service.ts | 19 +++++++++++++++++++ server/src/sockets/index.ts | 11 ++++++++++- server/src/store/canvas.store.ts | 5 +++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/server/src/services/canvas.service.ts b/server/src/services/canvas.service.ts index acb091e..4fdaa12 100644 --- a/server/src/services/canvas.service.ts +++ b/server/src/services/canvas.service.ts @@ -90,4 +90,23 @@ export class CanvasService { console.log(`[CanvasService] Saved lobby '${lobbyName}' to DB`); } + + // Unloads a lobby from memory, persisting it first + static async unloadLobby(lobbyName: string) { + if (!canvasStore.isLobbyInMemory(lobbyName)) return; + + console.log(`[CanvasService] Unloading idle lobby: ${lobbyName}`); + + // If there's a pending save timer, cancel it and save immediately + if (this.saveTimers.has(lobbyName)) { + clearTimeout(this.saveTimers.get(lobbyName)); + this.saveTimers.delete(lobbyName); + } + + // Force strict save + await this.saveToDB(lobbyName); + + // Remove from memory + canvasStore.removeLobby(lobbyName); + } } \ No newline at end of file diff --git a/server/src/sockets/index.ts b/server/src/sockets/index.ts index 8467f24..238aa39 100644 --- a/server/src/sockets/index.ts +++ b/server/src/sockets/index.ts @@ -111,11 +111,20 @@ export const setupSocket = (io: Server) => { // --- DISCONNECTING --- - socket.on('disconnecting', () => { + socket.on('disconnecting', async () => { // Notify rooms that user is leaving for (const room of socket.rooms) { if (room !== socket.id) { socket.to(room).emit(CONFIG.EVENTS.SERVER.USER_LEFT, (socket as AuthenticatedSocket).user); + + // Check if room is empty (excluding this socket) + const socketsInRoom = await io.in(room).fetchSockets(); + const remainingUsers = socketsInRoom.length - 1; // fetchSockets includes the disconnecting socket + + if (remainingUsers <= 0) { + // Room is empty, unload from hot storage + await CanvasService.unloadLobby(room); + } } } }); diff --git a/server/src/store/canvas.store.ts b/server/src/store/canvas.store.ts index 31d0daa..d8709db 100644 --- a/server/src/store/canvas.store.ts +++ b/server/src/store/canvas.store.ts @@ -48,6 +48,11 @@ export class CanvasStore { public getInMemoryLobbyIds(): string[] { return Array.from(this.lobbies.keys()); } + + public removeLobby(lobbyName: string): boolean { + console.log(`[CanvasStore] Removing lobby from memory: ${lobbyName}`); + return this.lobbies.delete(lobbyName); + } } export const canvasStore = new CanvasStore(); \ No newline at end of file From 2e68e84029791297970fd80ddbca451fe13f9c0f Mon Sep 17 00:00:00 2001 From: Matteo Susca Date: Thu, 29 Jan 2026 11:47:33 +0100 Subject: [PATCH 2/2] PIX-41 Introduce cleanup logic to disconnect the socket and reset editor state when PlayView unmounts. --- client/src/services/socket.service.ts | 7 +++++++ client/src/stores/editor.store.ts | 11 ++++++++++- client/src/views/PlayView.vue | 8 +++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/client/src/services/socket.service.ts b/client/src/services/socket.service.ts index 034b028..b6b2733 100644 --- a/client/src/services/socket.service.ts +++ b/client/src/services/socket.service.ts @@ -38,6 +38,13 @@ class SocketService { emitJoinLobby(lobbyName: string) { this.socket?.emit('JOIN_LOBBY', lobbyName); } + + disconnect() { + if (this.socket) { + this.socket.disconnect(); + this.socket = null; + } + } } export const socketService = new SocketService(); \ No newline at end of file diff --git a/client/src/stores/editor.store.ts b/client/src/stores/editor.store.ts index a669047..c87f3ea 100644 --- a/client/src/stores/editor.store.ts +++ b/client/src/stores/editor.store.ts @@ -229,6 +229,14 @@ export const useEditorStore = defineStore('editor', () => { }); } + const cleanup = () => { + socketService.disconnect(); + isConnected.value = false; + currentLobbyName.value = ''; + pixelsBuffer.value = []; + isDrawing.value = false; + }; + return { width, height, @@ -247,6 +255,7 @@ export const useEditorStore = defineStore('editor', () => { endStroke, clearCanvas, isConnected, - init + init, + cleanup }; }); \ No newline at end of file diff --git a/client/src/views/PlayView.vue b/client/src/views/PlayView.vue index 79361d5..f269f72 100644 --- a/client/src/views/PlayView.vue +++ b/client/src/views/PlayView.vue @@ -11,7 +11,7 @@ import { getLobbyById } from '../services/api'; // Setup Store const store = useEditorStore(); -const { width, height, pixels, palette, selectedColorIndex, isConnected, pixelUpdateEvent } = storeToRefs(store); +const { width, height, pixels, palette, pixelUpdateEvent } = storeToRefs(store); const route = useRoute(); @@ -34,6 +34,12 @@ onMounted(async () => { store.init(lobbyName); }); + +import { onUnmounted } from 'vue'; + +onUnmounted(() => { + store.cleanup(); +});