bulltrackers-module 1.0.536 → 1.0.538

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.
@@ -5,7 +5,7 @@
5
5
  * }
6
6
  */
7
7
  const { normalizeName, getEarliestDataDates } = require('../utils/utils');
8
- const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs } = require('../utils/data_loader');
8
+ const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs, getHistoryPartRefs } = require('../utils/data_loader');
9
9
  const { CachedDataLoader } = require('../data/CachedDataLoader');
10
10
  const { ContextFactory } = require('../context/ContextFactory');
11
11
  const { commitResults } = require('../persistence/ResultCommitter');
@@ -24,15 +24,31 @@ class StandardExecutor {
24
24
  const type = (c.userType || 'ALL').toUpperCase();
25
25
  requiredUserTypes.add(type);
26
26
  });
27
- // If any calc requires 'ALL' (or has no userType), we fetch everything
28
27
  const userTypeArray = requiredUserTypes.has('ALL') ? null : Array.from(requiredUserTypes);
29
28
 
29
+ // [OPTIMIZATION] Check for Target CID in manifests (On-Demand Optimization)
30
+ // If present, we will filter all data streams to strictly this user
31
+ const targetCid = calcs.find(c => c.targetCid)?.targetCid || calcs.find(c => c.manifest?.targetCid)?.manifest?.targetCid;
32
+ if (targetCid) {
33
+ logger.log('INFO', `[StandardExecutor] Running in Targeted Mode for CID: ${targetCid}`);
34
+ }
35
+
30
36
  const fullRoot = { ...rootData };
31
37
  if (calcs.some(c => c.isHistorical)) {
32
38
  const prev = new Date(date); prev.setUTCDate(prev.getUTCDate() - 1);
33
39
  const prevStr = prev.toISOString().slice(0, 10);
34
- // [FIX] Pass userTypeArray to filter yesterday's data loading
35
- fullRoot.yesterdayPortfolioRefs = await getPortfolioPartRefs(config, deps, prevStr, userTypeArray);
40
+
41
+ // Fetch yesterday's refs
42
+ let yRefs = await getPortfolioPartRefs(config, deps, prevStr, userTypeArray);
43
+
44
+ // [OPTIMIZATION] Filter Yesterday's Refs if targetCid is set
45
+ if (targetCid && yRefs) {
46
+ const originalCount = yRefs.length;
47
+ yRefs = yRefs.filter(r => !r.cid || String(r.cid) === String(targetCid));
48
+ logger.log('INFO', `[StandardExecutor] Filtered Yesterday's Refs: ${originalCount} -> ${yRefs.length}`);
49
+ }
50
+
51
+ fullRoot.yesterdayPortfolioRefs = yRefs;
36
52
  }
37
53
 
38
54
  const state = {};
@@ -46,18 +62,48 @@ class StandardExecutor {
46
62
  } catch (e) { logger.log('WARN', `Failed to init ${c.name}`); }
47
63
  }
48
64
 
49
- return await StandardExecutor.streamAndProcess(dStr, state, passName, config, deps, fullRoot, rootData.portfolioRefs, rootData.historyRefs, fetchedDeps, previousFetchedDeps, skipStatusWrite, userTypeArray);
65
+ // Pass targetCid to streamAndProcess
66
+ return await StandardExecutor.streamAndProcess(
67
+ dStr, state, passName, config, deps, fullRoot,
68
+ rootData.portfolioRefs, rootData.historyRefs,
69
+ fetchedDeps, previousFetchedDeps, skipStatusWrite,
70
+ userTypeArray, targetCid
71
+ );
50
72
  }
51
73
 
