bulltrackers-module 1.0.175 → 1.0.177
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/README.MD +1901 -2
- package/functions/computation-system/controllers/computation_controller.js +2 -16
- package/functions/computation-system/helpers/computation_manifest_builder.js +11 -47
- package/functions/computation-system/helpers/computation_pass_runner.js +72 -72
- package/functions/computation-system/helpers/orchestration_helpers.js +136 -99
- package/functions/task-engine/helpers/update_helpers.js +10 -2
- package/functions/task-engine/utils/firestore_batch_manager.js +224 -180
- package/package.json +1 -1
|
@@ -147,37 +147,25 @@ class ComputationExecutor {
|
|
|
147
147
|
*/
|
|
148
148
|
async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps) {
|
|
149
149
|
const { logger } = this.deps;
|
|
150
|
-
|
|
151
|
-
// Fix for the 'all' userType discrepancy:
|
|
152
|
-
const targetUserType = metadata.userType; // 'all', 'normal', or 'speculator'
|
|
153
|
-
|
|
150
|
+
const targetUserType = metadata.userType;
|
|
154
151
|
const mappings = await this.loader.loadMappings();
|
|
155
152
|
const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
|
|
156
|
-
|
|
157
153
|
for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
|
|
158
|
-
// 1. Get Yesterday's Portfolio (if available)
|
|
159
154
|
const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
|
|
160
|
-
|
|
161
|
-
// 2. Get Today's Trading History (if available)
|
|
162
155
|
const todayHistory = historyData ? historyData[userId] : null;
|
|
163
|
-
|
|
164
156
|
const actualUserType = todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
|
|
165
|
-
|
|
166
|
-
// Filtering Logic
|
|
167
157
|
if (targetUserType !== 'all') {
|
|
168
158
|
const mappedTarget = (targetUserType === 'speculator') ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
|
|
169
159
|
if (mappedTarget !== actualUserType) continue;
|
|
170
160
|
}
|
|
171
|
-
|
|
172
161
|
const context = ContextBuilder.buildPerUserContext({
|
|
173
162
|
todayPortfolio, yesterdayPortfolio,
|
|
174
|
-
todayHistory,
|
|
163
|
+
todayHistory,
|
|
175
164
|
userId, userType: actualUserType, dateStr, metadata, mappings, insights,
|
|
176
165
|
computedDependencies: computedDeps,
|
|
177
166
|
previousComputedDependencies: prevDeps,
|
|
178
167
|
config: this.config, deps: this.deps
|
|
179
168
|
});
|
|
180
|
-
|
|
181
169
|
try { await calcInstance.process(context); }
|
|
182
170
|
catch (e) { logger.log('WARN', `Calc ${metadata.name} failed for user ${userId}: ${e.message}`); }
|
|
183
171
|
}
|
|
@@ -187,14 +175,12 @@ class ComputationExecutor {
|
|
|
187
175
|
const mappings = await this.loader.loadMappings();
|
|
188
176
|
const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
|
|
189
177
|
const social = metadata.rootDataDependencies?.includes('social') ? { today: await this.loader.loadSocial(dateStr) } : null;
|
|
190
|
-
|
|
191
178
|
const context = ContextBuilder.buildMetaContext({
|
|
192
179
|
dateStr, metadata, mappings, insights, socialData: social,
|
|
193
180
|
computedDependencies: computedDeps,
|
|
194
181
|
previousComputedDependencies: prevDeps,
|
|
195
182
|
config: this.config, deps: this.deps
|
|
196
183
|
});
|
|
197
|
-
|
|
198
184
|
return await calcInstance.process(context);
|
|
199
185
|
}
|
|
200
186
|
}
|
|
@@ -11,17 +11,6 @@
|
|
|
11
11
|
* It has removed all logic for deprecated folder structures (e.g., /historical/).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
//Hacky solution to force tmp writes TODO : This Tmp write is really dodgy, not ideal but works, consider less hacky solutions to writing to filesystem
|
|
16
|
-
process.env.TMPDIR = '/tmp';
|
|
17
|
-
process.env.TMP = '/tmp';
|
|
18
|
-
process.env.TEMP = '/tmp';
|
|
19
|
-
const os = require('os');
|
|
20
|
-
os.tmpdir = () => '/tmp';
|
|
21
|
-
try { const temp = require('temp'); const path = require('path'); const fs = require('fs'); const tmp = '/tmp';
|
|
22
|
-
if (!fs.existsSync(tmp)) fs.mkdirSync(tmp); temp.dir = tmp; temp.path = () => path.join(tmp, 'temp-' + Math.random().toString(36).slice(2)); } catch {}
|
|
23
|
-
const Viz = require('graphviz');
|
|
24
|
-
|
|
25
14
|
/* --------------------------------------------------
|
|
26
15
|
* Pretty Console Helpers
|
|
27
16
|
* -------------------------------------------------- */
|
|
@@ -42,7 +31,7 @@ const log = {
|
|
|
42
31
|
const normalizeName = (name) => { if (typeof name !== 'string') return name; return name.trim().replace(/,$/, '').replace(/_/g, '-').toLowerCase(); };
|
|
43
32
|
|
|
44
33
|
/**
|
|
45
|
-
* Finds the closest string matches for a typo.
|
|
34
|
+
* Finds the closest string matches for a typo. // https://medium.com/@ethannam/understanding-the-levenshtein-distance-equation-for-beginners-c4285a5604f0
|
|
46
35
|
*/
|
|
47
36
|
function suggestClosest(name, candidates, n = 3) {
|
|
48
37
|
const levenshtein = (a = '', b = '') => {
|
|
@@ -56,7 +45,7 @@ function suggestClosest(name, candidates, n = 3) {
|
|
|
56
45
|
return dp[m][n];
|
|
57
46
|
};
|
|
58
47
|
const scores = candidates.map(c => [c, levenshtein(name, c)]);
|
|
59
|
-
scores.sort((a, b)
|
|
48
|
+
scores.sort((a, b) => a[1] - b[1]);
|
|
60
49
|
return scores.slice(0, n).map(s => s[0]);
|
|
61
50
|
}
|
|
62
51
|
|
|
@@ -125,7 +114,7 @@ function buildManifest(productLinesToRun = [], calculations) {
|
|
|
125
114
|
const metadata = Class.getMetadata();
|
|
126
115
|
const dependencies = Class.getDependencies().map(normalizeName);
|
|
127
116
|
// --- RULE 4: Check for isHistorical mismatch ---
|
|
128
|
-
if (metadata.isHistorical === true && !Class.toString().includes('yesterday')) { // UPDATED FOR MATH LAYER, THIS IS A LITTLE BRITTLE, BUT FOR NOW FINE...
|
|
117
|
+
if (metadata.isHistorical === true && !Class.toString().includes('yesterday')) { // UPDATED FOR MATH LAYER, TODO THIS IS A LITTLE BRITTLE, BUT FOR NOW FINE...
|
|
129
118
|
log.warn(`Calculation "${normalizedName}" is marked 'isHistorical: true' but does not seem to reference 'yesterday' data.`);
|
|
130
119
|
}
|
|
131
120
|
const manifestEntry = {
|
|
@@ -144,8 +133,6 @@ function buildManifest(productLinesToRun = [], calculations) {
|
|
|
144
133
|
inDegree.set(normalizedName, dependencies.length);
|
|
145
134
|
dependencies.forEach(dep => { if (!reverseAdjacency.has(dep)) reverseAdjacency.set(dep, []); reverseAdjacency.get(dep).push(normalizedName); }); }
|
|
146
135
|
|
|
147
|
-
// --- UPDATED ---
|
|
148
|
-
// This 'calculations' object is now the one passed in as an argument.
|
|
149
136
|
if (!calculations || typeof calculations !== 'object') {
|
|
150
137
|
log.fatal('Calculations object was not provided or is invalid.');
|
|
151
138
|
throw new Error('Manifest build failed: Invalid calculations object.');
|
|
@@ -185,14 +172,18 @@ function buildManifest(productLinesToRun = [], calculations) {
|
|
|
185
172
|
/* ---------------- 3. Filter for Product Lines ---------------- */
|
|
186
173
|
log.divider('Filtering by Product Line');
|
|
187
174
|
// 1. Find all "endpoint" calculations (the final signals) in the target product lines.
|
|
175
|
+
|
|
188
176
|
const productLineEndpoints = [];
|
|
189
177
|
for (const [name, entry] of manifestMap.entries()) { if (productLinesToRun.includes(entry.category)) { productLineEndpoints.push(name); } }
|
|
178
|
+
|
|
190
179
|
// 2. Add 'core' calculations as they are always included.
|
|
191
180
|
for (const [name, entry] of manifestMap.entries()) { if (entry.category === 'core') { productLineEndpoints.push(name); } }
|
|
181
|
+
|
|
192
182
|
// 3. Trace all dependencies upwards from these endpoints.
|
|
193
183
|
const requiredCalcs = getDependencySet(productLineEndpoints, adjacency);
|
|
194
184
|
log.info(`Identified ${productLineEndpoints.length} endpoint/core calculations.`);
|
|
195
185
|
log.info(`Traced dependencies: ${requiredCalcs.size} total calculations are required.`);
|
|
186
|
+
|
|
196
187
|
// 4. Create the final, filtered maps for sorting.
|
|
197
188
|
const filteredManifestMap = new Map();
|
|
198
189
|
const filteredInDegree = new Map();
|
|
@@ -206,17 +197,20 @@ function buildManifest(productLinesToRun = [], calculations) {
|
|
|
206
197
|
const sortedManifest = [];
|
|
207
198
|
const queue = [];
|
|
208
199
|
let maxPass = 0;
|
|
200
|
+
|
|
209
201
|
for (const [name, degree] of filteredInDegree) { if (degree === 0) { queue.push(name); filteredManifestMap.get(name).pass = 1; maxPass = 1; } }
|
|
210
202
|
queue.sort();
|
|
211
203
|
while (queue.length) {
|
|
212
204
|
const currentName = queue.shift();
|
|
213
205
|
const currentEntry = filteredManifestMap.get(currentName);
|
|
214
206
|
sortedManifest.push(currentEntry);
|
|
207
|
+
|
|
215
208
|
for (const neighborName of (filteredReverseAdjacency.get(currentName) || [])) { const newDegree = filteredInDegree.get(neighborName) - 1; filteredInDegree.set(neighborName, newDegree);
|
|
216
209
|
const neighborEntry = filteredManifestMap.get(neighborName);
|
|
217
210
|
if (neighborEntry.pass <= currentEntry.pass) { neighborEntry.pass = currentEntry.pass + 1; if (neighborEntry.pass > maxPass) maxPass = neighborEntry.pass; }
|
|
218
211
|
if (newDegree === 0) { queue.push(neighborName); } }
|
|
219
212
|
queue.sort(); }
|
|
213
|
+
|
|
220
214
|
if (sortedManifest.length !== filteredManifestMap.size) {
|
|
221
215
|
log.divider('Circular Dependency Detected');
|
|
222
216
|
const cycles = findCycles(filteredManifestMap, adjacency);
|
|
@@ -233,37 +227,7 @@ function buildManifest(productLinesToRun = [], calculations) {
|
|
|
233
227
|
return sortedManifest;
|
|
234
228
|
}
|
|
235
229
|
|
|
236
|
-
/**
|
|
237
|
-
* Generates an SVG dependency graph for a given manifest.
|
|
238
|
-
* @param {object[]} manifest - The sorted manifest array.
|
|
239
|
-
* @param {string} filename - The output filename (e.g., "full-dependency-tree.svg").
|
|
240
|
-
*/
|
|
241
|
-
async function generateSvgGraph(manifest, filename = 'dependency-tree.svg') {
|
|
242
|
-
log.divider(`Generating SVG Graph: ${filename}`);
|
|
243
|
-
const viz = new Viz({ worker: false });
|
|
244
|
-
const categories = [...new Set(manifest.map(e => e.category))];
|
|
245
|
-
const colors = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080'];
|
|
246
|
-
const colorMap = new Map(categories.map((cat, i) => [cat, colors[i % colors.length]]));
|
|
247
|
-
colorMap.set('core', '#a9a9a9');
|
|
248
|
-
let dot = `digraph Manifest {\n rankdir=LR;\n node [shape=box, style=filled, fontname="Helvetica"];\n layout=dot;\n overlap=false;\n splines=true;\n`;
|
|
249
|
-
for (const category of categories) {
|
|
250
|
-
if (category === 'core') continue;
|
|
251
|
-
dot += ` subgraph "cluster_${category}" {\n label="${category.toUpperCase()} Product Line";\n style=filled;\n color="#f0f0f0";\n`;
|
|
252
|
-
for (const entry of manifest.filter(e => e.category === category)) { dot += ` "${entry.name}" [label="${entry.name}\\n(Pass ${entry.pass})", fillcolor="${colorMap.get(category)}"];\n`; }
|
|
253
|
-
dot += ` }\n`; }
|
|
254
|
-
dot += ` subgraph "cluster_core" {\n label="CORE";\n style=filled;\n color="#e0e0e0";\n`;
|
|
255
|
-
for (const entry of manifest.filter(e => e.category === 'core')) { dot += ` "${entry.name}" [label="${entry.name}\\n(Pass ${entry.pass})", fillcolor="${colorMap.get('core')}"];\n`; }
|
|
256
|
-
dot += ` }\n`;
|
|
257
|
-
for (const entry of manifest) { for (const dep of entry.dependencies || []) { dot += ` "${dep}" -> "${entry.name}";\n`; } }
|
|
258
|
-
dot += '}\n';
|
|
259
|
-
try {
|
|
260
|
-
const svg = await viz.renderString(dot, { format: 'svg' });
|
|
261
|
-
const out = path.join('/tmp', filename);
|
|
262
|
-
fs.writeFileSync(out, svg);
|
|
263
|
-
log.success(`Dependency tree generated at ${out}`);
|
|
264
230
|
|
|
265
|
-
} catch (e) { log.error(`SVG generation failed: ${e.message}`); }
|
|
266
|
-
}
|
|
267
231
|
|
|
268
232
|
|
|
269
233
|
/**
|
|
@@ -282,4 +246,4 @@ function build(productLinesToRun, calculations) {
|
|
|
282
246
|
}
|
|
283
247
|
|
|
284
248
|
|
|
285
|
-
module.exports = { build
|
|
249
|
+
module.exports = { build};
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* V3.4: Optimized Orchestration using Status Document (Single Source of Truth).
|
|
4
|
-
* - Reads one 'computation_status' doc per day to decide what to run.
|
|
5
|
-
* - Reduces Firestore reads significantly.
|
|
6
|
-
* - explicitly marks failures as false in the status doc.
|
|
2
|
+
* FILENAME: bulltrackers-module/functions/computation-system/helpers/computation_pass_runner.js
|
|
7
3
|
*/
|
|
8
4
|
|
|
9
5
|
const {
|
|
10
6
|
groupByPass,
|
|
11
7
|
checkRootDataAvailability,
|
|
12
8
|
fetchExistingResults,
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
fetchGlobalComputationStatus, // <--- New Import
|
|
10
|
+
updateGlobalComputationStatus, // <--- New Import
|
|
15
11
|
runStandardComputationPass,
|
|
16
12
|
runMetaComputationPass,
|
|
17
13
|
checkRootDependencies
|
|
@@ -25,113 +21,117 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
25
21
|
if (!passToRun)
|
|
26
22
|
return logger.log('ERROR', '[PassRunner] No pass defined. Aborting.');
|
|
27
23
|
|
|
28
|
-
logger.log('INFO', `🚀 Starting PASS ${passToRun} with
|
|
24
|
+
logger.log('INFO', `🚀 Starting PASS ${passToRun} with Global Status Check...`);
|
|
29
25
|
|
|
30
|
-
// Hardcoded earliest dates
|
|
31
|
-
const earliestDates
|
|
26
|
+
// Hardcoded earliest dates
|
|
27
|
+
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') };
|
|
32
28
|
earliestDates.absoluteEarliest = Object.values(earliestDates).reduce((a,b) => a < b ? a : b);
|
|
33
29
|
|
|
34
|
-
const passes
|
|
30
|
+
const passes = groupByPass(computationManifest);
|
|
35
31
|
const calcsInThisPass = passes[passToRun] || [];
|
|
36
32
|
|
|
37
33
|
if (!calcsInThisPass.length)
|
|
38
34
|
return logger.log('WARN', `[PassRunner] No calcs for Pass ${passToRun}. Exiting.`);
|
|
39
35
|
|
|
40
|
-
// Determine date range to process
|
|
41
|
-
// We no longer need complex per-calc date derivation since the status doc handles "done-ness".
|
|
42
|
-
// We just iterate from absolute earliest to yesterday.
|
|
43
36
|
const passEarliestDate = earliestDates.absoluteEarliest;
|
|
44
|
-
const endDateUTC
|
|
37
|
+
const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
|
|
45
38
|
const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
|
|
46
39
|
|
|
47
|
-
const standardCalcs
|
|
48
|
-
const metaCalcs
|
|
40
|
+
const standardCalcs = calcsInThisPass.filter(c => c.type === 'standard');
|
|
41
|
+
const metaCalcs = calcsInThisPass.filter(c => c.type === 'meta');
|
|
49
42
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (statusData[calc.name] === true) return false;
|
|
43
|
+
// 1. Fetch Global Status ONCE (Memory Cache)
|
|
44
|
+
// Returns { "2023-10-27": { calcA: true, calcB: false }, ... }
|
|
45
|
+
const globalStatusData = await fetchGlobalComputationStatus(config, dependencies);
|
|
54
46
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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)
|
|
58
55
|
if (calc.dependencies && calc.dependencies.length > 0) {
|
|
59
|
-
const depsMet = calc.dependencies.every(depName =>
|
|
60
|
-
if (!depsMet) return false;
|
|
56
|
+
const depsMet = calc.dependencies.every(depName => dailyStatus[depName] === true);
|
|
57
|
+
if (!depsMet) return false;
|
|
61
58
|
}
|
|
62
|
-
|
|
63
59
|
return true;
|
|
64
60
|
};
|
|
65
61
|
|
|
62
|
+
// Process a single date and RETURN updates (do not write)
|
|
66
63
|
const processDate = async (dateStr) => {
|
|
67
|
-
// 1. Fetch the Single Status Document (Cheap Read)
|
|
68
|
-
const statusData = await fetchComputationStatus(dateStr, config, dependencies);
|
|
69
64
|
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const standardToRun = standardCalcs.filter(c => shouldRun(c, statusData));
|
|
73
|
-
const metaToRun = metaCalcs.filter(c => shouldRun(c, statusData));
|
|
65
|
+
const standardToRun = standardCalcs.filter(c => shouldRun(c, dateStr));
|
|
66
|
+
const metaToRun = metaCalcs.filter(c => shouldRun(c, dateStr));
|
|
74
67
|
|
|
75
|
-
|
|
76
|
-
// No checking root data, no loading results.
|
|
77
|
-
if (!standardToRun.length && !metaToRun.length) {
|
|
78
|
-
return; // logger.log('INFO', `[PassRunner] ${dateStr} complete or waiting for deps.`);
|
|
79
|
-
}
|
|
68
|
+
if (!standardToRun.length && !metaToRun.length) return null; // No work
|
|
80
69
|
|
|
81
|
-
// 3. Check Root Data Availability (Only done if we have work to do)
|
|
82
|
-
// We filter standardToRun further based on root data requirements
|
|
83
|
-
// (e.g. if calc needs history but history isn't ready for this date)
|
|
84
70
|
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
|
|
85
|
-
|
|
86
|
-
if (!rootData) {
|
|
87
|
-
// If root data is completely missing for the day, we can't run anything.
|
|
88
|
-
// We do NOT mark as false, because it's not a failure, just data unavailability.
|
|
89
|
-
// We just skip.
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
71
|
+
if (!rootData) return null;
|
|
92
72
|
|
|
93
|
-
// Further filter based on specific root data needs (e.g. checks "hasHistory")
|
|
94
73
|
const finalStandardToRun = standardToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
|
|
95
74
|
const finalMetaToRun = metaToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
|
|
96
75
|
|
|
97
|
-
if (!finalStandardToRun.length && !finalMetaToRun.length) return;
|
|
76
|
+
if (!finalStandardToRun.length && !finalMetaToRun.length) return null;
|
|
98
77
|
|
|
99
|
-
logger.log('INFO', `[PassRunner] Running ${dateStr}: ${finalStandardToRun.length}
|
|
78
|
+
logger.log('INFO', `[PassRunner] Running ${dateStr}: ${finalStandardToRun.length} std, ${finalMetaToRun.length} meta`);
|
|
79
|
+
|
|
80
|
+
const dateUpdates = {}; // { calcName: true/false }
|
|
100
81
|
|
|
101
82
|
try {
|
|
102
|
-
// 4. Fetch Data for Execution (Dependencies)
|
|
103
|
-
// We only fetch data required by the calcs that are actually running.
|
|
104
83
|
const calcsRunning = [...finalStandardToRun, ...finalMetaToRun];
|
|
105
84
|
const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
109
|
-
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
85
|
+
const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
86
|
+
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
110
87
|
const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
88
|
+
if (finalStandardToRun.length) {
|
|
89
|
+
const updates = await runStandardComputationPass(dateToProcess, finalStandardToRun, `Pass ${passToRun} (Std)`, config, dependencies, rootData, existingResults, previousResults, true); // skipStatusWrite=true
|
|
90
|
+
Object.assign(dateUpdates, updates);
|
|
91
|
+
}
|
|
92
|
+
if (finalMetaToRun.length) {
|
|
93
|
+
const updates = await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, true); // skipStatusWrite=true
|
|
94
|
+
Object.assign(dateUpdates, updates);
|
|
95
|
+
}
|
|
119
96
|
} catch (err) {
|
|
120
97
|
logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message });
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const failedUpdates = {};
|
|
124
|
-
[...finalStandardToRun, ...finalMetaToRun].forEach(c => failedUpdates[c.name] = false);
|
|
125
|
-
await updateComputationStatus(dateStr, failedUpdates, config, dependencies);
|
|
98
|
+
// Mark failures
|
|
99
|
+
[...finalStandardToRun, ...finalMetaToRun].forEach(c => dateUpdates[c.name] = false);
|
|
126
100
|
}
|
|
101
|
+
|
|
102
|
+
// Return the updates for this date
|
|
103
|
+
return { date: dateStr, updates: dateUpdates };
|
|
127
104
|
};
|
|
128
105
|
|
|
129
106
|
// Batch process dates
|
|
130
107
|
for (let i = 0; i < allExpectedDates.length; i += PARALLEL_BATCH_SIZE) {
|
|
131
108
|
const batch = allExpectedDates.slice(i, i + PARALLEL_BATCH_SIZE);
|
|
132
|
-
|
|
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
|
+
}
|
|
133
133
|
}
|
|
134
134
|
logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
module.exports = { runComputationPass };
|
|
137
|
+
module.exports = { runComputationPass };
|