bulltrackers-module 1.0.105 → 1.0.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.MD +222 -222
  2. package/functions/appscript-api/helpers/errors.js +19 -19
  3. package/functions/appscript-api/index.js +58 -58
  4. package/functions/computation-system/helpers/orchestration_helpers.js +647 -113
  5. package/functions/computation-system/utils/data_loader.js +191 -191
  6. package/functions/computation-system/utils/utils.js +149 -254
  7. package/functions/core/utils/firestore_utils.js +433 -433
  8. package/functions/core/utils/pubsub_utils.js +53 -53
  9. package/functions/dispatcher/helpers/dispatch_helpers.js +47 -47
  10. package/functions/dispatcher/index.js +52 -52
  11. package/functions/etoro-price-fetcher/helpers/handler_helpers.js +124 -124
  12. package/functions/fetch-insights/helpers/handler_helpers.js +91 -91
  13. package/functions/generic-api/helpers/api_helpers.js +379 -379
  14. package/functions/generic-api/index.js +150 -150
  15. package/functions/invalid-speculator-handler/helpers/handler_helpers.js +75 -75
  16. package/functions/orchestrator/helpers/discovery_helpers.js +226 -226
  17. package/functions/orchestrator/helpers/update_helpers.js +92 -92
  18. package/functions/orchestrator/index.js +147 -147
  19. package/functions/price-backfill/helpers/handler_helpers.js +116 -123
  20. package/functions/social-orchestrator/helpers/orchestrator_helpers.js +61 -61
  21. package/functions/social-task-handler/helpers/handler_helpers.js +288 -288
  22. package/functions/task-engine/handler_creator.js +78 -78
  23. package/functions/task-engine/helpers/discover_helpers.js +125 -125
  24. package/functions/task-engine/helpers/update_helpers.js +118 -118
  25. package/functions/task-engine/helpers/verify_helpers.js +162 -162
  26. package/functions/task-engine/utils/firestore_batch_manager.js +258 -258
  27. package/index.js +105 -113
  28. package/package.json +45 -45
  29. package/functions/computation-system/computation_dependencies.json +0 -120
  30. package/functions/computation-system/helpers/worker_helpers.js +0 -340
  31. package/functions/computation-system/utils/computation_state_manager.js +0 -178
  32. package/functions/computation-system/utils/dependency_graph.js +0 -191
  33. package/functions/speculator-cleanup-orchestrator/helpers/cleanup_helpers.js +0 -160
