bulltrackers-module 1.0.646 → 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.
|
@@ -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
|
|
|
@@ -69,27 +100,83 @@ async function fetchDependencies(date, calcs, config, deps, manifestLookup = {})
|
|
|
69
100
|
logger.log('INFO', `[DependencyFetcher] Fetching ${needed.size} dependencies for computation(s): ${calcNames} (date: ${dStr})`);
|
|
70
101
|
|
|
71
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
|
+
|
|
72
111
|
// CHANGED: Iterate over the entries to access both normalized and original names
|
|
73
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
|
+
|
|
74
118
|
try {
|
|
75
|
-
// Resolve Category from Lookup, default to 'analytics' if unknown
|
|
76
|
-
// Note: manifestLookup keys are expected to be normalized
|
|
77
|
-
const category = manifestLookup[normName] || 'analytics';
|
|
78
|
-
|
|
79
119
|
// Pass logger in config for fetchSingleResult
|
|
80
120
|
const fetchConfig = { ...config, logger };
|
|
81
121
|
|
|
82
122
|
// Fetch using the normalized name (system standard)
|
|
83
123
|
const data = await fetchSingleResult(db, fetchConfig, dStr, normName, category);
|
|
84
124
|
|
|
85
|
-
//
|
|
86
|
-
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
|
+
}
|
|
87
146
|
} catch (e) {
|
|
88
|
-
|
|
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
|
+
}
|
|
89
154
|
}
|
|
90
155
|
});
|
|
91
156
|
|
|
92
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
|
+
|
|
93
180
|
return results;
|
|
94
181
|
}
|
|
95
182
|
|
|
@@ -126,10 +213,17 @@ async function fetchResultSeries(endDateStr, calcNames, manifestLookup, config,
|
|
|
126
213
|
fetchOps.push(async () => {
|
|
127
214
|
const fetchConfig = { ...config, logger };
|
|
128
215
|
const val = await fetchSingleResult(db, fetchConfig, dateStr, rawName, category);
|
|
129
|
-
|
|
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)) {
|
|
130
220
|
if (!results[normName]) results[normName] = {};
|
|
131
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)`);
|
|
132
225
|
}
|
|
226
|
+
// If val is null, that's fine - missing dates in historical series are permissible
|
|
133
227
|
});
|
|
134
228
|
}
|
|
135
229
|
}
|
|
@@ -168,10 +262,28 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
|
|
|
168
262
|
.doc(name);
|
|
169
263
|
|
|
170
264
|
const snap = await docRef.get();
|
|
171
|
-
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
|
+
}
|
|
172
274
|
|
|
173
275
|
let data = snap.data();
|
|
174
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
|
+
|
|
175
287
|
// 1. Handle Compression
|
|
176
288
|
if (data._compressed && data.payload) {
|
|
177
289
|
try {
|
|
@@ -183,7 +295,12 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
|
|
|
183
295
|
data = { ...data, ...realData };
|
|
184
296
|
delete data.payload;
|
|
185
297
|
} catch (e) {
|
|
186
|
-
|
|
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
|
+
}
|
|
187
304
|
return null;
|
|
188
305
|
}
|
|
189
306
|
}
|
|
@@ -200,21 +317,52 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
|
|
|
200
317
|
const shardCol = docRef.collection('_shards');
|
|
201
318
|
const shardSnaps = await shardCol.get();
|
|
202
319
|
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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;
|
|
209
343
|
}
|
|
210
|
-
// Merge shard contents, ignoring internal metadata if it clashes
|
|
211
|
-
Object.entries(shardData).forEach(([k, v]) => {
|
|
212
|
-
if (!k.startsWith('_')) {
|
|
213
|
-
data[k] = v;
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
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}`);
|
|
217
364
|
}
|
|
365
|
+
return null;
|
|
218
366
|
}
|
|
219
367
|
|
|
220
368
|
return data;
|