bulltrackers-module 1.0.127 → 1.0.129
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.
- package/functions/computation-system/helpers/computation_pass_runner.js +20 -773
- package/functions/computation-system/helpers/orchestration_helpers.js +88 -867
- package/functions/computation-system/utils/data_loader.js +84 -151
- package/functions/computation-system/utils/utils.js +55 -98
- package/functions/orchestrator/helpers/discovery_helpers.js +40 -188
- package/functions/orchestrator/helpers/update_helpers.js +21 -61
- package/functions/orchestrator/index.js +42 -121
- package/functions/task-engine/handler_creator.js +22 -143
- package/functions/task-engine/helpers/discover_helpers.js +20 -90
- package/functions/task-engine/helpers/update_helpers.js +90 -185
- package/functions/task-engine/helpers/verify_helpers.js +43 -159
- package/functions/task-engine/utils/firestore_batch_manager.js +97 -290
- package/functions/task-engine/utils/task_engine_utils.js +99 -0
- package/package.json +1 -1
- package/functions/task-engine/utils/api_calls.js +0 -0
- package/functions/task-engine/utils/firestore_ops.js +0 -0
|
@@ -4,797 +4,44 @@
|
|
|
4
4
|
*
|
|
5
5
|
* This orchestrator is designed to be run by a separate Cloud Function for each "pass".
|
|
6
6
|
* It reads its pass number from the config and executes only those calculations.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - Pass 2+: Runs "meta" calcs, which it first supplies with dependencies
|
|
10
|
-
* by fetching the results of *previous* passes from Firestore.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const { FieldPath } = require('@google-cloud/firestore');
|
|
14
|
-
const {
|
|
15
|
-
getPortfolioPartRefs,
|
|
16
|
-
loadFullDayMap, // <-- Ensure loadFullDayMap is imported
|
|
17
|
-
loadDataByRefs,
|
|
18
|
-
loadDailyInsights,
|
|
19
|
-
loadDailySocialPostInsights,
|
|
20
|
-
getHistoryPartRefs // <-- IMPORT NEW FUNCTION
|
|
21
|
-
} = require('../utils/data_loader.js');
|
|
22
|
-
|
|
23
|
-
const {
|
|
24
|
-
normalizeName,
|
|
25
|
-
getExpectedDateStrings,
|
|
26
|
-
getFirstDateFromSourceData,
|
|
27
|
-
commitBatchInChunks
|
|
28
|
-
} = require('../utils/utils.js');
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Groups the manifest by pass number.
|
|
33
|
-
* @param {Array<object>} manifest - The computation manifest.
|
|
34
|
-
* @returns {object} { '1': [...], '2': [...] }
|
|
35
|
-
*/
|
|
36
|
-
function groupByPass(manifest) {
|
|
37
|
-
return manifest.reduce((acc, calc) => {
|
|
38
|
-
(acc[calc.pass] = acc[calc.pass] || []).push(calc);
|
|
39
|
-
return acc;
|
|
40
|
-
}, {});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Checks if a calculation's root data dependencies are met.
|
|
45
|
-
* @param {object} calcManifest - The manifest entry for the calculation.
|
|
46
|
-
* @param {object} rootDataStatus - The status object from checkRootDataAvailability.
|
|
47
|
-
* @returns {boolean} True if dependencies are met, false otherwise.
|
|
48
|
-
*/
|
|
49
|
-
function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
50
|
-
if (!calcManifest.rootDataDependencies || calcManifest.rootDataDependencies.length === 0) {
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
for (const dep of calcManifest.rootDataDependencies) {
|
|
54
|
-
if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) return false;
|
|
55
|
-
if (dep === 'insights' && !rootDataStatus.hasInsights) return false;
|
|
56
|
-
if (dep === 'social' && !rootDataStatus.hasSocial) return false;
|
|
57
|
-
if (dep === 'history' && !rootDataStatus.hasHistory) return false; // <-- ADDED THIS LINE
|
|
58
|
-
}
|
|
59
|
-
return true; // All dependencies were met
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Checks if the root data (portfolios, insights, social, history) exists for a given day.
|
|
65
|
-
* @param {string} dateStr - The date string to check (YYYY-MM-DD).
|
|
66
|
-
* @param {object} config - The computation system configuration object.
|
|
67
|
-
* @param {object} dependencies - Contains db, logger, calculationUtils.
|
|
68
|
-
* @returns {Promise<object>} { portfolioRefs, insightsData, socialData, historyRefs, hasPortfolio, hasInsights, hasSocial, hasHistory }
|
|
7
|
+
* This file contains the high-level "manual" of steps. The "how-to" logic
|
|
8
|
+
* is extracted into 'computation_system_utils.js'.
|
|
69
9
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
logger.log('INFO', `[PassRunner] Checking root data availability for ${dateStr}...`);
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
// --- MODIFIED: Add getHistoryPartRefs to the parallel load ---
|
|
76
|
-
const [portfolioRefs, insightsData, socialData, historyRefs] = await Promise.all([
|
|
77
|
-
getPortfolioPartRefs(config, dependencies, dateStr),
|
|
78
|
-
loadDailyInsights(config, dependencies, dateStr),
|
|
79
|
-
loadDailySocialPostInsights(config, dependencies, dateStr),
|
|
80
|
-
getHistoryPartRefs(config, dependencies, dateStr) // <-- ADD THIS
|
|
81
|
-
]);
|
|
10
|
+
const { groupByPass, checkRootDataAvailability, fetchDependenciesForPass, filterCalculations, runStandardComputationPass, runMetaComputationPass } = require('./orchestration_helpers.js');
|
|
11
|
+
const { getExpectedDateStrings, getFirstDateFromSourceData } = require('../utils/utils.js');
|
|
82
12
|
|
|
83
|
-
const hasPortfolio = (portfolioRefs && portfolioRefs.length > 0);
|
|
84
|
-
const hasInsights = !!insightsData;
|
|
85
|
-
const hasSocial = !!socialData;
|
|
86
|
-
const hasHistory = (historyRefs && historyRefs.length > 0); // <-- ADD THIS
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
portfolioRefs: portfolioRefs || [],
|
|
90
|
-
insightsData: insightsData || null,
|
|
91
|
-
socialData: socialData || null,
|
|
92
|
-
historyRefs: historyRefs || [], // <-- ADD THIS
|
|
93
|
-
hasPortfolio: hasPortfolio,
|
|
94
|
-
hasInsights: hasInsights,
|
|
95
|
-
hasSocial: hasSocial,
|
|
96
|
-
hasHistory: hasHistory // <-- ADD THIS
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
} catch (err) {
|
|
100
|
-
logger.log('ERROR', `[PassRunner] Error checking data availability for ${dateStr}`, { errorMessage: err.message });
|
|
101
|
-
return {
|
|
102
|
-
portfolioRefs: [], insightsData: null, socialData: null, historyRefs: [], // <-- ADD historyRefs
|
|
103
|
-
hasPortfolio: false, hasInsights: false, hasSocial: false, hasHistory: false // <-- ADD hasHistory
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* (NEW) Fetches all computed dependencies for a given pass and date from Firestore.
|
|
110
|
-
* @param {string} dateStr - The date string (YYYY-MM-DD).
|
|
111
|
-
* @param {Array<object>} calcsInPass - The manifest entries for this pass.
|
|
112
|
-
* @param {Array<object>} fullManifest - The *entire* computation manifest.
|
|
113
|
-
* @param {object} config - The computation system configuration.
|
|
114
|
-
* @param {object} dependencies - Contains db, logger.
|
|
115
|
-
* @returns {Promise<object>} A map of { 'calc-name': result, ... }
|
|
116
|
-
*/
|
|
117
|
-
async function fetchDependenciesForPass(dateStr, calcsInPass, fullManifest, config, dependencies) {
|
|
118
|
-
const { db, logger } = dependencies;
|
|
119
|
-
const { resultsCollection, resultsSubcollection, computationsSubcollection } = config;
|
|
120
|
-
|
|
121
|
-
// --- THIS IS THE FIX ---
|
|
122
|
-
// Build the manifestMap from the *fullManifest* so we can find
|
|
123
|
-
// dependencies from *all* passes, not just the current one.
|
|
124
|
-
const manifestMap = new Map();
|
|
125
|
-
for (const calc of fullManifest) {
|
|
126
|
-
manifestMap.set(normalizeName(calc.name), calc);
|
|
127
|
-
}
|
|
128
|
-
// --- END FIX ---
|
|
129
|
-
|
|
130
|
-
const requiredDeps = new Set();
|
|
131
|
-
|
|
132
|
-
// 1. Get all unique dependencies required by calcs in *this* pass
|
|
133
|
-
for (const calc of calcsInPass) {
|
|
134
|
-
if (calc.type === 'meta' && calc.dependencies) {
|
|
135
|
-
calc.dependencies.forEach(depName => requiredDeps.add(normalizeName(depName)));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (requiredDeps.size === 0) {
|
|
140
|
-
logger.log('INFO', `[PassRunner] No Firestore dependencies to fetch for this pass on ${dateStr}.`);
|
|
141
|
-
return {};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
logger.log('INFO', `[PassRunner] Fetching ${requiredDeps.size} dependencies from Firestore for ${dateStr}...`);
|
|
145
|
-
|
|
146
|
-
const docRefs = [];
|
|
147
|
-
const depNames = [];
|
|
148
|
-
|
|
149
|
-
// 2. Build the list of Firestore document references
|
|
150
|
-
for (const calcName of requiredDeps) {
|
|
151
|
-
const calcManifest = manifestMap.get(calcName); // Now correctly finds deps from other passes
|
|
152
|
-
if (!calcManifest) {
|
|
153
|
-
logger.log('ERROR', `[PassRunner] Cannot find manifest entry for dependency "${calcName}". This is a manifest error. Skipping dependency.`);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const category = calcManifest.category || 'unknown';
|
|
158
|
-
const docRef = db.collection(resultsCollection).doc(dateStr)
|
|
159
|
-
.collection(resultsSubcollection).doc(category)
|
|
160
|
-
.collection(computationsSubcollection).doc(calcName);
|
|
161
|
-
|
|
162
|
-
docRefs.push(docRef);
|
|
163
|
-
depNames.push(calcName);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// 3. Fetch all dependencies in one batch
|
|
167
|
-
const fetchedDependencies = {};
|
|
168
|
-
if (docRefs.length > 0) {
|
|
169
|
-
const snapshots = await db.getAll(...docRefs);
|
|
170
|
-
snapshots.forEach((doc, i) => {
|
|
171
|
-
const calcName = depNames[i];
|
|
172
|
-
if (doc.exists) {
|
|
173
|
-
fetchedDependencies[calcName] = doc.data();
|
|
174
|
-
} else {
|
|
175
|
-
fetchedDependencies[calcName] = null; // Mark as null if not found
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
logger.log('INFO', `[PassRunner] Successfully fetched ${Object.keys(fetchedDependencies).length} dependencies for ${dateStr}.`);
|
|
181
|
-
return fetchedDependencies;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Main pipe: pipe.computationSystem.runComputationPass
|
|
187
|
-
* @param {object} config - The computation system configuration object.
|
|
188
|
-
* @param {object} dependencies - Contains db, logger, calculationUtils.
|
|
189
|
-
* @param {Array<object>} computationManifest - The injected computation manifest.
|
|
190
|
-
* @returns {Promise<Object>} Summary of all passes.
|
|
191
|
-
*/
|
|
192
13
|
async function runComputationPass(config, dependencies, computationManifest) {
|
|
193
14
|
const { logger } = dependencies;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const passToRun = String(config.COMPUTATION_PASS_TO_RUN);
|
|
197
|
-
if (!passToRun) {
|
|
198
|
-
logger.log('ERROR', '[PassRunner] FATAL: COMPUTATION_PASS_TO_RUN is not defined in config. Aborting.');
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
logger.log('INFO', `🚀 [PassRunner] Starting run for PASS ${passToRun}...`);
|
|
202
|
-
// --- END NEW ---
|
|
15
|
+
const passToRun = String(config.COMPUTATION_PASS_TO_RUN); if (!passToRun) return logger.log('ERROR', '[PassRunner] No pass defined. Aborting.');
|
|
16
|
+
logger.log('INFO', `🚀 Starting PASS ${passToRun}...`);
|
|
203
17
|
|
|
204
|
-
const
|
|
205
|
-
const yesterday = new Date();
|
|
206
|
-
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
|
|
18
|
+
const yesterday = new Date(); yesterday.setUTCDate(yesterday.getUTCDate()-1);
|
|
207
19
|
const endDateUTC = new Date(Date.UTC(yesterday.getUTCFullYear(), yesterday.getUTCMonth(), yesterday.getUTCDate()));
|
|
208
|
-
|
|
209
20
|
const firstDate = await getFirstDateFromSourceData(config, dependencies);
|
|
210
|
-
const startDateUTC = firstDate
|
|
211
|
-
? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate()))
|
|
212
|
-
: new Date(config.earliestComputationDate + 'T00:00:00Z');
|
|
213
|
-
|
|
21
|
+
const startDateUTC = firstDate ? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate())) : new Date(config.earliestComputationDate+'T00:00:00Z');
|
|
214
22
|
const allExpectedDates = getExpectedDateStrings(startDateUTC, endDateUTC);
|
|
215
23
|
|
|
216
24
|
const passes = groupByPass(computationManifest);
|
|
217
|
-
const calcsInThisPass = passes[passToRun] || [];
|
|
218
|
-
|
|
219
|
-
if (calcsInThisPass.length === 0) {
|
|
220
|
-
logger.log('WARN', `[PassRunner] No calculations found in manifest for Pass ${passToRun}. Exiting.`);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
25
|
+
const calcsInThisPass = passes[passToRun] || []; if (!calcsInThisPass.length) return logger.log('WARN', `[PassRunner] No calcs for Pass ${passToRun}. Exiting.`);
|
|
223
26
|
|
|
224
|
-
const standardCalcs = calcsInThisPass.filter(c => c.type
|
|
225
|
-
const metaCalcs = calcsInThisPass.filter(c => c.type
|
|
226
|
-
logger.log('INFO', `[PassRunner] Found ${standardCalcs.length} standard and ${metaCalcs.length} meta calcs for Pass ${passToRun}.`);
|
|
27
|
+
const standardCalcs = calcsInThisPass.filter(c => c.type==='standard');
|
|
28
|
+
const metaCalcs = calcsInThisPass.filter(c => c.type==='meta');
|
|
227
29
|
|
|
228
|
-
|
|
229
|
-
// --- Process ONE DAY at a time, in order ---
|
|
230
30
|
for (const dateStr of allExpectedDates) {
|
|
231
|
-
const dateToProcess = new Date(dateStr
|
|
232
|
-
|
|
233
|
-
// 1. Check for root data (portfolios, insights, social, AND HISTORY)
|
|
234
|
-
const rootData = await checkRootDataAvailability(dateStr, config, dependencies);
|
|
235
|
-
const rootDataStatus = {
|
|
236
|
-
hasPortfolio: rootData.hasPortfolio,
|
|
237
|
-
hasInsights: rootData.hasInsights,
|
|
238
|
-
hasSocial: rootData.hasSocial,
|
|
239
|
-
hasHistory: rootData.hasHistory // <-- ADD THIS
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
if (!rootData.hasPortfolio && !rootData.hasInsights && !rootData.hasSocial && !rootData.hasHistory) {
|
|
243
|
-
logger.log('WARN', `[PassRunner] Skipping Pass ${passToRun} for ${dateStr} due to missing all root data.`);
|
|
244
|
-
continue; // Skip to the next day
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
logger.log('INFO', `[PassRunner] Processing Pass ${passToRun} for ${dateStr}...`);
|
|
248
|
-
|
|
249
|
-
// 2. (NEW) Fetch all dependencies for this pass and date from Firestore
|
|
250
|
-
// This is skipped for Pass 1, as `requiredDeps.size` will be 0
|
|
251
|
-
const fetchedDependencies = await fetchDependenciesForPass(
|
|
252
|
-
dateStr,
|
|
253
|
-
calcsInThisPass,
|
|
254
|
-
computationManifest, // <-- Pass the *full* manifest here
|
|
255
|
-
config,
|
|
256
|
-
dependencies
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
const skippedCalculations = new Set();
|
|
260
|
-
|
|
261
|
-
// 3. Filter calculations based on root data
|
|
262
|
-
const standardCalcsToRun = [];
|
|
263
|
-
for (const calcManifest of standardCalcs) {
|
|
264
|
-
if (checkRootDependencies(calcManifest, rootDataStatus)) {
|
|
265
|
-
standardCalcsToRun.push(calcManifest);
|
|
266
|
-
} else {
|
|
267
|
-
logger.log('INFO', `[Pass ${passToRun}] Skipping standard calc "${calcManifest.name}" for ${dateStr} due to missing root data.`);
|
|
268
|
-
skippedCalculations.add(calcManifest.name);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const metaCalcsToRun = [];
|
|
273
|
-
for (const calcManifest of metaCalcs) {
|
|
274
|
-
const calcName = calcManifest.name;
|
|
275
|
-
|
|
276
|
-
// Check 1: Are root data dependencies met?
|
|
277
|
-
if (!checkRootDependencies(calcManifest, rootDataStatus)) {
|
|
278
|
-
logger.log('INFO', `[Pass ${passToRun} (Meta)] Skipping meta calc "${calcName}" for ${dateStr} due to missing root data.`);
|
|
279
|
-
skippedCalculations.add(calcName);
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Check 2: Are *computed* dependencies (from previous passes) met?
|
|
284
|
-
let depCheck = true;
|
|
285
|
-
let missingDepName = '';
|
|
286
|
-
for (const depName of (calcManifest.dependencies || [])) {
|
|
287
|
-
const normalizedDepName = normalizeName(depName);
|
|
288
|
-
if (!fetchedDependencies[normalizedDepName]) { // Check if null or undefined
|
|
289
|
-
depCheck = false;
|
|
290
|
-
missingDepName = normalizedDepName;
|
|
291
|
-
break;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (depCheck) {
|
|
296
|
-
metaCalcsToRun.push(calcManifest);
|
|
297
|
-
} else {
|
|
298
|
-
logger.log('WARN', `[Pass ${passToRun} (Meta)] Skipping meta calc "${calcName}" for ${dateStr} due to missing computed dependency "${missingDepName}" from Firestore.`);
|
|
299
|
-
skippedCalculations.add(calcName);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// --- 4. Run the filtered calculations ---
|
|
31
|
+
const dateToProcess = new Date(dateStr+'T00:00:00Z');
|
|
304
32
|
try {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
config,
|
|
312
|
-
dependencies,
|
|
313
|
-
rootData
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Run meta calcs for this pass (e.g., Pass 2, 3, 4)
|
|
318
|
-
if (metaCalcsToRun.length > 0) {
|
|
319
|
-
await runMetaComputation(
|
|
320
|
-
dateToProcess,
|
|
321
|
-
metaCalcsToRun,
|
|
322
|
-
`Pass ${passToRun} (Meta)`,
|
|
323
|
-
config,
|
|
324
|
-
dependencies,
|
|
325
|
-
fetchedDependencies, // <-- This is the NEW object from Firestore
|
|
326
|
-
rootData
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
logger.log('SUCCESS', `[PassRunner] Completed Pass ${passToRun} for ${dateStr}.`);
|
|
33
|
+
const rootData = await checkRootDataAvailability(dateStr, config, dependencies); if (!rootData) continue;
|
|
34
|
+
const fetchedDeps = await fetchDependenciesForPass(dateStr, calcsInThisPass, computationManifest, config, dependencies);
|
|
35
|
+
const { standardCalcsToRun, metaCalcsToRun } = filterCalculations(standardCalcs, metaCalcs, rootData.status, fetchedDeps, passToRun, dateStr, logger);
|
|
36
|
+
if (standardCalcsToRun.length) await runStandardComputationPass(dateToProcess, standardCalcsToRun, `Pass ${passToRun} (Standard)`, config, dependencies, rootData);
|
|
37
|
+
if (metaCalcsToRun.length) await runMetaComputationPass(dateToProcess, metaCalcsToRun, `Pass ${passToRun} (Meta)`, config, dependencies, fetchedDeps, rootData);
|
|
38
|
+
logger.log('SUCCESS', `[PassRunner] Completed Pass ${passToRun} for ${dateStr}.`);
|
|
330
39
|
} catch (err) {
|
|
331
40
|
logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
|
|
332
41
|
}
|
|
333
|
-
} // End dates loop
|
|
334
|
-
|
|
335
|
-
logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
|
|
336
|
-
return summary;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Internal sub-pipe: Initializes calculator instances.
|
|
341
|
-
*/
|
|
342
|
-
function initializeCalculators(calculationsToRun, logger) {
|
|
343
|
-
const state = {};
|
|
344
|
-
for (const calcManifest of calculationsToRun) {
|
|
345
|
-
const calcName = normalizeName(calcManifest.name);
|
|
346
|
-
const CalculationClass = calcManifest.class;
|
|
347
|
-
|
|
348
|
-
if (typeof CalculationClass === 'function') {
|
|
349
|
-
try {
|
|
350
|
-
const instance = new CalculationClass();
|
|
351
|
-
instance.manifest = calcManifest; // Attach manifest data
|
|
352
|
-
state[calcName] = instance;
|
|
353
|
-
} catch (e) {
|
|
354
|
-
logger.warn(`[PassRunner] Init failed for ${calcName}`, { errorMessage: e.message });
|
|
355
|
-
state[calcName] = null;
|
|
356
|
-
}
|
|
357
|
-
} else {
|
|
358
|
-
logger.warn(`[PassRunner] Calculation class not found in manifest for: ${calcName}`);
|
|
359
|
-
state[calcName] = null;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return state;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Internal sub-pipe: Streams data and calls process() on "standard" calculators.
|
|
367
|
-
*/
|
|
368
|
-
async function streamAndProcess(
|
|
369
|
-
dateStr, todayRefs, state, passName, config, dependencies,
|
|
370
|
-
yesterdayPortfolios = {},
|
|
371
|
-
todayInsights = null,
|
|
372
|
-
yesterdayInsights = null,
|
|
373
|
-
todaySocialPostInsights = null,
|
|
374
|
-
yesterdaySocialPostInsights = null,
|
|
375
|
-
todayHistoryData = null, // <-- ADD THIS
|
|
376
|
-
yesterdayHistoryData = null // <-- ADD THIS
|
|
377
|
-
) {
|
|
378
|
-
const { logger, calculationUtils } = dependencies;
|
|
379
|
-
logger.log('INFO', `[${passName}] Streaming ${todayRefs.length} 'today' part docs for ${dateStr}...`);
|
|
380
|
-
|
|
381
|
-
const yesterdayDate = new Date(dateStr + 'T00:00:00Z');
|
|
382
|
-
yesterdayDate.setUTCDate(yesterdayDate.getUTCDate() - 1);
|
|
383
|
-
const yesterdayStr = yesterdayDate.toISOString().slice(0, 10);
|
|
384
|
-
|
|
385
|
-
const { instrumentToTicker, instrumentToSector } = await calculationUtils.loadInstrumentMappings();
|
|
386
|
-
|
|
387
|
-
const context = {
|
|
388
|
-
instrumentMappings: instrumentToTicker,
|
|
389
|
-
sectorMapping: instrumentToSector,
|
|
390
|
-
todayDateStr: dateStr,
|
|
391
|
-
yesterdayDateStr: yesterdayStr,
|
|
392
|
-
dependencies: dependencies,
|
|
393
|
-
config: config
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
const batchSize = config.partRefBatchSize || 10;
|
|
397
|
-
let isFirstUser = true;
|
|
398
|
-
|
|
399
|
-
for (let i = 0; i < todayRefs.length; i += batchSize) {
|
|
400
|
-
const batchRefs = todayRefs.slice(i, i + batchSize);
|
|
401
|
-
const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
|
|
402
|
-
|
|
403
|
-
for (const uid in todayPortfoliosChunk) {
|
|
404
|
-
const p = todayPortfoliosChunk[uid];
|
|
405
|
-
if (!p) continue;
|
|
406
|
-
const userType = p.PublicPositions ? 'speculator' : 'normal';
|
|
407
|
-
context.userType = userType;
|
|
408
|
-
|
|
409
|
-
for (const calcName in state) {
|
|
410
|
-
const calc = state[calcName];
|
|
411
|
-
if (!calc || typeof calc.process !== 'function') continue;
|
|
412
|
-
|
|
413
|
-
const manifestCalc = calc.manifest;
|
|
414
|
-
const isSocialOrInsights = manifestCalc.category === 'socialPosts' || manifestCalc.category === 'insights';
|
|
415
|
-
const isHistoricalCalc = manifestCalc.isHistorical === true;
|
|
416
|
-
const isSpeculatorCalc = manifestCalc.category === 'speculators';
|
|
417
|
-
let processArgs;
|
|
418
|
-
|
|
419
|
-
// --- MODIFIED: Add history data to the context args ---
|
|
420
|
-
const allContextArgs = [
|
|
421
|
-
context,
|
|
422
|
-
todayInsights,
|
|
423
|
-
yesterdayInsights,
|
|
424
|
-
todaySocialPostInsights,
|
|
425
|
-
yesterdaySocialPostInsights,
|
|
426
|
-
todayHistoryData, // <-- ADD THIS
|
|
427
|
-
yesterdayHistoryData // <-- ADD THIS
|
|
428
|
-
];
|
|
429
|
-
|
|
430
|
-
if (isSocialOrInsights) {
|
|
431
|
-
if (isFirstUser) {
|
|
432
|
-
processArgs = [null, null, null, ...allContextArgs];
|
|
433
|
-
} else {
|
|
434
|
-
continue; // Only run once
|
|
435
|
-
}
|
|
436
|
-
} else if (isHistoricalCalc) {
|
|
437
|
-
const pYesterday = yesterdayPortfolios[uid];
|
|
438
|
-
if (!pYesterday) continue;
|
|
439
|
-
processArgs = [p, pYesterday, uid, ...allContextArgs];
|
|
440
|
-
} else {
|
|
441
|
-
processArgs = [p, null, uid, ...allContextArgs];
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (!isSocialOrInsights) {
|
|
445
|
-
if ((userType === 'normal' && isSpeculatorCalc) ||
|
|
446
|
-
(userType === 'speculator' && !isSpeculatorCalc && calcName !== 'users-processed')) {
|
|
447
|
-
continue; // Skip: wrong user type
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
try {
|
|
452
|
-
await Promise.resolve(calc.process(...processArgs));
|
|
453
|
-
} catch (e) {
|
|
454
|
-
logger.log('WARN', `Process error in ${calcName} for user ${uid}`, { err: e.message });
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
isFirstUser = false;
|
|
458
|
-
}
|
|
459
42
|
}
|
|
460
|
-
|
|
461
|
-
// Handle case where there are no users but we still need to run insights/social calcs
|
|
462
|
-
if (todayRefs.length === 0 && isFirstUser) {
|
|
463
|
-
logger.log('INFO', `[${passName}] No user portfolios found for ${dateStr}. Running insights/social calcs once.`);
|
|
464
|
-
|
|
465
|
-
// --- MODIFIED: Add null history data ---
|
|
466
|
-
const allContextArgs = [
|
|
467
|
-
context,
|
|
468
|
-
todayInsights,
|
|
469
|
-
yesterdayInsights,
|
|
470
|
-
todaySocialPostInsights,
|
|
471
|
-
yesterdaySocialPostInsights,
|
|
472
|
-
todayHistoryData, // <-- ADD THIS
|
|
473
|
-
yesterdayHistoryData // <-- ADD THIS
|
|
474
|
-
];
|
|
475
|
-
|
|
476
|
-
for (const calcName in state) {
|
|
477
|
-
const calc = state[calcName];
|
|
478
|
-
if (!calc || typeof calc.process !== 'function') continue;
|
|
479
|
-
const manifestCalc = calc.manifest;
|
|
480
|
-
const isSocialOrInsights = manifestCalc.category === 'socialPosts' || manifestCalc.category === 'insights';
|
|
481
|
-
if (isSocialOrInsights) {
|
|
482
|
-
try {
|
|
483
|
-
await Promise.resolve(calc.process(null, null, null, ...allContextArgs));
|
|
484
|
-
} catch (e) {
|
|
485
|
-
logger.log('WARN', `Process error in ${calcName} for no-user run`, { err: e.message });
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
43
|
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Internal sub-pipe: Runs "standard" computations (Pass 1) for a single date.
|
|
495
|
-
*/
|
|
496
|
-
async function runUnifiedComputation(dateToProcess, calculationsToRun, passName, config, dependencies, rootData) {
|
|
497
|
-
const { db, logger } = dependencies;
|
|
498
|
-
const dateStr = dateToProcess.toISOString().slice(0, 10);
|
|
499
|
-
logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} calcs.`);
|
|
500
|
-
|
|
501
|
-
try {
|
|
502
|
-
// --- NEW: Get history root data from the orchestrator ---
|
|
503
|
-
const {
|
|
504
|
-
portfolioRefs: todayRefs,
|
|
505
|
-
insightsData: todayInsightsData,
|
|
506
|
-
socialData: todaySocialPostInsightsData,
|
|
507
|
-
historyRefs: todayHistoryRefs // <-- ADD THIS
|
|
508
|
-
} = rootData;
|
|
509
|
-
|
|
510
|
-
let yesterdayPortfolios = {};
|
|
511
|
-
let yesterdayInsightsData = null;
|
|
512
|
-
let yesterdaySocialPostInsightsData = null;
|
|
513
|
-
let todayHistoryData = null; // <-- ADD THIS
|
|
514
|
-
let yesterdayHistoryData = null; // <-- ADD THIS
|
|
515
|
-
|
|
516
|
-
// --- MODIFIED: Add checks for history data ---
|
|
517
|
-
const requiresYesterdayPortfolio = calculationsToRun.some(c => c.isHistorical === true);
|
|
518
|
-
const requiresYesterdayInsights = calculationsToRun.some(c => c.class.prototype.process.toString().includes('yesterdayInsights'));
|
|
519
|
-
const requiresYesterdaySocialPosts = calculationsToRun.some(c => c.class.prototype.process.toString().includes('yesterdaySocialPostInsights'));
|
|
520
|
-
const requiresHistory = calculationsToRun.some(c => c.rootDataDependencies.includes('history'));
|
|
521
|
-
const requiresYesterdayHistory = calculationsToRun.some(c => c.isHistorical === true && c.class.prototype.process.toString().includes('yesterdayHistoryData'));
|
|
522
|
-
// --- END MODIFICATION ---
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
// --- (This block is now much larger, handling all "yesterday" and "history" data) ---
|
|
526
|
-
if (requiresYesterdayPortfolio || requiresYesterdayInsights || requiresYesterdaySocialPosts || requiresHistory || requiresYesterdayHistory) {
|
|
527
|
-
|
|
528
|
-
if(requiresYesterdayInsights) {
|
|
529
|
-
let daysAgo = 1; const maxLookback = 30;
|
|
530
|
-
while (!yesterdayInsightsData && daysAgo <= maxLookback) {
|
|
531
|
-
const prev = new Date(dateToProcess); prev.setUTCDate(prev.getUTCDate() - daysAgo);
|
|
532
|
-
const prevStr = prev.toISOString().slice(0, 10);
|
|
533
|
-
yesterdayInsightsData = await loadDailyInsights(config, dependencies, prevStr);
|
|
534
|
-
if (yesterdayInsightsData) logger.log('INFO', `[${passName}] Found 'yesterday' instrument insights data from ${daysAgo} day(s) ago (${prevStr}).`);
|
|
535
|
-
else daysAgo++;
|
|
536
|
-
}
|
|
537
|
-
if (!yesterdayInsightsData) logger.log('WARN', `[${passName}] Could not find any 'yesterday' instrument insights data within a ${maxLookback} day lookback.`);
|
|
538
|
-
}
|
|
539
|
-
if(requiresYesterdaySocialPosts) {
|
|
540
|
-
let daysAgo = 1; const maxLookback = 30;
|
|
541
|
-
while (!yesterdaySocialPostInsightsData && daysAgo <= maxLookback) {
|
|
542
|
-
const prev = new Date(dateToProcess); prev.setUTCDate(prev.getUTCDate() - daysAgo);
|
|
543
|
-
const prevStr = prev.toISOString().slice(0, 10);
|
|
544
|
-
yesterdaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, prevStr);
|
|
545
|
-
if (yesterdaySocialPostInsightsData) logger.log('INFO', `[${passName}] Found 'yesterday' social post insights data from ${daysAgo} day(s) ago (${prevStr}).`);
|
|
546
|
-
else daysAgo++;
|
|
547
|
-
}
|
|
548
|
-
if (!yesterdaySocialPostInsightsData) logger.log('WARN', `[${passName}] Could not find any 'yesterday' social post insights data within a ${maxLookback} day lookback.`);
|
|
549
|
-
}
|
|
550
|
-
if (requiresYesterdayPortfolio) {
|
|
551
|
-
const prev = new Date(dateToProcess); prev.setUTCDate(prev.getUTCDate() - 1);
|
|
552
|
-
const prevStr = prev.toISOString().slice(0, 10);
|
|
553
|
-
const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, prevStr);
|
|
554
|
-
if (yesterdayRefs.length > 0) {
|
|
555
|
-
yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
|
|
556
|
-
logger.log('INFO', `[${passName}] Loaded yesterday's (${prevStr}) portfolio map for historical calcs.`);
|
|
557
|
-
} else {
|
|
558
|
-
logger.log('WARN', `[${passName}] Yesterday's (${prevStr}) portfolio data not found. Historical calcs requiring it will be skipped.`);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// --- NEW: Add logic to load Today's History Data (if needed) ---
|
|
563
|
-
if (requiresHistory && todayHistoryRefs.length > 0) {
|
|
564
|
-
logger.log('INFO', `[${passName}] Loading today's (${dateStr}) history map...`);
|
|
565
|
-
todayHistoryData = await loadFullDayMap(config, dependencies, todayHistoryRefs);
|
|
566
|
-
logger.log('INFO', `[${passName}] Loaded today's history map with ${Object.keys(todayHistoryData).length} users.`);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// --- NEW: Add logic to load Yesterday's History Data (if needed) ---
|
|
570
|
-
if (requiresYesterdayHistory) {
|
|
571
|
-
const prev = new Date(dateToProcess);
|
|
572
|
-
prev.setUTCDate(prev.getUTCDate() - 1);
|
|
573
|
-
const prevStr = prev.toISOString().slice(0, 10);
|
|
574
|
-
const yesterdayHistoryRefs = await getHistoryPartRefs(config, dependencies, prevStr);
|
|
575
|
-
if (yesterdayHistoryRefs.length > 0) {
|
|
576
|
-
yesterdayHistoryData = await loadFullDayMap(config, dependencies, yesterdayHistoryRefs);
|
|
577
|
-
logger.log('INFO', `[${passName}] Loaded yesterday's (${prevStr}) history map for historical calcs.`);
|
|
578
|
-
} else {
|
|
579
|
-
logger.log('WARN', `[${passName}] Yesterday's (${prevStr}) history data not found. Historical calcs requiring it will be skipped.`);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
const state = initializeCalculators(calculationsToRun, logger);
|
|
585
|
-
|
|
586
|
-
// --- MODIFIED: Pass new history data ---
|
|
587
|
-
await streamAndProcess(
|
|
588
|
-
dateStr,
|
|
589
|
-
todayRefs,
|
|
590
|
-
state,
|
|
591
|
-
passName,
|
|
592
|
-
config,
|
|
593
|
-
dependencies,
|
|
594
|
-
yesterdayPortfolios,
|
|
595
|
-
todayInsightsData,
|
|
596
|
-
yesterdayInsightsData,
|
|
597
|
-
todaySocialPostInsightsData,
|
|
598
|
-
yesterdaySocialPostInsightsData,
|
|
599
|
-
todayHistoryData, // <-- ADD THIS
|
|
600
|
-
yesterdayHistoryData // <-- ADD THIS
|
|
601
|
-
);
|
|
602
|
-
|
|
603
|
-
let successCount = 0;
|
|
604
|
-
const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
|
|
605
|
-
|
|
606
|
-
for (const calcName in state) {
|
|
607
|
-
const calc = state[calcName];
|
|
608
|
-
if (!calc || typeof calc.getResult !== 'function') {
|
|
609
|
-
if (!calc) logger.log('WARN', `[${passName}] Skipping ${calcName} for ${dateStr} because it failed to initialize.`);
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const category = calc.manifest.category || 'unknown';
|
|
614
|
-
let result = null;
|
|
615
|
-
try {
|
|
616
|
-
result = await Promise.resolve(calc.getResult());
|
|
617
|
-
|
|
618
|
-
// --- Database write logic (Identical to original file) ---
|
|
619
|
-
const pendingWrites = [];
|
|
620
|
-
const summaryData = {};
|
|
621
|
-
if (result && Object.keys(result).length > 0) {
|
|
622
|
-
let isSharded = false;
|
|
623
|
-
const shardedCollections = {
|
|
624
|
-
'sharded_user_profile': config.shardedUserProfileCollection,
|
|
625
|
-
'sharded_user_profitability': config.shardedProfitabilityCollection
|
|
626
|
-
};
|
|
627
|
-
for (const resultKey in shardedCollections) {
|
|
628
|
-
if (result[resultKey]) {
|
|
629
|
-
isSharded = true;
|
|
630
|
-
const shardCollectionName = shardedCollections[resultKey];
|
|
631
|
-
const shardedData = result[resultKey];
|
|
632
|
-
for (const shardId in shardedData) {
|
|
633
|
-
const shardDocData = shardedData[shardId];
|
|
634
|
-
if (shardDocData && Object.keys(shardDocData).length > 0) {
|
|
635
|
-
const shardRef = db.collection(shardCollectionName).doc(shardId);
|
|
636
|
-
pendingWrites.push({ ref: shardRef, data: shardedData[shardId] });
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
const { [resultKey]: _, ...otherResults } = result;
|
|
640
|
-
if (Object.keys(otherResults).length > 0) {
|
|
641
|
-
const computationDocRef = resultsCollectionRef.doc(category)
|
|
642
|
-
.collection(config.computationsSubcollection)
|
|
643
|
-
.doc(calcName);
|
|
644
|
-
pendingWrites.push({ ref: computationDocRef, data: otherResults });
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
if (!isSharded) {
|
|
649
|
-
const computationDocRef = resultsCollectionRef.doc(category)
|
|
650
|
-
.collection(config.computationsSubcollection)
|
|
651
|
-
.doc(calcName);
|
|
652
|
-
pendingWrites.push({ ref: computationDocRef, data: result });
|
|
653
|
-
}
|
|
654
|
-
if (!summaryData[category]) summaryData[category] = {};
|
|
655
|
-
summaryData[category][calcName] = true;
|
|
656
|
-
if (Object.keys(summaryData).length > 0) {
|
|
657
|
-
const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr);
|
|
658
|
-
pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
|
|
659
|
-
}
|
|
660
|
-
if (pendingWrites.length > 0) {
|
|
661
|
-
await commitBatchInChunks(config, dependencies, pendingWrites, `Commit ${passName} ${dateStr} [${calcName}]`);
|
|
662
|
-
successCount++;
|
|
663
|
-
}
|
|
664
|
-
} else {
|
|
665
|
-
if (result === null) logger.log('INFO', `[${passName}] Calculation ${calcName} returned null for ${dateStr}. This is expected if no data was processed.`);
|
|
666
|
-
else logger.log('WARN', `[${passName}] Calculation ${calcName} produced empty results {} for ${dateStr}. Skipping write.`);
|
|
667
|
-
}
|
|
668
|
-
} catch (e) {
|
|
669
|
-
logger.log('ERROR', `[${passName}] getResult/Commit failed for ${calcName} on ${dateStr}`, { err: e.message, stack: e.stack });
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
const completionStatus = successCount === calculationsToRun.length ? 'SUCCESS' : 'WARN';
|
|
673
|
-
logger.log(completionStatus, `[${passName}] Completed ${dateStr}. Success: ${successCount}/${calculationsToRun.length}.`);
|
|
674
|
-
|
|
675
|
-
} catch (err) {
|
|
676
|
-
logger.log('ERROR', `[${passName}] Fatal error for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
|
|
677
|
-
throw err; // Re-throw to stop the orchestrator for this day
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Internal sub-pipe: Runs "meta" computations (Pass 2, 3, 4) for a single date.
|
|
684
|
-
*/
|
|
685
|
-
async function runMetaComputation(
|
|
686
|
-
dateToProcess,
|
|
687
|
-
calculationsToRun, // This is the *filtered* list
|
|
688
|
-
passName,
|
|
689
|
-
config,
|
|
690
|
-
dependencies,
|
|
691
|
-
fetchedDependencies, // <-- This is the NEW object from Firestore
|
|
692
|
-
rootData
|
|
693
|
-
) {
|
|
694
|
-
const { db, logger } = dependencies;
|
|
695
|
-
const dateStr = dateToProcess.toISOString().slice(0, 10);
|
|
696
|
-
logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} calcs.`);
|
|
697
|
-
|
|
698
|
-
try {
|
|
699
|
-
const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
|
|
700
|
-
const compsSub = config.computationsSubcollection || 'computations';
|
|
701
|
-
let successCount = 0;
|
|
702
|
-
|
|
703
|
-
// Add rootData to dependencies for meta-calcs that need to stream users
|
|
704
|
-
const dependenciesForMetaCalc = {
|
|
705
|
-
...dependencies,
|
|
706
|
-
rootData: rootData
|
|
707
|
-
};
|
|
708
|
-
|
|
709
|
-
for (const manifestCalc of calculationsToRun) {
|
|
710
|
-
const calcName = normalizeName(manifestCalc.name);
|
|
711
|
-
const category = manifestCalc.category || 'unknown';
|
|
712
|
-
const CalcClass = manifestCalc.class;
|
|
713
|
-
|
|
714
|
-
if (typeof CalcClass !== 'function') {
|
|
715
|
-
logger.log('ERROR', `[${passName}] Invalid class in manifest for ${calcName}. Skipping.`);
|
|
716
|
-
continue;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const instance = new CalcClass();
|
|
720
|
-
let result = null;
|
|
721
|
-
try {
|
|
722
|
-
// --- Call process with the fetched dependencies ---
|
|
723
|
-
result = await Promise.resolve(instance.process(
|
|
724
|
-
dateStr,
|
|
725
|
-
dependenciesForMetaCalc,
|
|
726
|
-
config,
|
|
727
|
-
fetchedDependencies // <-- Pass the pre-fetched results
|
|
728
|
-
));
|
|
729
|
-
|
|
730
|
-
// --- Database write logic (Identical to original file) ---
|
|
731
|
-
const pendingWrites = [];
|
|
732
|
-
const summaryData = {};
|
|
733
|
-
if (result && Object.keys(result).length > 0) {
|
|
734
|
-
let isSharded = false;
|
|
735
|
-
const shardedCollections = {
|
|
736
|
-
'sharded_user_profile': config.shardedUserProfileCollection,
|
|
737
|
-
'sharded_user_profitability': config.shardedProfitabilityCollection
|
|
738
|
-
};
|
|
739
|
-
for (const resultKey in shardedCollections) {
|
|
740
|
-
if (result[resultKey]) {
|
|
741
|
-
isSharded = true;
|
|
742
|
-
const shardCollectionName = shardedCollections[resultKey];
|
|
743
|
-
if (!shardCollectionName) {
|
|
744
|
-
logger.log('ERROR', `[${passName}] Missing config key for sharded collection: ${resultKey}`);
|
|
745
|
-
continue;
|
|
746
|
-
}
|
|
747
|
-
const shardedData = result[resultKey];
|
|
748
|
-
for (const shardId in shardedData) {
|
|
749
|
-
const shardDocData = shardedData[shardId];
|
|
750
|
-
if (shardDocData && (Object.keys(shardDocData).length > 0)) {
|
|
751
|
-
const shardRef = db.collection(shardCollectionName).doc(shardId);
|
|
752
|
-
pendingWrites.push({ ref: shardRef, data: shardDocData, merge: true });
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
const { [resultKey]: _, ...otherResults } = result;
|
|
756
|
-
result = otherResults;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
if (result && Object.keys(result).length > 0) {
|
|
760
|
-
const computationDocRef = resultsCollectionRef.doc(category)
|
|
761
|
-
.collection(compsSub)
|
|
762
|
-
.doc(calcName);
|
|
763
|
-
pendingWrites.push({ ref: computationDocRef, data: result });
|
|
764
|
-
}
|
|
765
|
-
if (!summaryData[category]) summaryData[category] = {};
|
|
766
|
-
summaryData[category][calcName] = true;
|
|
767
|
-
if (Object.keys(summaryData).length > 0) {
|
|
768
|
-
const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr);
|
|
769
|
-
pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
|
|
770
|
-
}
|
|
771
|
-
if (pendingWrites.length > 0) {
|
|
772
|
-
await commitBatchInChunks(
|
|
773
|
-
config,
|
|
774
|
-
dependencies,
|
|
775
|
-
pendingWrites,
|
|
776
|
-
`Commit ${passName} ${dateStr} [${calcName}]`
|
|
777
|
-
);
|
|
778
|
-
successCount++;
|
|
779
|
-
}
|
|
780
|
-
} else {
|
|
781
|
-
logger.log('WARN', `[${passName}] Meta-calculation ${calcName} produced no results for ${dateStr}. Skipping write.`);
|
|
782
|
-
}
|
|
783
|
-
} catch (e) {
|
|
784
|
-
logger.log('ERROR', `[${passName}] Meta-calc process/commit failed for ${calcName} on ${dateStr}`, { err: e.message, stack: e.stack });
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
const completionStatus = successCount === calculationsToRun.length ? 'SUCCESS' : 'WARN';
|
|
789
|
-
logger.log(completionStatus, `[${passName}] Completed ${dateStr}. Success: ${successCount}/${calculationsToRun.length}.`);
|
|
790
|
-
|
|
791
|
-
} catch (err) {
|
|
792
|
-
logger.log('ERROR', `[${passName}] Fatal error for ${dateStr}`, { errorMessage: err.message, stack: err.stack });
|
|
793
|
-
throw err;
|
|
794
|
-
}
|
|
44
|
+
logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
|
|
795
45
|
}
|
|
796
46
|
|
|
797
|
-
|
|
798
|
-
module.exports = {
|
|
799
|
-
runComputationPass,
|
|
800
|
-
};
|
|
47
|
+
module.exports = { runComputationPass };
|