52
- // [UPDATED] Accepts requiredUserTypes param
53
- static async streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps, previousFetchedDeps, skipStatusWrite, requiredUserTypes = null) {
74
+ // [UPDATED] Added targetCid param
75
+ static async streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps, previousFetchedDeps, skipStatusWrite, requiredUserTypes = null, targetCid = null) {
54
76
  const { logger } = deps;
55
77
  const calcs = Object.values(state).filter(c => c && c.manifest);
56
78
  const streamingCalcs = calcs.filter(c => c.manifest.rootDataDependencies.includes('portfolio') || c.manifest.rootDataDependencies.includes('history'));
57
79
 
58
80
  if (streamingCalcs.length === 0) return { successUpdates: {}, failureReport: [] };
81
+
82
+ // --- 1. Resolve and Filter Portfolio Refs (Today) ---
83
+ let effectivePortfolioRefs = portfolioRefs;
84
+ if (!effectivePortfolioRefs) {
85
+ // If refs weren't provided by AvailabilityChecker, fetch them now
86
+ effectivePortfolioRefs = await getPortfolioPartRefs(config, deps, dateStr, requiredUserTypes);
87
+ }
88
+ if (targetCid && effectivePortfolioRefs) {
89
+ // Filter: Keep only refs that match the CID (or Legacy refs without CID)
90
+ effectivePortfolioRefs = effectivePortfolioRefs.filter(r => !r.cid || String(r.cid) === String(targetCid));
91
+ }
92
+
93
+ // --- 2. Resolve and Filter History Refs ---
94
+ let effectiveHistoryRefs = historyRefs;
95
+ const needsTradingHistory = streamingCalcs.some(c => c.manifest.rootDataDependencies.includes('history'));
96
+
97
+ if (needsTradingHistory) {
98
+ if (!effectiveHistoryRefs) {
99
+ effectiveHistoryRefs = await getHistoryPartRefs(config, deps, dateStr, requiredUserTypes);
100
+ }
101
+ if (targetCid && effectiveHistoryRefs) {
102
+ effectiveHistoryRefs = effectiveHistoryRefs.filter(r => !r.cid || String(r.cid) === String(targetCid));
103
+ }
104
+ }
59
105
 
60
- let totalReadOps = (portfolioRefs?.length || 0) + (historyRefs?.length || 0);
106
+ let totalReadOps = (effectivePortfolioRefs?.length || 0) + (effectiveHistoryRefs?.length || 0);
61
107
  if (rootData.yesterdayPortfolioRefs) totalReadOps += rootData.yesterdayPortfolioRefs.length;
62
108
  totalReadOps += 2;
63
109
 
@@ -83,6 +129,7 @@ class StandardExecutor {
83
129
  const setupDuration = performance.now() - startSetup;
84
130
  Object.keys(executionStats).forEach(name => executionStats[name].timings.setup += setupDuration);
85
131
 
132
+ // Yesterday's Refs are already filtered in run()
86
133
  const prevDate = new Date(dateStr + 'T00:00:00Z'); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
87
134
  const prevDateStr = prevDate.toISOString().slice(0, 10);
88
135
 
@@ -91,17 +138,13 @@ class StandardExecutor {
91
138
  earliestDates = await getEarliestDataDates(config, deps);
92
139
  }
93
140
 
94
- // [FIX] Pass requiredUserTypes to streamPortfolioData
95
- // Note: portfolioRefs (for today) might be null if not pre-fetched, allowing streamPortfolioData to fetch filtered refs
96
- const tP_iter = streamPortfolioData(config, deps, dateStr, portfolioRefs, requiredUserTypes);
141
+ // [FIX] Use effective/filtered refs
142
+ const tP_iter = streamPortfolioData(config, deps, dateStr, effectivePortfolioRefs, requiredUserTypes);
97
143
 
98
144
  const needsYesterdayPortfolio = streamingCalcs.some(c => c.manifest.isHistorical);
99
- // Yesterday's refs were already filtered in run(), so we pass them directly
100
145
  const yP_iter = (needsYesterdayPortfolio && rootData.yesterdayPortfolioRefs) ? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs) : null;
101
146
 
102
- const needsTradingHistory = streamingCalcs.some(c => c.manifest.rootDataDependencies.includes('history'));
103
- // [FIX] Pass requiredUserTypes to streamHistoryData
104
- const tH_iter = (needsTradingHistory) ? streamHistoryData(config, deps, dateStr, historyRefs, requiredUserTypes) : null;
147
+ const tH_iter = (needsTradingHistory) ? streamHistoryData(config, deps, dateStr, effectiveHistoryRefs, requiredUserTypes) : null;
105
148
 
106
149
  let yP_chunk = {}, tH_chunk = {};
107
150
  let usersSinceLastFlush = 0;
@@ -159,7 +202,7 @@ class StandardExecutor {
159
202
 
160
203
  // ... rest of the file (flushBuffer, mergeReports, executePerUser) ...
161
204
  static async flushBuffer(state, dateStr, passName, config, deps, shardIndexMap, executionStats, mode, skipStatusWrite, isInitialWrite = false) {
162
- // ... (No changes to flushBuffer)
205
+ const { logger } = deps;
163
206
  const transformedState = {};
164
207
  for (const [name, inst] of Object.entries(state)) {
165
208
  const rawResult = inst.results || {};
@@ -197,7 +240,6 @@ class StandardExecutor {
197
240
  }
198
241
 
199
242
  static mergeReports(successAcc, failureAcc, newResult) {
200
- // ... (No changes to mergeReports)
201
243
  if (!newResult) return;
202
244
  for (const [name, update] of Object.entries(newResult.successUpdates)) {
203
245
  if (!successAcc[name]) {
@@ -232,24 +274,23 @@ class StandardExecutor {
232
274
  const targetUserType = metadata.userType;
233
275
  // [NEW] Always load Global Helpers
234
276
  const mappings = await loader.loadMappings();
235
- const piMasterList = await loader.loadPIMasterList(); // [NEW] Loaded globally
277
+ const piMasterList = await loader.loadPopularInvestorMasterList(config, deps); // [NEW] Loaded globally
236
278
  const SCHEMAS = mathLayer.SCHEMAS;
237
279
 
238
280
  // 1. Load Root Data
239
281
  const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await loader.loadInsights(dateStr) } : null;
240
- const verifications = metadata.rootDataDependencies?.includes('verification') ? await loader.loadVerifications() : null;
241
- const rankings = metadata.rootDataDependencies?.includes('rankings') ? await loader.loadRankings(dateStr) : null;
282
+ const verifications = metadata.rootDataDependencies?.includes('verification') ? await loader.loadVerificationProfiles(config, deps) : null;
283
+ const rankings = metadata.rootDataDependencies?.includes('rankings') ? await loader.loadPopularInvestorRankings(config, deps, dateStr) : null;
242
284
 
243
285
  // [FIX] Load Yesterday's Rankings if isHistorical is true
244
286
  let yesterdayRankings = null;
245
287
  if (metadata.rootDataDependencies?.includes('rankings') && metadata.isHistorical) {
246
288
  const prevDate = new Date(dateStr); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
247
289
  const prevStr = prevDate.toISOString().slice(0, 10);
248
- // Assuming CachedDataLoader handles caching for efficiency
249
- yesterdayRankings = await loader.loadRankings(prevStr);
290
+ yesterdayRankings = await loader.loadPopularInvestorRankings(config, deps, prevStr);
250
291
  }
251
292
 
252
- const socialContainer = metadata.rootDataDependencies?.includes('social') ? await loader.loadSocial(dateStr) : null;
293
+ const socialContainer = metadata.rootDataDependencies?.includes('social') ? await loader.loadDailySocialPostInsights(config, deps, dateStr) : null;
253
294
 
254
295
  // [NEW] Load New Root Data Types for Profile Metrics
255
296
  // [FIX] Enforce canHaveMissingRoots
@@ -258,14 +299,13 @@ class StandardExecutor {
258
299
  let ratings = null;
259
300
  if (metadata.rootDataDependencies?.includes('ratings')) {
260
301
  try {
261
- ratings = await loader.loadRatings(dateStr);
302
+ ratings = await loader.loadPIRatings(config, deps, dateStr);
262
303
  } catch (e) {
263
304
  if (!allowMissing) {
264
305
  throw new Error(`[StandardExecutor] Required root 'ratings' failed to load for ${metadata.name}: ${e.message}`);
265
306
  }
266
307
  ratings = null;
267
308
  }
268
- // Check if result is null (clean fetch but empty/missing doc)
269
309
  if (!ratings && !allowMissing) {
270
310
  throw new Error(`[StandardExecutor] Required root 'ratings' is missing for ${metadata.name}`);
271
311
  }
@@ -274,7 +314,7 @@ class StandardExecutor {
274
314
  let pageViews = null;
275
315
  if (metadata.rootDataDependencies?.includes('pageViews')) {
276
316
  try {
277
- pageViews = await loader.loadPageViews(dateStr);
317
+ pageViews = await loader.loadPIPageViews(config, deps, dateStr);
278
318
  } catch (e) {
279
319
  if (!allowMissing) {
280
320
  throw new Error(`[StandardExecutor] Required root 'pageViews' failed to load for ${metadata.name}: ${e.message}`);
@@ -289,7 +329,7 @@ class StandardExecutor {
289
329
  let watchlistMembership = null;
290
330
  if (metadata.rootDataDependencies?.includes('watchlist')) {
291
331
  try {
292
- watchlistMembership = await loader.loadWatchlistMembership(dateStr);
332
+ watchlistMembership = await loader.loadWatchlistMembership(config, deps, dateStr);
293
333
  } catch (e) {
294
334
  if (!allowMissing) {
295
335
  throw new Error(`[StandardExecutor] Required root 'watchlist' failed to load for ${metadata.name}: ${e.message}`);
@@ -304,7 +344,7 @@ class StandardExecutor {
304
344
  let alertHistory = null;
305
345
  if (metadata.rootDataDependencies?.includes('alerts')) {
306
346
  try {
307
- alertHistory = await loader.loadAlertHistory(dateStr);
347
+ alertHistory = await loader.loadPIAlertHistory(config, deps, dateStr);
308
348
  } catch (e) {
309
349
  if (!allowMissing) {
310
350
  throw new Error(`[StandardExecutor] Required root 'alerts' failed to load for ${metadata.name}: ${e.message}`);
@@ -350,7 +390,6 @@ class StandardExecutor {
350
390
 
351
391
  const userVerification = verifications ? verifications[userId] : null;
352
392
 
353
- // [FIX] Extract current AND yesterday's rank entry for this user
354
393
  const userRanking = rankings ? (rankings.find(r => String(r.CustomerId) === String(userId)) || null) : null;
355
394
  const userRankingYesterday = yesterdayRankings ? (yesterdayRankings.find(r => String(r.CustomerId) === String(userId)) || null) : null;
356
395
 
@@ -373,23 +412,19 @@ class StandardExecutor {
373
412
  config, deps,
374
413
  verification: userVerification,
375
414
 
376
- // [FIX] Pass both ranking entries
377
415
  rankings: userRanking,
378
416
  yesterdayRankings: userRankingYesterday,
379
417
 
380
- // [FIX] Pass both global lists
381
418
  allRankings: rankings,
382
419
  allRankingsYesterday: yesterdayRankings,
383
420
 
384
421
  allVerifications: verifications,
385
422
 
386
- // [NEW] Pass New Root Data Types for Profile Metrics
387
423
  ratings: ratings || {},
388
424
  pageViews: pageViews || {},
389
425
  watchlistMembership: watchlistMembership || {},
390
426
  alertHistory: alertHistory || {},
391
427
 
392
- // [NEW] Pass Master List
393
428
  piMasterList,
394
429
  });
395
430
 
@@ -5,17 +5,62 @@
5
5
 
6
6
  const { FieldValue } = require('@google-cloud/firestore');
7
7
 
8
+ /**
9
+ * Get the detail field name for a data type
10
+ * @param {string} dataType - Data type identifier
11
+ * @returns {string} - Detail field name in the index document
12
+ */
13
+ function getDetailFieldName(dataType) {
14
+ const dataTypeMap = {
15
+ 'signedInUserPortfolio': 'signedInUserPortfolio',
16
+ 'signedInUserHistory': 'signedInUserHistory',
17
+ 'signedInUserSocial': 'signedInSocial',
18
+ 'piPortfolios': 'piPortfolios',
19
+ 'piHistory': 'piHistory',
20
+ 'piSocial': 'piSocial',
21
+ 'normalPortfolios': 'normalPortfolio',
22
+ 'normalHistory': 'normalHistory',
23
+ 'speculatorPortfolios': 'speculatorPortfolio',
24
+ 'speculatorHistory': 'speculatorHistory'
25
+ };
26
+ return dataTypeMap[dataType] || dataType;
27
+ }
28
+
8
29
  /**
9
30
  * Check if root data is already indexed for a specific date
10
31
  * @param {Firestore} db - Firestore instance
11
32
  * @param {string} dateStr - Date string (YYYY-MM-DD)
12
33
  * @param {string} availabilityCollection - Collection name for root data index
13
- * @returns {Promise<boolean>} - True if already indexed
34
+ * @param {Array<string>} dataTypesRun - Optional array of data types to verify are indexed (e.g., ['signedInUserPortfolio', 'piSocial'])
35
+ * @returns {Promise<boolean>} - True if already indexed (and all requested data types are indexed if provided)
14
36
  */
15
- async function isRootDataIndexed(db, dateStr, availabilityCollection = 'system_root_data_index') {
37
+ async function isRootDataIndexed(db, dateStr, availabilityCollection = 'system_root_data_index', dataTypesRun = []) {
16
38
  try {
17
39
  const indexDoc = await db.collection(availabilityCollection).doc(dateStr).get();
18
- return indexDoc.exists;
40
+ if (!indexDoc.exists) {
41
+ return false;
42
+ }
43
+
44
+ // If specific data types were requested, verify they are actually indexed
45
+ if (dataTypesRun && dataTypesRun.length > 0) {
46
+ const indexData = indexDoc.data();
47
+ const details = indexData?.details || {};
48
+
49
+ // Check if all requested data types are indexed (have true values)
50
+ // If any are false or missing, return false (needs indexing)
51
+ for (const dataType of dataTypesRun) {
52
+ const detailField = getDetailFieldName(dataType);
53
+ if (details[detailField] !== true) {
54
+ // At least one requested data type is not indexed
55
+ return false;
56
+ }
57
+ }
58
+ // All requested data types are indexed as true
59
+ return true;
60
+ }
61
+
62
+ // No specific data types requested - just check if document exists
63
+ return true;
19
64
  } catch (error) {
20
65
  // If we can't check, assume not indexed to be safe
21
66
  return false;
@@ -39,11 +84,43 @@ async function conditionallyRunRootDataIndexer({ db, logger, dateStr, rootDataIn
39
84
  const { runRootDataIndexer } = require('../../root-data-indexer/index');
40
85
  const availabilityCollection = rootDataIndexerConfig?.availabilityCollection || 'system_root_data_index';
41
86
 
42
- // Check if already indexed
43
- const alreadyIndexed = await isRootDataIndexed(db, dateStr, availabilityCollection);
44
- if (alreadyIndexed) {
45
- logger.log('INFO', `[RootDataIndexer] Root data already indexed for ${dateStr}, skipping`);
46
- return false;
87
+ // IMPORTANT: Only index data types that were successfully stored (dataTypesRun contains only successful operations)
88
+ // If a write failed, it won't be in dataTypesRun, so we won't try to index it
89
+ // However, we still run the indexer to check/update the types that did succeed
90
+
91
+ // Check if already indexed (verify specific data types if provided)
92
+ // Only check the data types that were actually run - if they're all already true, skip indexing (never overwrite true values)
93
+ if (dataTypesRun.length > 0) {
94
+ const alreadyIndexed = await isRootDataIndexed(db, dateStr, availabilityCollection, dataTypesRun);
95
+ if (alreadyIndexed) {
96
+ logger.log('INFO', `[RootDataIndexer] All requested data types (${dataTypesRun.join(', ')}) are already indexed as true for ${dateStr}, skipping (never overwrite true values)`);
97
+ return false;
98
+ }
99
+
100
+ // If document exists but data types aren't indexed, log it
101
+ try {
102
+ const indexDoc = await db.collection(availabilityCollection).doc(dateStr).get();
103
+ if (indexDoc.exists) {
104
+ const indexData = indexDoc.data();
105
+ const details = indexData?.details || {};
106
+ const missingTypes = dataTypesRun.filter(dt => {
107
+ const detailField = getDetailFieldName(dt);
108
+ return details[detailField] !== true;
109
+ });
110
+ if (missingTypes.length > 0) {
111
+ logger.log('INFO', `[RootDataIndexer] Index document exists for ${dateStr} but data types (${missingTypes.join(', ')}) are not indexed. Re-indexing...`);
112
+ }
113
+ }
114
+ } catch (e) {
115
+ // Ignore errors checking document
116
+ }
117
+ } else {
118
+ // No specific data types - check if document exists (legacy behavior)
119
+ const alreadyIndexed = await isRootDataIndexed(db, dateStr, availabilityCollection, []);
120
+ if (alreadyIndexed) {
121
+ logger.log('INFO', `[RootDataIndexer] Root data already indexed for ${dateStr}, skipping`);
122
+ return false;
123
+ }
47
124
  }
48
125
 
49
126
  // Use Firestore transaction to ensure only one instance runs the indexer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.536",
3
+ "version": "1.0.538",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [