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,478 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FILENAME: computation-system/data/DependencyFetcher.js
|
|
3
|
-
* @fileoverview Fetches dependencies for computations.
|
|
4
|
-
* UPDATED: Added 'fetchExistingResults' bridge for WorkflowOrchestrator compatibility.
|
|
5
|
-
* UPDATED: Uses 'manifestLookup' to resolve the correct category (Core vs Non-Core).
|
|
6
|
-
* UPDATED: Supports automatic reassembly of sharded results (_shards subcollection).
|
|
7
|
-
* UPDATED: Supports decompression of zipped results.
|
|
8
|
-
*/
|
|
9
|
-
const { normalizeName } = require('../utils/utils');
|
|
10
|
-
const zlib = require('zlib');
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Checks if data is effectively empty (no usable content).
|
|
14
|
-
* @param {any} data - The data to check
|
|
15
|
-
* @returns {boolean} True if data is empty/null/undefined or contains no meaningful content
|
|
16
|
-
*/
|
|
17
|
-
function isDataEmpty(data) {
|
|
18
|
-
if (!data || data === null || data === undefined) return true;
|
|
19
|
-
|
|
20
|
-
// Check if it's an object with only metadata fields
|
|
21
|
-
if (typeof data === 'object' && !Array.isArray(data)) {
|
|
22
|
-
const keys = Object.keys(data);
|
|
23
|
-
// If only metadata/internal fields, consider it empty
|
|
24
|
-
const metadataFields = ['_completed', '_compressed', '_sharded', '_shardCount', '_isPageMode', '_pageCount', '_lastUpdated', '_expireAt'];
|
|
25
|
-
const hasOnlyMetadata = keys.length > 0 && keys.every(k => metadataFields.includes(k) || k.startsWith('_'));
|
|
26
|
-
|
|
27
|
-
if (hasOnlyMetadata) return true;
|
|
28
|
-
|
|
29
|
-
// If object has no keys (after filtering metadata), it's empty
|
|
30
|
-
const dataKeys = keys.filter(k => !k.startsWith('_'));
|
|
31
|
-
if (dataKeys.length === 0) return true;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Check if it's an empty array
|
|
35
|
-
if (Array.isArray(data) && data.length === 0) return true;
|
|
36
|
-
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* BRIDGE FUNCTION: Matches WorkflowOrchestrator signature.
|
|
42
|
-
* Adapts (dateStr, calcs, manifest, ...) -> (dateObj, calcs, ..., manifestLookup).
|
|
43
|
-
* This fixes the 'fetchExistingResults is not a function' TypeError.
|
|
44
|
-
*/
|
|
45
|
-
async function fetchExistingResults(dateStr, calcs, fullManifest, config, deps, isHistoricalContext) {
|
|
46
|
-
const { logger } = deps;
|
|
47
|
-
|
|
48
|
-
// DEBUG: Log entry
|
|
49
|
-
logger.log('INFO', `[DependencyFetcher] 📥 fetchExistingResults called for date: ${dateStr}, calcs: ${calcs.length}, isHistorical: ${isHistoricalContext}`);
|
|
50
|
-
logger.log('INFO', `[DependencyFetcher] 📥 Calcs being processed: ${calcs.map(c => {
|
|
51
|
-
const name = c.name || c.constructor?.name || 'unknown';
|
|
52
|
-
const hasClass = !!c.class;
|
|
53
|
-
const classType = c.class ? (typeof c.class === 'function' ? 'function' : typeof c.class) : 'none';
|
|
54
|
-
return `${name} (has class: ${hasClass}, class type: ${classType})`;
|
|
55
|
-
}).join(', ')}`);
|
|
56
|
-
|
|
57
|
-
// 1. Build Manifest Lookup (Name -> Category)
|
|
58
|
-
const manifestLookup = {};
|
|
59
|
-
if (Array.isArray(fullManifest)) {
|
|
60
|
-
fullManifest.forEach(c => {
|
|
61
|
-
manifestLookup[normalizeName(c.name)] = c.category || 'analytics';
|
|
62
|
-
});
|
|
63
|
-
logger.log('INFO', `[DependencyFetcher] 📥 Built manifest lookup with ${Object.keys(manifestLookup).length} entries`);
|
|
64
|
-
} else {
|
|
65
|
-
logger.log('WARN', `[DependencyFetcher] ⚠️ fullManifest is not an array: ${typeof fullManifest}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// 2. Convert Date String to Date Object
|
|
69
|
-
// We append T00:00:00Z to ensure it parses as UTC date if only YYYY-MM-DD is provided.
|
|
70
|
-
const dateObj = new Date(dateStr + (dateStr.includes('T') ? '' : 'T00:00:00Z'));
|
|
71
|
-
|
|
72
|
-
// 3. Delegate to fetchDependencies
|
|
73
|
-
// CRITICAL: For historical context (yesterday's data), allow missing dependencies
|
|
74
|
-
// Historical lookbacks are optional - gaps in historical data are permissible
|
|
75
|
-
const result = await fetchDependencies(dateObj, calcs, config, deps, manifestLookup, isHistoricalContext);
|
|
76
|
-
|
|
77
|
-
// DEBUG: Log result
|
|
78
|
-
const resultKeys = Object.keys(result);
|
|
79
|
-
logger.log('INFO', `[DependencyFetcher] 📤 fetchExistingResults returning ${resultKeys.length} dependencies: ${resultKeys.length > 0 ? resultKeys.join(', ') : 'NONE'}`);
|
|
80
|
-
|
|
81
|
-
return result;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Fetches dependencies for a specific date (Standard pass).
|
|
86
|
-
* @param {Date} date - The target date.
|
|
87
|
-
* @param {Array} calcs - The computations requiring dependencies.
|
|
88
|
-
* @param {Object} config - System config.
|
|
89
|
-
* @param {Object} deps - System dependencies (db, logger).
|
|
90
|
-
* @param {Object} manifestLookup - Map of { [calcName]: categoryString }.
|
|
91
|
-
* @param {boolean} allowMissing - If true, missing/empty dependencies are allowed (for historical/lookback scenarios).
|
|
92
|
-
*/
|
|
93
|
-
async function fetchDependencies(date, calcs, config, deps, manifestLookup = {}, allowMissing = false) {
|
|
94
|
-
const { db, logger } = deps;
|
|
95
|
-
const dStr = date.toISOString().slice(0, 10);
|
|
96
|
-
|
|
97
|
-
// DEBUG: Log entry
|
|
98
|
-
logger.log('INFO', `[DependencyFetcher] 🔍 fetchDependencies called with ${calcs.length} calc(s): ${calcs.map(c => c.name || c.constructor?.name || 'unknown').join(', ')}`);
|
|
99
|
-
|
|
100
|
-
// 1. Identify unique dependencies needed
|
|
101
|
-
// CHANGED: Use a Map to track { normalizedName: originalName }
|
|
102
|
-
const needed = new Map();
|
|
103
|
-
|
|
104
|
-
calcs.forEach(c => {
|
|
105
|
-
const calcName = c.name || c.constructor?.name || 'unknown';
|
|
106
|
-
|
|
107
|
-
// DEBUG: Log what we're checking
|
|
108
|
-
logger.log('INFO', `[DependencyFetcher] 🔍 Processing calc: ${calcName}`);
|
|
109
|
-
logger.log('INFO', `[DependencyFetcher] - has class: ${!!c.class}`);
|
|
110
|
-
logger.log('INFO', `[DependencyFetcher] - class type: ${c.class ? (typeof c.class === 'function' ? 'function' : typeof c.class) : 'none'}`);
|
|
111
|
-
logger.log('INFO', `[DependencyFetcher] - class.getDependencies: ${c.class && typeof c.class.getDependencies === 'function' ? 'YES' : 'NO'}`);
|
|
112
|
-
logger.log('INFO', `[DependencyFetcher] - has getDependencies: ${typeof c.getDependencies === 'function' ? 'YES' : 'NO'}`);
|
|
113
|
-
logger.log('INFO', `[DependencyFetcher] - has dependencies array: ${Array.isArray(c.dependencies) ? `YES (${c.dependencies.length} items)` : 'NO'}`);
|
|
114
|
-
if (Array.isArray(c.dependencies)) {
|
|
115
|
-
logger.log('INFO', `[DependencyFetcher] - dependencies array: ${c.dependencies.join(', ')}`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// [FIX] Support both .getDependencies() method and .dependencies array
|
|
119
|
-
// CRITICAL: Prefer class.getDependencies() over manifest.dependencies
|
|
120
|
-
// because the class method returns original case-sensitive names,
|
|
121
|
-
// while manifest.dependencies contains normalized names
|
|
122
|
-
let reqs = [];
|
|
123
|
-
if (c.class && typeof c.class.getDependencies === 'function') {
|
|
124
|
-
// Use the class method - returns original case-sensitive names
|
|
125
|
-
reqs = c.class.getDependencies();
|
|
126
|
-
logger.log('INFO', `[DependencyFetcher] ✅ Using c.class.getDependencies() - returned: ${JSON.stringify(reqs)}`);
|
|
127
|
-
} else if (typeof c.getDependencies === 'function') {
|
|
128
|
-
// Fallback: direct method call (if c is the class itself)
|
|
129
|
-
reqs = c.getDependencies();
|
|
130
|
-
logger.log('INFO', `[DependencyFetcher] ✅ Using c.getDependencies() - returned: ${JSON.stringify(reqs)}`);
|
|
131
|
-
} else if (c.dependencies && Array.isArray(c.dependencies)) {
|
|
132
|
-
// Last resort: use manifest's dependencies array (normalized)
|
|
133
|
-
// This is less ideal because names are normalized, but we'll use them as-is
|
|
134
|
-
reqs = c.dependencies;
|
|
135
|
-
logger.log('INFO', `[DependencyFetcher] ⚠️ Using c.dependencies array (normalized) - returned: ${JSON.stringify(reqs)}`);
|
|
136
|
-
} else {
|
|
137
|
-
logger.log('WARN', `[DependencyFetcher] ❌ No way to get dependencies for ${calcName} - all methods failed`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (Array.isArray(reqs)) {
|
|
141
|
-
logger.log('INFO', `[DependencyFetcher] ✅ Found ${reqs.length} dependencies for ${calcName}: ${reqs.join(', ')}`);
|
|
142
|
-
reqs.forEach(r => {
|
|
143
|
-
// We map the normalized version to the original requested version
|
|
144
|
-
// This ensures we fetch the right file (normalized) but return it
|
|
145
|
-
// with the casing the user code expects (original).
|
|
146
|
-
needed.set(normalizeName(r), r);
|
|
147
|
-
});
|
|
148
|
-
} else {
|
|
149
|
-
logger.log('WARN', `[DependencyFetcher] ⚠️ reqs is not an array for ${calcName}: ${typeof reqs}`);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
if (needed.size === 0) {
|
|
154
|
-
logger.log('WARN', `[DependencyFetcher] ⚠️ No dependencies needed - returning empty object`);
|
|
155
|
-
return {};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const calcNames = calcs.map(c => c.name || c.constructor?.name || 'unknown').join(', ');
|
|
159
|
-
logger.log('INFO', `[DependencyFetcher] Fetching ${needed.size} dependencies for computation(s): ${calcNames} (date: ${dStr})`);
|
|
160
|
-
|
|
161
|
-
// DEBUG: Log what dependencies we're looking for
|
|
162
|
-
const depList = Array.from(needed.entries()).map(([norm, orig]) => `${orig} (normalized: ${norm})`).join(', ');
|
|
163
|
-
logger.log('INFO', `[DependencyFetcher] Dependencies requested: ${depList}`);
|
|
164
|
-
|
|
165
|
-
const results = {};
|
|
166
|
-
const missingDeps = [];
|
|
167
|
-
const emptyDeps = [];
|
|
168
|
-
|
|
169
|
-
// Helper to build path string
|
|
170
|
-
const buildPath = (category, normName) => {
|
|
171
|
-
return `${config.resultsCollection || 'computation_results'}/${dStr}/${config.resultsSubcollection || 'results'}/${category}/${config.computationsSubcollection || 'computations'}/${normName}`;
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
// CHANGED: Iterate over the entries to access both normalized and original names
|
|
175
|
-
const promises = Array.from(needed.entries()).map(async ([normName, originalName]) => {
|
|
176
|
-
// Resolve Category from Lookup, default to 'analytics' if unknown
|
|
177
|
-
// Note: manifestLookup keys are expected to be normalized
|
|
178
|
-
const category = manifestLookup[normName] || 'analytics';
|
|
179
|
-
const path = buildPath(category, normName);
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
// Pass logger in config for fetchSingleResult
|
|
183
|
-
const fetchConfig = { ...config, logger };
|
|
184
|
-
|
|
185
|
-
// Fetch using the normalized name (system standard)
|
|
186
|
-
const data = await fetchSingleResult(db, fetchConfig, dStr, normName, category);
|
|
187
|
-
|
|
188
|
-
// CRITICAL: Validate that dependency exists and has data
|
|
189
|
-
if (!data) {
|
|
190
|
-
missingDeps.push({ name: originalName, normalizedName: normName, path });
|
|
191
|
-
// Log level depends on context - ERROR for current date, INFO for historical
|
|
192
|
-
if (allowMissing) {
|
|
193
|
-
logger.log('INFO', `[DependencyFetcher] ⚠️ Missing dependency '${originalName}' (${normName}) from: ${path} (Historical context - allowed)`);
|
|
194
|
-
} else {
|
|
195
|
-
logger.log('ERROR', `[DependencyFetcher] ❌ Missing required dependency '${originalName}' (${normName}) from: ${path}`);
|
|
196
|
-
}
|
|
197
|
-
} else if (isDataEmpty(data)) {
|
|
198
|
-
emptyDeps.push({ name: originalName, normalizedName: normName, path });
|
|
199
|
-
// Log level depends on context - ERROR for current date, INFO for historical
|
|
200
|
-
if (allowMissing) {
|
|
201
|
-
logger.log('INFO', `[DependencyFetcher] ⚠️ Empty dependency '${originalName}' (${normName}) from: ${path} (Historical context - allowed)`);
|
|
202
|
-
} else {
|
|
203
|
-
logger.log('ERROR', `[DependencyFetcher] ❌ Empty dependency '${originalName}' (${normName}) from: ${path} - Document exists but contains no usable data`);
|
|
204
|
-
}
|
|
205
|
-
} else {
|
|
206
|
-
// CHANGED: Store result using the ORIGINAL name so context.computed['CaseSensitive'] works
|
|
207
|
-
results[originalName] = data;
|
|
208
|
-
// DEBUG: Log successful dependency load
|
|
209
|
-
const dataKeys = Object.keys(data);
|
|
210
|
-
logger.log('INFO', `[DependencyFetcher] ✅ Stored dependency '${originalName}' in results. Keys: ${dataKeys.length} (sample: ${dataKeys.slice(0, 5).join(', ')})`);
|
|
211
|
-
}
|
|
212
|
-
} catch (e) {
|
|
213
|
-
missingDeps.push({ name: originalName, normalizedName: normName, path, error: e.message });
|
|
214
|
-
// Log level depends on context - ERROR for current date, INFO for historical
|
|
215
|
-
if (allowMissing) {
|
|
216
|
-
logger.log('INFO', `[DependencyFetcher] ⚠️ Failed to load dependency '${originalName}' (${normName}) from: ${path} - Error: ${e.message} (Historical context - allowed)`);
|
|
217
|
-
} else {
|
|
218
|
-
logger.log('ERROR', `[DependencyFetcher] ❌ Failed to load dependency '${originalName}' (${normName}) from: ${path} - Error: ${e.message}`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
await Promise.all(promises);
|
|
224
|
-
|
|
225
|
-
// DEBUG: Log what we're returning
|
|
226
|
-
const resultKeys = Object.keys(results);
|
|
227
|
-
logger.log('INFO', `[DependencyFetcher] ✅ Returning ${resultKeys.length} dependencies: ${resultKeys.join(', ')}`);
|
|
228
|
-
|
|
229
|
-
// CRITICAL: Fail if any required dependencies are missing or empty
|
|
230
|
-
// EXCEPTION: For historical/lookback scenarios, missing dependencies are permissible
|
|
231
|
-
if ((missingDeps.length > 0 || emptyDeps.length > 0) && !allowMissing) {
|
|
232
|
-
const missingList = missingDeps.map(d => `'${d.name}' (path: ${d.path}${d.error ? `, error: ${d.error}` : ''})`).join(', ');
|
|
233
|
-
const emptyList = emptyDeps.map(d => `'${d.name}' (path: ${d.path})`).join(', ');
|
|
234
|
-
|
|
235
|
-
const errorMsg = `[DependencyFetcher] ❌ CRITICAL: Cannot proceed - Required dependencies missing or empty for computation(s): ${calcNames}\n` +
|
|
236
|
-
`Missing dependencies (${missingDeps.length}): ${missingList}\n` +
|
|
237
|
-
(emptyDeps.length > 0 ? `Empty dependencies (${emptyDeps.length}): ${emptyList}\n` : '') +
|
|
238
|
-
`Date: ${dStr}\n` +
|
|
239
|
-
`This computation will FAIL and no results will be saved.`;
|
|
240
|
-
|
|
241
|
-
logger.log('ERROR', errorMsg);
|
|
242
|
-
throw new Error(errorMsg);
|
|
243
|
-
} else if (missingDeps.length > 0 || emptyDeps.length > 0) {
|
|
244
|
-
// Historical/lookback context - log but allow missing dependencies
|
|
245
|
-
const missingList = missingDeps.map(d => `'${d.name}' (path: ${d.path})`).join(', ');
|
|
246
|
-
const emptyList = emptyDeps.map(d => `'${d.name}' (path: ${d.path})`).join(', ');
|
|
247
|
-
logger.log('INFO', `[DependencyFetcher] ⚠️ Historical/Lookback context: Missing/empty dependencies allowed for ${calcNames} on ${dStr}. Missing: ${missingList}${emptyDeps.length > 0 ? `, Empty: ${emptyList}` : ''}`);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return results;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Fetches result series (Historical data) for lookbacks.
|
|
255
|
-
* @param {string} endDateStr - The most recent date.
|
|
256
|
-
* @param {Array} calcNames - Names of computations to fetch.
|
|
257
|
-
* @param {Object} manifestLookup - Map of { [calcName]: categoryString }.
|
|
258
|
-
*/
|
|
259
|
-
async function fetchResultSeries(endDateStr, calcNames, manifestLookup, config, deps, lookbackDays) {
|
|
260
|
-
const { db, logger } = deps;
|
|
261
|
-
const results = {};
|
|
262
|
-
const dates = [];
|
|
263
|
-
|
|
264
|
-
// Generate date list (starting from yesterday relative to endDateStr)
|
|
265
|
-
const d = new Date(endDateStr);
|
|
266
|
-
for (let i = 0; i < lookbackDays; i++) {
|
|
267
|
-
d.setUTCDate(d.getUTCDate() - 1);
|
|
268
|
-
dates.push(d.toISOString().slice(0, 10));
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Initialize structure
|
|
272
|
-
calcNames.forEach(name => { results[normalizeName(name)] = {}; });
|
|
273
|
-
|
|
274
|
-
logger.log('INFO', `[DependencyFetcher] Loading series for ${calcNames.length} computation dependencies over ${lookbackDays} days: ${calcNames.join(', ')}`);
|
|
275
|
-
|
|
276
|
-
const fetchOps = [];
|
|
277
|
-
|
|
278
|
-
for (const dateStr of dates) {
|
|
279
|
-
for (const rawName of calcNames) {
|
|
280
|
-
const normName = normalizeName(rawName);
|
|
281
|
-
const category = manifestLookup[normName] || 'analytics';
|
|
282
|
-
|
|
283
|
-
fetchOps.push(async () => {
|
|
284
|
-
const fetchConfig = { ...config, logger };
|
|
285
|
-
const val = await fetchSingleResult(db, fetchConfig, dateStr, rawName, category);
|
|
286
|
-
// CRITICAL: For series/lookback, we allow missing dates (historical lookback may have gaps)
|
|
287
|
-
// This is expected behavior - not all historical dates will have data
|
|
288
|
-
// But we still validate that the data isn't empty if it exists
|
|
289
|
-
if (val && !isDataEmpty(val)) {
|
|
290
|
-
if (!results[normName]) results[normName] = {};
|
|
291
|
-
results[normName][dateStr] = val;
|
|
292
|
-
} else if (val && isDataEmpty(val)) {
|
|
293
|
-
// Log but don't fail - series can have gaps, empty data is treated as missing
|
|
294
|
-
logger.log('INFO', `[DependencyFetcher] ⚠️ Empty dependency '${rawName}' found at ${dateStr} in series (allowing gap - historical lookback)`);
|
|
295
|
-
}
|
|
296
|
-
// If val is null, that's fine - missing dates in historical series are permissible
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Limited concurrency batch execution (Batch size 20)
|
|
302
|
-
const BATCH_SIZE = 20;
|
|
303
|
-
for (let i = 0; i < fetchOps.length; i += BATCH_SIZE) {
|
|
304
|
-
await Promise.all(fetchOps.slice(i, i + BATCH_SIZE).map(fn => fn()));
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return results;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Core Helper: Fetches a single result, handles Sharding & Compression.
|
|
312
|
-
*/
|
|
313
|
-
async function fetchSingleResult(db, config, dateStr, name, category) {
|
|
314
|
-
const resultsCollection = config.resultsCollection || 'computation_results';
|
|
315
|
-
const resultsSubcollection = config.resultsSubcollection || 'results';
|
|
316
|
-
const computationsSubcollection = config.computationsSubcollection || 'computations';
|
|
317
|
-
|
|
318
|
-
const path = `${resultsCollection}/${dateStr}/${resultsSubcollection}/${category}/${computationsSubcollection}/${name}`;
|
|
319
|
-
|
|
320
|
-
// Log path - use console.log if logger not available (for backward compatibility)
|
|
321
|
-
if (config.logger) {
|
|
322
|
-
config.logger.log('INFO', `[DependencyFetcher] 📂 Loading Dependency '${name}' from: ${path}`);
|
|
323
|
-
} else {
|
|
324
|
-
console.log(`[DependencyFetcher] 📂 Loading Dependency '${name}' from: ${path}`);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const docRef = db.collection(resultsCollection)
|
|
328
|
-
.doc(dateStr)
|
|
329
|
-
.collection(resultsSubcollection)
|
|
330
|
-
.doc(category)
|
|
331
|
-
.collection(computationsSubcollection)
|
|
332
|
-
.doc(name);
|
|
333
|
-
|
|
334
|
-
const snap = await docRef.get();
|
|
335
|
-
if (!snap.exists) {
|
|
336
|
-
// Log the missing document path clearly
|
|
337
|
-
if (config.logger) {
|
|
338
|
-
config.logger.log('ERROR', `[DependencyFetcher] ❌ Document does not exist at: ${path}`);
|
|
339
|
-
} else {
|
|
340
|
-
console.error(`[DependencyFetcher] ❌ Document does not exist at: ${path}`);
|
|
341
|
-
}
|
|
342
|
-
return null;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
let data = snap.data();
|
|
346
|
-
|
|
347
|
-
// CRITICAL: Don't check if empty yet - we need to load shards/compressed data first
|
|
348
|
-
// A sharded document will only have metadata in the pointer doc, but the actual data is in shards
|
|
349
|
-
// A compressed document will only have metadata + payload, but the actual data is in the payload
|
|
350
|
-
|
|
351
|
-
// 1. Handle Compression
|
|
352
|
-
if (data._compressed && data.payload) {
|
|
353
|
-
try {
|
|
354
|
-
const buffer = (data.payload instanceof Buffer) ? data.payload : data.payload.toDate();
|
|
355
|
-
const decompressed = zlib.gunzipSync(buffer);
|
|
356
|
-
const jsonStr = decompressed.toString('utf8');
|
|
357
|
-
const realData = JSON.parse(jsonStr);
|
|
358
|
-
// Merge decompressed data
|
|
359
|
-
data = { ...data, ...realData };
|
|
360
|
-
delete data.payload;
|
|
361
|
-
} catch (e) {
|
|
362
|
-
const errorMsg = `Decompression failed for ${name}: ${e.message}`;
|
|
363
|
-
if (config.logger) {
|
|
364
|
-
config.logger.log('ERROR', `[DependencyFetcher] ❌ ${errorMsg} at: ${path}`);
|
|
365
|
-
} else {
|
|
366
|
-
console.error(`[DependencyFetcher] ❌ ${errorMsg} at: ${path}`);
|
|
367
|
-
}
|
|
368
|
-
return null;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// 2. Handle Sharding (MUST happen before empty check)
|
|
373
|
-
if (data._sharded) {
|
|
374
|
-
const shardPath = `${path}/_shards`;
|
|
375
|
-
if (config.logger) {
|
|
376
|
-
config.logger.log('INFO', `[DependencyFetcher] 📂 Loading Shards for '${name}' from: ${shardPath}`);
|
|
377
|
-
} else {
|
|
378
|
-
console.log(`[DependencyFetcher] 📂 Loading Shards for '${name}' from: ${shardPath}`);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const shardCol = docRef.collection('_shards');
|
|
382
|
-
const shardSnaps = await shardCol.get();
|
|
383
|
-
|
|
384
|
-
if (shardSnaps.empty) {
|
|
385
|
-
// No shards found - this is a problem
|
|
386
|
-
if (config.logger) {
|
|
387
|
-
config.logger.log('ERROR', `[DependencyFetcher] ❌ Document marked as sharded but no shards found at: ${shardPath}`);
|
|
388
|
-
} else {
|
|
389
|
-
console.error(`[DependencyFetcher] ❌ Document marked as sharded but no shards found at: ${shardPath}`);
|
|
390
|
-
}
|
|
391
|
-
return null; // Return null so it gets caught as missing
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Merge shard contents
|
|
395
|
-
let hasData = false;
|
|
396
|
-
shardSnaps.forEach(shard => {
|
|
397
|
-
let shardData = shard.data();
|
|
398
|
-
const shardId = shard.id;
|
|
399
|
-
if (config.logger) {
|
|
400
|
-
config.logger.log('TRACE', `[DependencyFetcher] 📂 Loading Shard '${shardId}' for '${name}' from: ${shardPath}/${shardId}`);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// CRITICAL: Shards themselves can be compressed (common in big data)
|
|
404
|
-
// Decompress the shard if needed before merging
|
|
405
|
-
if (shardData._compressed && shardData.payload) {
|
|
406
|
-
try {
|
|
407
|
-
const buffer = (shardData.payload instanceof Buffer) ? shardData.payload :
|
|
408
|
-
(shardData.payload._byteString ? Buffer.from(shardData.payload._byteString, 'base64') :
|
|
409
|
-
Buffer.from(shardData.payload));
|
|
410
|
-
const decompressed = zlib.gunzipSync(buffer);
|
|
411
|
-
const jsonStr = decompressed.toString('utf8');
|
|
412
|
-
const realData = JSON.parse(jsonStr);
|
|
413
|
-
// If it's double-encoded, parse again
|
|
414
|
-
const parsedData = (typeof realData === 'string') ? JSON.parse(realData) : realData;
|
|
415
|
-
shardData = { ...shardData, ...parsedData };
|
|
416
|
-
delete shardData.payload;
|
|
417
|
-
} catch (e) {
|
|
418
|
-
if (config.logger) {
|
|
419
|
-
config.logger.log('ERROR', `[DependencyFetcher] ❌ Failed to decompress shard '${shardId}' for '${name}': ${e.message}`);
|
|
420
|
-
} else {
|
|
421
|
-
console.error(`[DependencyFetcher] ❌ Failed to decompress shard '${shardId}' for '${name}': ${e.message}`);
|
|
422
|
-
}
|
|
423
|
-
// Continue with uncompressed data if decompression fails
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Merge shard contents, ignoring internal metadata if it clashes
|
|
428
|
-
Object.entries(shardData).forEach(([k, v]) => {
|
|
429
|
-
if (!k.startsWith('_')) {
|
|
430
|
-
data[k] = v;
|
|
431
|
-
hasData = true;
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
// If shards contained no actual data, treat as empty
|
|
437
|
-
if (!hasData) {
|
|
438
|
-
if (config.logger) {
|
|
439
|
-
config.logger.log('ERROR', `[DependencyFetcher] ❌ Shards found but contain no data at: ${shardPath}`);
|
|
440
|
-
} else {
|
|
441
|
-
console.error(`[DependencyFetcher] ❌ Shards found but contain no data at: ${shardPath}`);
|
|
442
|
-
}
|
|
443
|
-
return null;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// After loading shards, remove shard metadata from data object for cleaner output
|
|
447
|
-
// Keep only the actual data fields
|
|
448
|
-
const cleanedData = {};
|
|
449
|
-
const dataKeys = [];
|
|
450
|
-
Object.entries(data).forEach(([k, v]) => {
|
|
451
|
-
if (!k.startsWith('_')) {
|
|
452
|
-
cleanedData[k] = v;
|
|
453
|
-
dataKeys.push(k);
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
data = cleanedData;
|
|
457
|
-
|
|
458
|
-
// Log what we loaded for debugging
|
|
459
|
-
if (config.logger) {
|
|
460
|
-
config.logger.log('INFO', `[DependencyFetcher] ✅ Loaded ${shardSnaps.size} shard(s) for '${name}'. Data fields: ${dataKeys.length > 0 ? dataKeys.slice(0, 10).join(', ') + (dataKeys.length > 10 ? `... (+${dataKeys.length - 10} more)` : '') : 'none'}`);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Final validation: ensure we have usable data after all processing (decompression + sharding)
|
|
465
|
-
// Only check if we haven't already determined it's empty
|
|
466
|
-
if (isDataEmpty(data)) {
|
|
467
|
-
if (config.logger) {
|
|
468
|
-
config.logger.log('ERROR', `[DependencyFetcher] ❌ Dependency '${name}' loaded but is empty (no usable data) at: ${path}`);
|
|
469
|
-
} else {
|
|
470
|
-
console.error(`[DependencyFetcher] ❌ Dependency '${name}' loaded but is empty (no usable data) at: ${path}`);
|
|
471
|
-
}
|
|
472
|
-
return null;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return data;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
module.exports = { fetchDependencies, fetchResultSeries, fetchExistingResults };
|