bulltrackers-module 1.0.646 → 1.0.648
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,22 @@ 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
|
+
// CRITICAL: Don't check if empty yet - we need to load shards/compressed data first
|
|
278
|
+
// A sharded document will only have metadata in the pointer doc, but the actual data is in shards
|
|
279
|
+
// A compressed document will only have metadata + payload, but the actual data is in the payload
|
|
280
|
+
|
|
175
281
|
// 1. Handle Compression
|
|
176
282
|
if (data._compressed && data.payload) {
|
|
177
283
|
try {
|
|
@@ -183,12 +289,17 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
|
|
|
183
289
|
data = { ...data, ...realData };
|
|
184
290
|
delete data.payload;
|
|
185
291
|
} catch (e) {
|
|
186
|
-
|
|
292
|
+
const errorMsg = `Decompression failed for ${name}: ${e.message}`;
|
|
293
|
+
if (config.logger) {
|
|
294
|
+
config.logger.log('ERROR', `[DependencyFetcher] ❌ ${errorMsg} at: ${path}`);
|
|
295
|
+
} else {
|
|
296
|
+
console.error(`[DependencyFetcher] ❌ ${errorMsg} at: ${path}`);
|
|
297
|
+
}
|
|
187
298
|
return null;
|
|
188
299
|
}
|
|
189
300
|
}
|
|
190
301
|
|
|
191
|
-
// 2. Handle Sharding
|
|
302
|
+
// 2. Handle Sharding (MUST happen before empty check)
|
|
192
303
|
if (data._sharded) {
|
|
193
304
|
const shardPath = `${path}/_shards`;
|
|
194
305
|
if (config.logger) {
|
|
@@ -200,21 +311,95 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
|
|
|
200
311
|
const shardCol = docRef.collection('_shards');
|
|
201
312
|
const shardSnaps = await shardCol.get();
|
|
202
313
|
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
314
|
+
if (shardSnaps.empty) {
|
|
315
|
+
// No shards found - this is a problem
|
|
316
|
+
if (config.logger) {
|
|
317
|
+
config.logger.log('ERROR', `[DependencyFetcher] ❌ Document marked as sharded but no shards found at: ${shardPath}`);
|
|
318
|
+
} else {
|
|
319
|
+
console.error(`[DependencyFetcher] ❌ Document marked as sharded but no shards found at: ${shardPath}`);
|
|
320
|
+
}
|
|
321
|
+
return null; // Return null so it gets caught as missing
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Merge shard contents
|
|
325
|
+
let hasData = false;
|
|
326
|
+
shardSnaps.forEach(shard => {
|
|
327
|
+
let shardData = shard.data();
|
|
328
|
+
const shardId = shard.id;
|
|
329
|
+
if (config.logger) {
|
|
330
|
+
config.logger.log('TRACE', `[DependencyFetcher] 📂 Loading Shard '${shardId}' for '${name}' from: ${shardPath}/${shardId}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// CRITICAL: Shards themselves can be compressed (common in big data)
|
|
334
|
+
// Decompress the shard if needed before merging
|
|
335
|
+
if (shardData._compressed && shardData.payload) {
|
|
336
|
+
try {
|
|
337
|
+
const buffer = (shardData.payload instanceof Buffer) ? shardData.payload :
|
|
338
|
+
(shardData.payload._byteString ? Buffer.from(shardData.payload._byteString, 'base64') :
|
|
339
|
+
Buffer.from(shardData.payload));
|
|
340
|
+
const decompressed = zlib.gunzipSync(buffer);
|
|
341
|
+
const jsonStr = decompressed.toString('utf8');
|
|
342
|
+
const realData = JSON.parse(jsonStr);
|
|
343
|
+
// If it's double-encoded, parse again
|
|
344
|
+
const parsedData = (typeof realData === 'string') ? JSON.parse(realData) : realData;
|
|
345
|
+
shardData = { ...shardData, ...parsedData };
|
|
346
|
+
delete shardData.payload;
|
|
347
|
+
} catch (e) {
|
|
348
|
+
if (config.logger) {
|
|
349
|
+
config.logger.log('ERROR', `[DependencyFetcher] ❌ Failed to decompress shard '${shardId}' for '${name}': ${e.message}`);
|
|
350
|
+
} else {
|
|
351
|
+
console.error(`[DependencyFetcher] ❌ Failed to decompress shard '${shardId}' for '${name}': ${e.message}`);
|
|
214
352
|
}
|
|
215
|
-
|
|
353
|
+
// Continue with uncompressed data if decompression fails
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Merge shard contents, ignoring internal metadata if it clashes
|
|
358
|
+
Object.entries(shardData).forEach(([k, v]) => {
|
|
359
|
+
if (!k.startsWith('_')) {
|
|
360
|
+
data[k] = v;
|
|
361
|
+
hasData = true;
|
|
362
|
+
}
|
|
216
363
|
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// If shards contained no actual data, treat as empty
|
|
367
|
+
if (!hasData) {
|
|
368
|
+
if (config.logger) {
|
|
369
|
+
config.logger.log('ERROR', `[DependencyFetcher] ❌ Shards found but contain no data at: ${shardPath}`);
|
|
370
|
+
} else {
|
|
371
|
+
console.error(`[DependencyFetcher] ❌ Shards found but contain no data at: ${shardPath}`);
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// After loading shards, remove shard metadata from data object for cleaner output
|
|
377
|
+
// Keep only the actual data fields
|
|
378
|
+
const cleanedData = {};
|
|
379
|
+
const dataKeys = [];
|
|
380
|
+
Object.entries(data).forEach(([k, v]) => {
|
|
381
|
+
if (!k.startsWith('_')) {
|
|
382
|
+
cleanedData[k] = v;
|
|
383
|
+
dataKeys.push(k);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
data = cleanedData;
|
|
387
|
+
|
|
388
|
+
// Log what we loaded for debugging
|
|
389
|
+
if (config.logger) {
|
|
390
|
+
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'}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Final validation: ensure we have usable data after all processing (decompression + sharding)
|
|
395
|
+
// Only check if we haven't already determined it's empty
|
|
396
|
+
if (isDataEmpty(data)) {
|
|
397
|
+
if (config.logger) {
|
|
398
|
+
config.logger.log('ERROR', `[DependencyFetcher] ❌ Dependency '${name}' loaded but is empty (no usable data) at: ${path}`);
|
|
399
|
+
} else {
|
|
400
|
+
console.error(`[DependencyFetcher] ❌ Dependency '${name}' loaded but is empty (no usable data) at: ${path}`);
|
|
217
401
|
}
|
|
402
|
+
return null;
|
|
218
403
|
}
|
|
219
404
|
|
|
220
405
|
return data;
|