// stateManager.js let db = null; export const initDB = (firebaseDB) => { db = firebaseDB; }; const activeProjects = new Map(); export const StateManager = { /** * GET PROJECT * Hydrates from DB if not in memory. * Flattens { info, thumbnail, state } into a usable Memory Object. */ getProject: async (projectId) => { // 1. Try Memory if (activeProjects.has(projectId)) { return activeProjects.get(projectId); } // 2. Try Firebase (Hydration) if (db) { try { console.log(`[StateManager] ๐ŸŸก Hydrating project ${projectId} from DB...`); // We fetch the root because the Server AI needs EVERYTHING (context, history) to function. // If this was a client-side Dashboard, we would only fetch /info. const snapshot = await db.ref(`projects/${projectId}`).once('value'); if (snapshot.exists()) { const dbData = snapshot.val(); // FLATTEN DB STRUCTURE -> MEMORY STRUCTURE const memoryObject = { ...dbData.info, // Title, stats, description thumbnail: dbData.thumbnail?.url || null, // State might be undefined if just created, so default it commandQueue: dbData.state?.commandQueue || [], workerHistory: dbData.state?.workerHistory || [], pmHistory: dbData.state?.pmHistory || [], failureCount: dbData.state?.failureCount || 0, gdd: dbData.state?.gdd || null, // CRITICAL: Initialize lastActive to NOW so it doesn't expire immediately lastActive: Date.now(), lastUpdated: Date.now() }; // Load into Memory activeProjects.set(projectId, memoryObject); console.log(`[StateManager] ๐ŸŸข Project ${projectId} hydrated.`); return memoryObject; } } catch (err) { console.error(`[StateManager] Error reading DB for ${projectId}:`, err); } } return null; }, /** * UPDATE PROJECT * Updates Memory immediately. * Performs granular updates to DB (separating Info vs State). */ updateProject: async (projectId, data) => { let current = activeProjects.get(projectId); // Safety check: ensure we have base state if (!current) { current = await StateManager.getProject(projectId) || { commandQueue: [], workerHistory: [], pmHistory: [], failureCount: 0 }; } const timestamp = Date.now(); // 1. Update Memory (Flat Merge) const newData = { ...current, ...data, // CRITICAL: Initialize lastActive to NOW so it doesn't expire immediately lastActive: Date.now(), lastUpdated: Date.now() }; activeProjects.set(projectId, newData); // 2. Update Firebase (Nested Split) if (db) { const updates = {}; // We assume 'data' contains the specific fields that changed. // However, to be safe and robust, we map the current state to the correct buckets. // Bucket: Info if (data.status || data.stats || data.title) { updates[`projects/${projectId}/info/status`] = newData.status; if (newData.lastUpdated) updates[`projects/${projectId}/info/lastUpdated`] = newData.lastUpdated; // Add other info fields if they are mutable } // Bucket: State (History, Queue, GDD) - This is the most frequent update if (data.workerHistory || data.pmHistory || data.commandQueue || data.failureCount || data.gdd) { updates[`projects/${projectId}/state/workerHistory`] = newData.workerHistory; updates[`projects/${projectId}/state/pmHistory`] = newData.pmHistory; updates[`projects/${projectId}/state/commandQueue`] = newData.commandQueue; updates[`projects/${projectId}/state/failureCount`] = newData.failureCount; if (newData.gdd) updates[`projects/${projectId}/state/gdd`] = newData.gdd; } // Bucket: Thumbnail (Only update if explicitly passed in 'data', avoiding re-upload of massive string) if (data.thumbnail) { updates[`projects/${projectId}/thumbnail`] = {url: data.thumbnail}; } // Perform the update if (Object.keys(updates).length > 0) { db.ref().update(updates).catch(err => { console.error(`[StateManager] ๐Ÿ”ด DB Write Failed for ${projectId}:`, err); }); } } return newData; }, queueCommand: async (projectId, input) => { const project = await StateManager.getProject(projectId); if (!project) return; let command = null; if (typeof input === 'object' && input.type && input.payload) { command = input; } else if (typeof input === 'string') { const rawResponse = input; if (rawResponse.includes("[ASK_PM:")) return; if (rawResponse.includes("[GENERATE_IMAGE:") && !rawResponse.includes("```")) return; const codeMatch = rawResponse.match(/```(?:lua|luau)?([\s\S]*?)```/i); const readScriptMatch = rawResponse.match(/\[READ_SCRIPT:\s*(.*?)\]/); const readHierarchyMatch = rawResponse.match(/\[READ_HIERARCHY:\s*(.*?)\]/); const readLogsMatch = rawResponse.includes("[READ_LOGS]"); if (codeMatch) command = { type: "EXECUTE", payload: codeMatch[1].trim() }; else if (readScriptMatch) command = { type: "READ_SCRIPT", payload: readScriptMatch[1].trim() }; else if (readHierarchyMatch) command = { type: "READ_HIERARCHY", payload: readHierarchyMatch[1].trim() }; else if (readLogsMatch) command = { type: "READ_LOGS", payload: null }; } if (command) { project.commandQueue.push(command); console.log(`[${projectId}] Queued Action: ${command.type}`); // Sync queue to DB await StateManager.updateProject(projectId, { commandQueue: project.commandQueue }); } }, popCommand: async (projectId) => { const project = await StateManager.getProject(projectId); if (!project || !project.commandQueue.length) return null; const command = project.commandQueue.shift(); // Sync queue to DB await StateManager.updateProject(projectId, { commandQueue: project.commandQueue }); return command; }, cleanupMemory: () => { const now = Date.now(); const FOUR_HOURS = 4 * 60 * 60 * 1000; let count = 0; for (const [id, data] of activeProjects.entries()) { // FIX: If lastActive is missing/undefined/0, reset it to NOW. // This prevents the "54 year old project" bug where (now - 0) > 4 hours. if (!data.lastActive) { console.warn(`[StateManager] โš ๏ธ Project ${id} missing timestamp. Healing data...`); data.lastActive = now; continue; // Skip deleting this round } if (now - data.lastActive > FOUR_HOURS) { console.log(`[StateManager] ๐Ÿงน Removing expired project: ${id}`); activeProjects.delete(id); count++; } } if (count > 0) { console.log(`[StateManager] ๐Ÿ—‘๏ธ Cleaned ${count} projects from memory.`); } return count; } /* cleanupMemory: () => { const now = Date.now(); const FOUR_HOURS = 4 * 60 * 60 * 1000; let count = 0; for (const [id, data] of activeProjects.entries()) { if (now - (data.lastActive || 0) > FOUR_HOURS) { activeProjects.delete(id); count++; } } console.log(`[StateManager] ๐Ÿงน Memory Cleanup: Removed ${count} inactive projects.`); return count; } */ };