import { redisClient } from '../config/database-factory'; import { Pixel, PixelChunk, getChunkCoordinates, getChunkKey, getPixelKey, CANVAS_CONFIG } from '@gaplace/shared'; import { config } from '../config/env'; export class CanvasService { private readonly keyPrefix = config.redis.keyPrefix; async placePixel(canvasId: string, x: number, y: number, color: string, userId: string): Promise { try { const { chunkX, chunkY } = getChunkCoordinates(x, y); const chunkKey = `${this.keyPrefix}canvas:${canvasId}:chunk:${getChunkKey(chunkX, chunkY)}`; const pixelKey = getPixelKey(x % CANVAS_CONFIG.DEFAULT_CHUNK_SIZE, y % CANVAS_CONFIG.DEFAULT_CHUNK_SIZE); // Simple approach without pipeline for better compatibility await redisClient.hSet(chunkKey, pixelKey, color); // Update chunk metadata await redisClient.hSet(`${chunkKey}:meta`, { lastModified: Date.now().toString(), lastUser: userId, }); // Track user pixel count await redisClient.incr(`${this.keyPrefix}user:${userId}:pixels`); // Update canvas stats await redisClient.incr(`${this.keyPrefix}canvas:${canvasId}:totalPixels`); await redisClient.set(`${this.keyPrefix}canvas:${canvasId}:lastActivity`, Date.now().toString()); return true; } catch (error) { console.error('Error placing pixel:', error); return false; } } async getChunk(canvasId: string, chunkX: number, chunkY: number): Promise { try { const chunkKey = `${this.keyPrefix}canvas:${canvasId}:chunk:${getChunkKey(chunkX, chunkY)}`; const [pixelData, metadata] = await Promise.all([ redisClient.hGetAll(chunkKey), redisClient.hGetAll(`${chunkKey}:meta`) ]); if (Object.keys(pixelData).length === 0) { return null; } const pixels = new Map(); for (const [key, color] of Object.entries(pixelData)) { pixels.set(key, String(color)); } return { chunkX, chunkY, pixels, lastModified: parseInt(metadata.lastModified || '0'), }; } catch (error) { console.error('Error getting chunk:', error); return null; } } async getVisibleChunks(canvasId: string, startX: number, startY: number, endX: number, endY: number): Promise { const chunks: PixelChunk[] = []; const startChunkX = Math.floor(startX / CANVAS_CONFIG.DEFAULT_CHUNK_SIZE); const startChunkY = Math.floor(startY / CANVAS_CONFIG.DEFAULT_CHUNK_SIZE); const endChunkX = Math.floor(endX / CANVAS_CONFIG.DEFAULT_CHUNK_SIZE); const endChunkY = Math.floor(endY / CANVAS_CONFIG.DEFAULT_CHUNK_SIZE); const promises: Promise[] = []; for (let chunkX = startChunkX; chunkX <= endChunkX; chunkX++) { for (let chunkY = startChunkY; chunkY <= endChunkY; chunkY++) { promises.push(this.getChunk(canvasId, chunkX, chunkY)); } } const results = await Promise.all(promises); for (const chunk of results) { if (chunk) { chunks.push(chunk); } } return chunks; } async getCanvasStats(canvasId: string): Promise<{ totalPixels: number; lastActivity: number; activeUsers: number; }> { try { const [totalPixels, lastActivity, activeUsers] = await Promise.all([ redisClient.get(`${this.keyPrefix}canvas:${canvasId}:totalPixels`), redisClient.get(`${this.keyPrefix}canvas:${canvasId}:lastActivity`), redisClient.sCard(`${this.keyPrefix}canvas:${canvasId}:activeUsers`) ]); return { totalPixels: parseInt(totalPixels || '0'), lastActivity: parseInt(lastActivity || '0'), activeUsers: activeUsers || 0, }; } catch (error) { console.error('Error getting canvas stats:', error); return { totalPixels: 0, lastActivity: 0, activeUsers: 0, }; } } async addActiveUser(canvasId: string, userId: string): Promise { try { await redisClient.sAdd(`${this.keyPrefix}canvas:${canvasId}:activeUsers`, userId); // Set expiration to auto-remove inactive users await redisClient.expire(`${this.keyPrefix}canvas:${canvasId}:activeUsers`, 300); // 5 minutes } catch (error) { console.error('Error adding active user:', error); } } async removeActiveUser(canvasId: string, userId: string): Promise { try { await redisClient.sRem(`${this.keyPrefix}canvas:${canvasId}:activeUsers`, userId); } catch (error) { console.error('Error removing active user:', error); } } async getActiveUsers(canvasId: string): Promise { try { return await redisClient.sMembers(`${this.keyPrefix}canvas:${canvasId}:activeUsers`); } catch (error) { console.error('Error getting active users:', error); return []; } } }