bulltrackers-module 1.0.727 → 1.0.729
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.
|
@@ -142,7 +142,7 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
|
|
|
142
142
|
return JSON.parse(fileContent.toString('utf8'));
|
|
143
143
|
}
|
|
144
144
|
} catch (e) {
|
|
145
|
-
log.
|
|
145
|
+
log.log('ERROR', `[DependencyFetcher] ❌ GCS Fetch Failed for ${name}: ${e.message}`);
|
|
146
146
|
// Depending on strictness, we might return null here or allow it to fail hard.
|
|
147
147
|
// Returning null allows 'isDataEmpty' to catch it as "MISSING"
|
|
148
148
|
return null;
|
|
@@ -158,7 +158,7 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
|
|
|
158
158
|
data = { ...data, ...realData }; // Merge payload into base
|
|
159
159
|
delete data.payload;
|
|
160
160
|
} catch (e) {
|
|
161
|
-
log.
|
|
161
|
+
log.log('ERROR', `[DependencyFetcher] ❌ ${e.message} at ${docRef.path}`);
|
|
162
162
|
return null;
|
|
163
163
|
}
|
|
164
164
|
}
|
|
@@ -169,7 +169,7 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
|
|
|
169
169
|
if (data._sharded) {
|
|
170
170
|
const shardSnaps = await docRef.collection('_shards').get();
|
|
171
171
|
if (shardSnaps.empty) {
|
|
172
|
-
log.
|
|
172
|
+
log.log('ERROR', `[DependencyFetcher] ❌ Sharded doc has no shards: ${docRef.path}`);
|
|
173
173
|
return null;
|
|
174
174
|
}
|
|
175
175
|
|
|
@@ -186,7 +186,7 @@ async function fetchSingleResult(db, config, dateStr, name, category) {
|
|
|
186
186
|
const decomp = tryDecompress(sData.payload);
|
|
187
187
|
sData = (typeof decomp === 'string') ? JSON.parse(decomp) : decomp;
|
|
188
188
|
} catch (e) {
|
|
189
|
-
log.
|
|
189
|
+
log.log('ERROR', `[DependencyFetcher] ❌ Shard decompression failed (${shard.id}): ${e.message}`);
|
|
190
190
|
continue;
|
|
191
191
|
}
|
|
192
192
|
}
|
|
@@ -321,4 +321,251 @@ async function loadRetailFirestore(db, collectionName, dateStr) {
|
|
|
321
321
|
} catch (e) {
|
|
322
322
|
return {};
|
|
323
323
|
}
|
|
324
|
-
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// =============================================================================
|
|
327
|
+
// 8. STREAMING DATA (For StandardExecutor)
|
|
328
|
+
// =============================================================================
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get portfolio part references for streaming.
|
|
332
|
+
* In BigQuery mode, returns virtual refs that signal to load from BQ.
|
|
333
|
+
* @param {object} config - Configuration object
|
|
334
|
+
* @param {object} deps - Dependencies (db, logger)
|
|
335
|
+
* @param {string} dateStr - Date string (YYYY-MM-DD)
|
|
336
|
+
* @param {Array<string>|null} userTypes - User types to filter (null = all)
|
|
337
|
+
* @returns {Promise<Array>} Array of part references
|
|
338
|
+
*/
|
|
339
|
+
exports.getPortfolioPartRefs = async (config, deps, dateStr, userTypes = null) => {
|
|
340
|
+
const { logger } = deps;
|
|
341
|
+
|
|
342
|
+
// In BigQuery mode, return virtual refs with metadata
|
|
343
|
+
if (process.env.BIGQUERY_ENABLED !== 'false') {
|
|
344
|
+
logger.log('INFO', `[DataLoader] Using BigQuery virtual refs for portfolios (${dateStr})`);
|
|
345
|
+
return [{
|
|
346
|
+
_bigquery: true,
|
|
347
|
+
type: 'portfolio',
|
|
348
|
+
date: dateStr,
|
|
349
|
+
userTypes: userTypes
|
|
350
|
+
}];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Firestore fallback - return actual document references
|
|
354
|
+
const { db } = deps;
|
|
355
|
+
const refs = [];
|
|
356
|
+
|
|
357
|
+
// Check both PI and SignedIn collections
|
|
358
|
+
const collections = [
|
|
359
|
+
{ name: 'PopularInvestorPortfolios', userType: 'POPULAR_INVESTOR' },
|
|
360
|
+
{ name: 'SignedInUserPortfolios', userType: 'SIGNED_IN_USER' }
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
for (const col of collections) {
|
|
364
|
+
if (userTypes && !userTypes.includes(col.userType)) continue;
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const partsSnap = await db.collection(col.name)
|
|
368
|
+
.doc('latest')
|
|
369
|
+
.collection('snapshots')
|
|
370
|
+
.doc(dateStr)
|
|
371
|
+
.collection('parts')
|
|
372
|
+
.listDocuments();
|
|
373
|
+
|
|
374
|
+
for (const docRef of partsSnap) {
|
|
375
|
+
refs.push({ ref: docRef, userType: col.userType, collection: col.name });
|
|
376
|
+
}
|
|
377
|
+
} catch (e) {
|
|
378
|
+
logger.log('WARN', `[DataLoader] Failed to get portfolio refs from ${col.name}: ${e.message}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return refs;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Get history part references for streaming.
|
|
387
|
+
* @param {object} config - Configuration object
|
|
388
|
+
* @param {object} deps - Dependencies (db, logger)
|
|
389
|
+
* @param {string} dateStr - Date string (YYYY-MM-DD)
|
|
390
|
+
* @param {Array<string>|null} userTypes - User types to filter (null = all)
|
|
391
|
+
* @returns {Promise<Array>} Array of part references
|
|
392
|
+
*/
|
|
393
|
+
exports.getHistoryPartRefs = async (config, deps, dateStr, userTypes = null) => {
|
|
394
|
+
const { logger } = deps;
|
|
395
|
+
|
|
396
|
+
// In BigQuery mode, return virtual refs with metadata
|
|
397
|
+
if (process.env.BIGQUERY_ENABLED !== 'false') {
|
|
398
|
+
logger.log('INFO', `[DataLoader] Using BigQuery virtual refs for history (${dateStr})`);
|
|
399
|
+
return [{
|
|
400
|
+
_bigquery: true,
|
|
401
|
+
type: 'history',
|
|
402
|
+
date: dateStr,
|
|
403
|
+
userTypes: userTypes
|
|
404
|
+
}];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Firestore fallback - return actual document references
|
|
408
|
+
const { db } = deps;
|
|
409
|
+
const refs = [];
|
|
410
|
+
|
|
411
|
+
const collections = [
|
|
412
|
+
{ name: 'PopularInvestorTradeHistory', userType: 'POPULAR_INVESTOR' },
|
|
413
|
+
{ name: 'SignedInUserTradeHistory', userType: 'SIGNED_IN_USER' }
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
for (const col of collections) {
|
|
417
|
+
if (userTypes && !userTypes.includes(col.userType)) continue;
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const partsSnap = await db.collection(col.name)
|
|
421
|
+
.doc('latest')
|
|
422
|
+
.collection('snapshots')
|
|
423
|
+
.doc(dateStr)
|
|
424
|
+
.collection('parts')
|
|
425
|
+
.listDocuments();
|
|
426
|
+
|
|
427
|
+
for (const docRef of partsSnap) {
|
|
428
|
+
refs.push({ ref: docRef, userType: col.userType, collection: col.name });
|
|
429
|
+
}
|
|
430
|
+
} catch (e) {
|
|
431
|
+
logger.log('WARN', `[DataLoader] Failed to get history refs from ${col.name}: ${e.message}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return refs;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Stream portfolio data in chunks (async generator).
|
|
440
|
+
* Yields objects of { cid: portfolioData } for memory efficiency.
|
|
441
|
+
* @param {object} config - Configuration object
|
|
442
|
+
* @param {object} deps - Dependencies (db, logger)
|
|
443
|
+
* @param {string} dateStr - Date string (YYYY-MM-DD)
|
|
444
|
+
* @param {Array} refs - Part references from getPortfolioPartRefs
|
|
445
|
+
* @param {Array<string>|null} userTypes - User types to filter
|
|
446
|
+
* @yields {object} Chunk of portfolio data { cid: data }
|
|
447
|
+
*/
|
|
448
|
+
exports.streamPortfolioData = async function* (config, deps, dateStr, refs, userTypes = null) {
|
|
449
|
+
const { logger } = deps;
|
|
450
|
+
|
|
451
|
+
if (!refs || refs.length === 0) {
|
|
452
|
+
logger.log('WARN', `[DataLoader] No portfolio refs provided for streaming (${dateStr})`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// BigQuery mode - load all at once and yield in chunks
|
|
457
|
+
const isBigQuery = refs.some(r => r && r._bigquery === true);
|
|
458
|
+
|
|
459
|
+
if (isBigQuery && process.env.BIGQUERY_ENABLED !== 'false') {
|
|
460
|
+
try {
|
|
461
|
+
const types = userTypes || ['POPULAR_INVESTOR', 'SIGNED_IN_USER'];
|
|
462
|
+
const data = await queryPortfolioData(dateStr, null, types, logger);
|
|
463
|
+
|
|
464
|
+
if (data && Object.keys(data).length > 0) {
|
|
465
|
+
logger.log('INFO', `[DataLoader] ✅ Streaming ${Object.keys(data).length} portfolios from BigQuery (${dateStr})`);
|
|
466
|
+
|
|
467
|
+
// Yield in chunks of 100 users for memory efficiency
|
|
468
|
+
const CHUNK_SIZE = 100;
|
|
469
|
+
const entries = Object.entries(data);
|
|
470
|
+
|
|
471
|
+
for (let i = 0; i < entries.length; i += CHUNK_SIZE) {
|
|
472
|
+
const chunk = {};
|
|
473
|
+
entries.slice(i, i + CHUNK_SIZE).forEach(([cid, portfolio]) => {
|
|
474
|
+
chunk[cid] = portfolio;
|
|
475
|
+
});
|
|
476
|
+
yield chunk;
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
} catch (e) {
|
|
481
|
+
logger.log('ERROR', `[DataLoader] BigQuery portfolio stream failed: ${e.message}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Firestore fallback - stream from refs
|
|
486
|
+
for (const refInfo of refs) {
|
|
487
|
+
if (!refInfo.ref) continue;
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
const snap = await refInfo.ref.get();
|
|
491
|
+
if (snap.exists) {
|
|
492
|
+
const data = snap.data();
|
|
493
|
+
// Add user type metadata
|
|
494
|
+
const enriched = {};
|
|
495
|
+
for (const [cid, portfolio] of Object.entries(data)) {
|
|
496
|
+
enriched[cid] = { ...portfolio, _userType: refInfo.userType };
|
|
497
|
+
}
|
|
498
|
+
yield enriched;
|
|
499
|
+
}
|
|
500
|
+
} catch (e) {
|
|
501
|
+
logger.log('WARN', `[DataLoader] Failed to stream portfolio part: ${e.message}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Stream history data in chunks (async generator).
|
|
508
|
+
* @param {object} config - Configuration object
|
|
509
|
+
* @param {object} deps - Dependencies (db, logger)
|
|
510
|
+
* @param {string} dateStr - Date string (YYYY-MM-DD)
|
|
511
|
+
* @param {Array} refs - Part references from getHistoryPartRefs
|
|
512
|
+
* @param {Array<string>|null} userTypes - User types to filter
|
|
513
|
+
* @yields {object} Chunk of history data { cid: data }
|
|
514
|
+
*/
|
|
515
|
+
exports.streamHistoryData = async function* (config, deps, dateStr, refs, userTypes = null) {
|
|
516
|
+
const { logger } = deps;
|
|
517
|
+
|
|
518
|
+
if (!refs || refs.length === 0) {
|
|
519
|
+
logger.log('WARN', `[DataLoader] No history refs provided for streaming (${dateStr})`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// BigQuery mode - load all at once and yield in chunks
|
|
524
|
+
const isBigQuery = refs.some(r => r && r._bigquery === true);
|
|
525
|
+
|
|
526
|
+
if (isBigQuery && process.env.BIGQUERY_ENABLED !== 'false') {
|
|
527
|
+
try {
|
|
528
|
+
const types = userTypes || ['POPULAR_INVESTOR', 'SIGNED_IN_USER'];
|
|
529
|
+
const data = await queryHistoryData(dateStr, null, types, logger);
|
|
530
|
+
|
|
531
|
+
if (data && Object.keys(data).length > 0) {
|
|
532
|
+
logger.log('INFO', `[DataLoader] ✅ Streaming ${Object.keys(data).length} history records from BigQuery (${dateStr})`);
|
|
533
|
+
|
|
534
|
+
// Yield in chunks of 100 users
|
|
535
|
+
const CHUNK_SIZE = 100;
|
|
536
|
+
const entries = Object.entries(data);
|
|
537
|
+
|
|
538
|
+
for (let i = 0; i < entries.length; i += CHUNK_SIZE) {
|
|
539
|
+
const chunk = {};
|
|
540
|
+
entries.slice(i, i + CHUNK_SIZE).forEach(([cid, history]) => {
|
|
541
|
+
chunk[cid] = history;
|
|
542
|
+
});
|
|
543
|
+
yield chunk;
|
|
544
|
+
}
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
} catch (e) {
|
|
548
|
+
logger.log('ERROR', `[DataLoader] BigQuery history stream failed: ${e.message}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Firestore fallback - stream from refs
|
|
553
|
+
for (const refInfo of refs) {
|
|
554
|
+
if (!refInfo.ref) continue;
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const snap = await refInfo.ref.get();
|
|
558
|
+
if (snap.exists) {
|
|
559
|
+
const data = snap.data();
|
|
560
|
+
// Add user type metadata
|
|
561
|
+
const enriched = {};
|
|
562
|
+
for (const [cid, history] of Object.entries(data)) {
|
|
563
|
+
enriched[cid] = { ...history, _userType: refInfo.userType };
|
|
564
|
+
}
|
|
565
|
+
yield enriched;
|
|
566
|
+
}
|
|
567
|
+
} catch (e) {
|
|
568
|
+
logger.log('WARN', `[DataLoader] Failed to stream history part: ${e.message}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
};
|
|
@@ -55,11 +55,32 @@ async function withRetry(fn, operationName, maxRetries = 3) {
|
|
|
55
55
|
|
|
56
56
|
/** * Recursively sanitizes data for Firestore compatibility.
|
|
57
57
|
* Converts 'undefined' to 'null' to prevent validation errors.
|
|
58
|
+
* Converts invalid Dates to null to prevent Timestamp serialization errors.
|
|
59
|
+
* Converts NaN numbers to null to prevent serialization errors.
|
|
58
60
|
*/
|
|
59
61
|
function sanitizeForFirestore(data) {
|
|
60
62
|
if (data === undefined) return null;
|
|
61
63
|
if (data === null) return null;
|
|
62
|
-
|
|
64
|
+
|
|
65
|
+
// Handle Date objects - validate they're not Invalid Date (NaN timestamp)
|
|
66
|
+
if (data instanceof Date) {
|
|
67
|
+
if (isNaN(data.getTime())) {
|
|
68
|
+
// Invalid Date - return null instead of crashing Firestore
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle NaN numbers (would cause serialization issues)
|
|
75
|
+
if (typeof data === 'number' && isNaN(data)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Handle Infinity (also not valid for Firestore)
|
|
80
|
+
if (data === Infinity || data === -Infinity) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
// Handle Firestore Types if necessary (e.g. GeoPoint, Timestamp), passed through as objects usually.
|
|
64
85
|
|
|
65
86
|
if (Array.isArray(data)) {
|