@@ -1,433 +1,433 @@
1
- /**
2
- * @fileoverview Core Firestore utility functions.
3
- * REFACTORED: All functions are now stateless and receive dependencies.
4
- * 'db' (Firestore instance) and 'logger' are passed via a 'dependencies' object.
5
- */
6
- const { FieldValue, FieldPath } = require('@google-cloud/firestore');
7
-
8
- /**
9
- * Fetches and merges the most recent portfolio data for all normal users.
10
- * @param {object} dependencies - Contains db, logger.
11
- * @param {object} config - Configuration object.
12
- * @param {string} config.normalUserCollectionName - e.g., 'NormalUserPortfolios'.
13
- * @param {string} config.snapshotsSubCollectionName - e.g., 'snapshots'.
14
- * @param {string} config.partsSubCollectionName - e.g., 'parts'.
15
- * @returns {Promise<object>} A single object containing all user portfolios.
16
- */
17
- async function getLatestNormalUserPortfolios(dependencies, config) {
18
- const { db, logger } = dependencies;
19
- const { normalUserCollectionName, snapshotsSubCollectionName, partsSubCollectionName } = config;
20
-
21
- logger.log('INFO', `[Core Utils] Fetching latest portfolios from ${normalUserCollectionName}...`);
22
- const allPortfolios = {};
23
-
24
- const yesterday = new Date();
25
- yesterday.setDate(yesterday.getDate() - 1);
26
- const dateString = yesterday.toISOString().slice(0, 10);
27
-
28
- const blockDocs = await db.collection(normalUserCollectionName).listDocuments();
29
-
30
- for (const blockDoc of blockDocs) {
31
- const snapshotDocRef = blockDoc.collection(snapshotsSubCollectionName).doc(dateString);
32
- const partsCollectionRef = snapshotDocRef.collection(partsSubCollectionName);
33
- const partsSnapshot = await partsCollectionRef.get();
34
-
35
- if (!partsSnapshot.empty) {
36
- partsSnapshot.forEach(partDoc => {
37
- Object.assign(allPortfolios, partDoc.data());
38
- });
39
- }
40
- }
41
-
42
- logger.log('INFO', `[Core Utils] Found ${Object.keys(allPortfolios).length} user portfolios from ${dateString}'s snapshot.`);
43
- return allPortfolios;
44
- }
45
-
46
- /**
47
- * Resets the proxy locks map in the performance document.
48
- * @param {object} dependencies - Contains db, logger.
49
- * @param {object} config - Configuration object.
50
- * @param {string} config.proxyPerformanceDocPath - e.g., 'system_state/proxy_performance'.
51
- * @returns {Promise<void>}
52
- */
53
- async function resetProxyLocks(dependencies, config) {
54
- const { db, logger } = dependencies;
55
- const { proxyPerformanceDocPath } = config;
56
-
57
- logger.log('INFO','[Core Utils] Resetting proxy locks...');
58
- try {
59
- if (!proxyPerformanceDocPath) {
60
- logger.log('ERROR', '[Core Utils] Missing proxyPerformanceDocPath. Cannot reset locks.');
61
- return;
62
- }
63
-
64
- const perfDocRef = db.doc(proxyPerformanceDocPath);
65
-
66
- await perfDocRef.update({
67
- locks: FieldValue.delete()
68
- });
69
-
70
- logger.log('INFO',`[Core Utils] Proxy locks map reset in ${proxyPerformanceDocPath}.`);
71
- } catch (error) {
72
- if (error.code === 5) { // 5 = NOT_FOUND
73
- logger.log('WARN',`[Core Utils] Proxy performance doc or 'locks' field not found at ${proxyPerformanceDocPath}. No locks to reset.`);
74
- } else {
75
- logger.log('ERROR','[Core Utils] Error resetting proxy locks', { errorMessage: error.message, path: proxyPerformanceDocPath });
76
- }
77
- }
78
- }
79
-
80
- /**
81
- * Fetches and aggregates block capacities.
82
- * @param {object} dependencies - Contains db, logger.
83
- * @param {object} config - Configuration object.
84
- * @param {string} userType - 'normal' or 'speculator'.
85
- * @param {string} config.speculatorBlockCountsDocPath - Path to speculator counts doc.
86
- * @param {string} config.normalBlockCountsDocPath - Path to normal counts doc.
87
- * @returns {Promise<object>} Object containing counts.
88
- */
89
- async function getBlockCapacities(dependencies, config, userType) {
90
- const { db, logger } = dependencies;
91
- const { speculatorBlockCountsDocPath, normalBlockCountsDocPath } = config;
92
-
93
- logger.log('INFO',`[Core Utils] Getting block capacities for ${userType}...`);
94
- try {
95
- const docPath = userType === 'speculator'
96
- ? speculatorBlockCountsDocPath
97
- : normalBlockCountsDocPath;
98
-
99
- if (!docPath) {
100
- logger.log('ERROR', `[Core Utils] Missing block counts document path for ${userType}.`);
101
- return {};
102
- }
103
-
104
- const countsRef = db.doc(docPath);
105
- const countsDoc = await countsRef.get();
106
- if (!countsDoc.exists) {
107
- logger.log('WARN',`[Core Utils] Block counts document not found for ${userType} at ${docPath}. Returning empty.`);
108
- return {};
109
- }
110
- return countsDoc.data().counts || {};
111
- } catch (error) {
112
- logger.log('ERROR',`[Core Utils] Error getting block capacities for ${userType}`, { errorMessage: error.message });
113
- throw error;
114
- }
115
- }
116
-
117
- /**
118
- * Fetches user IDs from multiple sources for exclusion lists.
119
- * @param {object} dependencies - Contains db, logger.
120
- * @param {object} config - Configuration object.
121
- * @param {string} userType - 'normal' or 'speculator'.
122
- * @param {string} config.specBlocksCollection - e.g., 'SpeculatorBlocks'.
123
- * @param {string} config.pendingSpecCollection - e.g., 'PendingSpeculators'.
124
- * @param {string} config.invalidSpecCollection - e.g., 'InvalidSpeculators'.
125
- * @param {Set<string>} config.existingNormalUserIds - PRE-FETCHED normal user IDs.
126
- * @returns {Promise<Set<string>>} A Set containing unique user IDs.
127
- */
128
- async function getExclusionIds(dependencies, config, userType) {
129
- const { db, logger } = dependencies;
130
- const {
131
- specBlocksCollection,
132
- pendingSpecCollection,
133
- invalidSpecCollection,
134
- existingNormalUserIds // Get the pre-fetched IDs
135
- } = config;
136
-
137
- logger.log('INFO',`[Core Utils] Getting exclusion IDs for ${userType} discovery...`);
138
-
139
- const exclusionIds = new Set(existingNormalUserIds);
140
- logger.log('TRACE', `[Core Utils] Loaded ${exclusionIds.size} existing normal user IDs for exclusion.`);
141
-
142
- const promises = [];
143
-
144
- try {
145
- // 1. Existing Speculators
146
- const specBlocksRef = db.collection(specBlocksCollection);
147
- promises.push(specBlocksRef.get().then(snapshot => {
148
- snapshot.forEach(doc => {
149
- const users = doc.data().users || {};
150
- Object.keys(users).forEach(key => exclusionIds.add(key.split('.')[1]));
151
- });
152
- logger.log('TRACE','[Core Utils] Fetched existing speculator IDs for exclusion.');
153
- }));
154
-
155
- // 2. Pending Speculators
156
- if (userType === 'speculator') {
157
- const pendingRef = db.collection(pendingSpecCollection);
158
- promises.push(pendingRef.get().then(snapshot => {
159
- snapshot.forEach(doc => {
160
- Object.keys(doc.data().users || {}).forEach(cid => exclusionIds.add(cid));
161
- });
162
- logger.log('TRACE','[Core Utils] Fetched pending speculator IDs for exclusion.');
163
- }));
164
- }
165
-
166
- // 3. Invalid Speculators
167
- const invalidRef = db.collection(invalidSpecCollection);
168
- promises.push(invalidRef.get().then(snapshot => {
169
- snapshot.forEach(doc => {
170
- const data = doc.data();
171
- if (data) {
172
- Object.keys(data.users || {}).forEach(cid => exclusionIds.add(cid));
173
- }
174
- });
175
- logger.log('TRACE','[Core Utils] Fetched invalid speculator IDs for exclusion.');
176
- }));
177
-
178
- await Promise.all(promises);
179
- logger.log('INFO',`[Core Utils] Total unique exclusion IDs found: ${exclusionIds.size}`);
180
- return exclusionIds;
181
-
182
- } catch (error)
183
- {
184
- logger.log('ERROR','[Core Utils] Error getting exclusion IDs', { errorMessage: error.message });
185
- throw error;
186
- }
187
- }
188
-
189
- /**
190
- * Scans normal user portfolios for potential speculator candidates.
191
- * @param {object} dependencies - Contains db, logger.
192
- * @param {Set<string>} exclusionIds - IDs to exclude.
193
- * @param {Set<number>} speculatorInstrumentSet - Set of instrument IDs.
194
- * @param {object} latestNormalPortfolios - PRE-FETCHED portfolio object.
195
- * @returns {Promise<string[]>} Array of prioritized speculator CIDs.
196
- */
197
- async function getPrioritizedSpeculators(dependencies, exclusionIds, speculatorInstrumentSet, latestNormalPortfolios) {
198
- const { logger } = dependencies;
199
- logger.log('INFO','[Core Utils] Scanning normal users for prioritized speculators...');
200
- const candidates = new Set();
201
-
202
- try {
203
- for (const userId in latestNormalPortfolios) {
204
- if (exclusionIds.has(userId)) continue;
205
-
206
- const portfolio = latestNormalPortfolios[userId];
207
- const holdsSpeculatorAsset = portfolio?.AggregatedPositions?.some(p =>
208
- speculatorInstrumentSet.has(p.InstrumentID)
209
- );
210
-
211
- if (holdsSpeculatorAsset) {
212
- candidates.add(userId);
213
- }
214
- }
215
- logger.log('INFO',`[Core Utils] Found ${candidates.size} potential prioritized speculators.`);
216
- return Array.from(candidates);
217
- } catch (error) {
218
- logger.log('ERROR','[Core Utils] Error getting prioritized speculators', { errorMessage: error.message });
219
- throw error;
220
- }
221
- }
222
-
223
- /**
224
- * Deletes all documents within a specified collection path.
225
- * @param {object} dependencies - Contains db, logger.
226
- * @param {string} collectionPath - The path to the collection to clear.
227
- * @param {number} maxBatchSize - Firestore batch size limit.
228
- * @returns {Promise<void>}
229
- */
230
- async function clearCollection(dependencies, collectionPath, maxBatchSize = 400) {
231
- const { db, logger } = dependencies;
232
- logger.log('WARN', `[Core Utils] Starting SCORCHED EARTH delete for collection: ${collectionPath}...`);
233
- try {
234
- const collectionRef = db.collection(collectionPath);
235
- let query = collectionRef.limit(maxBatchSize);
236
- let snapshot;
237
- let deleteCount = 0;
238
-
239
- while (true) {
240
- snapshot = await query.get();
241
- if (snapshot.size === 0) {
242
- break;
243
- }
244
-
245
- const batch = db.batch();
246
- snapshot.docs.forEach(doc => batch.delete(doc.ref));
247
- await batch.commit();
248
- deleteCount += snapshot.size;
249
-
250
- if (snapshot.size < maxBatchSize) {
251
- break;
252
- }
253
- query = collectionRef.limit(maxBatchSize);
254
- }
255
- logger.log('SUCCESS', `[Core Utils] Scorched earth complete. Deleted ${deleteCount} documents from ${collectionPath}.`);
256
- } catch (error) {
257
- logger.log('ERROR', `[Core Utils] Error clearing collection ${collectionPath}`, { errorMessage: error.message });
258
- throw error;
259
- }
260
- }
261
-
262
- /**
263
- * Writes an array of user IDs into sharded Firestore documents.
264
- * @param {object} dependencies - Contains db, logger.
265
- * @param {object} config - Configuration object.
266
- * @param {string} config.collectionPath - Base path for sharded documents (e.g., 'PendingSpeculators').
267
- * @param {string[]} config.items - The user IDs to write.
268
- * @param {Date} config.timestamp - Timestamp to associate with each user ID.
269
- * @param {number} config.maxFieldsPerDoc - Max user IDs per sharded document.
270
- * @param {number} config.maxWritesPerBatch - Max documents to write per batch commit.
271
- * @returns {Promise<void>}
272
- */
273
- async function batchWriteShardedIds(dependencies, config) {
274
- const { db, logger } = dependencies;
275
- const {
276
- collectionPath,
277
- items,
278
- timestamp,
279
- maxFieldsPerDoc,
280
- maxWritesPerBatch
281
- } = config;
282
-
283
- logger.log('INFO', `[Core Utils] Batch writing ${items.length} IDs to sharded path: ${collectionPath} (max ${maxFieldsPerDoc}/doc, ${maxWritesPerBatch} docs/batch)...`);
284
- if (items.length === 0) return;
285
-
286
- try {
287
- const collectionRef = db.collection(collectionPath);
288
- let batch = db.batch();
289
- let currentDocFields = {};
290
- let currentFieldCount = 0;
291
- let batchWriteCount = 0;
292
- let docCounter = 0;
293
-
294
- for (let i = 0; i < items.length; i++) {
295
- const userId = items[i];
296
- const key = `users.${userId}`;
297
- currentDocFields[key] = timestamp;
298
- currentFieldCount++;
299
-
300
- if (currentFieldCount >= maxFieldsPerDoc || i === items.length - 1) {
301
- const docRef = collectionRef.doc(`pending_${docCounter}_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`);
302
- batch.set(docRef, currentDocFields);
303
- batchWriteCount++;
304
-
305
- currentDocFields = {};
306
- currentFieldCount = 0;
307
- docCounter++;
308
-
309
- if (batchWriteCount >= maxWritesPerBatch || i === items.length - 1) {
310
- await batch.commit();
311
- batch = db.batch();
312
- batchWriteCount = 0;
313
- }
314
- }
315
- }
316
- logger.log('SUCCESS', `[Core Utils] Sharded write complete for ${collectionPath}. Created ${docCounter} documents.`);
317
- } catch (error) {
318
- logger.log('ERROR', `[Core Utils] Error during sharded write to ${collectionPath}`, { errorMessage: error.message });
319
- throw error;
320
- }
321
- }
322
-
323
- /**
324
- * Gets normal users whose timestamp indicates they need updating.
325
- * @param {object} dependencies - Contains db, logger.
326
- * @param {object} config - Configuration object.
327
- * @param {Date} config.dateThreshold - Users processed before this date will be returned.
328
- * @param {string} config.normalUserCollectionName - e.g., 'NormalUserPortfolios'.
329
- * @param {string} config.normalUserTimestampsDocId - e.g., 'normal'.
330
- * @returns {Promise<string[]>} Array of user IDs to update.
331
- */
332
- async function getNormalUsersToUpdate(dependencies, config) {
333
- const { db, logger } = dependencies;
334
- const { dateThreshold, normalUserCollectionName, normalUserTimestampsDocId } = config;
335
-
336
- logger.log('INFO','[Core Utils] Getting normal users to update...');
337
- const usersToUpdate = [];
338
- try {
339
- const timestampDocRef = db.collection(normalUserCollectionName)
340
- .doc('timestamps')
341
- .collection('users')
342
- .doc(normalUserTimestampsDocId);
343
- const timestampDoc = await timestampDocRef.get();
344
-
345
- if (!timestampDoc.exists) {
346
- logger.log('WARN',`[Core Utils] Normal user timestamp document not found at ${timestampDocRef.path}.`);
347
- return [];
348
- }
349
-
350
- const timestamps = timestampDoc.data().users || {};
351
- for (const userId in timestamps) {
352
- const lastProcessed = timestamps[userId]?.toDate ? timestamps[userId].toDate() : new Date(0);
353
- if (lastProcessed < dateThreshold) {
354
- usersToUpdate.push(userId);
355
- }
356
- }
357
- logger.log('INFO',`[Core Utils] Found ${usersToUpdate.length} normal users to update.`);
358
- return usersToUpdate;
359
- } catch (error) {
360
- logger.log('ERROR','[Core Utils] Error getting normal users to update', { errorMessage: error.message });
361
- throw error;
362
- }
363
- }
364
-
365
- /**
366
- * Gets speculator users/instruments whose data needs updating.
367
- * @param {object} dependencies - Contains db, logger.
368
- * @param {object} config - Configuration object.
369
- * @param {Date} config.dateThreshold - Verification date threshold.
370
- * @param {Date} config.gracePeriodThreshold - Last held asset date threshold.
371
- * @param {string} config.speculatorBlocksCollectionName - e.g., 'SpeculatorBlocks'.
372
- * @returns {Promise<object[]>} Array of objects like { userId: string, instrumentId: number }.
373
- */
374
- async function getSpeculatorsToUpdate(dependencies, config) {
375
- const { db, logger } = dependencies;
376
- const { dateThreshold, gracePeriodThreshold, speculatorBlocksCollectionName } = config;
377
-
378
- logger.log('INFO','[Core Utils] Getting speculators to update...');
379
- const updates = [];
380
- try {
381
- const blocksRef = db.collection(speculatorBlocksCollectionName);
382
- const snapshot = await blocksRef.get();
383
-
384
- if (snapshot.empty) {
385
- logger.log('INFO','[Core Utils] No speculator blocks found.');
386
- return [];
387
- }
388
-
389
- snapshot.forEach(doc => {
390
- const blockData = doc.data();
391
-
392
- // Iterate over the document's top-level keys
393
- for (const key in blockData) {
394
- // Filter for keys that match the 'users.CID' format
395
- if (!key.startsWith('users.')) continue;
396
-
397
- const userId = key.split('.')[1];
398
- if (!userId) continue; // Safety check
399
-
400
- const userData = blockData[key]; // Get the user's map
401
-
402
- const lastVerified = userData.lastVerified?.toDate ? userData.lastVerified.toDate() : new Date(0);
403
- const lastHeld = userData.lastHeldSpeculatorAsset?.toDate ? userData.lastHeldSpeculatorAsset.toDate() : new Date(0);
404
-
405
- if (lastVerified < dateThreshold && lastHeld > gracePeriodThreshold) {
406
- if (userData.instruments && Array.isArray(userData.instruments)) {
407
- userData.instruments.forEach(instrumentId => {
408
- updates.push({ userId, instrumentId });
409
- });
410
- }
411
- }
412
- }
413
- });
414
-
415
- logger.log('INFO',`[Core Utils] Found ${updates.length} speculator user/instrument pairs to update.`);
416
- return updates;
417
- } catch (error) {
418
- logger.log('ERROR','[Core Utils] Error getting speculators to update', { errorMessage: error.message });
419
- throw error;
420
- }
421
- }
422
-
423
- module.exports = {
424
- getLatestNormalUserPortfolios,
425
- resetProxyLocks,
426
- getBlockCapacities,
427
- getExclusionIds,
428
- getPrioritizedSpeculators,
429
- clearCollection,
430
- batchWriteShardedIds,
431
- getNormalUsersToUpdate,
432
- getSpeculatorsToUpdate,
433
- };
1
+ /**
2
+ * @fileoverview Core Firestore utility functions.
3
+ * REFACTORED: All functions are now stateless and receive dependencies.
4
+ * 'db' (Firestore instance) and 'logger' are passed via a 'dependencies' object.
5
+ */
6
+ const { FieldValue, FieldPath } = require('@google-cloud/firestore');
7
+
8
+ /**
9
+ * Fetches and merges the most recent portfolio data for all normal users.
10
+ * @param {object} dependencies - Contains db, logger.
11
+ * @param {object} config - Configuration object.
12
+ * @param {string} config.normalUserCollectionName - e.g., 'NormalUserPortfolios'.
13
+ * @param {string} config.snapshotsSubCollectionName - e.g., 'snapshots'.
14
+ * @param {string} config.partsSubCollectionName - e.g., 'parts'.
15
+ * @returns {Promise<object>} A single object containing all user portfolios.
16
+ */
17
+ async function getLatestNormalUserPortfolios(dependencies, config) {
18
+ const { db, logger } = dependencies;
19
+ const { normalUserCollectionName, snapshotsSubCollectionName, partsSubCollectionName } = config;
20
+
21
+ logger.log('INFO', `[Core Utils] Fetching latest portfolios from ${normalUserCollectionName}...`);
22
+ const allPortfolios = {};
23
+
24
+ const yesterday = new Date();
25
+ yesterday.setDate(yesterday.getDate() - 1);
26
+ const dateString = yesterday.toISOString().slice(0, 10);
27
+
28
+ const blockDocs = await db.collection(normalUserCollectionName).listDocuments();
29
+
30
+ for (const blockDoc of blockDocs) {
31
+ const snapshotDocRef = blockDoc.collection(snapshotsSubCollectionName).doc(dateString);
32
+ const partsCollectionRef = snapshotDocRef.collection(partsSubCollectionName);
33
+ const partsSnapshot = await partsCollectionRef.get();
34
+
35
+ if (!partsSnapshot.empty) {
36
+ partsSnapshot.forEach(partDoc => {
37
+ Object.assign(allPortfolios, partDoc.data());
38
+ });
39
+ }
40
+ }
41
+
42
+ logger.log('INFO', `[Core Utils] Found ${Object.keys(allPortfolios).length} user portfolios from ${dateString}'s snapshot.`);
43
+ return allPortfolios;
44
+ }
45
+
46
+ /**
47
+ * Resets the proxy locks map in the performance document.
48
+ * @param {object} dependencies - Contains db, logger.
49
+ * @param {object} config - Configuration object.
50
+ * @param {string} config.proxyPerformanceDocPath - e.g., 'system_state/proxy_performance'.
51
+ * @returns {Promise<void>}
52
+ */
53
+ async function resetProxyLocks(dependencies, config) {
54
+ const { db, logger } = dependencies;
55
+ const { proxyPerformanceDocPath } = config;
56
+
57
+ logger.log('INFO','[Core Utils] Resetting proxy locks...');
58
+ try {
59
+ if (!proxyPerformanceDocPath) {
60
+ logger.log('ERROR', '[Core Utils] Missing proxyPerformanceDocPath. Cannot reset locks.');
61
+ return;
62
+ }
63
+
64
+ const perfDocRef = db.doc(proxyPerformanceDocPath);
65
+
66
+ await perfDocRef.update({
67
+ locks: FieldValue.delete()
68
+ });
69
+
70
+ logger.log('INFO',`[Core Utils] Proxy locks map reset in ${proxyPerformanceDocPath}.`);
71
+ } catch (error) {
72
+ if (error.code === 5) { // 5 = NOT_FOUND
73
+ logger.log('WARN',`[Core Utils] Proxy performance doc or 'locks' field not found at ${proxyPerformanceDocPath}. No locks to reset.`);
74
+ } else {
75
+ logger.log('ERROR','[Core Utils] Error resetting proxy locks', { errorMessage: error.message, path: proxyPerformanceDocPath });
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Fetches and aggregates block capacities.
82
+ * @param {object} dependencies - Contains db, logger.
83
+ * @param {object} config - Configuration object.
84
+ * @param {string} userType - 'normal' or 'speculator'.
85
+ * @param {string} config.speculatorBlockCountsDocPath - Path to speculator counts doc.
86
+ * @param {string} config.normalBlockCountsDocPath - Path to normal counts doc.
87
+ * @returns {Promise<object>} Object containing counts.
88
+ */
89
+ async function getBlockCapacities(dependencies, config, userType) {
90
+ const { db, logger } = dependencies;
91
+ const { speculatorBlockCountsDocPath, normalBlockCountsDocPath } = config;
92
+
93
+ logger.log('INFO',`[Core Utils] Getting block capacities for ${userType}...`);
94
+ try {
95
+ const docPath = userType === 'speculator'
96
+ ? speculatorBlockCountsDocPath
97
+ : normalBlockCountsDocPath;
98
+
99
+ if (!docPath) {
100
+ logger.log('ERROR', `[Core Utils] Missing block counts document path for ${userType}.`);
101
+ return {};
102
+ }
103
+
104
+ const countsRef = db.doc(docPath);
105
+ const countsDoc = await countsRef.get();
106
+ if (!countsDoc.exists) {
107
+ logger.log('WARN',`[Core Utils] Block counts document not found for ${userType} at ${docPath}. Returning empty.`);
108
+ return {};
109
+ }
110
+ return countsDoc.data().counts || {};
111
+ } catch (error) {
112
+ logger.log('ERROR',`[Core Utils] Error getting block capacities for ${userType}`, { errorMessage: error.message });
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Fetches user IDs from multiple sources for exclusion lists.
119
+ * @param {object} dependencies - Contains db, logger.
120
+ * @param {object} config - Configuration object.
121
+ * @param {string} userType - 'normal' or 'speculator'.
122
+ * @param {string} config.specBlocksCollection - e.g., 'SpeculatorBlocks'.
123
+ * @param {string} config.pendingSpecCollection - e.g., 'PendingSpeculators'.
124
+ * @param {string} config.invalidSpecCollection - e.g., 'InvalidSpeculators'.
125
+ * @param {Set<string>} config.existingNormalUserIds - PRE-FETCHED normal user IDs.
126
+ * @returns {Promise<Set<string>>} A Set containing unique user IDs.
127
+ */
128
+ async function getExclusionIds(dependencies, config, userType) {
129
+ const { db, logger } = dependencies;
130
+ const {
131
+ specBlocksCollection,
132
+ pendingSpecCollection,
133
+ invalidSpecCollection,
134
+ existingNormalUserIds // Get the pre-fetched IDs
135
+ } = config;
136
+
137
+ logger.log('INFO',`[Core Utils] Getting exclusion IDs for ${userType} discovery...`);
138
+
139
+ const exclusionIds = new Set(existingNormalUserIds);
140
+ logger.log('TRACE', `[Core Utils] Loaded ${exclusionIds.size} existing normal user IDs for exclusion.`);
141
+
142
+ const promises = [];
143
+
144
+ try {
145
+ // 1. Existing Speculators
146
+ const specBlocksRef = db.collection(specBlocksCollection);
147
+ promises.push(specBlocksRef.get().then(snapshot => {
148
+ snapshot.forEach(doc => {
149
+ const users = doc.data().users || {};
150
+ Object.keys(users).forEach(key => exclusionIds.add(key.split('.')[1]));
151
+ });
152
+ logger.log('TRACE','[Core Utils] Fetched existing speculator IDs for exclusion.');
153
+ }));
154
+
155
+ // 2. Pending Speculators
156
+ if (userType === 'speculator') {
157
+ const pendingRef = db.collection(pendingSpecCollection);
158
+ promises.push(pendingRef.get().then(snapshot => {
159
+ snapshot.forEach(doc => {
160
+ Object.keys(doc.data().users || {}).forEach(cid => exclusionIds.add(cid));
161
+ });
162
+ logger.log('TRACE','[Core Utils] Fetched pending speculator IDs for exclusion.');
163
+ }));
164
+ }
165
+
166
+ // 3. Invalid Speculators
167
+ const invalidRef = db.collection(invalidSpecCollection);
168
+ promises.push(invalidRef.get().then(snapshot => {
169
+ snapshot.forEach(doc => {
170
+ const data = doc.data();
171
+ if (data) {
172
+ Object.keys(data.users || {}).forEach(cid => exclusionIds.add(cid));
173
+ }
174
+ });
175
+ logger.log('TRACE','[Core Utils] Fetched invalid speculator IDs for exclusion.');
176
+ }));
177
+
178
+ await Promise.all(promises);
179
+ logger.log('INFO',`[Core Utils] Total unique exclusion IDs found: ${exclusionIds.size}`);
180
+ return exclusionIds;
181
+
182
+ } catch (error)
183
+ {
184
+ logger.log('ERROR','[Core Utils] Error getting exclusion IDs', { errorMessage: error.message });
185
+ throw error;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Scans normal user portfolios for potential speculator candidates.
191
+ * @param {object} dependencies - Contains db, logger.
192
+ * @param {Set<string>} exclusionIds - IDs to exclude.
193
+ * @param {Set<number>} speculatorInstrumentSet - Set of instrument IDs.
194
+ * @param {object} latestNormalPortfolios - PRE-FETCHED portfolio object.
195
+ * @returns {Promise<string[]>} Array of prioritized speculator CIDs.
196
+ */
197
+ async function getPrioritizedSpeculators(dependencies, exclusionIds, speculatorInstrumentSet, latestNormalPortfolios) {
198
+ const { logger } = dependencies;
199
+ logger.log('INFO','[Core Utils] Scanning normal users for prioritized speculators...');
200
+ const candidates = new Set();
201
+
202
+ try {
203
+ for (const userId in latestNormalPortfolios) {
204
+ if (exclusionIds.has(userId)) continue;
205
+
206
+ const portfolio = latestNormalPortfolios[userId];
207
+ const holdsSpeculatorAsset = portfolio?.AggregatedPositions?.some(p =>
208
+ speculatorInstrumentSet.has(p.InstrumentID)
209
+ );
210
+
211
+ if (holdsSpeculatorAsset) {
212
+ candidates.add(userId);
213
+ }
214
+ }
215
+ logger.log('INFO',`[Core Utils] Found ${candidates.size} potential prioritized speculators.`);
216
+ return Array.from(candidates);
217
+ } catch (error) {
218
+ logger.log('ERROR','[Core Utils] Error getting prioritized speculators', { errorMessage: error.message });
219
+ throw error;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Deletes all documents within a specified collection path.
225
+ * @param {object} dependencies - Contains db, logger.
226
+ * @param {string} collectionPath - The path to the collection to clear.
227
+ * @param {number} maxBatchSize - Firestore batch size limit.
228
+ * @returns {Promise<void>}
229
+ */
230
+ async function clearCollection(dependencies, collectionPath, maxBatchSize = 400) {
231
+ const { db, logger } = dependencies;
232
+ logger.log('WARN', `[Core Utils] Starting SCORCHED EARTH delete for collection: ${collectionPath}...`);
233
+ try {
234
+ const collectionRef = db.collection(collectionPath);
235
+ let query = collectionRef.limit(maxBatchSize);
236
+ let snapshot;
237
+ let deleteCount = 0;
238
+
239
+ while (true) {
240
+ snapshot = await query.get();
241
+ if (snapshot.size === 0) {
242
+ break;
243
+ }
244
+
245
+ const batch = db.batch();
246
+ snapshot.docs.forEach(doc => batch.delete(doc.ref));
247
+ await batch.commit();
248
+ deleteCount += snapshot.size;
249
+
250
+ if (snapshot.size < maxBatchSize) {
251
+ break;
252
+ }
253
+ query = collectionRef.limit(maxBatchSize);
254
+ }
255
+ logger.log('SUCCESS', `[Core Utils] Scorched earth complete. Deleted ${deleteCount} documents from ${collectionPath}.`);
256
+ } catch (error) {
257
+ logger.log('ERROR', `[Core Utils] Error clearing collection ${collectionPath}`, { errorMessage: error.message });
258
+ throw error;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Writes an array of user IDs into sharded Firestore documents.
264
+ * @param {object} dependencies - Contains db, logger.
265
+ * @param {object} config - Configuration object.
266
+ * @param {string} config.collectionPath - Base path for sharded documents (e.g., 'PendingSpeculators').
267
+ * @param {string[]} config.items - The user IDs to write.
268
+ * @param {Date} config.timestamp - Timestamp to associate with each user ID.
269
+ * @param {number} config.maxFieldsPerDoc - Max user IDs per sharded document.
270
+ * @param {number} config.maxWritesPerBatch - Max documents to write per batch commit.
271
+ * @returns {Promise<void>}
272
+ */
273
+ async function batchWriteShardedIds(dependencies, config) {
274
+ const { db, logger } = dependencies;
275
+ const {
276
+ collectionPath,
277
+ items,
278
+ timestamp,
279
+ maxFieldsPerDoc,
280
+ maxWritesPerBatch
281
+ } = config;
282
+
283
+ logger.log('INFO', `[Core Utils] Batch writing ${items.length} IDs to sharded path: ${collectionPath} (max ${maxFieldsPerDoc}/doc, ${maxWritesPerBatch} docs/batch)...`);
284
+ if (items.length === 0) return;
285
+
286
+ try {
287
+ const collectionRef = db.collection(collectionPath);
288
+ let batch = db.batch();
289
+ let currentDocFields = {};
290
+ let currentFieldCount = 0;
291
+ let batchWriteCount = 0;
292
+ let docCounter = 0;
293
+
294
+ for (let i = 0; i < items.length; i++) {
295
+ const userId = items[i];
296
+ const key = `users.${userId}`;
297
+ currentDocFields[key] = timestamp;
298
+ currentFieldCount++;
299
+
300
+ if (currentFieldCount >= maxFieldsPerDoc || i === items.length - 1) {
301
+ const docRef = collectionRef.doc(`pending_${docCounter}_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`);
302
+ batch.set(docRef, currentDocFields);
303
+ batchWriteCount++;
304
+
305
+ currentDocFields = {};
306
+ currentFieldCount = 0;
307
+ docCounter++;
308
+
309
+ if (batchWriteCount >= maxWritesPerBatch || i === items.length - 1) {
310
+ await batch.commit();
311
+ batch = db.batch();
312
+ batchWriteCount = 0;
313
+ }
314
+ }
315
+ }
316
+ logger.log('SUCCESS', `[Core Utils] Sharded write complete for ${collectionPath}. Created ${docCounter} documents.`);
317
+ } catch (error) {
318
+ logger.log('ERROR', `[Core Utils] Error during sharded write to ${collectionPath}`, { errorMessage: error.message });
319
+ throw error;
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Gets normal users whose timestamp indicates they need updating.
325
+ * @param {object} dependencies - Contains db, logger.
326
+ * @param {object} config - Configuration object.
327
+ * @param {Date} config.dateThreshold - Users processed before this date will be returned.
328
+ * @param {string} config.normalUserCollectionName - e.g., 'NormalUserPortfolios'.
329
+ * @param {string} config.normalUserTimestampsDocId - e.g., 'normal'.
330
+ * @returns {Promise<string[]>} Array of user IDs to update.
331
+ */
332
+ async function getNormalUsersToUpdate(dependencies, config) {
333
+ const { db, logger } = dependencies;
334
+ const { dateThreshold, normalUserCollectionName, normalUserTimestampsDocId } = config;
335
+
336
+ logger.log('INFO','[Core Utils] Getting normal users to update...');
337
+ const usersToUpdate = [];
338
+ try {
339
+ const timestampDocRef = db.collection(normalUserCollectionName)
340
+ .doc('timestamps')
341
+ .collection('users')
342
+ .doc(normalUserTimestampsDocId);
343
+ const timestampDoc = await timestampDocRef.get();
344
+
345
+ if (!timestampDoc.exists) {
346
+ logger.log('WARN',`[Core Utils] Normal user timestamp document not found at ${timestampDocRef.path}.`);
347
+ return [];
348
+ }
349
+
350
+ const timestamps = timestampDoc.data().users || {};
351
+ for (const userId in timestamps) {
352
+ const lastProcessed = timestamps[userId]?.toDate ? timestamps[userId].toDate() : new Date(0);
353
+ if (lastProcessed < dateThreshold) {
354
+ usersToUpdate.push(userId);
355
+ }
356
+ }
357
+ logger.log('INFO',`[Core Utils] Found ${usersToUpdate.length} normal users to update.`);
358
+ return usersToUpdate;
359
+ } catch (error) {
360
+ logger.log('ERROR','[Core Utils] Error getting normal users to update', { errorMessage: error.message });
361
+ throw error;
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Gets speculator users/instruments whose data needs updating.
367
+ * @param {object} dependencies - Contains db, logger.
368
+ * @param {object} config - Configuration object.
369
+ * @param {Date} config.dateThreshold - Verification date threshold.
370
+ * @param {Date} config.gracePeriodThreshold - Last held asset date threshold.
371
+ * @param {string} config.speculatorBlocksCollectionName - e.g., 'SpeculatorBlocks'.
372
+ * @returns {Promise<object[]>} Array of objects like { userId: string, instrumentId: number }.
373
+ */
374
+ async function getSpeculatorsToUpdate(dependencies, config) {
375
+ const { db, logger } = dependencies;
376
+ const { dateThreshold, gracePeriodThreshold, speculatorBlocksCollectionName } = config;
377
+
378
+ logger.log('INFO','[Core Utils] Getting speculators to update...');
379
+ const updates = [];
380
+ try {
381
+ const blocksRef = db.collection(speculatorBlocksCollectionName);
382
+ const snapshot = await blocksRef.get();
383
+
384
+ if (snapshot.empty) {
385
+ logger.log('INFO','[Core Utils] No speculator blocks found.');
386
+ return [];
387
+ }
388
+
389
+ snapshot.forEach(doc => {
390
+ const blockData = doc.data();
391
+
392
+ // Iterate over the document's top-level keys
393
+ for (const key in blockData) {
394
+ // Filter for keys that match the 'users.CID' format
395
+ if (!key.startsWith('users.')) continue;
396
+
397
+ const userId = key.split('.')[1];
398
+ if (!userId) continue; // Safety check
399
+
400
+ const userData = blockData[key]; // Get the user's map
401
+
402
+ const lastVerified = userData.lastVerified?.toDate ? userData.lastVerified.toDate() : new Date(0);
403
+ const lastHeld = userData.lastHeldSpeculatorAsset?.toDate ? userData.lastHeldSpeculatorAsset.toDate() : new Date(0);
404
+
405
+ if (lastVerified < dateThreshold && lastHeld > gracePeriodThreshold) {
406
+ if (userData.instruments && Array.isArray(userData.instruments)) {
407
+ userData.instruments.forEach(instrumentId => {
408
+ updates.push({ userId, instrumentId });
409
+ });
410
+ }
411
+ }
412
+ }
413
+ });
414
+
415
+ logger.log('INFO',`[Core Utils] Found ${updates.length} speculator user/instrument pairs to update.`);
416
+ return updates;
417
+ } catch (error) {
418
+ logger.log('ERROR','[Core Utils] Error getting speculators to update', { errorMessage: error.message });
419
+ throw error;
420
+ }
421
+ }
422
+
423
+ module.exports = {
424
+ getLatestNormalUserPortfolios,
425
+ resetProxyLocks,
426
+ getBlockCapacities,
427
+ getExclusionIds,
428
+ getPrioritizedSpeculators,
429
+ clearCollection,
430
+ batchWriteShardedIds,
431
+ getNormalUsersToUpdate,
432
+ getSpeculatorsToUpdate,
433
+ };