bulltrackers-module 1.0.732 → 1.0.733
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/orchestrator/index.js +19 -17
- package/index.js +8 -29
- package/package.json +1 -1
- package/functions/computation-system/WorkflowOrchestrator.js +0 -213
- package/functions/computation-system/config/monitoring_config.js +0 -31
- package/functions/computation-system/config/validation_overrides.js +0 -10
- package/functions/computation-system/context/ContextFactory.js +0 -143
- package/functions/computation-system/context/ManifestBuilder.js +0 -379
- package/functions/computation-system/data/AvailabilityChecker.js +0 -236
- package/functions/computation-system/data/CachedDataLoader.js +0 -325
- package/functions/computation-system/data/DependencyFetcher.js +0 -455
- package/functions/computation-system/executors/MetaExecutor.js +0 -279
- package/functions/computation-system/executors/PriceBatchExecutor.js +0 -108
- package/functions/computation-system/executors/StandardExecutor.js +0 -465
- package/functions/computation-system/helpers/computation_dispatcher.js +0 -750
- package/functions/computation-system/helpers/computation_worker.js +0 -375
- package/functions/computation-system/helpers/monitor.js +0 -64
- package/functions/computation-system/helpers/on_demand_helpers.js +0 -154
- package/functions/computation-system/layers/extractors.js +0 -1097
- package/functions/computation-system/layers/index.js +0 -40
- package/functions/computation-system/layers/mathematics.js +0 -522
- package/functions/computation-system/layers/profiling.js +0 -537
- package/functions/computation-system/layers/validators.js +0 -170
- package/functions/computation-system/legacy/AvailabilityCheckerOld.js +0 -388
- package/functions/computation-system/legacy/CachedDataLoaderOld.js +0 -357
- package/functions/computation-system/legacy/DependencyFetcherOld.js +0 -478
- package/functions/computation-system/legacy/MetaExecutorold.js +0 -364
- package/functions/computation-system/legacy/StandardExecutorold.js +0 -476
- package/functions/computation-system/legacy/computation_dispatcherold.js +0 -944
- package/functions/computation-system/logger/logger.js +0 -297
- package/functions/computation-system/persistence/ContractValidator.js +0 -81
- package/functions/computation-system/persistence/FirestoreUtils.js +0 -56
- package/functions/computation-system/persistence/ResultCommitter.js +0 -283
- package/functions/computation-system/persistence/ResultsValidator.js +0 -130
- package/functions/computation-system/persistence/RunRecorder.js +0 -142
- package/functions/computation-system/persistence/StatusRepository.js +0 -52
- package/functions/computation-system/reporter_epoch.js +0 -6
- package/functions/computation-system/scripts/UpdateContracts.js +0 -128
- package/functions/computation-system/services/SnapshotService.js +0 -148
- package/functions/computation-system/simulation/Fabricator.js +0 -285
- package/functions/computation-system/simulation/SeededRandom.js +0 -41
- package/functions/computation-system/simulation/SimRunner.js +0 -51
- package/functions/computation-system/system_epoch.js +0 -2
- package/functions/computation-system/tools/BuildReporter.js +0 -531
- package/functions/computation-system/tools/ContractDiscoverer.js +0 -144
- package/functions/computation-system/tools/DeploymentValidator.js +0 -536
- package/functions/computation-system/tools/FinalSweepReporter.js +0 -322
- package/functions/computation-system/topology/HashManager.js +0 -55
- package/functions/computation-system/topology/ManifestLoader.js +0 -47
- package/functions/computation-system/utils/data_loader.js +0 -675
- package/functions/computation-system/utils/schema_capture.js +0 -121
- package/functions/computation-system/utils/utils.js +0 -188
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FILENAME: computation-system/data/DependencyFetcher.js
|
|
3
|
-
* @fileoverview Fetches dependencies for computations.
|
|
4
|
-
* REFACTORED: Unified fetch logic, streamlined decompression/sharding/GCS.
|
|
5
|
-
* UPDATED: Properly checks isPage/isAlert flags to determine BigQuery vs Firestore routing.
|
|
6
|
-
*/
|
|
7
|
-
const { normalizeName } = require('../utils/utils');
|
|
8
|
-
const zlib = require('zlib');
|
|
9
|
-
const { Storage } = require('@google-cloud/storage');
|
|
10
|
-
|
|
11
|
-
const storage = new Storage(); // Singleton Client
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Helper to check if a computation is isPage or isAlert by looking up manifest
|
|
15
|
-
* @param {string} normalizedName - Normalized computation name
|
|
16
|
-
* @param {object} config - Config object that may contain manifest info
|
|
17
|
-
* @param {string} category - Computation category (fallback for alerts)
|
|
18
|
-
* @returns {Promise<{isPage: boolean, isAlert: boolean}>}
|
|
19
|
-
*/
|
|
20
|
-
async function checkComputationFlags(normalizedName, config, category) {
|
|
21
|
-
let isPage = false;
|
|
22
|
-
let isAlert = false;
|
|
23
|
-
|
|
24
|
-
// Try to get manifest from config if available
|
|
25
|
-
if (config.getCalculations && typeof config.getCalculations === 'function') {
|
|
26
|
-
try {
|
|
27
|
-
const calculations = config.getCalculations(config);
|
|
28
|
-
const manifest = calculations.find(c => normalizeName(c.name) === normalizedName);
|
|
29
|
-
if (manifest) {
|
|
30
|
-
isPage = manifest.isPage === true;
|
|
31
|
-
isAlert = manifest.isAlertComputation === true;
|
|
32
|
-
return { isPage, isAlert };
|
|
33
|
-
}
|
|
34
|
-
} catch (e) {
|
|
35
|
-
// Fall through to category-based detection
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Fallback: infer from category (alerts category = alert computation)
|
|
40
|
-
// Note: isPage cannot be inferred from category alone, so defaults to false
|
|
41
|
-
isAlert = category === 'alerts';
|
|
42
|
-
|
|
43
|
-
return { isPage, isAlert };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// =============================================================================
|
|
47
|
-
// HELPERS
|
|
48
|
-
// =============================================================================
|
|
49
|
-
|
|
50
|
-
/** Checks if data is effectively empty (null or only metadata keys) */
|
|
51
|
-
function isDataEmpty(data) {
|
|
52
|
-
if (data == null) return true;
|
|
53
|
-
if (Array.isArray(data)) return data.length === 0;
|
|
54
|
-
if (typeof data === 'object') {
|
|
55
|
-
// Return true if NO keys exist that DON'T start with '_'
|
|
56
|
-
return !Object.keys(data).some(k => !k.startsWith('_'));
|
|
57
|
-
}
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Robust decompression helper (Buffer, Base64, or Firestore Binary) */
|
|
62
|
-
function tryDecompress(payload) {
|
|
63
|
-
if (!payload) return null;
|
|
64
|
-
try {
|
|
65
|
-
const buffer = (payload instanceof Buffer) ? payload :
|
|
66
|
-
(payload._byteString ? Buffer.from(payload._byteString, 'base64') :
|
|
67
|
-
(payload.toDate ? payload.toDate() : Buffer.from(payload)));
|
|
68
|
-
|
|
69
|
-
return JSON.parse(zlib.gunzipSync(buffer).toString('utf8'));
|
|
70
|
-
} catch (e) {
|
|
71
|
-
throw new Error(`Decompression failed: ${e.message}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// =============================================================================
|
|
76
|
-
// CORE FETCH LOGIC
|
|
77
|
-
// =============================================================================
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Fetches, decompresses, and reassembles (if sharded or on GCS) a single result document.
|
|
81
|
-
* NEW: For non-alert, non-page computations, tries BigQuery first (cheaper, no sharding/compression).
|
|
82
|
-
* @param {object} db - Firestore database instance
|
|
83
|
-
* @param {object} config - Configuration object (may contain logger, manifestLookup, etc.)
|
|
84
|
-
* @param {string} dateStr - Date string in YYYY-MM-DD format
|
|
85
|
-
* @param {string} name - Computation name (normalized)
|
|
86
|
-
* @param {string} category - Computation category
|
|
87
|
-
* @returns {Promise<object|null>} Result data or null if not found
|
|
88
|
-
*/
|
|
89
|
-
async function fetchSingleResult(db, config, dateStr, name, category) {
|
|
90
|
-
const { resultsCollection = 'computation_results', resultsSubcollection = 'results', computationsSubcollection = 'computations' } = config;
|
|
91
|
-
const log = config.logger || console;
|
|
92
|
-
|
|
93
|
-
// Check if this is an alert or page computation by looking up manifest
|
|
94
|
-
const { isPage: isPageComputation, isAlert: isAlertComputation } = await checkComputationFlags(name, config, category);
|
|
95
|
-
|
|
96
|
-
// Try BigQuery first for non-alert, non-page computations (reduces Firestore reads)
|
|
97
|
-
// isPage and isAlert computations are always written to Firestore (in addition to BigQuery),
|
|
98
|
-
// so we should check Firestore for them, but can also try BigQuery as a fallback
|
|
99
|
-
if (!isAlertComputation && !isPageComputation && process.env.BIGQUERY_ENABLED !== 'false') {
|
|
100
|
-
try {
|
|
101
|
-
const { queryComputationResult } = require('../../core/utils/bigquery_utils');
|
|
102
|
-
const bigqueryResult = await queryComputationResult(name, category, dateStr, log);
|
|
103
|
-
|
|
104
|
-
if (bigqueryResult && !isDataEmpty(bigqueryResult)) {
|
|
105
|
-
log.log('INFO', `[DependencyFetcher] ✅ Using BigQuery for ${name} (${dateStr}, ${category})`);
|
|
106
|
-
return bigqueryResult;
|
|
107
|
-
}
|
|
108
|
-
} catch (bqError) {
|
|
109
|
-
log.log('WARN', `[DependencyFetcher] BigQuery fetch failed for ${name}, falling back to Firestore: ${bqError.message}`);
|
|
110
|
-
// Fall through to Firestore
|
|
111
|
-
}
|
|
112
|
-
} else if (isAlertComputation || isPageComputation) {
|
|
113
|
-
log.log('INFO', `[DependencyFetcher] 📄 Using Firestore for ${isAlertComputation ? 'alert' : 'page'} computation ${name} (${dateStr})`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Fallback to Firestore (for alerts, pages, or if BigQuery fails)
|
|
117
|
-
const docRef = db.collection(resultsCollection).doc(dateStr)
|
|
118
|
-
.collection(resultsSubcollection).doc(category)
|
|
119
|
-
.collection(computationsSubcollection).doc(name);
|
|
120
|
-
|
|
121
|
-
const snap = await docRef.get();
|
|
122
|
-
if (!snap.exists) return null;
|
|
123
|
-
|
|
124
|
-
let data = snap.data();
|
|
125
|
-
|
|
126
|
-
// -------------------------------------------------------------------------
|
|
127
|
-
// 1. GCS POINTER HANDLER (New)
|
|
128
|
-
// -------------------------------------------------------------------------
|
|
129
|
-
if (data.gcsUri || (data._gcs && data.gcsBucket && data.gcsPath)) {
|
|
130
|
-
try {
|
|
131
|
-
const bucketName = data.gcsBucket || data.gcsUri.split('/')[2];
|
|
132
|
-
const fileName = data.gcsPath || data.gcsUri.split('/').slice(3).join('/');
|
|
133
|
-
|
|
134
|
-
// Stream download is memory efficient for large files
|
|
135
|
-
const [fileContent] = await storage.bucket(bucketName).file(fileName).download();
|
|
136
|
-
|
|
137
|
-
// Assume Gzip (as writer does it), if fails try plain
|
|
138
|
-
try {
|
|
139
|
-
return JSON.parse(zlib.gunzipSync(fileContent).toString('utf8'));
|
|
140
|
-
} catch (gzipErr) {
|
|
141
|
-
// Fallback for uncompressed GCS files
|
|
142
|
-
return JSON.parse(fileContent.toString('utf8'));
|
|
143
|
-
}
|
|
144
|
-
} catch (e) {
|
|
145
|
-
log.log('ERROR', `[DependencyFetcher] ❌ GCS Fetch Failed for ${name}: ${e.message}`);
|
|
146
|
-
// Depending on strictness, we might return null here or allow it to fail hard.
|
|
147
|
-
// Returning null allows 'isDataEmpty' to catch it as "MISSING"
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// -------------------------------------------------------------------------
|
|
153
|
-
// 2. FIRESTORE COMPRESSED HANDLER
|
|
154
|
-
// -------------------------------------------------------------------------
|
|
155
|
-
if (data._compressed && data.payload) {
|
|
156
|
-
try {
|
|
157
|
-
const realData = tryDecompress(data.payload);
|
|
158
|
-
data = { ...data, ...realData }; // Merge payload into base
|
|
159
|
-
delete data.payload;
|
|
160
|
-
} catch (e) {
|
|
161
|
-
log.log('ERROR', `[DependencyFetcher] ❌ ${e.message} at ${docRef.path}`);
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// -------------------------------------------------------------------------
|
|
167
|
-
// 3. FIRESTORE SHARDED HANDLER
|
|
168
|
-
// -------------------------------------------------------------------------
|
|
169
|
-
if (data._sharded) {
|
|
170
|
-
const shardSnaps = await docRef.collection('_shards').get();
|
|
171
|
-
if (shardSnaps.empty) {
|
|
172
|
-
log.log('ERROR', `[DependencyFetcher] ❌ Sharded doc has no shards: ${docRef.path}`);
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Initialize merged data with any non-metadata fields from the pointer doc
|
|
177
|
-
const merged = {};
|
|
178
|
-
Object.entries(data).forEach(([k, v]) => { if (!k.startsWith('_')) merged[k] = v; });
|
|
179
|
-
|
|
180
|
-
for (const shard of shardSnaps.docs) {
|
|
181
|
-
let sData = shard.data();
|
|
182
|
-
|
|
183
|
-
// Decompress Shard if needed
|
|
184
|
-
if (sData._compressed && sData.payload) {
|
|
185
|
-
try {
|
|
186
|
-
const decomp = tryDecompress(sData.payload);
|
|
187
|
-
sData = (typeof decomp === 'string') ? JSON.parse(decomp) : decomp;
|
|
188
|
-
} catch (e) {
|
|
189
|
-
log.log('ERROR', `[DependencyFetcher] ❌ Shard decompression failed (${shard.id}): ${e.message}`);
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Merge Shard Content
|
|
195
|
-
Object.entries(sData).forEach(([k, v]) => {
|
|
196
|
-
if (!k.startsWith('_')) merged[k] = v;
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Return null if result is empty after merging
|
|
201
|
-
if (Object.keys(merged).length === 0) return null;
|
|
202
|
-
return merged;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// -------------------------------------------------------------------------
|
|
206
|
-
// 4. STANDARD DOCUMENT HANDLER
|
|
207
|
-
// -------------------------------------------------------------------------
|
|
208
|
-
const clean = {};
|
|
209
|
-
let hasContent = false;
|
|
210
|
-
Object.entries(data).forEach(([k, v]) => {
|
|
211
|
-
if (!k.startsWith('_')) { clean[k] = v; hasContent = true; }
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
return hasContent ? clean : null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// =============================================================================
|
|
218
|
-
// PUBLIC METHODS
|
|
219
|
-
// =============================================================================
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Fetches dependencies for a specific date (Standard pass).
|
|
223
|
-
*/
|
|
224
|
-
async function fetchDependencies(date, calcs, config, deps, manifestLookup = {}, allowMissing = false) {
|
|
225
|
-
const { db, logger } = deps;
|
|
226
|
-
const dStr = date.toISOString().slice(0, 10);
|
|
227
|
-
|
|
228
|
-
// 1. Resolve Dependencies (Normalize Name -> Original Name)
|
|
229
|
-
const needed = new Map();
|
|
230
|
-
calcs.forEach(c => {
|
|
231
|
-
// Priority: class.getDependencies() > instance.getDependencies() > manifest.dependencies
|
|
232
|
-
const getDeps = c.class?.getDependencies || c.getDependencies;
|
|
233
|
-
const reqs = (typeof getDeps === 'function') ? getDeps.call(c.class || c) : c.dependencies;
|
|
234
|
-
|
|
235
|
-
if (Array.isArray(reqs)) {
|
|
236
|
-
reqs.forEach(r => needed.set(normalizeName(r), r));
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
if (needed.size === 0) return {};
|
|
241
|
-
|
|
242
|
-
logger.log('INFO', `[DependencyFetcher] 🔍 Fetching ${needed.size} dependencies for ${dStr}`);
|
|
243
|
-
|
|
244
|
-
const results = {};
|
|
245
|
-
const errors = [];
|
|
246
|
-
|
|
247
|
-
// 2. Fetch All Dependencies in Parallel
|
|
248
|
-
await Promise.all(Array.from(needed.entries()).map(async ([norm, orig]) => {
|
|
249
|
-
const category = manifestLookup[norm] || 'analytics';
|
|
250
|
-
const path = `${config.resultsCollection || 'computation_results'}/${dStr}/.../${category}/${norm}`;
|
|
251
|
-
|
|
252
|
-
try {
|
|
253
|
-
const data = await fetchSingleResult(db, { ...config, logger }, dStr, norm, category);
|
|
254
|
-
|
|
255
|
-
if (!data || isDataEmpty(data)) {
|
|
256
|
-
// Determine severity based on context
|
|
257
|
-
const status = data ? 'EMPTY' : 'MISSING';
|
|
258
|
-
errors.push({ name: orig, path, reason: status });
|
|
259
|
-
|
|
260
|
-
// Log immediately for visibility
|
|
261
|
-
const level = allowMissing ? 'INFO' : 'ERROR';
|
|
262
|
-
logger.log(level, `[DependencyFetcher] ⚠️ Dependency '${orig}' ${status} at ${path}`);
|
|
263
|
-
} else {
|
|
264
|
-
results[orig] = data; // Store using Original Name
|
|
265
|
-
}
|
|
266
|
-
} catch (e) {
|
|
267
|
-
errors.push({ name: orig, path, reason: e.message });
|
|
268
|
-
logger.log('ERROR', `[DependencyFetcher] ❌ Error loading '${orig}': ${e.message}`);
|
|
269
|
-
}
|
|
270
|
-
}));
|
|
271
|
-
|
|
272
|
-
// 3. Final Validation
|
|
273
|
-
if (errors.length > 0 && !allowMissing) {
|
|
274
|
-
throw new Error(`[DependencyFetcher] CRITICAL: Missing required dependencies: ${errors.map(e => e.name).join(', ')}`);
|
|
275
|
-
} else if (errors.length > 0) {
|
|
276
|
-
logger.log('INFO', `[DependencyFetcher] ⚠️ Allowed missing/empty dependencies in Historical context: ${errors.map(e => e.name).join(', ')}`);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return results;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Fetches result series (Historical data) for lookbacks.
|
|
284
|
-
* @param {string} endDateStr - The end date for the series
|
|
285
|
-
* @param {string[]} calcNames - Original (case-sensitive) computation names
|
|
286
|
-
* @param {Object} manifestLookup - Map of normalizedName -> category
|
|
287
|
-
* @param {Object} config - Configuration object
|
|
288
|
-
* @param {Object} deps - Dependencies (db, logger)
|
|
289
|
-
* @param {number} lookbackDays - Number of days to look back
|
|
290
|
-
*/
|
|
291
|
-
async function fetchResultSeries(endDateStr, calcNames, manifestLookup, config, deps, lookbackDays) {
|
|
292
|
-
const { db, logger } = deps;
|
|
293
|
-
const results = {}; // normalizedName -> { date -> data }
|
|
294
|
-
const { resultsCollection = 'computation_results', resultsSubcollection = 'results', computationsSubcollection = 'computations' } = config;
|
|
295
|
-
|
|
296
|
-
// Initialize results structure
|
|
297
|
-
calcNames.forEach(n => results[normalizeName(n)] = {});
|
|
298
|
-
|
|
299
|
-
// Generate Date List (going backwards from endDate)
|
|
300
|
-
const dates = [];
|
|
301
|
-
const d = new Date(endDateStr);
|
|
302
|
-
for (let i = 0; i < lookbackDays; i++) {
|
|
303
|
-
d.setUTCDate(d.getUTCDate() - 1);
|
|
304
|
-
dates.push(d.toISOString().slice(0, 10));
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const startDateStr = dates[dates.length - 1]; // Oldest date
|
|
308
|
-
const queryEndDateStr = dates[0]; // Newest date (for BigQuery query)
|
|
309
|
-
|
|
310
|
-
// [DEBUG] Log the manifest lookup and resolved categories
|
|
311
|
-
logger.log('INFO', `[DependencyFetcher] 🔍 ManifestLookup has ${Object.keys(manifestLookup).length} entries`);
|
|
312
|
-
for (const rawName of calcNames) {
|
|
313
|
-
const norm = normalizeName(rawName);
|
|
314
|
-
const category = manifestLookup[norm] || 'analytics';
|
|
315
|
-
const samplePath = `${resultsCollection}/${dates[0]}/${resultsSubcollection}/${category}/${computationsSubcollection}/${rawName}`;
|
|
316
|
-
logger.log('INFO', `[DependencyFetcher] 📍 '${rawName}' -> category='${category}' -> Path: ${samplePath}`);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// =========================================================================
|
|
320
|
-
// BIGQUERY FIRST: Try batch query for all dates at once
|
|
321
|
-
// =========================================================================
|
|
322
|
-
if (process.env.BIGQUERY_ENABLED !== 'false') {
|
|
323
|
-
try {
|
|
324
|
-
const { queryComputationResultsRange } = require('../../core/utils/bigquery_utils');
|
|
325
|
-
|
|
326
|
-
// Query each computation in parallel
|
|
327
|
-
const bigqueryPromises = calcNames.map(async (rawName) => {
|
|
328
|
-
const norm = normalizeName(rawName);
|
|
329
|
-
const category = manifestLookup[norm] || 'analytics';
|
|
330
|
-
|
|
331
|
-
const bigqueryRows = await queryComputationResultsRange(
|
|
332
|
-
rawName,
|
|
333
|
-
category,
|
|
334
|
-
startDateStr,
|
|
335
|
-
queryEndDateStr,
|
|
336
|
-
logger
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
if (bigqueryRows && bigqueryRows.length > 0) {
|
|
340
|
-
logger.log('INFO', `[DependencyFetcher] ✅ Using BigQuery for ${rawName} series: ${bigqueryRows.length} dates`);
|
|
341
|
-
|
|
342
|
-
// Map BigQuery results to results structure
|
|
343
|
-
for (const row of bigqueryRows) {
|
|
344
|
-
if (row.data && !isDataEmpty(row.data)) {
|
|
345
|
-
results[norm][row.date] = row.data;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return { name: rawName, found: bigqueryRows.length };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return { name: rawName, found: 0 };
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
const bigqueryResults = await Promise.all(bigqueryPromises);
|
|
356
|
-
const totalFound = bigqueryResults.reduce((sum, r) => sum + r.found, 0);
|
|
357
|
-
|
|
358
|
-
if (totalFound > 0) {
|
|
359
|
-
logger.log('INFO', `[DependencyFetcher] ✅ BigQuery retrieved ${totalFound} computation result records across ${calcNames.length} computations`);
|
|
360
|
-
|
|
361
|
-
// Fill in any missing dates from Firestore (fallback)
|
|
362
|
-
const missingOps = [];
|
|
363
|
-
for (const dateStr of dates) {
|
|
364
|
-
for (const rawName of calcNames) {
|
|
365
|
-
const norm = normalizeName(rawName);
|
|
366
|
-
// Only fetch if we don't have this date already
|
|
367
|
-
if (!results[norm] || !results[norm][dateStr]) {
|
|
368
|
-
const category = manifestLookup[norm] || 'analytics';
|
|
369
|
-
missingOps.push(async () => {
|
|
370
|
-
const val = await fetchSingleResult(db, { ...config, logger }, dateStr, rawName, category);
|
|
371
|
-
if (val && !isDataEmpty(val)) {
|
|
372
|
-
results[norm][dateStr] = val;
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Fetch missing dates from Firestore
|
|
380
|
-
if (missingOps.length > 0) {
|
|
381
|
-
logger.log('INFO', `[DependencyFetcher] 📂 Fetching ${missingOps.length} missing dates from Firestore (fallback)`);
|
|
382
|
-
const BATCH_SIZE = 20;
|
|
383
|
-
for (let i = 0; i < missingOps.length; i += BATCH_SIZE) {
|
|
384
|
-
await Promise.all(missingOps.slice(i, i + BATCH_SIZE).map(fn => fn()));
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Log final summary
|
|
389
|
-
for (const rawName of calcNames) {
|
|
390
|
-
const norm = normalizeName(rawName);
|
|
391
|
-
const foundDates = Object.keys(results[norm] || {});
|
|
392
|
-
logger.log('INFO', `[DependencyFetcher] ✅ '${rawName}' found data for ${foundDates.length}/${lookbackDays} days (BigQuery + Firestore)`);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return results;
|
|
396
|
-
} else {
|
|
397
|
-
logger.log('INFO', `[DependencyFetcher] ⚠️ BigQuery returned no results, falling back to Firestore`);
|
|
398
|
-
}
|
|
399
|
-
} catch (bqError) {
|
|
400
|
-
logger.log('WARN', `[DependencyFetcher] BigQuery series query failed, falling back to Firestore: ${bqError.message}`);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// =========================================================================
|
|
405
|
-
// FIRESTORE FALLBACK: Original logic (backwards compatibility)
|
|
406
|
-
// =========================================================================
|
|
407
|
-
logger.log('INFO', `[DependencyFetcher] 📂 Using Firestore for computation result series: ${calcNames.length} calcs x ${lookbackDays} days`);
|
|
408
|
-
|
|
409
|
-
// Build Fetch Operations
|
|
410
|
-
const ops = [];
|
|
411
|
-
for (const dateStr of dates) {
|
|
412
|
-
for (const rawName of calcNames) {
|
|
413
|
-
const norm = normalizeName(rawName);
|
|
414
|
-
const category = manifestLookup[norm] || 'analytics';
|
|
415
|
-
|
|
416
|
-
ops.push(async () => {
|
|
417
|
-
const val = await fetchSingleResult(db, { ...config, logger }, dateStr, rawName, category);
|
|
418
|
-
if (val && !isDataEmpty(val)) {
|
|
419
|
-
results[norm][dateStr] = val;
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Execute in Batches (Limit Concurrency)
|
|
426
|
-
logger.log('INFO', `[DependencyFetcher] 📚 Loading series: ${calcNames.length} calcs x ${lookbackDays} days (${ops.length} ops)`);
|
|
427
|
-
const BATCH_SIZE = 20;
|
|
428
|
-
for (let i = 0; i < ops.length; i += BATCH_SIZE) {
|
|
429
|
-
await Promise.all(ops.slice(i, i + BATCH_SIZE).map(fn => fn()));
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// [DEBUG] Log results summary
|
|
433
|
-
for (const rawName of calcNames) {
|
|
434
|
-
const norm = normalizeName(rawName);
|
|
435
|
-
const foundDates = Object.keys(results[norm] || {});
|
|
436
|
-
logger.log('INFO', `[DependencyFetcher] ✅ '${rawName}' found data for ${foundDates.length}/${lookbackDays} days`);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
return results;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Bridge function for WorkflowOrchestrator compatibility.
|
|
444
|
-
*/
|
|
445
|
-
async function fetchExistingResults(dateStr, calcs, fullManifest, config, deps, isHistoricalContext) {
|
|
446
|
-
const lookup = {};
|
|
447
|
-
if (Array.isArray(fullManifest)) {
|
|
448
|
-
fullManifest.forEach(c => lookup[normalizeName(c.name)] = c.category || 'analytics');
|
|
449
|
-
}
|
|
450
|
-
// Ensure Date Object
|
|
451
|
-
const dateObj = new Date(dateStr.includes('T') ? dateStr : dateStr + 'T00:00:00Z');
|
|
452
|
-
return fetchDependencies(dateObj, calcs, config, deps, lookup, isHistoricalContext);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
module.exports = { fetchDependencies, fetchResultSeries, fetchExistingResults };
|