everydaycats commited on
Commit
70e8361
·
verified ·
1 Parent(s): dd3f8ec

Update stateManager.js

Browse files
Files changed (1) hide show
  1. stateManager.js +67 -69
stateManager.js CHANGED
@@ -1,10 +1,6 @@
1
- import admin from 'firebase-admin';
2
-
3
- // Initialize Firebase (Assuming it's initialized in server.js, but we need the ref here)
4
- // We will rely on server.js to initialize the app, or we can lazy-load the db ref.
5
  let db = null;
6
 
7
- // This function allows server.js to pass the initialized DB instance
8
  export const initDB = (firebaseDB) => {
9
  db = firebaseDB;
10
  };
@@ -14,7 +10,8 @@ const activeProjects = new Map();
14
  export const StateManager = {
15
  /**
16
  * GET PROJECT
17
- * Strategy: Memory First -> DB Fallback -> Null
 
18
  */
19
  getProject: async (projectId) => {
20
  // 1. Try Memory
@@ -26,19 +23,29 @@ export const StateManager = {
26
  if (db) {
27
  try {
28
  console.log(`[StateManager] 🟡 Hydrating project ${projectId} from DB...`);
 
 
29
  const snapshot = await db.ref(`projects/${projectId}`).once('value');
 
30
  if (snapshot.exists()) {
31
- const projectData = snapshot.val();
32
 
33
- // Ensure arrays exist (Firebase doesn't store empty arrays)
34
- if (!projectData.commandQueue) projectData.commandQueue = [];
35
- if (!projectData.workerHistory) projectData.workerHistory = [];
36
- if (!projectData.pmHistory) projectData.pmHistory = [];
 
 
 
 
 
 
 
37
 
38
  // Load into Memory
39
- activeProjects.set(projectId, projectData);
40
  console.log(`[StateManager] 🟢 Project ${projectId} hydrated.`);
41
- return projectData;
42
  }
43
  } catch (err) {
44
  console.error(`[StateManager] Error reading DB for ${projectId}:`, err);
@@ -50,122 +57,113 @@ export const StateManager = {
50
 
51
  /**
52
  * UPDATE PROJECT
53
- * Strategy: Update Memory -> Async Write to DB
 
54
  */
55
  updateProject: async (projectId, data) => {
56
- // We get the current state from memory (or hydrate it if missing, though usually called after get)
57
  let current = activeProjects.get(projectId);
58
 
59
- // If it wasn't in memory, try to get it first to avoid overwriting with partial data
60
  if (!current) {
61
  current = await StateManager.getProject(projectId) || {
62
- commandQueue: [],
63
- workerHistory: [],
64
- pmHistory: [],
65
- failureCount: 0
66
  };
67
  }
68
 
69
  const timestamp = Date.now();
70
 
71
- // Merge Data
72
  const newData = {
73
  ...current,
74
  ...data,
75
- lastUpdated: timestamp, // UCT timestamp for sync
76
  lastActive: timestamp
77
  };
78
-
79
- // 1. Write to Memory
80
  activeProjects.set(projectId, newData);
81
 
82
- // 2. Write to Firebase (Fire and Forget)
83
  if (db) {
84
- db.ref(`projects/${projectId}`).update(data).catch(err => {
85
- console.error(`[StateManager] 🔴 DB Write Failed for ${projectId}:`, err);
86
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
  return newData;
90
  },
91
 
92
- /**
93
- * QUEUE COMMAND
94
- * Parses input, updates command list in Memory & DB
95
- */
96
  queueCommand: async (projectId, input) => {
97
- // Ensure project is loaded
98
  const project = await StateManager.getProject(projectId);
99
  if (!project) return;
100
 
101
  let command = null;
102
 
103
- // A. HANDLE PRE-FORMED OBJECTS
104
  if (typeof input === 'object' && input.type && input.payload) {
105
  command = input;
106
  }
107
- // B. HANDLE RAW TEXT RESPONSE
108
  else if (typeof input === 'string') {
109
  const rawResponse = input;
110
-
111
- if (rawResponse.includes("[ASK_PM:")) {
112
- console.log(`[${projectId}] Skipped Plugin Queue (Internal PM Consultation)`);
113
- return;
114
- }
115
-
116
- if (rawResponse.includes("[GENERATE_IMAGE:") && !rawResponse.includes("```")) {
117
- return;
118
- }
119
 
120
  const codeMatch = rawResponse.match(/```(?:lua|luau)?([\s\S]*?)```/i);
121
  const readScriptMatch = rawResponse.match(/\[READ_SCRIPT:\s*(.*?)\]/);
122
  const readHierarchyMatch = rawResponse.match(/\[READ_HIERARCHY:\s*(.*?)\]/);
123
  const readLogsMatch = rawResponse.includes("[READ_LOGS]");
124
 
125
- if (codeMatch) {
126
- command = { type: "EXECUTE", payload: codeMatch[1].trim() };
127
- } else if (readScriptMatch) {
128
- command = { type: "READ_SCRIPT", payload: readScriptMatch[1].trim() };
129
- } else if (readHierarchyMatch) {
130
- command = { type: "READ_HIERARCHY", payload: readHierarchyMatch[1].trim() };
131
- } else if (readLogsMatch) {
132
- command = { type: "READ_LOGS", payload: null };
133
- }
134
  }
135
 
136
  if (command) {
137
  project.commandQueue.push(command);
138
  console.log(`[${projectId}] Queued Action: ${command.type}`);
139
-
140
- // Persist the updated queue to both
141
  await StateManager.updateProject(projectId, { commandQueue: project.commandQueue });
142
- } else {
143
- if (typeof input === 'string' && input.length < 500) {
144
- console.log(`[${projectId}] Chatter (No Command):`, input);
145
- }
146
  }
147
  },
148
 
149
- /**
150
- * POP COMMAND
151
- * Removes from Memory & DB
152
- */
153
  popCommand: async (projectId) => {
154
  const project = await StateManager.getProject(projectId);
155
  if (!project || !project.commandQueue.length) return null;
156
 
157
  const command = project.commandQueue.shift();
158
-
159
- // Sync the removal to DB
160
  await StateManager.updateProject(projectId, { commandQueue: project.commandQueue });
161
 
162
  return command;
163
  },
164
 
165
- /**
166
- * CLEANUP
167
- * Removes projects from Memory if inactive for > 4 hours
168
- */
169
  cleanupMemory: () => {
170
  const now = Date.now();
171
  const FOUR_HOURS = 4 * 60 * 60 * 1000;
 
1
+ // stateManager.js
 
 
 
2
  let db = null;
3
 
 
4
  export const initDB = (firebaseDB) => {
5
  db = firebaseDB;
6
  };
 
10
  export const StateManager = {
11
  /**
12
  * GET PROJECT
13
+ * Hydrates from DB if not in memory.
14
+ * Flattens { info, thumbnail, state } into a usable Memory Object.
15
  */
16
  getProject: async (projectId) => {
17
  // 1. Try Memory
 
23
  if (db) {
24
  try {
25
  console.log(`[StateManager] 🟡 Hydrating project ${projectId} from DB...`);
26
+ // We fetch the root because the Server AI needs EVERYTHING (context, history) to function.
27
+ // If this was a client-side Dashboard, we would only fetch /info.
28
  const snapshot = await db.ref(`projects/${projectId}`).once('value');
29
+
30
  if (snapshot.exists()) {
31
+ const dbData = snapshot.val();
32
 
33
+ // FLATTEN DB STRUCTURE -> MEMORY STRUCTURE
34
+ const memoryObject = {
35
+ ...dbData.info, // Title, stats, description
36
+ thumbnail: dbData.thumbnail || null,
37
+ // State might be undefined if just created, so default it
38
+ commandQueue: dbData.state?.commandQueue || [],
39
+ workerHistory: dbData.state?.workerHistory || [],
40
+ pmHistory: dbData.state?.pmHistory || [],
41
+ failureCount: dbData.state?.failureCount || 0,
42
+ gdd: dbData.state?.gdd || null
43
+ };
44
 
45
  // Load into Memory
46
+ activeProjects.set(projectId, memoryObject);
47
  console.log(`[StateManager] 🟢 Project ${projectId} hydrated.`);
48
+ return memoryObject;
49
  }
50
  } catch (err) {
51
  console.error(`[StateManager] Error reading DB for ${projectId}:`, err);
 
57
 
58
  /**
59
  * UPDATE PROJECT
60
+ * Updates Memory immediately.
61
+ * Performs granular updates to DB (separating Info vs State).
62
  */
63
  updateProject: async (projectId, data) => {
 
64
  let current = activeProjects.get(projectId);
65
 
66
+ // Safety check: ensure we have base state
67
  if (!current) {
68
  current = await StateManager.getProject(projectId) || {
69
+ commandQueue: [], workerHistory: [], pmHistory: [], failureCount: 0
 
 
 
70
  };
71
  }
72
 
73
  const timestamp = Date.now();
74
 
75
+ // 1. Update Memory (Flat Merge)
76
  const newData = {
77
  ...current,
78
  ...data,
79
+ lastUpdated: timestamp,
80
  lastActive: timestamp
81
  };
 
 
82
  activeProjects.set(projectId, newData);
83
 
84
+ // 2. Update Firebase (Nested Split)
85
  if (db) {
86
+ const updates = {};
87
+
88
+ // We assume 'data' contains the specific fields that changed.
89
+ // However, to be safe and robust, we map the current state to the correct buckets.
90
+
91
+ // Bucket: Info
92
+ if (data.status || data.stats || data.title) {
93
+ updates[`projects/${projectId}/info/status`] = newData.status;
94
+ if (newData.lastUpdated) updates[`projects/${projectId}/info/lastUpdated`] = newData.lastUpdated;
95
+ // Add other info fields if they are mutable
96
+ }
97
+
98
+ // Bucket: State (History, Queue, GDD) - This is the most frequent update
99
+ if (data.workerHistory || data.pmHistory || data.commandQueue || data.failureCount || data.gdd) {
100
+ updates[`projects/${projectId}/state/workerHistory`] = newData.workerHistory;
101
+ updates[`projects/${projectId}/state/pmHistory`] = newData.pmHistory;
102
+ updates[`projects/${projectId}/state/commandQueue`] = newData.commandQueue;
103
+ updates[`projects/${projectId}/state/failureCount`] = newData.failureCount;
104
+ if (newData.gdd) updates[`projects/${projectId}/state/gdd`] = newData.gdd;
105
+ }
106
+
107
+ // Bucket: Thumbnail (Only update if explicitly passed in 'data', avoiding re-upload of massive string)
108
+ if (data.thumbnail) {
109
+ updates[`projects/${projectId}/thumbnail`] = data.thumbnail;
110
+ }
111
+
112
+ // Perform the update
113
+ if (Object.keys(updates).length > 0) {
114
+ db.ref().update(updates).catch(err => {
115
+ console.error(`[StateManager] 🔴 DB Write Failed for ${projectId}:`, err);
116
+ });
117
+ }
118
  }
119
 
120
  return newData;
121
  },
122
 
 
 
 
 
123
  queueCommand: async (projectId, input) => {
 
124
  const project = await StateManager.getProject(projectId);
125
  if (!project) return;
126
 
127
  let command = null;
128
 
 
129
  if (typeof input === 'object' && input.type && input.payload) {
130
  command = input;
131
  }
 
132
  else if (typeof input === 'string') {
133
  const rawResponse = input;
134
+ if (rawResponse.includes("[ASK_PM:")) return;
135
+ if (rawResponse.includes("[GENERATE_IMAGE:") && !rawResponse.includes("```")) return;
 
 
 
 
 
 
 
136
 
137
  const codeMatch = rawResponse.match(/```(?:lua|luau)?([\s\S]*?)```/i);
138
  const readScriptMatch = rawResponse.match(/\[READ_SCRIPT:\s*(.*?)\]/);
139
  const readHierarchyMatch = rawResponse.match(/\[READ_HIERARCHY:\s*(.*?)\]/);
140
  const readLogsMatch = rawResponse.includes("[READ_LOGS]");
141
 
142
+ if (codeMatch) command = { type: "EXECUTE", payload: codeMatch[1].trim() };
143
+ else if (readScriptMatch) command = { type: "READ_SCRIPT", payload: readScriptMatch[1].trim() };
144
+ else if (readHierarchyMatch) command = { type: "READ_HIERARCHY", payload: readHierarchyMatch[1].trim() };
145
+ else if (readLogsMatch) command = { type: "READ_LOGS", payload: null };
 
 
 
 
 
146
  }
147
 
148
  if (command) {
149
  project.commandQueue.push(command);
150
  console.log(`[${projectId}] Queued Action: ${command.type}`);
151
+ // Sync queue to DB
 
152
  await StateManager.updateProject(projectId, { commandQueue: project.commandQueue });
 
 
 
 
153
  }
154
  },
155
 
 
 
 
 
156
  popCommand: async (projectId) => {
157
  const project = await StateManager.getProject(projectId);
158
  if (!project || !project.commandQueue.length) return null;
159
 
160
  const command = project.commandQueue.shift();
161
+ // Sync queue to DB
 
162
  await StateManager.updateProject(projectId, { commandQueue: project.commandQueue });
163
 
164
  return command;
165
  },
166
 
 
 
 
 
167
  cleanupMemory: () => {
168
  const now = Date.now();
169
  const FOUR_HOURS = 4 * 60 * 60 * 1000;