everydaycats commited on
Commit
2e4b248
·
verified ·
1 Parent(s): 3fb6614

Update stateManager.js

Browse files
Files changed (1) hide show
  1. stateManager.js +55 -69
stateManager.js CHANGED
@@ -1,5 +1,4 @@
1
- import admin from 'firebase-admin';
2
-
3
  let db = null;
4
 
5
  export const initDB = (firebaseDB) => {
@@ -7,67 +6,50 @@ export const initDB = (firebaseDB) => {
7
  };
8
 
9
  const activeProjects = new Map();
10
- // New: Prevents multiple requests from triggering DB fetches for the same ID simultaneously
11
- const hydrationPromises = new Map();
12
 
13
  export const StateManager = {
14
  /**
15
  * GET PROJECT
16
- * Fixes: Concurrency handling & String normalization
 
17
  */
18
- getProject: async (rawId) => {
19
- const projectId = String(rawId).trim(); // Force string
20
-
21
  // 1. Try Memory
22
  if (activeProjects.has(projectId)) {
23
- const project = activeProjects.get(projectId);
24
- // Update activity timestamp on read so it doesn't get cleaned up while active
25
- project.lastActive = Date.now();
26
- return project;
27
  }
28
 
29
- // 2. Check if already hydrating (Race Condition Fix)
30
- if (hydrationPromises.has(projectId)) {
31
- return hydrationPromises.get(projectId);
32
- }
33
-
34
- // 3. Try Firebase (Hydration)
35
  if (db) {
36
- const hydrationTask = (async () => {
37
- try {
38
- console.log(`[StateManager] 🟡 Hydrating project ${projectId} from DB...`);
39
- const snapshot = await db.ref(`projects/${projectId}`).once('value');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
- if (snapshot.exists()) {
42
- const dbData = snapshot.val();
43
-
44
- // Flatten DB structure for Memory usage
45
- const memoryObject = {
46
- ...dbData.info,
47
- thumbnail: null, // dbData.thumbnail || null,
48
- commandQueue: dbData.state?.commandQueue || [],
49
- workerHistory: dbData.state?.workerHistory || [],
50
- pmHistory: dbData.state?.pmHistory || [],
51
- failureCount: dbData.state?.failureCount || 0,
52
- gdd: dbData.state?.gdd || null,
53
- lastActive: Date.now(), // Set initial active time
54
- lastUpdated: dbData.info?.lastUpdated || Date.now()
55
- };
56
-
57
- activeProjects.set(projectId, memoryObject);
58
- console.log(`[StateManager] 🟢 Project ${projectId} hydrated.`);
59
- return memoryObject;
60
- }
61
- } catch (err) {
62
- console.error(`[StateManager] Error reading DB for ${projectId}:`, err);
63
- } finally {
64
- hydrationPromises.delete(projectId); // Clear lock
65
  }
66
- return null;
67
- })();
68
-
69
- hydrationPromises.set(projectId, hydrationTask);
70
- return hydrationTask;
71
  }
72
 
73
  return null;
@@ -75,11 +57,13 @@ export const StateManager = {
75
 
76
  /**
77
  * UPDATE PROJECT
 
 
78
  */
79
- updateProject: async (rawId, data) => {
80
- const projectId = String(rawId).trim();
81
  let current = activeProjects.get(projectId);
82
 
 
83
  if (!current) {
84
  current = await StateManager.getProject(projectId) || {
85
  commandQueue: [], workerHistory: [], pmHistory: [], failureCount: 0
@@ -88,7 +72,7 @@ export const StateManager = {
88
 
89
  const timestamp = Date.now();
90
 
91
- // 1. Update Memory
92
  const newData = {
93
  ...current,
94
  ...data,
@@ -97,17 +81,21 @@ export const StateManager = {
97
  };
98
  activeProjects.set(projectId, newData);
99
 
100
- // 2. Update Firebase
101
  if (db) {
102
  const updates = {};
103
 
104
- // Info Bucket
 
 
 
105
  if (data.status || data.stats || data.title) {
106
  updates[`projects/${projectId}/info/status`] = newData.status;
107
- updates[`projects/${projectId}/info/lastUpdated`] = timestamp;
 
108
  }
109
 
110
- // State Bucket (Most frequent)
111
  if (data.workerHistory || data.pmHistory || data.commandQueue || data.failureCount || data.gdd) {
112
  updates[`projects/${projectId}/state/workerHistory`] = newData.workerHistory;
113
  updates[`projects/${projectId}/state/pmHistory`] = newData.pmHistory;
@@ -116,22 +104,23 @@ export const StateManager = {
116
  if (newData.gdd) updates[`projects/${projectId}/state/gdd`] = newData.gdd;
117
  }
118
 
119
- // Thumbnail Bucket
120
  if (data.thumbnail) {
121
  updates[`projects/${projectId}/thumbnail`] = data.thumbnail.url;
122
  }
123
 
 
124
  if (Object.keys(updates).length > 0) {
125
- // No await here - let it write in background to not block the AI
126
- db.ref().update(updates).catch(err => console.error(`[DB Write Error]`, err));
 
127
  }
128
  }
129
 
130
  return newData;
131
  },
132
 
133
- queueCommand: async (rawId, input) => {
134
- const projectId = String(rawId).trim();
135
  const project = await StateManager.getProject(projectId);
136
  if (!project) return;
137
 
@@ -142,7 +131,6 @@ export const StateManager = {
142
  }
143
  else if (typeof input === 'string') {
144
  const rawResponse = input;
145
- // Logic filters
146
  if (rawResponse.includes("[ASK_PM:")) return;
147
  if (rawResponse.includes("[GENERATE_IMAGE:") && !rawResponse.includes("```")) return;
148
 
@@ -160,18 +148,17 @@ export const StateManager = {
160
  if (command) {
161
  project.commandQueue.push(command);
162
  console.log(`[${projectId}] Queued Action: ${command.type}`);
 
163
  await StateManager.updateProject(projectId, { commandQueue: project.commandQueue });
164
  }
165
  },
166
 
167
- popCommand: async (rawId) => {
168
- const projectId = String(rawId).trim();
169
- // This call ensures hydration AND updates lastActive
170
  const project = await StateManager.getProject(projectId);
171
-
172
  if (!project || !project.commandQueue.length) return null;
173
 
174
  const command = project.commandQueue.shift();
 
175
  await StateManager.updateProject(projectId, { commandQueue: project.commandQueue });
176
 
177
  return command;
@@ -183,7 +170,6 @@ export const StateManager = {
183
  let count = 0;
184
 
185
  for (const [id, data] of activeProjects.entries()) {
186
- // Only clean if it hasn't been active in 4 hours
187
  if (now - (data.lastActive || 0) > FOUR_HOURS) {
188
  activeProjects.delete(id);
189
  count++;
 
1
+ // stateManager.js
 
2
  let db = null;
3
 
4
  export const initDB = (firebaseDB) => {
 
6
  };
7
 
8
  const activeProjects = new Map();
 
 
9
 
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
18
  if (activeProjects.has(projectId)) {
19
+ return activeProjects.get(projectId);
 
 
 
20
  }
21
 
22
+ // 2. Try Firebase (Hydration)
 
 
 
 
 
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);
52
+ }
 
 
53
  }
54
 
55
  return null;
 
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
 
72
 
73
  const timestamp = Date.now();
74
 
75
+ // 1. Update Memory (Flat Merge)
76
  const newData = {
77
  ...current,
78
  ...data,
 
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;
 
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.url;
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
 
 
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
 
 
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;
 
170
  let count = 0;
171
 
172
  for (const [id, data] of activeProjects.entries()) {
 
173
  if (now - (data.lastActive || 0) > FOUR_HOURS) {
174
  activeProjects.delete(id);
175
  count++;