bulltrackers-module 1.0.644 → 1.0.647
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.
|
@@ -231,7 +231,8 @@ function buildManifest(productLinesToRun = [], calculations) {
|
|
|
231
231
|
const manifestEntry = {
|
|
232
232
|
name: normalizedName,
|
|
233
233
|
class: Class,
|
|
234
|
-
|
|
234
|
+
// [CHANGED] Strictly use the folderName as the category.
|
|
235
|
+
category: folderName,
|
|
235
236
|
sourcePackage: folderName,
|
|
236
237
|
type: metadata.type,
|
|
237
238
|
isPage: metadata.isPage === true,
|
|
@@ -84,6 +84,9 @@ class CachedDataLoader {
|
|
|
84
84
|
|
|
85
85
|
async loadInsights(dateStr) {
|
|
86
86
|
if (this.cache.insights.has(dateStr)) return this.cache.insights.get(dateStr);
|
|
87
|
+
const collectionName = this.config.insightsCollectionName || 'daily_instrument_insights';
|
|
88
|
+
const path = `${collectionName}/${dateStr}`;
|
|
89
|
+
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'insights' from: ${path}`);
|
|
87
90
|
const promise = loadDailyInsights(this.config, this.deps, dateStr);
|
|
88
91
|
this.cache.insights.set(dateStr, promise);
|
|
89
92
|
return promise;
|
|
@@ -91,6 +94,9 @@ class CachedDataLoader {
|
|
|
91
94
|
|
|
92
95
|
async loadSocial(dateStr) {
|
|
93
96
|
if (this.cache.social.has(dateStr)) return this.cache.social.get(dateStr);
|
|
97
|
+
const collectionName = this.config.socialInsightsCollection || 'daily_social_insights';
|
|
98
|
+
const path = `${collectionName}/${dateStr}`;
|
|
99
|
+
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'social' from: ${path}`);
|
|
94
100
|
const promise = loadDailySocialPostInsights(this.config, this.deps, dateStr);
|
|
95
101
|
this.cache.social.set(dateStr, promise);
|
|
96
102
|
return promise;
|
|
@@ -98,6 +104,9 @@ class CachedDataLoader {
|
|
|
98
104
|
|
|
99
105
|
async loadVerifications() {
|
|
100
106
|
if (this.cache.verifications) return this.cache.verifications;
|
|
107
|
+
const collectionName = this.config.verificationsCollection || 'verification_profiles';
|
|
108
|
+
const path = `${collectionName}`;
|
|
109
|
+
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'verifications' from: ${path}`);
|
|
101
110
|
const verifications = await loadVerificationProfiles(this.config, this.deps);
|
|
102
111
|
this.cache.verifications = verifications;
|
|
103
112
|
return verifications;
|
|
@@ -105,6 +114,9 @@ class CachedDataLoader {
|
|
|
105
114
|
|
|
106
115
|
async loadRankings(dateStr) {
|
|
107
116
|
if (this.cache.rankings.has(dateStr)) return this.cache.rankings.get(dateStr);
|
|
117
|
+
const collectionName = this.config.popularInvestorRankingsCollection || 'popular_investor_rankings';
|
|
118
|
+
const path = `${collectionName}/${dateStr}`;
|
|
119
|
+
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'rankings' from: ${path}`);
|
|
108
120
|
const promise = loadPopularInvestorRankings(this.config, this.deps, dateStr);
|
|
109
121
|
this.cache.rankings.set(dateStr, promise);
|
|
110
122
|
return promise;
|
|
@@ -112,6 +124,9 @@ class CachedDataLoader {
|
|
|
112
124
|
|
|
113
125
|
async loadRatings(dateStr) {
|
|
114
126
|
if (this.cache.ratings.has(dateStr)) return this.cache.ratings.get(dateStr);
|
|
127
|
+
const collectionName = this.config.piRatingsCollection || 'PIRatingsData';
|
|
128
|
+
const path = `${collectionName}/${dateStr}`;
|
|
129
|
+
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'ratings' from: ${path}`);
|
|
115
130
|
const promise = loadPIRatings(this.config, this.deps, dateStr);
|
|
116
131
|
this.cache.ratings.set(dateStr, promise);
|
|
117
132
|
return promise;
|
|
@@ -119,6 +134,9 @@ class CachedDataLoader {
|
|
|
119
134
|
|
|
120
135
|
async loadPageViews(dateStr) {
|
|
121
136
|
if (this.cache.pageViews.has(dateStr)) return this.cache.pageViews.get(dateStr);
|
|
137
|
+
const collectionName = this.config.piPageViewsCollection || 'PIPageViewsData';
|
|
138
|
+
const path = `${collectionName}/${dateStr}`;
|
|
139
|
+
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'pageViews' from: ${path}`);
|
|
122
140
|
const promise = loadPIPageViews(this.config, this.deps, dateStr);
|
|
123
141
|
this.cache.pageViews.set(dateStr, promise);
|
|
124
142
|
return promise;
|
|
@@ -126,6 +144,9 @@ class CachedDataLoader {
|
|
|
126
144
|
|
|
127
145
|
async loadWatchlistMembership(dateStr) {
|
|
128
146
|
if (this.cache.watchlistMembership.has(dateStr)) return this.cache.watchlistMembership.get(dateStr);
|
|
147
|
+
const collectionName = this.config.watchlistMembershipCollection || 'WatchlistMembershipData';
|
|
148
|
+
const path = `${collectionName}/${dateStr}`;
|
|
149
|
+
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'watchlistMembership' from: ${path}`);
|
|
129
150
|
const promise = loadWatchlistMembershipData(this.config, this.deps, dateStr);
|
|
130
151
|
this.cache.watchlistMembership.set(dateStr, promise);
|
|
131
152
|
return promise;
|
|
@@ -133,6 +154,9 @@ class CachedDataLoader {
|
|
|
133
154
|
|
|
134
155
|
async loadAlertHistory(dateStr) {
|
|
135
156
|
if (this.cache.alertHistory.has(dateStr)) return this.cache.alertHistory.get(dateStr);
|
|
157
|
+
const collectionName = this.config.piAlertHistoryCollection || 'PIAlertHistoryData';
|
|
158
|
+
const path = `${collectionName}/${dateStr}`;
|
|
159
|
+
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'alertHistory' from: ${path}`);
|
|
136
160
|
const promise = loadPIAlertHistory(this.config, this.deps, dateStr);
|
|
137
161
|
this.cache.alertHistory.set(dateStr, promise);
|
|
138
162
|
return promise;
|
|
@@ -243,6 +267,10 @@ class CachedDataLoader {
|
|
|
243
267
|
let foundCount = 0;
|
|
244
268
|
|
|
245
269
|
if (batchRefs.length > 0) {
|
|
270
|
+
// Log summary of all paths being loaded
|
|
271
|
+
const paths = batchRefs.map(ref => ref.path).join(', ');
|
|
272
|
+
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Batch loading ${batchRefs.length} documents for '${loaderMethod}': ${paths}`);
|
|
273
|
+
|
|
246
274
|
try {
|
|
247
275
|
const snapshots = await this.deps.db.getAll(...batchRefs);
|
|
248
276
|
|
|
@@ -279,12 +307,16 @@ class CachedDataLoader {
|
|
|
279
307
|
}
|
|
280
308
|
}
|
|
281
309
|
|
|
282
|
-
|
|
310
|
+
const summary = {
|
|
283
311
|
dates: Object.keys(results).sort(),
|
|
284
312
|
data: results,
|
|
285
313
|
found: foundCount,
|
|
286
314
|
requested: lookbackDays
|
|
287
315
|
};
|
|
316
|
+
|
|
317
|
+
this.deps.logger?.log('INFO', `[CachedDataLoader] ✅ Loaded ${foundCount}/${lookbackDays} dates for '${loaderMethod}' (found: ${summary.dates.join(', ') || 'none'})`);
|
|
318
|
+
|
|
319
|
+
return summary;
|
|
288
320
|
}
|
|
289
321
|
|
|
290
322
|
/**
|
|
@@ -300,6 +332,7 @@ class CachedDataLoader {
|
|
|
300
332
|
d.setUTCDate(d.getUTCDate() - i);
|
|
301
333
|
const dString = d.toISOString().slice(0, 10);
|
|
302
334
|
|
|
335
|
+
// Log path for legacy loader (will be logged by individual load methods)
|
|
303
336
|
promises.push(
|
|
304
337
|
this[loaderMethod](dString)
|
|
305
338
|
.then(data => ({ date: dString, data }))
|
|
@@ -9,6 +9,34 @@
|
|
|
9
9
|
const { normalizeName } = require('../utils/utils');
|
|
10
10
|
const zlib = require('zlib');
|
|
11
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
|
+
|
|
12
40
|
/**
|
|
13
41
|
* BRIDGE FUNCTION: Matches WorkflowOrchestrator signature.
|
|
14
42
|
* Adapts (dateStr, calcs, manifest, ...) -> (dateObj, calcs, ..., manifestLookup).
|
|
@@ -28,7 +56,9 @@ async function fetchExistingResults(dateStr, calcs, fullManifest, config, deps,
|
|
|
28
56
|
const dateObj = new Date(dateStr + (dateStr.includes('T') ? '' : 'T00:00:00Z'));
|
|
29
57
|
|
|
30
58
|
// 3. Delegate to fetchDependencies
|
|
31
|
-
|
|
59
|
+
// CRITICAL: For historical context (yesterday's data), allow missing dependencies
|
|
60
|
+
// Historical lookbacks are optional - gaps in historical data are permissible
|
|
61
|
+
return fetchDependencies(dateObj, calcs, config, deps, manifestLookup, isHistoricalContext);
|
|
32
62
|
}
|
|
33
63
|
|
|
34
64
|
/**
|
|
@@ -38,8 +68,9 @@ async function fetchExistingResults(dateStr, calcs, fullManifest, config, deps,
|
|
|
38
68
|
* @param {Object} config - System config.
|
|
39
69
|
* @param {Object} deps - System dependencies (db, logger).
|
|
40
70
|
* @param {Object} manifestLookup - Map of { [calcName]: categoryString }.
|
|
71
|
+
* @param {boolean} allowMissing - If true, missing/empty dependencies are allowed (for historical/lookback scenarios).
|
|
41
72
|
*/
|
|
42
|
-
async function fetchDependencies(date, calcs, config, deps, manifestLookup = {}) {
|
|
73
|
+
async function fetchDependencies(date, calcs, config, deps, manifestLookup = {}, allowMissing = false) {
|
|
43
74
|
const { db, logger } = deps;
|
|
44
75
|
const dStr = date.toISOString().slice(0, 10);
|
|
45
76
|
|
|
@@ -65,27 +96,87 @@ async function fetchDependencies(date, calcs, config, deps, manifestLookup = {})
|
|
|
65
96
|
|
|
66
97
|
if (needed.size === 0) return {};
|
|
67
98
|
|
|
68
|
-
|
|
99
|
+
const calcNames = calcs.map(c => c.name || c.constructor?.name || 'unknown').join(', ');
|
|
100
|
+
logger.log('INFO', `[DependencyFetcher] Fetching ${needed.size} dependencies for computation(s): ${calcNames} (date: ${dStr})`);
|
|
69
101
|
|
|
70
102
|
const results = {};
|
|
103
|
+
const missingDeps = [];
|
|
104
|
+
const emptyDeps = [];
|
|
105
|
+
|
|
106
|
+
// Helper to build path string
|
|
107
|
+
const buildPath = (category, normName) => {
|
|
108
|
+
return `${config.resultsCollection || 'computation_results'}/${dStr}/${config.resultsSubcollection || 'results'}/${category}/${config.computationsSubcollection || 'computations'}/${normName}`;
|
|
109
|
+
};
|
|
110
|
+
|
|
71
111
|
// CHANGED: Iterate over the entries to access both normalized and original names
|
|
72
112
|
const promises = Array.from(needed.entries()).map(async ([normName, originalName]) => {
|
|
113
|
+
// Resolve Category from Lookup, default to 'analytics' if unknown
|
|
114
|
+
// Note: manifestLookup keys are expected to be normalized
|
|
115
|
+
const category = manifestLookup[normName] || 'analytics';
|
|
116
|
+
const path = buildPath(category, normName);
|
|
117
|
+
|
|
73
118
|
try {
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
const category = manifestLookup[normName] || 'analytics';
|
|
119
|
+
// Pass logger in config for fetchSingleResult
|
|
120
|
+
const fetchConfig = { ...config, logger };
|
|
77
121
|
|
|
78
122
|
// Fetch using the normalized name (system standard)
|
|
79
|
-
const data = await fetchSingleResult(db,
|
|
123
|
+
const data = await fetchSingleResult(db, fetchConfig, dStr, normName, category);
|
|
80
124
|
|
|
81
|
-
//
|
|
82
|
-
if (data)
|
|
125
|
+
// CRITICAL: Validate that dependency exists and has data
|
|
126
|
+
if (!data) {
|
|
127
|
+
missingDeps.push({ name: originalName, normalizedName: normName, path });
|
|
128
|
+
// Log level depends on context - ERROR for current date, INFO for historical
|
|
129
|
+
if (allowMissing) {
|
|
130
|
+
logger.log('INFO', `[DependencyFetcher] ⚠️ Missing dependency '${originalName}' (${normName}) from: ${path} (Historical context - allowed)`);
|
|
131
|
+
} else {
|
|
132
|
+
logger.log('ERROR', `[DependencyFetcher] ❌ Missing required dependency '${originalName}' (${normName}) from: ${path}`);
|
|
133
|
+
}
|
|
134
|
+
} else if (isDataEmpty(data)) {
|
|
135
|
+
emptyDeps.push({ name: originalName, normalizedName: normName, path });
|
|
136
|
+
// Log level depends on context - ERROR for current date, INFO for historical
|
|
137
|
+
if (allowMissing) {
|
|
138
|
+
logger.log('INFO', `[DependencyFetcher] ⚠️ Empty dependency '${originalName}' (${normName}) from: ${path} (Historical context - allowed)`);
|
|
139
|
+
} else {
|
|
140
|
+
logger.log('ERROR', `[DependencyFetcher] ❌ Empty dependency '${originalName}' (${normName}) from: ${path} - Document exists but contains no usable data`);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
// CHANGED: Store result using the ORIGINAL name so context.computed['CaseSensitive'] works
|
|
144
|
+
results[originalName] = data;
|
|
145
|
+
}
|
|
83
146
|
} catch (e) {
|
|
84
|
-
|
|
147
|
+
missingDeps.push({ name: originalName, normalizedName: normName, path, error: e.message });
|
|
148
|
+
// Log level depends on context - ERROR for current date, INFO for historical
|
|
149
|
+
if (allowMissing) {
|
|
150
|
+
logger.log('INFO', `[DependencyFetcher] ⚠️ Failed to load dependency '${originalName}' (${normName}) from: ${path} - Error: ${e.message} (Historical context - allowed)`);
|
|
151
|
+
} else {
|
|
152
|
+
logger.log('ERROR', `[DependencyFetcher] ❌ Failed to load dependency '${originalName}' (${normName}) from: ${path} - Error: ${e.message}`);
|
|
153
|
+
}
|
|
85
154
|
}
|
|
86
155
|
});
|
|
87
156
|
|
|
88
157
|
await Promise.all(promises);
|
|
158
|
+
|
|
159
|
+
// CRITICAL: Fail if any required dependencies are missing or empty
|
|
160
|
+
// EXCEPTION: For historical/lookback scenarios, missing dependencies are permissible
|
|
161
|
+
if ((missingDeps.length > 0 || emptyDeps.length > 0) && !allowMissing) {
|
|
162
|
+
const missingList = missingDeps.map(d => `'${d.name}' (path: ${d.path}${d.error ? `, error: ${d.error}` : ''})`).join(', ');
|
|
163
|
+
const emptyList = emptyDeps.map(d => `'${d.name}' (path: ${d.path})`).join(', ');
|
|
164
|
+
|
|
165
|
+
const errorMsg = `[DependencyFetcher] ❌ CRITICAL: Cannot proceed - Required dependencies missing or empty for computation(s): ${calcNames}\n` +
|
|
166
|
+
`Missing dependencies (${missingDeps.length}): ${missingList}\n` +
|
|
167
|
+
(emptyDeps.length > 0 ? `Empty dependencies (${emptyDeps.length}): ${emptyList}\n` : '') +
|
|
168
|
+
`Date: ${dStr}\n` +
|
|
169
|
+
`This computation will FAIL and no results will be saved.`;
|
|
170
|
+
|
|
171
|
+
logger.log('ERROR', errorMsg);
|
|
172
|
+
throw new Error(errorMsg);
|
|
173
|
+
} else if (missingDeps.length > 0 || emptyDeps.length > 0) {
|
|
174
|
+
// Historical/lookback context - log but allow missing dependencies
|
|
175
|
+
const missingList = missingDeps.map(d => `'${d.name}' (path: ${d.path})`).join(', ');
|
|
176
|
+
const emptyList = emptyDeps.map(d => `'${d.name}' (path: ${d.path})`).join(', ');
|
|
177
|
+
logger.log('INFO', `[DependencyFetcher] ⚠️ Historical/Lookback context: Missing/empty dependencies allowed for ${calcNames} on ${dStr}. Missing: ${missingList}${emptyDeps.length > 0 ? `, Empty: ${emptyList}` : ''}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
89
180
|
return results;
|
|
90
181
|
}
|
|
91
182
|
|
|
@@ -110,7 +201,7 @@ async function fetchResultSeries(endDateStr, calcNames, manifestLookup, config,
|
|
|
110
201
|
// Initialize structure
|
|
111
202
|
calcNames.forEach(name => { results[normalizeName(name)] = {}; });
|
|
112
203
|
|
|
113
|
-
logger.log('INFO', `[DependencyFetcher] Loading series for ${calcNames.length}
|
|
204
|
+
logger.log('INFO', `[DependencyFetcher] Loading series for ${calcNames.length} computation dependencies over ${lookbackDays} days: ${calcNames.join(', ')}`);
|
|
114
205
|
|
|
115
206
|
const fetchOps = [];
|
|
116
207
|
|
|
@@ -120,11 +211,19 @@ async function fetchResultSeries(endDateStr, calcNames, manifestLookup, config,
|
|
|
120
211
|
const category = manifestLookup[normName] || 'analytics';
|
|
121
212
|
|
|
122
213
|
fetchOps.push(async () => {
|
|
123
|
-
const
|
|
124
|
-
|
|
214
|
+
const fetchConfig = { ...config, logger };
|
|
215
|
+
const val = await fetchSingleResult(db, fetchConfig, dateStr, rawName, category);
|
|
216
|
+
// CRITICAL: For series/lookback, we allow missing dates (historical lookback may have gaps)
|
|
217
|
+
// This is expected behavior - not all historical dates will have data
|
|
218
|
+
// But we still validate that the data isn't empty if it exists
|
|
219
|
+
if (val && !isDataEmpty(val)) {
|
|
125
220
|
if (!results[normName]) results[normName] = {};
|
|
126
221
|
results[normName][dateStr] = val;
|
|
222
|
+
} else if (val && isDataEmpty(val)) {
|
|
223
|
+
// Log but don't fail - series can have gaps, empty data is treated as missing
|
|
224
|
+
logger.log('INFO', `[DependencyFetcher] ⚠️ Empty dependency '${rawName}' found at ${dateStr} in series (allowing gap - historical lookback)`);
|
|
127
225
|
}
|
|
226
|
+
// If val is null, that's fine - missing dates in historical series are permissible
|
|
128
227
|
});
|
|
129
228
|
}
|
|
130
229
|
}
|
|
@@ -142,18 +241,49 @@ async function fetchResultSeries(endDateStr, calcNames, manifestLookup, config,
|
|
|
142
241
|
* Core Helper: Fetches a single result, handles Sharding & Compression.
|
|
143
242
|
*/
|
|
144
243
|
async function fetchSingleResult(db, config, dateStr, name, category) {
|
|
145
|
-
const
|
|
244
|
+
const resultsCollection = config.resultsCollection || 'computation_results';
|
|
245
|
+
const resultsSubcollection = config.resultsSubcollection || 'results';
|
|
246
|
+
const computationsSubcollection = config.computationsSubcollection || 'computations';
|
|
247
|
+
|
|
248
|
+
const path = `${resultsCollection}/${dateStr}/${resultsSubcollection}/${category}/${computationsSubcollection}/${name}`;
|
|
249
|
+
|
|
250
|
+
// Log path - use console.log if logger not available (for backward compatibility)
|
|
251
|
+
if (config.logger) {
|
|
252
|
+
config.logger.log('INFO', `[DependencyFetcher] 📂 Loading Dependency '${name}' from: ${path}`);
|
|
253
|
+
} else {
|
|
254
|
+
console.log(`[DependencyFetcher] 📂 Loading Dependency '${name}' from: ${path}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const docRef = db.collection(resultsCollection)
|
|
146
258
|
.doc(dateStr)
|
|
147
|
-
.collection(
|
|
259
|
+
.collection(resultsSubcollection)
|
|
148
260
|
.doc(category)
|
|
149
|
-
.collection(
|
|
261
|
+
.collection(computationsSubcollection)
|
|
150
262
|
.doc(name);
|
|
151
263
|
|
|
152
264
|
const snap = await docRef.get();
|
|
153
|
-
if (!snap.exists)
|
|
265
|
+
if (!snap.exists) {
|
|
266
|
+
// Log the missing document path clearly
|
|
267
|
+
if (config.logger) {
|
|
268
|
+
config.logger.log('ERROR', `[DependencyFetcher] ❌ Document does not exist at: ${path}`);
|
|
269
|
+
} else {
|
|
270
|
+
console.error(`[DependencyFetcher] ❌ Document does not exist at: ${path}`);
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
154
274
|
|
|
155
275
|
let data = snap.data();
|
|
156
276
|
|
|
277
|
+
// Check if document exists but is effectively empty (only metadata)
|
|
278
|
+
if (isDataEmpty(data)) {
|
|
279
|
+
if (config.logger) {
|
|
280
|
+
config.logger.log('ERROR', `[DependencyFetcher] ❌ Document exists but is empty at: ${path} (only contains metadata fields)`);
|
|
281
|
+
} else {
|
|
282
|
+
console.error(`[DependencyFetcher] ❌ Document exists but is empty at: ${path} (only contains metadata fields)`);
|
|
283
|
+
}
|
|
284
|
+
return null; // Return null so it gets caught as missing
|
|
285
|
+
}
|
|
286
|
+
|
|
157
287
|
// 1. Handle Compression
|
|
158
288
|
if (data._compressed && data.payload) {
|
|
159
289
|
try {
|
|
@@ -165,27 +295,74 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
|
|
|
165
295
|
data = { ...data, ...realData };
|
|
166
296
|
delete data.payload;
|
|
167
297
|
} catch (e) {
|
|
168
|
-
|
|
298
|
+
const errorMsg = `Decompression failed for ${name}: ${e.message}`;
|
|
299
|
+
if (config.logger) {
|
|
300
|
+
config.logger.log('ERROR', `[DependencyFetcher] ❌ ${errorMsg} at: ${path}`);
|
|
301
|
+
} else {
|
|
302
|
+
console.error(`[DependencyFetcher] ❌ ${errorMsg} at: ${path}`);
|
|
303
|
+
}
|
|
169
304
|
return null;
|
|
170
305
|
}
|
|
171
306
|
}
|
|
172
307
|
|
|
173
308
|
// 2. Handle Sharding
|
|
174
309
|
if (data._sharded) {
|
|
310
|
+
const shardPath = `${path}/_shards`;
|
|
311
|
+
if (config.logger) {
|
|
312
|
+
config.logger.log('INFO', `[DependencyFetcher] 📂 Loading Shards for '${name}' from: ${shardPath}`);
|
|
313
|
+
} else {
|
|
314
|
+
console.log(`[DependencyFetcher] 📂 Loading Shards for '${name}' from: ${shardPath}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
175
317
|
const shardCol = docRef.collection('_shards');
|
|
176
318
|
const shardSnaps = await shardCol.get();
|
|
177
319
|
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
320
|
+
if (shardSnaps.empty) {
|
|
321
|
+
// No shards found - this is a problem
|
|
322
|
+
if (config.logger) {
|
|
323
|
+
config.logger.log('ERROR', `[DependencyFetcher] ❌ Document marked as sharded but no shards found at: ${shardPath}`);
|
|
324
|
+
} else {
|
|
325
|
+
console.error(`[DependencyFetcher] ❌ Document marked as sharded but no shards found at: ${shardPath}`);
|
|
326
|
+
}
|
|
327
|
+
return null; // Return null so it gets caught as missing
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Merge shard contents
|
|
331
|
+
let hasData = false;
|
|
332
|
+
shardSnaps.forEach(shard => {
|
|
333
|
+
const shardData = shard.data();
|
|
334
|
+
const shardId = shard.id;
|
|
335
|
+
if (config.logger) {
|
|
336
|
+
config.logger.log('TRACE', `[DependencyFetcher] 📂 Loading Shard '${shardId}' for '${name}' from: ${shardPath}/${shardId}`);
|
|
337
|
+
}
|
|
338
|
+
// Merge shard contents, ignoring internal metadata if it clashes
|
|
339
|
+
Object.entries(shardData).forEach(([k, v]) => {
|
|
340
|
+
if (!k.startsWith('_')) {
|
|
341
|
+
data[k] = v;
|
|
342
|
+
hasData = true;
|
|
343
|
+
}
|
|
187
344
|
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// If shards contained no actual data, treat as empty
|
|
348
|
+
if (!hasData) {
|
|
349
|
+
if (config.logger) {
|
|
350
|
+
config.logger.log('ERROR', `[DependencyFetcher] ❌ Shards found but contain no data at: ${shardPath}`);
|
|
351
|
+
} else {
|
|
352
|
+
console.error(`[DependencyFetcher] ❌ Shards found but contain no data at: ${shardPath}`);
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Final validation: ensure we have usable data after all processing
|
|
359
|
+
if (isDataEmpty(data)) {
|
|
360
|
+
if (config.logger) {
|
|
361
|
+
config.logger.log('ERROR', `[DependencyFetcher] ❌ Dependency '${name}' loaded but is empty (no usable data) at: ${path}`);
|
|
362
|
+
} else {
|
|
363
|
+
console.error(`[DependencyFetcher] ❌ Dependency '${name}' loaded but is empty (no usable data) at: ${path}`);
|
|
188
364
|
}
|
|
365
|
+
return null;
|
|
189
366
|
}
|
|
190
367
|
|
|
191
368
|
return data;
|