bulltrackers-module 1.0.181 → 1.0.182

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.
@@ -6,13 +6,15 @@ const {
6
6
  groupByPass,
7
7
  checkRootDataAvailability,
8
8
  fetchExistingResults,
9
- fetchGlobalComputationStatus, // <--- New Import
10
- updateGlobalComputationStatus, // <--- New Import
9
+ fetchComputationStatus,
10
+ updateComputationStatus,
11
11
  runStandardComputationPass,
12
12
  runMetaComputationPass,
13
13
  checkRootDependencies
14
14
  } = require('./orchestration_helpers.js');
15
- const { getExpectedDateStrings } = require('../utils/utils.js');
15
+
16
+ const { getExpectedDateStrings, normalizeName } = require('../utils/utils.js');
17
+
16
18
  const PARALLEL_BATCH_SIZE = 7;
17
19
 
18
20
  async function runComputationPass(config, dependencies, computationManifest) {
@@ -21,7 +23,7 @@ async function runComputationPass(config, dependencies, computationManifest) {
21
23
  if (!passToRun)
22
24
  return logger.log('ERROR', '[PassRunner] No pass defined. Aborting.');
23
25
 
24
- logger.log('INFO', `🚀 Starting PASS ${passToRun} with Global Status Check...`);
26
+ logger.log('INFO', `🚀 Starting PASS ${passToRun} (Targeting /computation_status/{YYYY-MM-DD})...`);
25
27
 
26
28
  // Hardcoded earliest dates
27
29
  const earliestDates = { portfolio: new Date('2025-09-25T00:00:00Z'), history: new Date('2025-11-05T00:00:00Z'), social: new Date('2025-10-30T00:00:00Z'), insights: new Date('2025-08-26T00:00:00Z') };
@@ -40,36 +42,47 @@ async function runComputationPass(config, dependencies, computationManifest) {
40
42
  const standardCalcs = calcsInThisPass.filter(c => c.type === 'standard');
41
43
  const metaCalcs = calcsInThisPass.filter(c => c.type === 'meta');
42
44
 
43
- // 1. Fetch Global Status ONCE (Memory Cache)
44
- // Returns { "2023-10-27": { calcA: true, calcB: false }, ... }
45
- const globalStatusData = await fetchGlobalComputationStatus(config, dependencies);
46
-
47
- // Helper: Check status using in-memory data
48
- const shouldRun = (calc, dateStr) => {
49
- const dailyStatus = globalStatusData[dateStr] || {};
50
-
51
- // 1. If explicitly TRUE, ignore.
52
- if (dailyStatus[calc.name] === true) return false;
53
-
54
- // 2. Check dependencies (using same in-memory status)
55
- if (calc.dependencies && calc.dependencies.length > 0) {
56
- const depsMet = calc.dependencies.every(depName => dailyStatus[depName] === true);
57
- if (!depsMet) return false;
58
- }
59
- return true;
60
- };
61
-
62
- // Process a single date and RETURN updates (do not write)
45
+ // Process a single date
63
46
  const processDate = async (dateStr) => {
64
47
  const dateToProcess = new Date(dateStr + 'T00:00:00Z');
65
- const standardToRun = standardCalcs.filter(c => shouldRun(c, dateStr));
66
- const metaToRun = metaCalcs.filter(c => shouldRun(c, dateStr));
48
+
49
+ // 1. Fetch Status for THIS specific date only
50
+ // This ensures Pass 2 sees exactly what Pass 1 wrote for this date.
51
+ const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
52
+
53
+ // Helper: Check status using the fetched daily data
54
+ const shouldRun = (calc) => {
55
+ const cName = normalizeName(calc.name);
56
+
57
+ // A. If recorded as TRUE -> Ignore (already ran)
58
+ if (dailyStatus[cName] === true) return false;
59
+
60
+ // B. If recorded as FALSE or UNDEFINED -> Run it (retry or new)
61
+ // But first, check if we have the necessary data dependencies.
62
+
63
+ if (calc.dependencies && calc.dependencies.length > 0) {
64
+ // Check if prerequisites (from previous passes on THIS date) are complete
65
+ const missing = calc.dependencies.filter(depName => dailyStatus[normalizeName(depName)] !== true);
66
+ if (missing.length > 0) {
67
+ // Dependency missing: cannot run yet.
68
+ return false;
69
+ }
70
+ }
71
+
72
+ // If we are here, status is false/undefined AND dependencies are met.
73
+ return true;
74
+ };
75
+
76
+ const standardToRun = standardCalcs.filter(shouldRun);
77
+ const metaToRun = metaCalcs.filter(shouldRun);
67
78
 
68
- if (!standardToRun.length && !metaToRun.length) return null; // No work
79
+ if (!standardToRun.length && !metaToRun.length) return null; // No work for this date
69
80
 
81
+ // 2. Check Root Data Availability (Portfolio, History, etc.)
70
82
  const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
71
83
  if (!rootData) return null;
72
84
 
85
+ // 3. Filter again based on Root Data availability
73
86
  const finalStandardToRun = standardToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
74
87
  const finalMetaToRun = metaToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
75
88
 
@@ -85,52 +98,35 @@ async function runComputationPass(config, dependencies, computationManifest) {
85
98
  const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
86
99
  const prevDateStr = prevDate.toISOString().slice(0, 10);
87
100
  const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
101
+
102
+ // Note: We use skipStatusWrite=true because we want to batch write the status at the end of this function
88
103
  if (finalStandardToRun.length) {
89
- const updates = await runStandardComputationPass(dateToProcess, finalStandardToRun, `Pass ${passToRun} (Std)`, config, dependencies, rootData, existingResults, previousResults, true); // skipStatusWrite=true
104
+ const updates = await runStandardComputationPass(dateToProcess, finalStandardToRun, `Pass ${passToRun} (Std)`, config, dependencies, rootData, existingResults, previousResults, true);
90
105
  Object.assign(dateUpdates, updates);
91
106
  }
92
107
  if (finalMetaToRun.length) {
93
- const updates = await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, true); // skipStatusWrite=true
108
+ const updates = await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, true);
94
109
  Object.assign(dateUpdates, updates);
95
110
  }
96
111
  } catch (err) {
97
112
  logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message });
98
- // Mark failures
99
- [...finalStandardToRun, ...finalMetaToRun].forEach(c => dateUpdates[c.name] = false);
113
+ [...finalStandardToRun, ...finalMetaToRun].forEach(c => dateUpdates[normalizeName(c.name)] = false);
114
+ }
115
+
116
+ // 4. Write "true" or "false" results for THIS specific date immediately
117
+ if (Object.keys(dateUpdates).length > 0) {
118
+ await updateComputationStatus(dateStr, dateUpdates, config, dependencies);
100
119
  }
