bulltrackers-module 1.0.21 → 1.0.23
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/handler_creator.js +41 -0
- package/functions/computation-system/helpers/orchestration_helpers.js +154 -89
- package/functions/computation-system/index.js +3 -1
- package/functions/computation-system/utils/data_loader.js +31 -25
- package/functions/computation-system/utils/utils.js +99 -72
- package/functions/dispatcher/index.js +1 -1
- package/functions/generic-api/helpers/api_helpers.js +33 -20
- package/functions/generic-api/index.js +10 -8
- package/functions/task-engine/handler_creator.js +103 -0
- package/functions/task-engine/index.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Factory function to create the Computation System handler.
|
|
3
|
+
*/
|
|
4
|
+
const { Firestore } = require('@google-cloud/firestore');
|
|
5
|
+
const { logger: defaultLogger } = require("sharedsetup")(__filename);
|
|
6
|
+
const { runComputationOrchestrator } = require('./helpers/orchestration_helpers');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates the Computation System Cloud Function handler (HTTP triggered).
|
|
10
|
+
* @param {object} config - The Computation System configuration object.
|
|
11
|
+
* @param {object} dependencies - Shared dependencies { firestore, logger }.
|
|
12
|
+
* @returns {Function} The async Cloud Function handler for HTTP requests.
|
|
13
|
+
*/
|
|
14
|
+
function createComputationSystemHandler(config, dependencies) {
|
|
15
|
+
// Use dependencies passed from index.js
|
|
16
|
+
const firestore = dependencies.firestore || new Firestore();
|
|
17
|
+
const logger = dependencies.logger || defaultLogger;
|
|
18
|
+
|
|
19
|
+
return async (req, res) => {
|
|
20
|
+
const functionName = 'ComputationSystem';
|
|
21
|
+
logger.log('INFO', `[${functionName}] Starting...`);
|
|
22
|
+
try {
|
|
23
|
+
// Pass the config down to the orchestrator
|
|
24
|
+
const summary = await runComputationOrchestrator(firestore, logger, config);
|
|
25
|
+
|
|
26
|
+
logger.log('SUCCESS', `[${functionName}] Done.`);
|
|
27
|
+
if (!res.headersSent) {
|
|
28
|
+
res.status(200).send({ message: 'Computation complete', summary });
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
logger.log('ERROR', `[${functionName}] Failed.`, { errorMessage: error.message, errorStack: error.stack });
|
|
32
|
+
if (!res.headersSent) {
|
|
33
|
+
res.status(500).send({ message: 'Computation Error', error: error.message });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
createComputationSystemHandler,
|
|
41
|
+
};
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Core orchestration logic for the Computation System.
|
|
3
|
-
* Contains both the high-level pass orchestrator and the low-level
|
|
4
|
-
* function for processing a single date.
|
|
5
3
|
*/
|
|
6
4
|
|
|
7
5
|
const { FieldPath } = require('@google-cloud/firestore');
|
|
8
6
|
const { getPortfolioPartRefs, loadFullDayMap, loadDataByRefs } = require('../utils/data_loader.js');
|
|
9
7
|
const {
|
|
8
|
+
// Note: calculation categories are now sourced from utils.js which gets them from the unified package
|
|
10
9
|
historicalCalculations, dailyCalculations, HISTORICAL_CALC_NAMES,
|
|
11
|
-
withRetry,
|
|
12
|
-
|
|
10
|
+
withRetry, // Ensure withRetry is correctly imported if needed here, or rely on utils.js export
|
|
11
|
+
getExpectedDateStrings, processJobsInParallel, getFirstDateFromSourceData,
|
|
12
|
+
commitBatchInChunks, unifiedUtils // unifiedUtils comes from the calculations package via utils.js
|
|
13
13
|
} = require('../utils/utils.js');
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
// --- CORE CALCULATION HELPERS
|
|
16
|
+
// --- CORE CALCULATION HELPERS ---
|
|
17
17
|
|
|
18
|
-
/** Initializes calculator instances
|
|
18
|
+
/** Initializes calculator instances. */
|
|
19
19
|
function initializeCalculators(calculationsToRun, sourcePackage) {
|
|
20
20
|
const state = {};
|
|
21
21
|
for (const { category, calcName } of calculationsToRun) {
|
|
@@ -24,54 +24,61 @@ function initializeCalculators(calculationsToRun, sourcePackage) {
|
|
|
24
24
|
try {
|
|
25
25
|
state[`${category}/${calcName}`] = new CalculationClass();
|
|
26
26
|
} catch (e) {
|
|
27
|
-
// Use logger, but it's not passed here, so console.warn
|
|
28
27
|
console.warn(`[Orchestrator] Init failed for ${category}/${calcName}`, { errorMessage: e.message });
|
|
29
|
-
state[`${category}/${calcName}`] = null;
|
|
28
|
+
state[`${category}/${calcName}`] = null; // Mark as failed
|
|
30
29
|
}
|
|
31
30
|
} else {
|
|
32
31
|
console.warn(`[Orchestrator] Calculation class not found: ${category}/${calcName}`);
|
|
32
|
+
state[`${category}/${calcName}`] = null; // Mark as missing
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
return state;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** Streams day's data
|
|
39
|
-
async function streamAndProcess(firestore, dateStr, todayRefs, state, passName, logger, yesterdayPortfolios = {}) {
|
|
38
|
+
/** Streams day's data, calls process() on calculators, using config for batch size. */
|
|
39
|
+
async function streamAndProcess(firestore, dateStr, todayRefs, state, passName, logger, config, yesterdayPortfolios = {}) {
|
|
40
40
|
logger.log('INFO', `[${passName}] Streaming ${todayRefs.length} 'today' part docs for ${dateStr}...`);
|
|
41
|
+
// unifiedUtils comes from calculations package via utils.js
|
|
41
42
|
const { instrumentToTicker, instrumentToSector } = await unifiedUtils.loadInstrumentMappings();
|
|
42
43
|
const context = { instrumentMappings: instrumentToTicker, sectorMapping: instrumentToSector };
|
|
44
|
+
const batchSize = config.partRefBatchSize || 10; // Use config for chunk size
|
|
43
45
|
|
|
44
|
-
for (let i = 0; i < todayRefs.length; i +=
|
|
45
|
-
const batchRefs = todayRefs.slice(i, i +
|
|
46
|
-
|
|
46
|
+
for (let i = 0; i < todayRefs.length; i += batchSize) {
|
|
47
|
+
const batchRefs = todayRefs.slice(i, i + batchSize);
|
|
48
|
+
// Pass config to data loader
|
|
49
|
+
const todayPortfoliosChunk = await loadDataByRefs(firestore, batchRefs, config);
|
|
47
50
|
|
|
48
51
|
for (const uid in todayPortfoliosChunk) {
|
|
49
52
|
const p = todayPortfoliosChunk[uid];
|
|
50
|
-
if (!p) continue;
|
|
51
|
-
|
|
53
|
+
if (!p) continue; // Skip if portfolio data is null/undefined for this user
|
|
54
|
+
|
|
55
|
+
// Determine user type based on portfolio structure
|
|
52
56
|
const userType = p.PublicPositions ? 'speculator' : 'normal';
|
|
53
|
-
|
|
57
|
+
|
|
54
58
|
for (const key in state) {
|
|
55
59
|
const calc = state[key];
|
|
60
|
+
// Skip if calculator failed initialization or has no process method
|
|
56
61
|
if (!calc || typeof calc.process !== 'function') continue;
|
|
57
62
|
|
|
58
63
|
const [category, calcName] = key.split('/');
|
|
59
64
|
let processArgs;
|
|
60
65
|
|
|
61
66
|
if (HISTORICAL_CALC_NAMES.has(calcName)) {
|
|
67
|
+
// Historical calculations require yesterday's data
|
|
62
68
|
const pYesterday = yesterdayPortfolios[uid];
|
|
63
|
-
if (!pYesterday) continue; // Skip if no yesterday data
|
|
64
|
-
processArgs = [p, pYesterday, uid];
|
|
69
|
+
if (!pYesterday) continue; // Skip if no yesterday data
|
|
70
|
+
processArgs = [p, pYesterday, uid]; // pToday, pYesterday, userId
|
|
65
71
|
} else {
|
|
66
|
-
// Filter daily
|
|
72
|
+
// Filter daily calculations based on user type and calculation category
|
|
67
73
|
if ((userType === 'normal' && category === 'speculators') ||
|
|
68
74
|
(userType === 'speculator' && !['speculators', 'sanity'].includes(category))) {
|
|
69
|
-
continue;
|
|
75
|
+
continue; // Skip speculator calcs for normal users, and non-speculator/sanity calcs for speculators
|
|
70
76
|
}
|
|
71
|
-
processArgs = [p, uid, context];
|
|
77
|
+
processArgs = [p, uid, context]; // pToday, userId, context
|
|
72
78
|
}
|
|
73
|
-
|
|
79
|
+
|
|
74
80
|
try {
|
|
81
|
+
// Allow process method to be async or sync
|
|
75
82
|
await Promise.resolve(calc.process(...processArgs));
|
|
76
83
|
} catch (e) {
|
|
77
84
|
logger.log('WARN', `Process error in ${key} for user ${uid}`, { err: e.message });
|
|
@@ -82,95 +89,128 @@ async function streamAndProcess(firestore, dateStr, todayRefs, state, passName,
|
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
|
|
85
|
-
// --- LOW-LEVEL ORCHESTRATOR
|
|
92
|
+
// --- LOW-LEVEL ORCHESTRATOR ---
|
|
86
93
|
|
|
87
94
|
/**
|
|
88
|
-
* Runs
|
|
89
|
-
* @param {Firestore} firestore
|
|
90
|
-
* @param {Function} logger
|
|
91
|
-
* @param {Date} dateToProcess
|
|
92
|
-
* @param {Array} calculationsToRun
|
|
93
|
-
* @param {
|
|
94
|
-
* @param {
|
|
95
|
-
* @
|
|
95
|
+
* Runs computations for a single date using the provided config.
|
|
96
|
+
* @param {Firestore} firestore Firestore client.
|
|
97
|
+
* @param {Function} logger Logger instance.
|
|
98
|
+
* @param {Date} dateToProcess The date to process.
|
|
99
|
+
* @param {Array<object>} calculationsToRun List of { category, calcName }.
|
|
100
|
+
* @param {string} passName Name for logging.
|
|
101
|
+
* @param {object} sourcePackage Object containing calculation classes.
|
|
102
|
+
* @param {object} config The computation system configuration object.
|
|
103
|
+
* @returns {Promise<object>} Execution summary.
|
|
96
104
|
*/
|
|
97
|
-
async function runUnifiedComputation(firestore, logger, dateToProcess, calculationsToRun, passName, sourcePackage) {
|
|
105
|
+
async function runUnifiedComputation(firestore, logger, dateToProcess, calculationsToRun, passName, sourcePackage, config) { // Added config
|
|
98
106
|
const dateStr = dateToProcess.toISOString().slice(0, 10);
|
|
99
107
|
logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} calcs.`);
|
|
100
108
|
|
|
101
109
|
try {
|
|
102
110
|
// --- 1. Data Loading ---
|
|
103
|
-
|
|
111
|
+
// Pass config to data loader functions
|
|
112
|
+
const todayRefs = await getPortfolioPartRefs(firestore, dateStr, config);
|
|
104
113
|
if (todayRefs.length === 0) {
|
|
114
|
+
logger.log('INFO', `[${passName}] No portfolio data found for ${dateStr}. Skipping.`);
|
|
105
115
|
return { success: true, date: dateStr, message: "No portfolio data for today." };
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
let yesterdayPortfolios = {};
|
|
119
|
+
// Check if any calculation *in this run* needs yesterday's data
|
|
109
120
|
const requiresYesterday = calculationsToRun.some(c => sourcePackage[c.category]?.[c.calcName]?.prototype?.process.length === 3);
|
|
110
121
|
|
|
111
122
|
if (requiresYesterday) {
|
|
112
123
|
const prev = new Date(dateToProcess);
|
|
113
124
|
prev.setUTCDate(prev.getUTCDate() - 1);
|
|
114
125
|
const prevStr = prev.toISOString().slice(0, 10);
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
// Pass config here too
|
|
127
|
+
const yesterdayRefs = await getPortfolioPartRefs(firestore, prevStr, config);
|
|
128
|
+
if (yesterdayRefs.length > 0) {
|
|
129
|
+
yesterdayPortfolios = await loadFullDayMap(firestore, yesterdayRefs, config);
|
|
130
|
+
logger.log('INFO', `[${passName}] Loaded yesterday's (${prevStr}) portfolio map for historical calcs.`);
|
|
131
|
+
} else {
|
|
132
|
+
logger.log('WARN', `[${passName}] Yesterday's (${prevStr}) portfolio data not found. Historical calcs requiring it will be skipped.`);
|
|
133
|
+
}
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
// --- 2. In-Memory Processing ---
|
|
120
137
|
const state = initializeCalculators(calculationsToRun, sourcePackage);
|
|
121
|
-
|
|
138
|
+
// Pass config here
|
|
139
|
+
await streamAndProcess(firestore, dateStr, todayRefs, state, passName, logger, config, yesterdayPortfolios);
|
|
122
140
|
|
|
123
|
-
// --- 3.
|
|
141
|
+
// --- 3. Write Results Per Calculation ---
|
|
124
142
|
let successCount = 0;
|
|
125
|
-
|
|
143
|
+
// Use config for collection names
|
|
144
|
+
const resultsCollectionRef = firestore.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
|
|
126
145
|
|
|
127
146
|
for (const key in state) {
|
|
128
147
|
const calc = state[key];
|
|
148
|
+
// Skip if calculator failed init or has no getResult
|
|
129
149
|
if (!calc || typeof calc.getResult !== 'function') continue;
|
|
130
|
-
|
|
150
|
+
|
|
131
151
|
const [category, calcName] = key.split('/');
|
|
132
|
-
const pendingWrites = [];
|
|
133
|
-
const summaryData = {};
|
|
134
152
|
|
|
135
153
|
try {
|
|
154
|
+
// Allow getResult to be async or sync
|
|
136
155
|
const result = await Promise.resolve(calc.getResult());
|
|
156
|
+
const pendingWrites = [];
|
|
157
|
+
const summaryData = {}; // Summary for this specific calculation's success
|
|
137
158
|
|
|
159
|
+
// Only proceed if result is not null/undefined and has data
|
|
138
160
|
if (result && Object.keys(result).length > 0) {
|
|
139
|
-
|
|
140
161
|
if (result.sharded_user_profitability) {
|
|
162
|
+
// Handle sharded profitability - use config for collection name
|
|
141
163
|
for (const shardId in result.sharded_user_profitability) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
164
|
+
const shardData = result.sharded_user_profitability[shardId];
|
|
165
|
+
if (shardData && Object.keys(shardData).length > 0) {
|
|
166
|
+
// Use config collection name
|
|
167
|
+
const shardRef = firestore.collection(config.shardedProfitabilityCollection).doc(shardId);
|
|
168
|
+
// Use merge:true to update existing shard docs safely
|
|
169
|
+
pendingWrites.push({ ref: shardRef, data: { profits: shardData } });
|
|
170
|
+
}
|
|
147
171
|
}
|
|
172
|
+
// Mark sharded calculation as processed in summary
|
|
173
|
+
if (!summaryData[category]) summaryData[category] = {};
|
|
174
|
+
summaryData[category][calcName] = true;
|
|
175
|
+
|
|
148
176
|
} else {
|
|
149
|
-
|
|
150
|
-
|
|
177
|
+
// Handle normal results - use config for subcollection names
|
|
178
|
+
const computationDocRef = resultsCollectionRef.doc(category)
|
|
179
|
+
.collection(config.computationsSubcollection)
|
|
151
180
|
.doc(calcName);
|
|
152
181
|
pendingWrites.push({ ref: computationDocRef, data: result });
|
|
182
|
+
|
|
183
|
+
// Mark normal calculation as processed in summary
|
|
184
|
+
if (!summaryData[category]) summaryData[category] = {};
|
|
185
|
+
summaryData[category][calcName] = true;
|
|
153
186
|
}
|
|
154
187
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
188
|
+
// Add summary update to the top-level date document using merge
|
|
189
|
+
if (Object.keys(summaryData).length > 0) {
|
|
190
|
+
const topLevelDocRef = firestore.collection(config.resultsCollection).doc(dateStr);
|
|
191
|
+
pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Commit writes if any were generated
|
|
195
|
+
if (pendingWrites.length > 0) {
|
|
196
|
+
await commitBatchInChunks(
|
|
197
|
+
firestore,
|
|
198
|
+
pendingWrites,
|
|
199
|
+
`Commit ${passName} ${dateStr} [${key}]`,
|
|
200
|
+
config // Pass config
|
|
201
|
+
);
|
|
202
|
+
successCount++;
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
logger.log('INFO', `[${passName}] Calculation ${key} produced no results for ${dateStr}. Skipping write.`);
|
|
167
206
|
}
|
|
168
207
|
} catch (e) {
|
|
169
|
-
logger.log('ERROR', `[${passName}] getResult/Commit failed for ${key}`, { err: e.message });
|
|
208
|
+
logger.log('ERROR', `[${passName}] getResult/Commit failed for ${key} on ${dateStr}`, { err: e.message });
|
|
170
209
|
}
|
|
171
210
|
}
|
|
172
|
-
|
|
173
|
-
|
|
211
|
+
|
|
212
|
+
const completionStatus = successCount === calculationsToRun.length ? 'SUCCESS' : 'WARN';
|
|
213
|
+
logger.log(completionStatus, `[${passName}] Completed ${dateStr}. Success: ${successCount}/${calculationsToRun.length}.`);
|
|
174
214
|
return { success: true, date: dateStr, successful: successCount, failed: calculationsToRun.length - successCount };
|
|
175
215
|
|
|
176
216
|
} catch (err) {
|
|
@@ -180,21 +220,22 @@ async function runUnifiedComputation(firestore, logger, dateToProcess, calculati
|
|
|
180
220
|
}
|
|
181
221
|
|
|
182
222
|
|
|
183
|
-
// --- HIGH-LEVEL ORCHESTRATOR
|
|
223
|
+
// --- HIGH-LEVEL ORCHESTRATOR ---
|
|
184
224
|
|
|
185
225
|
/**
|
|
186
|
-
* Main entry point for the Unified Computation System logic.
|
|
187
|
-
* @param {Firestore} firestore
|
|
188
|
-
* @param {Function} logger
|
|
189
|
-
* @
|
|
226
|
+
* Main entry point for the Unified Computation System logic, using config.
|
|
227
|
+
* @param {Firestore} firestore Firestore client.
|
|
228
|
+
* @param {Function} logger Logger instance.
|
|
229
|
+
* @param {object} config The computation system configuration object.
|
|
230
|
+
* @returns {Promise<Object>} Summary of all passes.
|
|
190
231
|
*/
|
|
191
|
-
async function runComputationOrchestrator(firestore, logger) {
|
|
232
|
+
async function runComputationOrchestrator(firestore, logger, config) { // Added config
|
|
192
233
|
const summary = { pass1_results: [], pass2_results: [] };
|
|
193
234
|
const yesterday = new Date();
|
|
194
235
|
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
|
|
195
236
|
const endDateUTC = new Date(Date.UTC(yesterday.getUTCFullYear(), yesterday.getUTCMonth(), yesterday.getUTCDate()));
|
|
196
237
|
|
|
197
|
-
// --- Master Calculation Lists ---
|
|
238
|
+
// --- Master Calculation Lists (sourced via utils.js) ---
|
|
198
239
|
const masterHistoricalList = Object.entries(historicalCalculations).flatMap(([cat, calcs]) =>
|
|
199
240
|
Object.keys(calcs).map(name => ({ category: cat, calcName: name }))
|
|
200
241
|
);
|
|
@@ -204,63 +245,87 @@ async function runComputationOrchestrator(firestore, logger) {
|
|
|
204
245
|
const masterFullList = [...masterHistoricalList, ...masterDailyList];
|
|
205
246
|
|
|
206
247
|
// --- Date & Job Discovery ---
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
248
|
+
// Pass config here
|
|
249
|
+
const firstDate = await getFirstDateFromSourceData(firestore, config);
|
|
250
|
+
// Use config for fallback date if needed
|
|
251
|
+
const startDateUTC = firstDate
|
|
252
|
+
? new Date(Date.UTC(firstDate.getUTCFullYear(), firstDate.getUTCMonth(), firstDate.getUTCDate()))
|
|
253
|
+
: new Date(config.earliestComputationDate + 'T00:00:00Z'); // Ensure UTC
|
|
254
|
+
|
|
210
255
|
const allExpectedDates = getExpectedDateStrings(startDateUTC, endDateUTC);
|
|
211
|
-
|
|
256
|
+
// Use config for resultsCollection
|
|
257
|
+
const insightDocs = await withRetry(() => firestore.collection(config.resultsCollection).get(), "ListAllInsightDocs");
|
|
212
258
|
const existingDateIds = new Set(insightDocs.docs.map(d => d.id).filter(id => /^\d{4}-\d{2}-\d{2}$/.test(id)));
|
|
213
259
|
|
|
214
260
|
// --- PASS 1: Find and process entirely missing days (DAILY CALCS ONLY) ---
|
|
215
261
|
const missingDates = allExpectedDates.filter(dateStr => !existingDateIds.has(dateStr));
|
|
216
262
|
const pass1Jobs = missingDates.map(date => ({ date, missing: masterDailyList }));
|
|
217
|
-
|
|
263
|
+
|
|
264
|
+
logger.log('INFO', `[Orchestrator] Pass 1: Found ${pass1Jobs.length} missing dates to process (daily calcs only).`);
|
|
218
265
|
const pass1Results = await processJobsInParallel(
|
|
219
266
|
pass1Jobs,
|
|
220
|
-
|
|
221
|
-
'Pass 1'
|
|
267
|
+
// Pass config to runUnifiedComputation
|
|
268
|
+
(date, missing) => runUnifiedComputation(firestore, logger, date, missing, 'Pass 1 (Daily Only)', dailyCalculations, config),
|
|
269
|
+
'Pass 1',
|
|
270
|
+
config // Pass config to processJobsInParallel
|
|
222
271
|
);
|
|
272
|
+
// Process results (handle fulfilled/rejected)
|
|
223
273
|
summary.pass1_results = pass1Results.map((r, i) => r.status === 'fulfilled' ? r.value : { success: false, date: pass1Jobs[i].date, error: r.reason?.message });
|
|
224
274
|
|
|
275
|
+
|
|
225
276
|
// --- PASS 2: Find and process incomplete days ---
|
|
226
277
|
const pass2Jobs = [];
|
|
227
|
-
|
|
278
|
+
// Use config for resultsCollection
|
|
279
|
+
const updatedInsightDocs = await withRetry(() => firestore.collection(config.resultsCollection).where(FieldPath.documentId(), 'in', allExpectedDates).get(), "ListAllInsightDocs-Pass2");
|
|
228
280
|
|
|
229
281
|
updatedInsightDocs.forEach(doc => {
|
|
230
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(doc.id)) return;
|
|
282
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(doc.id)) return; // Skip non-date docs
|
|
231
283
|
|
|
232
284
|
const data = doc.data();
|
|
285
|
+
// Check which computations are missing based on the summary flags in the top-level doc
|
|
233
286
|
const missingCalcs = masterFullList.filter(({ category, calcName }) => !data?.[category]?.[calcName]);
|
|
234
287
|
|
|
235
288
|
if (missingCalcs.length > 0) {
|
|
236
289
|
pass2Jobs.push({ date: doc.id, missing: missingCalcs });
|
|
237
290
|
}
|
|
238
291
|
});
|
|
239
|
-
|
|
292
|
+
|
|
293
|
+
logger.log('INFO', `[Orchestrator] Pass 2: Found ${pass2Jobs.length} incomplete dates to process.`);
|
|
240
294
|
const pass2Results = await processJobsInParallel(
|
|
241
295
|
pass2Jobs,
|
|
242
|
-
(date, missing) => {
|
|
296
|
+
async (date, missing) => { // Make task function async
|
|
243
297
|
const historicalMissing = missing.filter(c => HISTORICAL_CALC_NAMES.has(c.calcName));
|
|
244
298
|
const dailyMissing = missing.filter(c => !HISTORICAL_CALC_NAMES.has(c.calcName));
|
|
245
|
-
const
|
|
299
|
+
const results = []; // Collect results from both runs if applicable
|
|
300
|
+
|
|
246
301
|
if (historicalMissing.length > 0) {
|
|
247
|
-
|
|
302
|
+
logger.log('INFO', `[Pass 2] Running ${historicalMissing.length} historical calcs for ${date.toISOString().slice(0, 10)}.`);
|
|
303
|
+
// Pass config here
|
|
304
|
+
const histResult = await runUnifiedComputation(firestore, logger, date, historicalMissing, 'Pass 2 (Historical)', historicalCalculations, config);
|
|
305
|
+
results.push(histResult);
|
|
248
306
|
}
|
|
249
307
|
if (dailyMissing.length > 0) {
|
|
250
|
-
|
|
308
|
+
logger.log('INFO', `[Pass 2] Running ${dailyMissing.length} daily calcs for ${date.toISOString().slice(0, 10)}.`);
|
|
309
|
+
// Pass config here
|
|
310
|
+
const dailyResult = await runUnifiedComputation(firestore, logger, date, dailyMissing, 'Pass 2 (Daily)', dailyCalculations, config);
|
|
311
|
+
results.push(dailyResult);
|
|
251
312
|
}
|
|
252
|
-
|
|
313
|
+
// Return combined results or a summary
|
|
314
|
+
return results.length === 1 ? results[0] : { date: date.toISOString().slice(0,10), results };
|
|
253
315
|
},
|
|
254
|
-
'Pass 2'
|
|
316
|
+
'Pass 2',
|
|
317
|
+
config // Pass config to processJobsInParallel
|
|
255
318
|
);
|
|
319
|
+
// Process results (handle fulfilled/rejected)
|
|
256
320
|
summary.pass2_results = pass2Results.map((r, i) => r.status === 'fulfilled' ? r.value : { success: false, date: pass2Jobs[i].date, error: r.reason?.message });
|
|
257
321
|
|
|
322
|
+
|
|
258
323
|
// --- FINISH ---
|
|
259
|
-
|
|
324
|
+
logger.log('INFO', '[Orchestrator] Computation orchestration finished.');
|
|
260
325
|
return summary;
|
|
261
326
|
}
|
|
262
327
|
|
|
263
328
|
module.exports = {
|
|
264
329
|
runComputationOrchestrator,
|
|
265
|
-
//
|
|
330
|
+
// runUnifiedComputation is only used internally now
|
|
266
331
|
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Exports the computation system's main logic.
|
|
2
|
+
* @fileoverview Exports the computation system's main logic and handler creator.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const { runComputationOrchestrator } = require('./helpers/orchestration_helpers.js');
|
|
6
|
+
const { createComputationSystemHandler } = require('./handler_creator.js'); // <-- Import the new factory
|
|
6
7
|
|
|
7
8
|
module.exports = {
|
|
8
9
|
runComputationOrchestrator,
|
|
10
|
+
createComputationSystemHandler, // <-- Export the factory
|
|
9
11
|
};
|
|
@@ -4,25 +4,26 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { logger } = require("sharedsetup")(__filename);
|
|
7
|
-
// Import withRetry from the unified calculations package
|
|
8
7
|
const { withRetry } = require('aiden-shared-calculations-unified').utils;
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
const PART_REF_BATCH_SIZE = 50; // Number of document snapshots to load at once
|
|
9
|
+
// REMOVE Constants like COLLECTIONS_TO_QUERY, PART_REF_BATCH_SIZE
|
|
12
10
|
|
|
11
|
+
// Modify getPortfolioPartRefs to accept config
|
|
13
12
|
/**
|
|
14
13
|
* Gets a list of all portfolio "part" document references for a given date.
|
|
15
|
-
* This function is lightweight and only fetches references, not data.
|
|
16
14
|
*
|
|
17
15
|
* @param {Firestore} firestore A Firestore client instance.
|
|
18
16
|
* @param {string} dateString The date in YYYY-MM-DD format.
|
|
17
|
+
* @param {object} config The computation system configuration object.
|
|
19
18
|
* @returns {Promise<Firestore.DocumentReference[]>} An array of DocumentReferences.
|
|
20
19
|
*/
|
|
21
|
-
async function getPortfolioPartRefs(firestore, dateString) {
|
|
20
|
+
async function getPortfolioPartRefs(firestore, dateString, config) {
|
|
22
21
|
logger.log('INFO', `Getting portfolio part references for date: ${dateString}`);
|
|
23
22
|
const allPartRefs = [];
|
|
23
|
+
// Use config for collection names
|
|
24
|
+
const collectionsToQuery = [config.normalUserPortfolioCollection, config.speculatorPortfolioCollection];
|
|
24
25
|
|
|
25
|
-
for (const collectionName of
|
|
26
|
+
for (const collectionName of collectionsToQuery) {
|
|
26
27
|
const blockDocsQuery = firestore.collection(collectionName);
|
|
27
28
|
const blockDocRefs = await withRetry(
|
|
28
29
|
() => blockDocsQuery.listDocuments(),
|
|
@@ -30,12 +31,13 @@ async function getPortfolioPartRefs(firestore, dateString) {
|
|
|
30
31
|
);
|
|
31
32
|
|
|
32
33
|
if (blockDocRefs.length === 0) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
logger.log('WARN', `No block documents found in collection: ${collectionName}`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
36
37
|
|
|
37
38
|
for (const blockDocRef of blockDocRefs) {
|
|
38
|
-
|
|
39
|
+
// Use config for subcollection names
|
|
40
|
+
const partsCollectionRef = blockDocRef.collection(config.snapshotsSubcollection).doc(dateString).collection(config.partsSubcollection);
|
|
39
41
|
const partDocs = await withRetry(
|
|
40
42
|
() => partsCollectionRef.listDocuments(),
|
|
41
43
|
`listDocuments(${partsCollectionRef.path})`
|
|
@@ -48,30 +50,26 @@ async function getPortfolioPartRefs(firestore, dateString) {
|
|
|
48
50
|
return allPartRefs;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
// Modify loadDataByRefs to accept config
|
|
52
54
|
/**
|
|
53
55
|
* Loads data from an array of DocumentReferences using firestore.getAll().
|
|
54
|
-
* Merges all data into a single map keyed by user ID.
|
|
55
56
|
*
|
|
56
57
|
* @param {Firestore} firestore A Firestore client instance.
|
|
57
58
|
* @param {Firestore.DocumentReference[]} refs An array of DocumentReferences to load.
|
|
59
|
+
* @param {object} config The computation system configuration object.
|
|
58
60
|
* @returns {Promise<object>} A single map of { [userId]: portfolioData }.
|
|
59
61
|
*/
|
|
60
|
-
async function loadDataByRefs(firestore, refs) {
|
|
61
|
-
if (!refs || refs.length === 0) {
|
|
62
|
-
return {};
|
|
63
|
-
}
|
|
64
|
-
|
|
62
|
+
async function loadDataByRefs(firestore, refs, config) {
|
|
63
|
+
if (!refs || refs.length === 0) { return {}; }
|
|
65
64
|
const mergedPortfolios = {};
|
|
65
|
+
const batchSize = config.partRefBatchSize || 50; // Use config
|
|
66
66
|
|
|
67
|
-
for (let i = 0; i < refs.length; i +=
|
|
68
|
-
const batchRefs = refs.slice(i, i +
|
|
69
|
-
|
|
67
|
+
for (let i = 0; i < refs.length; i += batchSize) { // Use config batch size
|
|
68
|
+
const batchRefs = refs.slice(i, i + batchSize); // Use config batch size
|
|
70
69
|
const snapshots = await withRetry(
|
|
71
70
|
() => firestore.getAll(...batchRefs),
|
|
72
|
-
`getAll(batch ${Math.floor(i /
|
|
71
|
+
`getAll(batch ${Math.floor(i / batchSize)})` // Use config batch size
|
|
73
72
|
);
|
|
74
|
-
|
|
75
73
|
for (const doc of snapshots) {
|
|
76
74
|
if (doc.exists) {
|
|
77
75
|
const data = doc.data();
|
|
@@ -86,11 +84,19 @@ async function loadDataByRefs(firestore, refs) {
|
|
|
86
84
|
return mergedPortfolios;
|
|
87
85
|
}
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
// Modify loadFullDayMap to accept config
|
|
88
|
+
/**
|
|
89
|
+
* Loads all portfolio data for a day by streaming part references.
|
|
90
|
+
* @param {Firestore} firestore A Firestore client instance.
|
|
91
|
+
* @param {Firestore.DocumentReference[]} partRefs Array of part document references.
|
|
92
|
+
* @param {object} config The computation system configuration object.
|
|
93
|
+
* @returns {Promise<object>} A single map of { [userId]: portfolioData }.
|
|
94
|
+
*/
|
|
95
|
+
async function loadFullDayMap(firestore, partRefs, config) {
|
|
91
96
|
if (partRefs.length === 0) return {};
|
|
92
97
|
logger.log('TRACE', `Loading full day map from ${partRefs.length} references...`);
|
|
93
|
-
|
|
98
|
+
// Pass config here
|
|
99
|
+
const fullMap = await loadDataByRefs(firestore, partRefs, config);
|
|
94
100
|
logger.log('TRACE', `Full day map loaded with ${Object.keys(fullMap).length} users.`);
|
|
95
101
|
return fullMap;
|
|
96
102
|
}
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Helper functions for the Unified Computation System.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Categorizes calculations and provides utility functions like retry logic,
|
|
4
|
+
* batch committing, date generation, parallel processing, and finding earliest dates.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { FieldValue, FieldPath } = require('@google-cloud/firestore');
|
|
8
8
|
const { logger } = require("sharedsetup")(__filename);
|
|
9
|
+
// Import calculations and utils from the unified package
|
|
9
10
|
const { calculations, utils } = require('aiden-shared-calculations-unified');
|
|
11
|
+
const { withRetry } = utils; // Get withRetry from the package utils
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// --- Configuration ---
|
|
14
|
-
const BATCH_SIZE_LIMIT = 500;
|
|
15
|
-
const MAX_CONCURRENT_DATES = 3;
|
|
13
|
+
// REMOVE Constants like BATCH_SIZE_LIMIT, MAX_CONCURRENT_DATES
|
|
16
14
|
|
|
17
15
|
// --- Calculation Categorization ---
|
|
16
|
+
// This relies on the structure exported by 'aiden-shared-calculations-unified'
|
|
18
17
|
const HISTORICAL_CALC_NAMES = new Set([
|
|
19
18
|
'paper-vs-diamond-hands', 'smart-money-flow', 'profitability-migration',
|
|
20
19
|
'user-profitability-tracker', 'sector-rotation', 'crowd-conviction-score',
|
|
@@ -25,6 +24,7 @@ const HISTORICAL_CALC_NAMES = new Set([
|
|
|
25
24
|
const historicalCalculations = {};
|
|
26
25
|
const dailyCalculations = {};
|
|
27
26
|
|
|
27
|
+
// Assuming 'calculations' from the package has the same category structure
|
|
28
28
|
for (const category in calculations) {
|
|
29
29
|
for (const calcName in calculations[category]) {
|
|
30
30
|
if (HISTORICAL_CALC_NAMES.has(calcName)) {
|
|
@@ -37,22 +37,28 @@ for (const category in calculations) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
40
|
// --- UTILITY FUNCTIONS ---
|
|
42
41
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Commits Firestore batch writes in chunks based on config limit.
|
|
44
|
+
* @param {Firestore} firestore A Firestore client instance.
|
|
45
|
+
* @param {Array<object>} writes Array of { ref: DocumentReference, data: object }.
|
|
46
|
+
* @param {string} operationName Name for logging.
|
|
47
|
+
* @param {object} config The computation system configuration object.
|
|
48
|
+
*/
|
|
49
|
+
async function commitBatchInChunks(firestore, writes, operationName, config) {
|
|
50
|
+
const batchSizeLimit = config.batchSizeLimit || 450; // Use config with default
|
|
45
51
|
if (writes.length === 0) {
|
|
46
52
|
logger.log('WARN', `[${operationName}] No writes to commit.`);
|
|
47
53
|
return;
|
|
48
54
|
}
|
|
49
|
-
for (let i = 0; i < writes.length; i +=
|
|
55
|
+
for (let i = 0; i < writes.length; i += batchSizeLimit) { // Use config limit
|
|
50
56
|
const batch = firestore.batch();
|
|
51
|
-
const chunk = writes.slice(i, i +
|
|
57
|
+
const chunk = writes.slice(i, i + batchSizeLimit); // Use config limit
|
|
52
58
|
chunk.forEach(write => batch.set(write.ref, write.data, { merge: true }));
|
|
53
|
-
|
|
54
|
-
const chunkNum = (i /
|
|
55
|
-
const totalChunks = Math.ceil(writes.length /
|
|
59
|
+
|
|
60
|
+
const chunkNum = Math.floor(i / batchSizeLimit) + 1; // Use config limit
|
|
61
|
+
const totalChunks = Math.ceil(writes.length / batchSizeLimit); // Use config limit
|
|
56
62
|
await withRetry(
|
|
57
63
|
() => batch.commit(),
|
|
58
64
|
`${operationName} (Chunk ${chunkNum}/${totalChunks})`
|
|
@@ -67,22 +73,35 @@ async function commitBatchInChunks(firestore, writes, operationName) {
|
|
|
67
73
|
function getExpectedDateStrings(startDate, endDate) {
|
|
68
74
|
const dateStrings = [];
|
|
69
75
|
if (startDate <= endDate) {
|
|
70
|
-
|
|
76
|
+
// Ensure dates are treated as UTC to avoid timezone issues
|
|
77
|
+
const startUTC = new Date(Date.UTC(startDate.getUTCFullYear(), startDate.getUTCMonth(), startDate.getUTCDate()));
|
|
78
|
+
const endUTC = new Date(Date.UTC(endDate.getUTCFullYear(), endDate.getUTCMonth(), endDate.getUTCDate()));
|
|
79
|
+
for (let d = startUTC; d <= endUTC; d.setUTCDate(d.getUTCDate() + 1)) {
|
|
71
80
|
dateStrings.push(new Date(d).toISOString().slice(0, 10));
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
83
|
return dateStrings;
|
|
75
84
|
}
|
|
76
85
|
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Processes job objects in parallel batches based on config limit.
|
|
89
|
+
* @param {Array<object>} jobs Array of jobs, each with { date, missing }.
|
|
90
|
+
* @param {Function} taskFunction Async function to process a job (receives Date, missing array).
|
|
91
|
+
* @param {string} passName Name for logging.
|
|
92
|
+
* @param {object} config The computation system configuration object.
|
|
93
|
+
* @returns {Promise<Array<PromiseSettledResult>>} Array of promise settlement results.
|
|
94
|
+
*/
|
|
95
|
+
async function processJobsInParallel(jobs, taskFunction, passName, config) {
|
|
79
96
|
const results = [];
|
|
97
|
+
const maxConcurrentDates = config.maxConcurrentDates || 3; // Use config
|
|
80
98
|
if (jobs.length > 0) {
|
|
81
|
-
logger.log('INFO', `[${passName}] Processing ${jobs.length} jobs with ${
|
|
82
|
-
jobs.sort((a, b) => new Date(a.date) - new Date(b.date)); // Ensure chronological
|
|
83
|
-
for (let i = 0; i < jobs.length; i +=
|
|
84
|
-
const jobBatch = jobs.slice(i, i +
|
|
85
|
-
|
|
99
|
+
logger.log('INFO', `[${passName}] Processing ${jobs.length} jobs with ${maxConcurrentDates} parallel workers.`); // Use config
|
|
100
|
+
jobs.sort((a, b) => new Date(a.date) - new Date(b.date)); // Ensure chronological
|
|
101
|
+
for (let i = 0; i < jobs.length; i += maxConcurrentDates) { // Use config
|
|
102
|
+
const jobBatch = jobs.slice(i, i + maxConcurrentDates); // Use config
|
|
103
|
+
// taskFunction now receives config implicitly when called via runUnifiedComputation
|
|
104
|
+
const promises = jobBatch.map(job => taskFunction(new Date(job.date + 'T00:00:00Z'), job.missing)); // Ensure date is parsed correctly
|
|
86
105
|
results.push(...await Promise.allSettled(promises));
|
|
87
106
|
}
|
|
88
107
|
} else {
|
|
@@ -92,10 +111,13 @@ async function processJobsInParallel(jobs, taskFunction, passName) {
|
|
|
92
111
|
}
|
|
93
112
|
|
|
94
113
|
/**
|
|
95
|
-
* Finds the earliest date document
|
|
96
|
-
*
|
|
114
|
+
* Finds the earliest date document in a collection's snapshots.
|
|
115
|
+
* @param {Firestore} firestore A Firestore client instance.
|
|
116
|
+
* @param {string} collectionName The top-level portfolio collection name.
|
|
117
|
+
* @param {object} config The computation system configuration object.
|
|
118
|
+
* @returns {Promise<Date|null>} The earliest date found, or null.
|
|
97
119
|
*/
|
|
98
|
-
async function getFirstDateFromCollection(firestore, collectionName) {
|
|
120
|
+
async function getFirstDateFromCollection(firestore, collectionName, config) {
|
|
99
121
|
let earliestDate = null;
|
|
100
122
|
try {
|
|
101
123
|
const blockDocRefs = await withRetry(
|
|
@@ -103,29 +125,32 @@ async function getFirstDateFromCollection(firestore, collectionName) {
|
|
|
103
125
|
`GetBlocks(${collectionName})`
|
|
104
126
|
);
|
|
105
127
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
128
|
+
if (blockDocRefs.length === 0) {
|
|
129
|
+
logger.log('WARN', `No block documents found in collection: ${collectionName}`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const blockDocRef of blockDocRefs) {
|
|
134
|
+
// Use config subcollection name
|
|
135
|
+
const snapshotQuery = blockDocRef.collection(config.snapshotsSubcollection)
|
|
136
|
+
.where(FieldPath.documentId(), '>=', '2000-01-01') // Basic date format check
|
|
137
|
+
.orderBy(FieldPath.documentId(), 'asc')
|
|
138
|
+
.limit(1);
|
|
139
|
+
|
|
140
|
+
const snapshotSnap = await withRetry(
|
|
141
|
+
() => snapshotQuery.get(),
|
|
142
|
+
`GetEarliestSnapshot(${blockDocRef.path})`
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Validate the ID format is YYYY-MM-DD before parsing
|
|
146
|
+
if (!snapshotSnap.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshotSnap.docs[0].id)) {
|
|
147
|
+
const foundDate = new Date(snapshotSnap.docs[0].id + 'T00:00:00Z'); // Treat as UTC
|
|
148
|
+
if (!earliestDate || foundDate < earliestDate) {
|
|
149
|
+
earliestDate = foundDate;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
110
153
|
|
|
111
|
-
for (const blockDocRef of blockDocRefs) {
|
|
112
|
-
const snapshotQuery = blockDocRef.collection('snapshots')
|
|
113
|
-
.where(FieldPath.documentId(), '>=', '2000-01-01')
|
|
114
|
-
.orderBy(FieldPath.documentId(), 'asc')
|
|
115
|
-
.limit(1);
|
|
116
|
-
|
|
117
|
-
const snapshotSnap = await withRetry(
|
|
118
|
-
() => snapshotQuery.get(),
|
|
119
|
-
`GetEarliestSnapshot(${blockDocRef.path})`
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
if (!snapshotSnap.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshotSnap.docs[0].id)) {
|
|
123
|
-
const foundDate = new Date(snapshotSnap.docs[0].id);
|
|
124
|
-
if (!earliestDate || foundDate < earliestDate) {
|
|
125
|
-
earliestDate = foundDate;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
154
|
} catch (e) {
|
|
130
155
|
logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message });
|
|
131
156
|
}
|
|
@@ -133,32 +158,34 @@ async function getFirstDateFromCollection(firestore, collectionName) {
|
|
|
133
158
|
}
|
|
134
159
|
|
|
135
160
|
/**
|
|
136
|
-
* Determines the absolute earliest date from source portfolio data collections.
|
|
161
|
+
* Determines the absolute earliest date from source portfolio data collections using config.
|
|
162
|
+
* @param {Firestore} firestore A Firestore client instance.
|
|
163
|
+
* @param {object} config The computation system configuration object.
|
|
164
|
+
* @returns {Promise<Date>} The earliest date found or a default fallback date.
|
|
137
165
|
*/
|
|
138
|
-
async function getFirstDateFromSourceData(firestore) {
|
|
166
|
+
async function getFirstDateFromSourceData(firestore, config) {
|
|
139
167
|
logger.log('INFO', 'Querying for the earliest date from source portfolio data...');
|
|
140
|
-
|
|
141
|
-
const investorDate = await getFirstDateFromCollection(firestore,
|
|
142
|
-
const speculatorDate = await getFirstDateFromCollection(firestore,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return d;
|
|
168
|
+
// Use config for collection names
|
|
169
|
+
const investorDate = await getFirstDateFromCollection(firestore, config.normalUserPortfolioCollection, config);
|
|
170
|
+
const speculatorDate = await getFirstDateFromCollection(firestore, config.speculatorPortfolioCollection, config);
|
|
171
|
+
|
|
172
|
+
let earliestDate;
|
|
173
|
+
// Determine the earlier of the two dates found
|
|
174
|
+
if (investorDate && speculatorDate) {
|
|
175
|
+
earliestDate = investorDate < speculatorDate ? investorDate : speculatorDate;
|
|
176
|
+
} else {
|
|
177
|
+
earliestDate = investorDate || speculatorDate; // Use whichever one was found
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (earliestDate) {
|
|
181
|
+
logger.log('INFO', `Found earliest source data date: ${earliestDate.toISOString().slice(0, 10)}`);
|
|
182
|
+
return earliestDate;
|
|
183
|
+
} else {
|
|
184
|
+
// Fallback using config.earliestComputationDate or a hardcoded default
|
|
185
|
+
const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
|
|
186
|
+
logger.log('WARN', `No source data found. Defaulting first date to: ${fallbackDate.toISOString().slice(0, 10)}`);
|
|
187
|
+
return fallbackDate;
|
|
188
|
+
}
|
|
162
189
|
}
|
|
163
190
|
|
|
164
191
|
module.exports = {
|
|
@@ -10,7 +10,7 @@ const { dispatchTasksInBatches } = require('./helpers/dispatch_helpers');
|
|
|
10
10
|
* @param {object} config - Configuration object loaded from the calling function's context.
|
|
11
11
|
* @returns {Function} The Cloud Function handler.
|
|
12
12
|
*/
|
|
13
|
-
function createDispatcherHandler(
|
|
13
|
+
function createDispatcherHandler(config, pubsubClient) {
|
|
14
14
|
return async (message, context) => {
|
|
15
15
|
try {
|
|
16
16
|
// 1. Decode Message
|
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { FieldPath } = require('@google-cloud/firestore');
|
|
7
|
-
const MAX_DATE_RANGE = 100;
|
|
7
|
+
// REMOVE: const MAX_DATE_RANGE = 100;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Validates request parameters.
|
|
10
|
+
* Validates request parameters using config.
|
|
11
11
|
* @param {object} query - The request query object.
|
|
12
|
+
* @param {object} config - The Generic API V2 configuration object.
|
|
12
13
|
* @returns {string|null} An error message if validation fails, otherwise null.
|
|
13
14
|
*/
|
|
14
|
-
const validateRequest = (query) => {
|
|
15
|
+
const validateRequest = (query, config) => {
|
|
15
16
|
if (!query.computations) return "Missing 'computations' parameter.";
|
|
16
17
|
if (!query.startDate || !/^\d{4}-\d{2}-\d{2}$/.test(query.startDate)) return "Missing or invalid 'startDate'.";
|
|
17
18
|
if (!query.endDate || !/^\d{4}-\d{2}-\d{2}$/.test(query.endDate)) return "Missing or invalid 'endDate'.";
|
|
@@ -20,9 +21,10 @@ const validateRequest = (query) => {
|
|
|
20
21
|
const end = new Date(query.endDate);
|
|
21
22
|
if (end < start) return "'endDate' must be after 'startDate'.";
|
|
22
23
|
|
|
24
|
+
const maxDateRange = config.maxDateRange || 100; // Use config value with default
|
|
23
25
|
const diffTime = Math.abs(end - start);
|
|
24
26
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
|
25
|
-
if (diffDays >
|
|
27
|
+
if (diffDays > maxDateRange) return `Date range cannot exceed ${maxDateRange} days.`;
|
|
26
28
|
|
|
27
29
|
return null;
|
|
28
30
|
};
|
|
@@ -38,7 +40,7 @@ const buildCalculationMap = (unifiedCalculations) => {
|
|
|
38
40
|
for (const calcName in unifiedCalculations[category]) {
|
|
39
41
|
calcMap[calcName] = {
|
|
40
42
|
category: category,
|
|
41
|
-
|
|
43
|
+
// Path construction moved to fetchUnifiedData where config is available
|
|
42
44
|
};
|
|
43
45
|
}
|
|
44
46
|
}
|
|
@@ -55,7 +57,7 @@ const getDateStringsInRange = (startDate, endDate) => {
|
|
|
55
57
|
const dates = [];
|
|
56
58
|
const current = new Date(startDate + 'T00:00:00Z');
|
|
57
59
|
const end = new Date(endDate + 'T00:00:00Z');
|
|
58
|
-
|
|
60
|
+
|
|
59
61
|
while (current <= end) {
|
|
60
62
|
dates.push(current.toISOString().slice(0, 10));
|
|
61
63
|
current.setUTCDate(current.getUTCDate() + 1);
|
|
@@ -64,16 +66,22 @@ const getDateStringsInRange = (startDate, endDate) => {
|
|
|
64
66
|
};
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
|
-
* Fetches all requested computation data from Firestore for the given date range.
|
|
69
|
+
* Fetches all requested computation data from Firestore for the given date range using config.
|
|
70
|
+
* @param {object} config - The Generic API V2 configuration object.
|
|
68
71
|
* @param {Firestore} firestore - A Firestore client instance.
|
|
69
72
|
* @param {Logger} logger - A logger instance.
|
|
70
73
|
* @param {string[]} calcKeys - Array of computation keys to fetch.
|
|
71
74
|
* @param {string[]} dateStrings - Array of dates to fetch for.
|
|
72
|
-
* @param {Object} calcMap - The pre-built calculation lookup map.
|
|
75
|
+
* @param {Object} calcMap - The pre-built calculation lookup map (only category needed).
|
|
73
76
|
* @returns {Promise<Object>} A nested object of [date][computationKey] = data.
|
|
74
77
|
*/
|
|
75
|
-
const fetchUnifiedData = async (firestore, logger, calcKeys, dateStrings, calcMap) => {
|
|
78
|
+
const fetchUnifiedData = async (config, firestore, logger, calcKeys, dateStrings, calcMap) => {
|
|
76
79
|
const response = {};
|
|
80
|
+
// Use collection/subcollection names from config
|
|
81
|
+
const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
|
|
82
|
+
const resultsSub = config.resultsSubcollection || 'results';
|
|
83
|
+
const compsSub = config.computationsSubcollection || 'computations';
|
|
84
|
+
|
|
77
85
|
try {
|
|
78
86
|
for (const date of dateStrings) {
|
|
79
87
|
response[date] = {};
|
|
@@ -83,14 +91,15 @@ const fetchUnifiedData = async (firestore, logger, calcKeys, dateStrings, calcMa
|
|
|
83
91
|
for (const key of calcKeys) {
|
|
84
92
|
const pathInfo = calcMap[key];
|
|
85
93
|
if (pathInfo) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.collection(
|
|
89
|
-
|
|
94
|
+
// Construct path using config values
|
|
95
|
+
const docRef = firestore.collection(insightsCollection).doc(date)
|
|
96
|
+
.collection(resultsSub).doc(pathInfo.category)
|
|
97
|
+
.collection(compsSub).doc(key);
|
|
98
|
+
|
|
90
99
|
docRefs.push(docRef);
|
|
91
100
|
keyPaths.push(key);
|
|
92
101
|
} else {
|
|
93
|
-
logger.log('WARN', `[${date}] No path found for computation key: ${key}`);
|
|
102
|
+
logger.log('WARN', `[${date}] No path info found for computation key: ${key}`);
|
|
94
103
|
}
|
|
95
104
|
}
|
|
96
105
|
|
|
@@ -114,17 +123,19 @@ const fetchUnifiedData = async (firestore, logger, calcKeys, dateStrings, calcMa
|
|
|
114
123
|
};
|
|
115
124
|
|
|
116
125
|
/**
|
|
117
|
-
* Creates the main Express request handler for the API.
|
|
126
|
+
* Creates the main Express request handler for the API, injecting config.
|
|
127
|
+
* @param {object} config - The Generic API V2 configuration object.
|
|
118
128
|
* @param {Firestore} firestore - A Firestore client instance.
|
|
119
129
|
* @param {Logger} logger - A logger instance.
|
|
120
130
|
* @param {Object} calcMap - The pre-built calculation lookup map.
|
|
121
131
|
* @returns {Function} An async Express request handler.
|
|
122
132
|
*/
|
|
123
|
-
const createApiHandler = (firestore, logger, calcMap) => {
|
|
133
|
+
const createApiHandler = (config, firestore, logger, calcMap) => {
|
|
124
134
|
return async (req, res) => {
|
|
125
|
-
|
|
135
|
+
// Pass config to validator
|
|
136
|
+
const validationError = validateRequest(req.query, config);
|
|
126
137
|
if (validationError) {
|
|
127
|
-
logger.log('WARN', 'Bad Request', { error: validationError });
|
|
138
|
+
logger.log('WARN', 'API Bad Request', { error: validationError, query: req.query });
|
|
128
139
|
return res.status(400).send({ status: 'error', message: validationError });
|
|
129
140
|
}
|
|
130
141
|
|
|
@@ -132,7 +143,8 @@ const createApiHandler = (firestore, logger, calcMap) => {
|
|
|
132
143
|
const computationKeys = req.query.computations.split(',');
|
|
133
144
|
const dateStrings = getDateStringsInRange(req.query.startDate, req.query.endDate);
|
|
134
145
|
|
|
135
|
-
|
|
146
|
+
// Pass config to data fetcher
|
|
147
|
+
const data = await fetchUnifiedData(config, firestore, logger, computationKeys, dateStrings, calcMap);
|
|
136
148
|
|
|
137
149
|
res.status(200).send({
|
|
138
150
|
status: 'success',
|
|
@@ -152,5 +164,6 @@ const createApiHandler = (firestore, logger, calcMap) => {
|
|
|
152
164
|
|
|
153
165
|
module.exports = {
|
|
154
166
|
buildCalculationMap,
|
|
155
|
-
createApiHandler,
|
|
167
|
+
createApiHandler, // Export the factory for the handler
|
|
168
|
+
// Internal helpers like validateRequest, getDateStringsInRange, fetchUnifiedData are not exported
|
|
156
169
|
};
|
|
@@ -9,24 +9,26 @@ const { buildCalculationMap, createApiHandler } = require('./helpers/api_helpers
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Creates and configures the Express app for the Generic API.
|
|
12
|
-
* @param {
|
|
13
|
-
* @param {
|
|
14
|
-
* @param {Object} unifiedCalculations The calculations manifest.
|
|
12
|
+
* @param {object} config - The Generic API V2 configuration object.
|
|
13
|
+
* @param {object} dependencies - Shared dependencies { firestore, logger }.
|
|
14
|
+
* @param {Object} unifiedCalculations - The calculations manifest.
|
|
15
15
|
* @returns {express.Application} The configured Express app.
|
|
16
16
|
*/
|
|
17
|
-
function createApiApp(
|
|
17
|
+
function createApiApp(config, dependencies, unifiedCalculations) {
|
|
18
18
|
const app = express();
|
|
19
|
-
|
|
19
|
+
const { firestore, logger } = dependencies; // Extract dependencies
|
|
20
|
+
|
|
20
21
|
// --- Pre-compute Calculation Map ---
|
|
22
|
+
// This doesn't need config directly
|
|
21
23
|
const calcMap = buildCalculationMap(unifiedCalculations);
|
|
22
|
-
|
|
24
|
+
|
|
23
25
|
// --- Middleware ---
|
|
24
26
|
app.use(cors({ origin: true }));
|
|
25
27
|
app.use(express.json());
|
|
26
28
|
|
|
27
29
|
// --- Main API Endpoint ---
|
|
28
|
-
// The handler is created with its dependencies injected
|
|
29
|
-
app.get('/', createApiHandler(firestore, logger, calcMap));
|
|
30
|
+
// The handler is created with its dependencies and config injected
|
|
31
|
+
app.get('/', createApiHandler(config, firestore, logger, calcMap));
|
|
30
32
|
|
|
31
33
|
// --- Health Check Endpoint ---
|
|
32
34
|
app.get('/health', (req, res) => {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Factory function to create the Task Engine handler.
|
|
3
|
+
*/
|
|
4
|
+
const { Firestore } = require('@google-cloud/firestore'); // Needed if db is not passed in
|
|
5
|
+
const { PubSub } = require('@google-cloud/pubsub'); // Needed if pubsub is not passed in
|
|
6
|
+
const { logger: defaultLogger } = require("sharedsetup")(__filename); // Use default logger if none provided
|
|
7
|
+
const { core } = require('../../'); // Access core utilities like managers
|
|
8
|
+
const TaskEngineHelpers = require('./helpers');
|
|
9
|
+
const TaskEngineUtils = require('./utils');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates the Task Engine Cloud Function handler.
|
|
13
|
+
* @param {object} config - The Task Engine configuration object (e.g., cfg.taskEngine).
|
|
14
|
+
* @param {object} [dependencies] - Optional dependencies. If not provided, initializes defaults.
|
|
15
|
+
* @param {Firestore} [dependencies.firestore] - Firestore client instance.
|
|
16
|
+
* @param {PubSub} [dependencies.pubsub] - PubSub client instance.
|
|
17
|
+
* @param {object} [dependencies.logger] - Logger instance.
|
|
18
|
+
* @returns {Function} The async Cloud Function handler for Pub/Sub messages.
|
|
19
|
+
*/
|
|
20
|
+
function createTaskEngineHandler(config, dependencies = {}) {
|
|
21
|
+
// Initialize clients using provided dependencies or defaults
|
|
22
|
+
const firestore = dependencies.firestore || new Firestore();
|
|
23
|
+
const pubsub = dependencies.pubsub || new PubSub();
|
|
24
|
+
const logger = dependencies.logger || defaultLogger; // Use provided or default logger
|
|
25
|
+
|
|
26
|
+
// Initialize managers and batch manager within this scope
|
|
27
|
+
const headerManager = new core.utils.IntelligentHeaderManager(firestore, logger, config);
|
|
28
|
+
const proxyManager = new core.utils.IntelligentProxyManager(firestore, logger, config);
|
|
29
|
+
const batchManager = new TaskEngineUtils.FirestoreBatchManager(firestore, headerManager, config); // Pass headerManager
|
|
30
|
+
|
|
31
|
+
const CLIENTS = {
|
|
32
|
+
pubsub,
|
|
33
|
+
nativeFirestore: firestore, // Pass the initialized Firestore instance
|
|
34
|
+
headerManager,
|
|
35
|
+
proxyManager,
|
|
36
|
+
batchManager
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Return the actual handler function
|
|
40
|
+
return async (message) => {
|
|
41
|
+
// Basic check for message structure (Cloud Functions Pub/Sub trigger)
|
|
42
|
+
if (!message || !message.data) {
|
|
43
|
+
logger.log('ERROR', '[TaskEngine Module] Received invalid message structure.', { message });
|
|
44
|
+
// Acknowledge Pub/Sub by not throwing, but log error
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let task;
|
|
49
|
+
try {
|
|
50
|
+
// Decode the message
|
|
51
|
+
task = JSON.parse(Buffer.from(message.data, 'base64').toString('utf-8'));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
logger.log('ERROR', '[TaskEngine Module] Failed to parse Pub/Sub message data.', { error: e.message, data: message.data });
|
|
54
|
+
// Acknowledge Pub/Sub by not throwing
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Generate a unique ID for logging this specific task execution
|
|
59
|
+
const taskId = `${task.type || 'unknown'}-${task.userType || 'unknown'}-${task.userId || task.cids?.[0] || 'batch'}-${Date.now()}`;
|
|
60
|
+
logger.log('INFO', `[TaskEngine/${taskId}] Received.`);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Optional: Random delay
|
|
64
|
+
await new Promise(resolve => setTimeout(resolve, Math.random() * 500 + 250));
|
|
65
|
+
|
|
66
|
+
// Determine the correct handler function based on task type
|
|
67
|
+
const handlerFunction = {
|
|
68
|
+
discover: TaskEngineHelpers.handleDiscover,
|
|
69
|
+
verify: TaskEngineHelpers.handleVerify,
|
|
70
|
+
update: TaskEngineHelpers.handleUpdate
|
|
71
|
+
}[task.type];
|
|
72
|
+
|
|
73
|
+
// Execute the handler or log an error if the type is unknown
|
|
74
|
+
if (handlerFunction) {
|
|
75
|
+
await handlerFunction(task, taskId, CLIENTS, config); // Pass CLIENTS and config
|
|
76
|
+
logger.log('SUCCESS', `[TaskEngine/${taskId}] Done.`);
|
|
77
|
+
} else {
|
|
78
|
+
logger.log('ERROR', `[TaskEngine/${taskId}] Unknown task type received: ${task.type}`);
|
|
79
|
+
// Acknowledge Pub/Sub by not throwing
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
// Log the error but crucially *do not re-throw* if you want Pub/Sub to ack the message
|
|
83
|
+
// Only re-throw if you want Pub/Sub to attempt redelivery based on its settings.
|
|
84
|
+
// Your existing logic logs but then re-throws - decide if that's the desired behavior.
|
|
85
|
+
logger.log('ERROR', `[TaskEngine/${taskId}] Failed.`, { errorMessage: error.message, errorStack: error.stack });
|
|
86
|
+
// throw error; // Optional: Uncomment if you want Pub/Sub to retry
|
|
87
|
+
} finally {
|
|
88
|
+
// Ensure batches are flushed eventually, though the batch manager handles timed flushes.
|
|
89
|
+
// A final flush here might be redundant if the instance stays alive, but could be useful
|
|
90
|
+
// if the instance might terminate immediately after processing.
|
|
91
|
+
try {
|
|
92
|
+
// Consider if flushing on every single message is desired vs relying on timed flush
|
|
93
|
+
// await batchManager.flushBatches();
|
|
94
|
+
} catch (flushError) {
|
|
95
|
+
logger.log('ERROR', `[TaskEngine/${taskId}] Error during final flush attempt.`, { error: flushError.message });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
createTaskEngineHandler,
|
|
103
|
+
};
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
const helpers = require('./helpers');
|
|
6
6
|
const utils = require('./utils');
|
|
7
|
+
const { createTaskEngineHandler } = require('./handler_creator'); // <-- Import the new factory
|
|
7
8
|
|
|
8
9
|
module.exports = {
|
|
9
10
|
helpers,
|
|
10
11
|
utils,
|
|
12
|
+
createTaskEngineHandler, // <-- Export the factory
|
|
11
13
|
};
|