bulltrackers-module 1.0.117 → 1.0.119

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