101
120
 
102
- // Return the updates for this date
103
121
  return { date: dateStr, updates: dateUpdates };
104
122
  };
105
123
 
106
124
  // Batch process dates
107
125
  for (let i = 0; i < allExpectedDates.length; i += PARALLEL_BATCH_SIZE) {
108
126
  const batch = allExpectedDates.slice(i, i + PARALLEL_BATCH_SIZE);
109
-
110
- // Run batch in parallel
111
- const results = await Promise.all(batch.map(processDate));
112
-
113
- // Aggregate updates from the batch
114
- const batchUpdates = {};
115
- let hasUpdates = false;
116
-
117
- results.forEach(res => {
118
- if (res && res.updates && Object.keys(res.updates).length > 0) {
119
- batchUpdates[res.date] = res.updates;
120
- hasUpdates = true;
121
-
122
- // Also update our local in-memory copy so subsequent logic in this run sees it (though passes usually rely on prev days)
123
- if (!globalStatusData[res.date]) globalStatusData[res.date] = {};
124
- Object.assign(globalStatusData[res.date], res.updates);
125
- }
126
- });
127
-
128
- // Write status ONCE per batch
129
- if (hasUpdates) {
130
- await updateGlobalComputationStatus(batchUpdates, config, dependencies);
131
- logger.log('INFO', `[PassRunner] Batched status update for ${Object.keys(batchUpdates).length} dates.`);
132
- }
127
+ await Promise.all(batch.map(processDate));
133
128
  }
129
+
134
130
  logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
135
131
  }
136
132
 
@@ -10,30 +10,31 @@ const { FieldValue } = require('@google-cloud/firestore');
10
10
 
