From 786dfbe1e9e54cc7e27cbaf3969230f72b9bd0cb Mon Sep 17 00:00:00 2001 From: Matteo Susca Date: Thu, 22 Jan 2026 16:18:18 +0100 Subject: [PATCH 01/11] PIX-30 canvas scheme --- server/src/models/Canvas.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 server/src/models/Canvas.ts diff --git a/server/src/models/Canvas.ts b/server/src/models/Canvas.ts new file mode 100644 index 0000000..b22a6d0 --- /dev/null +++ b/server/src/models/Canvas.ts @@ -0,0 +1,19 @@ +import { Schema, model, Document, Types } from 'mongoose'; + +export interface ICanvas extends Document { + lobby: Types.ObjectId; // Back-link to Lobby (useful for maintenance) + width: number; + height: number; + data: Buffer; + lastModified: Date; +} + +const canvasSchema = new Schema({ + lobby: { type: Schema.Types.ObjectId, ref: 'Lobby', required: true }, + width: { type: Number, default: 64 }, + height: { type: Number, default: 64 }, + data: { type: Buffer, required: true }, + lastModified: { type: Date, default: Date.now } +}); + +export const Canvas = model('Canvas', canvasSchema); \ No newline at end of file From c46ee3ba1cb320f544dfe51e17ae191d739ac705 Mon Sep 17 00:00:00 2001 From: Matteo Susca Date: Thu, 22 Jan 2026 17:27:57 +0100 Subject: [PATCH 02/11] PIX-30 Lobby scheme --- server/src/models/Lobby.ts | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 server/src/models/Lobby.ts diff --git a/server/src/models/Lobby.ts b/server/src/models/Lobby.ts new file mode 100644 index 0000000..a17333c --- /dev/null +++ b/server/src/models/Lobby.ts @@ -0,0 +1,59 @@ +import { Schema, model, Document, Types, Model } from 'mongoose'; +import { Canvas } from './Canvas'; + +export interface ILobby extends Document { + name: string; + owner?: Types.ObjectId; // Links to your User schema + canvas: Types.ObjectId; // Links to Canvas schema + allowedUsers: Types.ObjectId[]; + bannedUsers: Types.ObjectId[]; + createdAt: Date; + updatedAt: Date; +} + +// Static method interface +interface LobbyModel extends Model { + createWithCanvas(name: string, ownerId?: string): Promise; +} + +const lobbySchema = new Schema({ + name: { + type: String, + required: true, + unique: true, + trim: true + }, + owner: { type: Schema.Types.ObjectId, ref: 'User' }, + canvas: { type: Schema.Types.ObjectId, ref: 'Canvas', required: true }, + bannedUsers: [{ type: Schema.Types.ObjectId, ref: 'User' }] +}, { timestamps: true }); + +// --- FACTORY METHOD: Creates Lobby + Empty Canvas atomically --- +lobbySchema.statics.createWithCanvas = async function (name: string, ownerId?: string) { + const lobbyId = new Types.ObjectId(); + const canvasId = new Types.ObjectId(); + + // 64x64 pixels initialized to 0 (Transparent/White depending on palette) + const emptyBuffer = Buffer.alloc(64 * 64, 0); + + const canvas = new Canvas({ + _id: canvasId, + lobby: lobbyId, + width: 64, + height: 64, + data: emptyBuffer + }); + + const lobby = new this({ + _id: lobbyId, + name, + owner: ownerId ? new Types.ObjectId(ownerId) : undefined, + canvas: canvasId + }); + + // Save both + await Promise.all([canvas.save(), lobby.save()]); + return lobby; +}; + +export const Lobby = model('Lobby', lobbySchema); \ No newline at end of file From 6dcddff378ed982f3abfb30b6d1a5f9858316659 Mon Sep 17 00:00:00 2001 From: Matteo Susca Date: Thu, 22 Jan 2026 18:26:50 +0100 Subject: [PATCH 03/11] PIX-30 changes to config.ts annd make it used --- server/src/config.ts | 6 ++++++ server/src/db/connect.ts | 3 ++- server/src/index.ts | 6 +++--- server/src/models/Canvas.ts | 5 +++-- server/src/models/Lobby.ts | 9 +++++---- server/src/services/UserService.ts | 5 +++-- 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/server/src/config.ts b/server/src/config.ts index 7c85851..540254c 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -1,6 +1,12 @@ export const CONFIG = { // Server settings PORT: process.env.PORT || 3000, + MONGO_URI: process.env.MONGO_URI || "mongodb://localhost:27017/pixie", + CLIENT_ORIGIN: process.env.CLIENT_ORIGIN || "http://localhost:5173", + JWT: { + SECRET: process.env.JWT_SECRET || "dev-key", + EXPIRES_IN: "7d", + }, // Game Logic settings CANVAS: { diff --git a/server/src/db/connect.ts b/server/src/db/connect.ts index 4cbad30..5443002 100644 --- a/server/src/db/connect.ts +++ b/server/src/db/connect.ts @@ -1,7 +1,8 @@ import mongoose from "mongoose" import { seedUsers } from "./seed" -const MONGO_URI = process.env.MONGO_URI || "mongodb://localhost:27017/pixie" +import { CONFIG } from "../config" +const MONGO_URI = CONFIG.MONGO_URI export const connectDB = async (): Promise => { try { diff --git a/server/src/index.ts b/server/src/index.ts index 7571421..5a4dcd2 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -7,7 +7,7 @@ import router from "./routes/index"; import { setupSocket } from "./sockets/index"; // Import the Socket Manager import { CONFIG } from "./config"; -const PORT = process.env.PORT || 3000; +const PORT = CONFIG.PORT; // Configuration const app = express(); @@ -18,7 +18,7 @@ const httpServer = createServer(app); // Middleware app.use( cors({ - origin: "*", + origin: CONFIG.CLIENT_ORIGIN, methods: ["GET", "POST", "PUT", "DELETE"], allowedHeaders: ["Content-Type", "Authorization"], }) @@ -34,7 +34,7 @@ app.use("/api", router); // --- SOCKET.IO SETUP (Real-time) --- const io = new Server(httpServer, { cors: { - origin: "*", // Allow all origins for development + origin: CONFIG.CLIENT_ORIGIN, // Allow all origins for development methods: ["GET", "POST"] } }); diff --git a/server/src/models/Canvas.ts b/server/src/models/Canvas.ts index b22a6d0..77956d7 100644 --- a/server/src/models/Canvas.ts +++ b/server/src/models/Canvas.ts @@ -1,4 +1,5 @@ import { Schema, model, Document, Types } from 'mongoose'; +import { CONFIG } from '../config'; export interface ICanvas extends Document { lobby: Types.ObjectId; // Back-link to Lobby (useful for maintenance) @@ -10,8 +11,8 @@ export interface ICanvas extends Document { const canvasSchema = new Schema({ lobby: { type: Schema.Types.ObjectId, ref: 'Lobby', required: true }, - width: { type: Number, default: 64 }, - height: { type: Number, default: 64 }, + width: { type: Number, default: CONFIG.CANVAS.WIDTH }, + height: { type: Number, default: CONFIG.CANVAS.HEIGHT }, data: { type: Buffer, required: true }, lastModified: { type: Date, default: Date.now } }); diff --git a/server/src/models/Lobby.ts b/server/src/models/Lobby.ts index a17333c..a66b74b 100644 --- a/server/src/models/Lobby.ts +++ b/server/src/models/Lobby.ts @@ -1,5 +1,6 @@ import { Schema, model, Document, Types, Model } from 'mongoose'; import { Canvas } from './Canvas'; +import { CONFIG } from '../config'; export interface ILobby extends Document { name: string; @@ -33,14 +34,14 @@ lobbySchema.statics.createWithCanvas = async function (name: string, ownerId?: s const lobbyId = new Types.ObjectId(); const canvasId = new Types.ObjectId(); - // 64x64 pixels initialized to 0 (Transparent/White depending on palette) - const emptyBuffer = Buffer.alloc(64 * 64, 0); + // Initialized to 0 (Transparent/White depending on palette) + const emptyBuffer = Buffer.alloc(CONFIG.CANVAS.WIDTH * CONFIG.CANVAS.HEIGHT, 0); const canvas = new Canvas({ _id: canvasId, lobby: lobbyId, - width: 64, - height: 64, + width: CONFIG.CANVAS.WIDTH, + height: CONFIG.CANVAS.HEIGHT, data: emptyBuffer }); diff --git a/server/src/services/UserService.ts b/server/src/services/UserService.ts index 096a7eb..728ca80 100644 --- a/server/src/services/UserService.ts +++ b/server/src/services/UserService.ts @@ -1,5 +1,6 @@ import jwt from "jsonwebtoken" import { IUser, User } from "../models/User" +import { CONFIG } from "../config" interface LoginResponse { username: string @@ -19,7 +20,7 @@ export class UserService { * Handle user login/registration and token generation */ static async login(username: string): Promise { - const JWT_SECRET = process.env.JWT_SECRET || "dev-key" + const JWT_SECRET = CONFIG.JWT.SECRET let user = await User.findOne({ username }) let isNewUser = false @@ -29,7 +30,7 @@ export class UserService { isNewUser = true } - const token = jwt.sign({ id: user._id, username: user.username }, JWT_SECRET, { expiresIn: "7d" }) + const token = jwt.sign({ id: user._id, username: user.username }, JWT_SECRET, { expiresIn: CONFIG.JWT.EXPIRES_IN as any }) return { username: user.username, From 0607f7ebf9b4b1e60e6c71b1eb9b8cac2a3e4122 Mon Sep 17 00:00:00 2001 From: Matteo Susca Date: Fri, 23 Jan 2026 10:33:39 +0100 Subject: [PATCH 04/11] PIX-30 lobby.store renamed to canvas.store --- server/src/models/Lobby.ts | 1 + server/src/store/canvas.store.ts | 42 ++++++++++++++++++++++++++++++++ server/src/store/lobby.store.ts | 36 --------------------------- 3 files changed, 43 insertions(+), 36 deletions(-) create mode 100644 server/src/store/canvas.store.ts delete mode 100644 server/src/store/lobby.store.ts diff --git a/server/src/models/Lobby.ts b/server/src/models/Lobby.ts index a66b74b..20f563c 100644 --- a/server/src/models/Lobby.ts +++ b/server/src/models/Lobby.ts @@ -26,6 +26,7 @@ const lobbySchema = new Schema({ }, owner: { type: Schema.Types.ObjectId, ref: 'User' }, canvas: { type: Schema.Types.ObjectId, ref: 'Canvas', required: true }, + allowedUsers: [{ type: Schema.Types.ObjectId, ref: 'User' }], bannedUsers: [{ type: Schema.Types.ObjectId, ref: 'User' }] }, { timestamps: true }); diff --git a/server/src/store/canvas.store.ts b/server/src/store/canvas.store.ts new file mode 100644 index 0000000..b5929e7 --- /dev/null +++ b/server/src/store/canvas.store.ts @@ -0,0 +1,42 @@ +import { CONFIG } from '../config'; + +export class CanvasStore { + private buffers: Map = new Map(); + + public isLobbyInMemory(lobbyId: string): boolean { + return this.buffers.has(lobbyId); + } + + public getLobbyPixelData(lobbyId: string): Uint8Array | undefined { + return this.buffers.get(lobbyId); + } + + // Load data from DB buffer to RAM Uint8Array + public loadLobbyToMemory(lobbyId: string, data: Buffer | Uint8Array): Uint8Array { + console.log(`[CanvasStore] Loading lobby: ${lobbyId}`); + const memoryBuffer = new Uint8Array(data); + this.buffers.set(lobbyId, memoryBuffer); + return memoryBuffer; + } + + public modifyPixelColor(lobbyId: string, index: number, color: number): boolean { + const buffer = this.buffers.get(lobbyId); + if (!buffer) return false; + + // Boundary check + if (index < 0 || index >= buffer.length) return false; + + // Optimization: only update if value changed + if (buffer[index] !== color) { + buffer[index] = color; + return true; + } + return false; + } + + public getInMemoryLobbyIds(): string[] { + return Array.from(this.buffers.keys()); + } +} + +export const canvasStore = new CanvasStore(); \ No newline at end of file diff --git a/server/src/store/lobby.store.ts b/server/src/store/lobby.store.ts deleted file mode 100644 index cef879f..0000000 --- a/server/src/store/lobby.store.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { CONFIG } from '../config'; - -/** - * LobbyStore - * Manages the "Hot Storage" (RAM) for all active lobbies. - * Singleton pattern ensures we share the same state across the app. - */ -export class LobbyStore { - private lobbies: Map = new Map(); - - public getLobbyBuffer(lobbyId: string): Uint8Array { - return this.lobbies.get(lobbyId) ?? this.createLobby(lobbyId); - } - - private createLobby(lobbyId: string): Uint8Array { - console.log(`[LobbyStore] Initializing new RAM buffer for lobby: ${lobbyId}`); - const size = CONFIG.CANVAS.WIDTH * CONFIG.CANVAS.HEIGHT; - const buffer = new Uint8Array(size); - this.lobbies.set(lobbyId, buffer); - return buffer; - } - - public setPixel(lobbyId: string, index: number, color: number): boolean { - const buffer = this.getLobbyBuffer(lobbyId); - - if (index < 0 || index >= buffer.length) return false; - - if (buffer[index] !== color) { - buffer[index] = color; - return true; - } - return false; - } -} - -export const lobbyStore = new LobbyStore(); \ No newline at end of file From 7184294468fdaed7124cd7831b0f86098564803b Mon Sep 17 00:00:00 2001 From: Matteo Susca Date: Fri, 23 Jan 2026 11:02:38 +0100 Subject: [PATCH 05/11] PIX-30 created service for lobby and canvas --- server/src/services/canvas.service.ts | 63 ++++++++++++++++++++------- server/src/services/lobby.service.ts | 21 +++++++++ 2 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 server/src/services/lobby.service.ts diff --git a/server/src/services/canvas.service.ts b/server/src/services/canvas.service.ts index b1a4dae..7b3315d 100644 --- a/server/src/services/canvas.service.ts +++ b/server/src/services/canvas.service.ts @@ -1,31 +1,62 @@ -import { lobbyStore } from '../store/lobby.store'; +import { canvasStore } from '../store/canvas.store'; +import { Lobby } from '../models/Lobby'; +import { Canvas } from '../models/Canvas'; import { CONFIG } from '../config'; export class CanvasService { - static getLobbyState(lobbyId: string): Uint8Array { - return lobbyStore.getLobbyBuffer(lobbyId); + /** + * Retrieves the current pixel state. + * If not in RAM, fetches from DB and hydrates the Store. + */ + static async getState(lobbyName: string): Promise { + // 1. Hot Storage (RAM) + if (canvasStore.isLobbyInMemory(lobbyName)) { + return canvasStore.getLobbyPixelData(lobbyName)!; + } + + // 2. Cold Storage (DB) + const lobby = await Lobby.findOne({ name: lobbyName }); + if (!lobby) throw new Error(`Lobby '${lobbyName}' not found`); + + const canvas = await Canvas.findById(lobby.canvas); + if (!canvas) throw new Error(`Canvas data missing for lobby '${lobbyName}'`); + + // 3. Hydrate RAM + return canvasStore.loadLobbyToMemory(lobbyName, canvas.data); } - static drawPixel(lobbyId: string, x: number, y: number, color: number) { + /** + * Updates a pixel in RAM. + * Returns true if the pixel actually changed. + */ + static draw(lobbyName: string, x: number, y: number, color: number) { if (x < 0 || x >= CONFIG.CANVAS.WIDTH || y < 0 || y >= CONFIG.CANVAS.HEIGHT) { - console.warn(`[CanvasService] Out of bounds draw attempt: ${x}, ${y}`); - return null; - } - - if (color < 0 || color > CONFIG.MAX_COLOR_ID) { - console.warn(`[CanvasService] Invalid color index: ${color}`); - return null; + return false; } const index = y * CONFIG.CANVAS.WIDTH + x; + return canvasStore.modifyPixelColor(lobbyName, index, color); + } - const hasChanged = lobbyStore.setPixel(lobbyId, index, color); + /** + * Persists the current RAM state to MongoDB. + */ + static async saveToDB(lobbyName: string) { + const memoryBuffer = canvasStore.getLobbyPixelData(lobbyName); + if (!memoryBuffer) return; // Nothing to save - if (hasChanged) { - return { x, y, color }; - } + const lobby = await Lobby.findOne({ name: lobbyName }); + if (!lobby) return; + + // Convert Uint8Array back to Node Buffer for Mongoose + const dataBuffer = Buffer.from(memoryBuffer); + + await Canvas.findByIdAndUpdate(lobby.canvas, { + data: dataBuffer, + lastModified: new Date() + }); - return null; + console.log(`[CanvasService] Saved lobby '${lobbyName}' to DB`); } } \ No newline at end of file diff --git a/server/src/services/lobby.service.ts b/server/src/services/lobby.service.ts new file mode 100644 index 0000000..34926c0 --- /dev/null +++ b/server/src/services/lobby.service.ts @@ -0,0 +1,21 @@ +import { Lobby } from '../models/Lobby'; + +export class LobbyService { + + static async create(name: string, ownerId?: string) { + // Uses the Factory Method defined in the Model + return await Lobby.createWithCanvas(name, ownerId); + } + + static async getAll() { + // Returns list with owner info, sorted by newest + return await Lobby.find() + .select('name owner createdAt allowedUsers') + .populate('owner', 'username') + .sort({ createdAt: -1 }); + } + + static async getByName(name: string) { + return await Lobby.findOne({ name }).populate('owner', 'username'); + } +} \ No newline at end of file From 2849cd23389e8cd7fa6e9cd08bdd9898cfd5742a Mon Sep 17 00:00:00 2001 From: Matteo Susca Date: Fri, 23 Jan 2026 11:05:26 +0100 Subject: [PATCH 06/11] PIX-30 refactor canvas.service --- server/src/services/canvas.service.ts | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/server/src/services/canvas.service.ts b/server/src/services/canvas.service.ts index 7b3315d..d22821a 100644 --- a/server/src/services/canvas.service.ts +++ b/server/src/services/canvas.service.ts @@ -5,31 +5,22 @@ import { CONFIG } from '../config'; export class CanvasService { - /** - * Retrieves the current pixel state. - * If not in RAM, fetches from DB and hydrates the Store. - */ + // Retrieves pixel state from memory, loading from DB if necessary static async getState(lobbyName: string): Promise { - // 1. Hot Storage (RAM) if (canvasStore.isLobbyInMemory(lobbyName)) { return canvasStore.getLobbyPixelData(lobbyName)!; } - // 2. Cold Storage (DB) const lobby = await Lobby.findOne({ name: lobbyName }); if (!lobby) throw new Error(`Lobby '${lobbyName}' not found`); const canvas = await Canvas.findById(lobby.canvas); if (!canvas) throw new Error(`Canvas data missing for lobby '${lobbyName}'`); - // 3. Hydrate RAM return canvasStore.loadLobbyToMemory(lobbyName, canvas.data); } - /** - * Updates a pixel in RAM. - * Returns true if the pixel actually changed. - */ + // Updates a pixel in memory if coordinates are valid static draw(lobbyName: string, x: number, y: number, color: number) { if (x < 0 || x >= CONFIG.CANVAS.WIDTH || y < 0 || y >= CONFIG.CANVAS.HEIGHT) { return false; @@ -39,21 +30,16 @@ export class CanvasService { return canvasStore.modifyPixelColor(lobbyName, index, color); } - /** - * Persists the current RAM state to MongoDB. - */ + // Persists the current in-memory state to the database static async saveToDB(lobbyName: string) { const memoryBuffer = canvasStore.getLobbyPixelData(lobbyName); - if (!memoryBuffer) return; // Nothing to save + if (!memoryBuffer) return; const lobby = await Lobby.findOne({ name: lobbyName }); if (!lobby) return; - // Convert Uint8Array back to Node Buffer for Mongoose - const dataBuffer = Buffer.from(memoryBuffer); - await Canvas.findByIdAndUpdate(lobby.canvas, { - data: dataBuffer, + data: Buffer.from(memoryBuffer), lastModified: new Date() }); From cf1c15d879fb4a2b1eaf8251976356d112557271 Mon Sep 17 00:00:00 2001 From: Matteo Susca Date: Fri, 23 Jan 2026 11:38:28 +0100 Subject: [PATCH 07/11] PIX-30 api, routes and controller for lobby --- server/src/controllers/lobby.controller.ts | 40 ++++++++++++++++++++++ server/src/routes/api.ts | 5 +++ server/src/routes/lobby.routes.ts | 12 +++++++ 3 files changed, 57 insertions(+) create mode 100644 server/src/controllers/lobby.controller.ts create mode 100644 server/src/routes/lobby.routes.ts diff --git a/server/src/controllers/lobby.controller.ts b/server/src/controllers/lobby.controller.ts new file mode 100644 index 0000000..45307b6 --- /dev/null +++ b/server/src/controllers/lobby.controller.ts @@ -0,0 +1,40 @@ +import { Request, Response } from 'express'; +import { LobbyService } from '../services/lobby.service'; + +export class LobbyController { + + // POST /api/lobbies + static async create(req: Request, res: Response) { + try { + const { name, ownerId } = req.body; + + if (!name) { + return res.status(400).json({ error: 'Lobby name is required' }); + } + + const lobby = await LobbyService.create(name, ownerId); + + return res.status(201).json(lobby); + + } catch (error: any) { + // Duplicate name + if (error.code === 11000) { + return res.status(409).json({ error: 'Lobby name already taken' }); + } + + console.error('[LobbyController] Create Error:', error); + return res.status(500).json({ error: 'Internal Server Error' }); + } + } + + // GET /api/lobbies + static async getAll(req: Request, res: Response) { + try { + const lobbies = await LobbyService.getAll(); + return res.json(lobbies); + } catch (error) { + console.error('[LobbyController] GetAll Error:', error); + return res.status(500).json({ error: 'Internal Server Error' }); + } + } +} \ No newline at end of file diff --git a/server/src/routes/api.ts b/server/src/routes/api.ts index ab03d25..b13bdfe 100644 --- a/server/src/routes/api.ts +++ b/server/src/routes/api.ts @@ -1,6 +1,7 @@ import express, { Router } from "express" import { LoginController } from "../controllers/LoginController" import { UserController } from "../controllers/UserController" +import { LobbyController } from "../controllers/lobby.controller" const router: Router = express.Router() @@ -10,4 +11,8 @@ router.post("/login", LoginController.login) // Users router.get("/users", UserController.getAll) +// Lobbies +router.post("/lobbies", LobbyController.create) +router.get("/lobbies", LobbyController.getAll) + export default router diff --git a/server/src/routes/lobby.routes.ts b/server/src/routes/lobby.routes.ts new file mode 100644 index 0000000..afd1434 --- /dev/null +++ b/server/src/routes/lobby.routes.ts @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { LobbyController } from '../controllers/lobby.controller'; + +const router = Router(); + +// Crea una nuova stanza +router.post('/', LobbyController.create); + +// Ottieni lista stanze +router.get('/', LobbyController.getAll); + +export default router; \ No newline at end of file From 2b865e8e7bcd45e8994afd3ca180b59bd46d84aa Mon Sep 17 00:00:00 2001 From: Matteo Susca Date: Fri, 23 Jan 2026 11:52:55 +0100 Subject: [PATCH 08/11] PIX-30 removed unused lobby.routes --- server/src/routes/lobby.routes.ts | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 server/src/routes/lobby.routes.ts diff --git a/server/src/routes/lobby.routes.ts b/server/src/routes/lobby.routes.ts deleted file mode 100644 index afd1434..0000000 --- a/server/src/routes/lobby.routes.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Router } from 'express'; -import { LobbyController } from '../controllers/lobby.controller'; - -const router = Router(); - -// Crea una nuova stanza -router.post('/', LobbyController.create); - -// Ottieni lista stanze -router.get('/', LobbyController.getAll); - -export default router; \ No newline at end of file From a768224453ef994c4daece6b7f184b35223500a2 Mon Sep 17 00:00:00 2001 From: Matteo Susca Date: Fri, 23 Jan 2026 14:17:00 +0100 Subject: [PATCH 09/11] PIX-30 Request Coalescing with promise and name consinstency --- client/src/services/socket.service.ts | 6 +++--- server/src/services/canvas.service.ts | 30 ++++++++++++++++++++++----- server/src/sockets/index.ts | 29 +++++++++++++++----------- server/src/store/canvas.store.ts | 18 ++++++++-------- 4 files changed, 54 insertions(+), 29 deletions(-) diff --git a/client/src/services/socket.service.ts b/client/src/services/socket.service.ts index 967232e..978d90e 100644 --- a/client/src/services/socket.service.ts +++ b/client/src/services/socket.service.ts @@ -17,12 +17,12 @@ class SocketService { this.socket?.on('PIXEL_UPDATE', cb); } - emitDraw(payload: { lobbyId: string; x: number; y: number; color: number }) { + emitDraw(payload: { lobbyName: string; x: number; y: number; color: number }) { this.socket?.emit('DRAW', payload); } - emitJoinLobby(lobbyId: string) { - this.socket?.emit('JOIN_LOBBY', lobbyId); + emitJoinLobby(lobbyName: string) { + this.socket?.emit('JOIN_LOBBY', lobbyName); } } diff --git a/server/src/services/canvas.service.ts b/server/src/services/canvas.service.ts index d22821a..3b1cf66 100644 --- a/server/src/services/canvas.service.ts +++ b/server/src/services/canvas.service.ts @@ -5,19 +5,39 @@ import { CONFIG } from '../config'; export class CanvasService { + // Request Coalescing: Track pending loads to prevent duplicate DB fetches + private static pendingLoads: Map> = new Map(); + // Retrieves pixel state from memory, loading from DB if necessary static async getState(lobbyName: string): Promise { + // 1. Fast Path: Already in memory if (canvasStore.isLobbyInMemory(lobbyName)) { return canvasStore.getLobbyPixelData(lobbyName)!; } - const lobby = await Lobby.findOne({ name: lobbyName }); - if (!lobby) throw new Error(`Lobby '${lobbyName}' not found`); + // 2. Coalescing Path: Already loading, wait for existing promise + if (this.pendingLoads.has(lobbyName)) { + return this.pendingLoads.get(lobbyName)!; + } + + // 3. Slow Path: Fetch from DB + const loadPromise = (async () => { + try { + const lobby = await Lobby.findOne({ name: lobbyName }); + if (!lobby) throw new Error(`Lobby '${lobbyName}' not found`); + + const canvas = await Canvas.findById(lobby.canvas); + if (!canvas) throw new Error(`Canvas data missing for lobby '${lobbyName}'`); - const canvas = await Canvas.findById(lobby.canvas); - if (!canvas) throw new Error(`Canvas data missing for lobby '${lobbyName}'`); + return canvasStore.loadLobbyToMemory(lobbyName, canvas.data); + } finally { + // Cleanup promise when done (success or failure) + this.pendingLoads.delete(lobbyName); + } + })(); - return canvasStore.loadLobbyToMemory(lobbyName, canvas.data); + this.pendingLoads.set(lobbyName, loadPromise); + return loadPromise; } // Updates a pixel in memory if coordinates are valid diff --git a/server/src/sockets/index.ts b/server/src/sockets/index.ts index 609f94b..cb8f128 100644 --- a/server/src/sockets/index.ts +++ b/server/src/sockets/index.ts @@ -8,34 +8,39 @@ export const setupSocket = (io: Server) => { // --- EVENT: JOIN_LOBBY --- // User requests to enter a specific room - socket.on(CONFIG.EVENTS.CLIENT.JOIN_LOBBY, (lobbyId: string) => { - console.log(`[Socket] ${socket.id} joining lobby: ${lobbyId}`); + socket.on(CONFIG.EVENTS.CLIENT.JOIN_LOBBY, async (lobbyName: string) => { + console.log(`[Socket] ${socket.id} joining lobby: ${lobbyName}`); // 1. Join the Socket.io room channel - socket.join(lobbyId); + socket.join(lobbyName); - // 2. Get current state from Service - const state = CanvasService.getLobbyState(lobbyId); + try { + // 2. Get current state from Service + const state = await CanvasService.getState(lobbyName); - // 3. Send state back to the user - socket.emit(CONFIG.EVENTS.SERVER.INIT_STATE, state); + // 3. Send state back to the user + socket.emit(CONFIG.EVENTS.SERVER.INIT_STATE, state); + } catch (error) { + console.error(`[Socket] Error joining lobby ${lobbyName}:`, error); + socket.emit(CONFIG.EVENTS.SERVER.ERROR, { message: "Failed to join lobby" }); + } }); // --- EVENT: DRAW --- // User wants to color a pixel socket.on(CONFIG.EVENTS.CLIENT.DRAW, (payload: any) => { // Payload validation could happen here or in a DTO - const { lobbyId, x, y, color } = payload; + const { lobbyName, x, y, color } = payload; - if (!lobbyId) return; + if (!lobbyName) return; // 1. Process Logic via Service - const result = CanvasService.drawPixel(lobbyId, x, y, color); + const success = CanvasService.draw(lobbyName, x, y, color); // 2. Broadcast if successful - if (result) { + if (success) { // Send to everyone in the room EXCEPT the sender - socket.to(lobbyId).emit(CONFIG.EVENTS.SERVER.PIXEL_UPDATE, result); + socket.to(lobbyName).emit(CONFIG.EVENTS.SERVER.PIXEL_UPDATE, { x, y, color }); } }); diff --git a/server/src/store/canvas.store.ts b/server/src/store/canvas.store.ts index b5929e7..b06dbcf 100644 --- a/server/src/store/canvas.store.ts +++ b/server/src/store/canvas.store.ts @@ -3,24 +3,24 @@ import { CONFIG } from '../config'; export class CanvasStore { private buffers: Map = new Map(); - public isLobbyInMemory(lobbyId: string): boolean { - return this.buffers.has(lobbyId); + public isLobbyInMemory(lobbyName: string): boolean { + return this.buffers.has(lobbyName); } - public getLobbyPixelData(lobbyId: string): Uint8Array | undefined { - return this.buffers.get(lobbyId); + public getLobbyPixelData(lobbyName: string): Uint8Array | undefined { + return this.buffers.get(lobbyName); } // Load data from DB buffer to RAM Uint8Array - public loadLobbyToMemory(lobbyId: string, data: Buffer | Uint8Array): Uint8Array { - console.log(`[CanvasStore] Loading lobby: ${lobbyId}`); + public loadLobbyToMemory(lobbyName: string, data: Buffer | Uint8Array): Uint8Array { + console.log(`[CanvasStore] Loading lobby: ${lobbyName}`); const memoryBuffer = new Uint8Array(data); - this.buffers.set(lobbyId, memoryBuffer); + this.buffers.set(lobbyName, memoryBuffer); return memoryBuffer; } - public modifyPixelColor(lobbyId: string, index: number, color: number): boolean { - const buffer = this.buffers.get(lobbyId); + public modifyPixelColor(lobbyName: string, index: number, color: number): boolean { + const buffer = this.buffers.get(lobbyName); if (!buffer) return false; // Boundary check From 0137f0065a42dbe172f260e3980122a15003f4ed Mon Sep 17 00:00:00 2001 From: Matteo Susca Date: Fri, 23 Jan 2026 14:17:08 +0100 Subject: [PATCH 10/11] PIX-30 db seeding --- client/src/stores/editor.store.ts | 4 ++-- server/src/db/connect.ts | 3 ++- server/src/db/seed.ts | 37 +++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/client/src/stores/editor.store.ts b/client/src/stores/editor.store.ts index 84e1d44..ce20fb5 100644 --- a/client/src/stores/editor.store.ts +++ b/client/src/stores/editor.store.ts @@ -68,7 +68,7 @@ export const useEditorStore = defineStore('editor', () => { // Notify Server socketService.emitDraw({ - lobbyId: 'default', + lobbyName: 'Default Lobby', x, y, color: selectedColorIndex.value @@ -85,7 +85,7 @@ export const useEditorStore = defineStore('editor', () => { function init() { socketService.connect(); - socketService.emitJoinLobby('default'); + socketService.emitJoinLobby('Default Lobby'); isConnected.value = true; // Logic to bind Model events to ViewModel state diff --git a/server/src/db/connect.ts b/server/src/db/connect.ts index 5443002..fc1fd39 100644 --- a/server/src/db/connect.ts +++ b/server/src/db/connect.ts @@ -1,5 +1,5 @@ import mongoose from "mongoose" -import { seedUsers } from "./seed" +import { seedUsers, seedLobbies } from "./seed" import { CONFIG } from "../config" const MONGO_URI = CONFIG.MONGO_URI @@ -17,6 +17,7 @@ export const connectDB = async (): Promise => { console.log("[INFO] MongoDB is ready") await seedUsers() + await seedLobbies() } catch (err) { const error = err as Error console.error("[ERROR] MongoDB Connection Error:", error.message) diff --git a/server/src/db/seed.ts b/server/src/db/seed.ts index 22accd5..7d269dc 100644 --- a/server/src/db/seed.ts +++ b/server/src/db/seed.ts @@ -1,4 +1,7 @@ import { User } from "../models/User" +import { Lobby } from "../models/Lobby" +import { Canvas } from "../models/Canvas" +import { CONFIG } from "../config" export const seedUsers = async (): Promise => { const users = [ @@ -20,3 +23,37 @@ export const seedUsers = async (): Promise => { console.error("Seed failed:", error) } } + +export const seedLobbies = async (): Promise => { + try { + console.log("Checking lobby seeds...") + const lobbyName = "Default Lobby" + const existingLobby = await Lobby.findOne({ name: lobbyName }) + + if (!existingLobby) { + console.log(`Creating '${lobbyName}'...`) + const newLobby = await Lobby.createWithCanvas(lobbyName) + + // Get the canvas to draw some initial pixels + const canvas = await Canvas.findById(newLobby.canvas) + if (canvas) { + // Draw a red diagonal line + const width = CONFIG.CANVAS.WIDTH + const height = CONFIG.CANVAS.HEIGHT + const color = 4 // Assuming 4 is Red in the palette, or just a distinct color + + for (let i = 0; i < Math.min(width, height); i++) { + const index = i * width + i + canvas.data[index] = color + } + + await canvas.save() + console.log(`Seeded '${lobbyName}' with a diagonal pattern`) + } + } else { + console.log(`'${lobbyName}' already exists`) + } + } catch (error) { + console.error("Lobby seed failed:", error) + } +} From 7747b6cee413d51eb647b54bdff23d9a598b4d1d Mon Sep 17 00:00:00 2001 From: Matteo Susca Date: Fri, 23 Jan 2026 15:36:18 +0100 Subject: [PATCH 11/11] PIX-30 fixed seeding error and import errors --- server/src/controllers/lobby.controller.ts | 2 +- server/src/db/connect.ts | 4 +- server/src/db/seed.ts | 52 +++++++++++++--------- server/src/models/Canvas.ts | 2 +- server/src/models/Lobby.ts | 4 +- server/src/services/lobby.service.ts | 2 +- server/src/store/canvas.store.ts | 2 +- 7 files changed, 38 insertions(+), 30 deletions(-) diff --git a/server/src/controllers/lobby.controller.ts b/server/src/controllers/lobby.controller.ts index 45307b6..843fbc9 100644 --- a/server/src/controllers/lobby.controller.ts +++ b/server/src/controllers/lobby.controller.ts @@ -1,5 +1,5 @@ import { Request, Response } from 'express'; -import { LobbyService } from '../services/lobby.service'; +import { LobbyService } from '../services/lobby.service.js'; export class LobbyController { diff --git a/server/src/db/connect.ts b/server/src/db/connect.ts index fc1fd39..fbd0339 100644 --- a/server/src/db/connect.ts +++ b/server/src/db/connect.ts @@ -1,7 +1,7 @@ import mongoose from "mongoose" -import { seedUsers, seedLobbies } from "./seed" +import { seedUsers, seedLobbies } from "./seed.js" -import { CONFIG } from "../config" +import { CONFIG } from "../config.js" const MONGO_URI = CONFIG.MONGO_URI export const connectDB = async (): Promise => { diff --git a/server/src/db/seed.ts b/server/src/db/seed.ts index 7d269dc..899e301 100644 --- a/server/src/db/seed.ts +++ b/server/src/db/seed.ts @@ -1,7 +1,7 @@ -import { User } from "../models/User" -import { Lobby } from "../models/Lobby" -import { Canvas } from "../models/Canvas" -import { CONFIG } from "../config" +import { User } from "../models/User.js" +import { Lobby, ILobby } from "../models/Lobby.js" +import { Canvas } from "../models/Canvas.js" +import { CONFIG } from "../config.js" export const seedUsers = async (): Promise => { const users = [ @@ -28,30 +28,38 @@ export const seedLobbies = async (): Promise => { try { console.log("Checking lobby seeds...") const lobbyName = "Default Lobby" - const existingLobby = await Lobby.findOne({ name: lobbyName }) + let lobby: ILobby | null = await Lobby.findOne({ name: lobbyName }) - if (!existingLobby) { + if (!lobby) { console.log(`Creating '${lobbyName}'...`) - const newLobby = await Lobby.createWithCanvas(lobbyName) + lobby = await Lobby.createWithCanvas(lobbyName) + } else { + console.log(`'${lobbyName}' already exists`) + } - // Get the canvas to draw some initial pixels - const canvas = await Canvas.findById(newLobby.canvas) + if (lobby) { + // Get the canvas to check/draw pixels + const canvas = await Canvas.findById(lobby.canvas) if (canvas) { - // Draw a red diagonal line - const width = CONFIG.CANVAS.WIDTH - const height = CONFIG.CANVAS.HEIGHT - const color = 4 // Assuming 4 is Red in the palette, or just a distinct color - - for (let i = 0; i < Math.min(width, height); i++) { - const index = i * width + i - canvas.data[index] = color - } + const hasContent = canvas.data.some((pixel) => pixel !== 0) + + if (!hasContent) { + console.log(`Canvas for '${lobbyName}' is empty. Seeding pattern...`) + // Draw a red diagonal line + const width = CONFIG.CANVAS.WIDTH + const height = CONFIG.CANVAS.HEIGHT + const color = 4 // Red - await canvas.save() - console.log(`Seeded '${lobbyName}' with a diagonal pattern`) + for (let i = 0; i < Math.min(width, height); i++) { + const index = i * width + i + canvas.data[index] = color + } + + canvas.markModified("data") + await canvas.save() + console.log(`Seeded '${lobbyName}' with a diagonal pattern`) + } } - } else { - console.log(`'${lobbyName}' already exists`) } } catch (error) { console.error("Lobby seed failed:", error) diff --git a/server/src/models/Canvas.ts b/server/src/models/Canvas.ts index 77956d7..81ced49 100644 --- a/server/src/models/Canvas.ts +++ b/server/src/models/Canvas.ts @@ -1,5 +1,5 @@ import { Schema, model, Document, Types } from 'mongoose'; -import { CONFIG } from '../config'; +import { CONFIG } from '../config.js'; export interface ICanvas extends Document { lobby: Types.ObjectId; // Back-link to Lobby (useful for maintenance) diff --git a/server/src/models/Lobby.ts b/server/src/models/Lobby.ts index 20f563c..cd10990 100644 --- a/server/src/models/Lobby.ts +++ b/server/src/models/Lobby.ts @@ -1,6 +1,6 @@ import { Schema, model, Document, Types, Model } from 'mongoose'; -import { Canvas } from './Canvas'; -import { CONFIG } from '../config'; +import { Canvas } from './Canvas.js'; +import { CONFIG } from '../config.js'; export interface ILobby extends Document { name: string; diff --git a/server/src/services/lobby.service.ts b/server/src/services/lobby.service.ts index 34926c0..d14e2e9 100644 --- a/server/src/services/lobby.service.ts +++ b/server/src/services/lobby.service.ts @@ -1,4 +1,4 @@ -import { Lobby } from '../models/Lobby'; +import { Lobby } from '../models/Lobby.js'; export class LobbyService { diff --git a/server/src/store/canvas.store.ts b/server/src/store/canvas.store.ts index b06dbcf..b0ebf3f 100644 --- a/server/src/store/canvas.store.ts +++ b/server/src/store/canvas.store.ts @@ -1,4 +1,4 @@ -import { CONFIG } from '../config'; +import { CONFIG } from '../config.js'; export class CanvasStore { private buffers: Map = new Map();