11
11
  class FirestoreBatchManager {
12
12
  constructor(db, headerManager, logger, config) {
13
- this.db = db;
13
+ this.db = db;
14
14
  this.headerManager = headerManager;
15
- this.logger = logger;
16
- this.config = config;
17
- this.portfolioBatch = {};
18
- this.timestampBatch = {};
19
- this.tradingHistoryBatch = {};
15
+ this.logger = logger;
16
+ this.config = config;
17
+ this.portfolioBatch = {};
18
+ this.timestampBatch = {};
19
+ this.tradingHistoryBatch = {};
20
20
  this.speculatorTimestampFixBatch = {};
21
21
 
22
22
  // Username map cache
23
- this.usernameMap = new Map();
24
- this.usernameMapUpdates = {};
23
+ this.usernameMap = new Map();
24
+ this.usernameMapUpdates = {};
25
25
  this.usernameMapLastLoaded = 0;
26
26
 
27
27
  // History fetch cache (NEW)
28
28
  this.historyFetchedUserIds = new Set();
29
29
  this.historyCacheTimestamp = Date.now();
30
- this.HISTORY_CACHE_TTL_MS = config.HISTORY_CACHE_TTL_MS || 600000;
30
+ this.HISTORY_CACHE_TTL_MS = config.HISTORY_CACHE_TTL_MS || 600000;
31
31
 
32
- this.processedSpeculatorCids = new Set();
33
- this.usernameMapCollectionName = config.FIRESTORE_COLLECTION_USERNAME_MAP;
34
- this.normalHistoryCollectionName = config.FIRESTORE_COLLECTION_NORMAL_HISTORY;
32
+ this.processedSpeculatorCids = new Set();
33
+ this.usernameMapCollectionName = config.FIRESTORE_COLLECTION_USERNAME_MAP;
34
+ this.normalHistoryCollectionName = config.FIRESTORE_COLLECTION_NORMAL_HISTORY;
35
35
  this.speculatorHistoryCollectionName = config.FIRESTORE_COLLECTION_SPECULATOR_HISTORY;
36
- this.batchTimeout = null;
36
+ this.batchTimeout = null;
37
+
37
38
  logger.log('INFO', 'FirestoreBatchManager initialized.');
38
39
  }
39
40
 
@@ -50,16 +51,10 @@ class FirestoreBatchManager {
50
51
 
51
52
  _getUsernameShardId(cid) { return `cid_map_shard_${Math.floor(parseInt(cid) / 10000) % 10}`; }
52
53
 
53
- // --- CRITICAL FIX: Removed aggressive timeout flush ---
54
- // With sequential processing, the timer was firing too often, causing 1 write per user (expensive).
55
- // Now we only flush if we hit the memory limit (MAX_BATCH_SIZE) or when explicitly called at the end.
56
54
  _scheduleFlush() {
57
55
  const maxBatch = this.config.TASK_ENGINE_MAX_BATCH_SIZE ? Number(this.config.TASK_ENGINE_MAX_BATCH_SIZE) : 400;
58
56
  const totalOps = this._estimateBatchSize();
59
- if (totalOps >= maxBatch) {
60
- this.flushBatches();
61
- return;
62
- }
57
+ if (totalOps >= maxBatch) { this.flushBatches(); return; }
63
58
  }
64
59
 
65
60
  _estimateBatchSize() {
@@ -144,54 +139,33 @@ class FirestoreBatchManager {
144
139
  */
145
140
  _flushDataBatch(batchData, firestoreBatch, logName) {
146
141
  let count = 0;
147
-
148
- // 1. Determine Shard Strategy
149
- // If we expect ~1500 users in a block and want 200 users per shard:
150
- // We need ceil(1500 / 200) = 8 shards total (part_0 to part_7).
151
- // Any ID, no matter how random, will map to one of these 8 buckets.
152
142
  const TARGET_USERS = this.config.DISCOVERY_ORCHESTRATOR_TARGET_USERS_PER_BLOCK ? Number(this.config.DISCOVERY_ORCHESTRATOR_TARGET_USERS_PER_BLOCK) : 1500;
153
143
  const SHARD_CAPACITY = this.config.TASK_ENGINE_MAX_USERS_PER_SHARD ? Number(this.config.TASK_ENGINE_MAX_USERS_PER_SHARD) : 200;
154
-
155
- // Ensure at least 1 shard exists
156
144
  const TOTAL_SHARDS = Math.max(1, Math.ceil(TARGET_USERS / SHARD_CAPACITY));
157
-
158
145
  for (const basePath in batchData) {
159
146
  const users = batchData[basePath];
160
147
  const userIds = Object.keys(users);
161
148
  if (!userIds.length) continue;
162
-
163
149
  const updatesByShard = {};
164
-
165
150
  for (const userId of userIds) {
166
151
  const cid = parseInt(userId, 10);
167
152
  let shardId;
168
153
 
169
154
  if (!isNaN(cid)) {
170
- // --- MODULO SHARDING ---
171
- // Even if IDs are 10, 1000000, 500... they will round-robin into
172
- // the fixed set of shards (e.g. 8 shards), ensuring density.
173
155
  const shardIndex = cid % TOTAL_SHARDS;
174
156
  shardId = `part_${shardIndex}`;
175
- } else {
176
- shardId = 'part_misc';
177
- }
157
+ } else { shardId = 'part_misc'; }
178
158
 
179
- if (!updatesByShard[shardId]) {
180
- updatesByShard[shardId] = {};
181
- }
159
+ if (!updatesByShard[shardId]) { updatesByShard[shardId] = {}; }
182
160
  updatesByShard[shardId][userId] = users[userId];
183
161
  }
184
-
185
162
  for (const shardId in updatesByShard) {
186
163
  const chunkData = updatesByShard[shardId];
187
164
  const docRef = this.db.collection(`${basePath}/parts`).doc(shardId);
188
- // merge: true ensures we append to the doc if it was started in a previous batch
189
165
  firestoreBatch.set(docRef, chunkData, { merge: true });
190
166
  count++;
191
167
  }
192
-
193
168
  this.logger.log('INFO', `[BATCH] Staged ${userIds.length} ${logName} users into ${Object.keys(updatesByShard).length} buckets (Modulo ${TOTAL_SHARDS}) for ${basePath}.`);
194
-
195
169
  delete batchData[basePath];
196
170
  }
197
171
  return count;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.181",
3
+ "version": "1.0.182",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [