bulltrackers-module 1.0.702 → 1.0.704

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.
@@ -119,7 +119,8 @@ class CachedDataLoader {
119
119
  // =========================================================================
120
120
  // GENERIC LOADER HELPER
121
121
  // =========================================================================
122
- async _loadGeneric(methodName, key) {
122
+ // [FIX] Accepts ...args to pass down filters (like requiredUserTypes)
123
+ async _loadGeneric(methodName, key, ...args) {
123
124
  const def = LOADER_DEFINITIONS[methodName];
124
125
  if (!def) throw new Error(`Unknown loader method: ${methodName}`);
125
126
 
@@ -132,7 +133,7 @@ class CachedDataLoader {
132
133
  this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading '${def.cache}' from: ${collection}/${key}`);
133
134
  }
134
135
 
135
- const promise = def.fn(this.config, this.deps, key);
136
+ const promise = def.fn(this.config, this.deps, key, ...args);
136
137
  cacheMap.set(key, promise);
137
138
  return promise;
138
139
  }
@@ -151,8 +152,8 @@ class CachedDataLoader {
151
152
  async loadWatchlistMembership(dateStr) { return this._loadGeneric('loadWatchlistMembership', dateStr); }
152
153
  async loadAlertHistory(dateStr) { return this._loadGeneric('loadAlertHistory' , dateStr); }
153
154
  async loadPIWatchlistData(piCid) { return this._loadGeneric('loadPIWatchlistData' , String(piCid)); }
154
- // <--- ADDED PORTFOLIOS ACCESSOR
155
- async loadPortfolios(dateStr) { return this._loadGeneric('loadPortfolios' , dateStr); }
155
+ // <--- ADDED PORTFOLIOS ACCESSOR with extra arg
156
+ async loadPortfolios(dateStr, userTypes) { return this._loadGeneric('loadPortfolios', dateStr, userTypes); }
156
157
 
157
158
  // =========================================================================
158
159
  // SPECIALIZED LOADERS (Non-Standard Patterns)
@@ -204,13 +205,14 @@ class CachedDataLoader {
204
205
  /**
205
206
  * Optimistically loads data series using Batch Reads (db.getAll).
206
207
  * Uses Availability Index to minimize costs.
208
+ * [FIX] Now accepts ...args to pass context (e.g. requiredUserTypes)
207
209
  */
208
- async loadSeries(loaderMethod, dateStr, lookbackDays) {
210
+ async loadSeries(loaderMethod, dateStr, lookbackDays, ...args) {
209
211
  const def = LOADER_DEFINITIONS[loaderMethod];
210
212
  if (!def) throw new Error(`[CachedDataLoader] Unknown series method ${loaderMethod}`);
211
213
 
212
214
  // Fallback to legacy loop if method isn't configured for batching (missing config/flag)
213
- if (!def.configKey || !def.flag) return this._loadSeriesLegacy(loaderMethod, dateStr, lookbackDays);
215
+ if (!def.configKey || !def.flag) return this._loadSeriesLegacy(loaderMethod, dateStr, lookbackDays, ...args);
214
216
 
215
217
  // 1. Calculate Date Range
216
218
  const endDate = new Date(dateStr);
@@ -265,7 +267,7 @@ class CachedDataLoader {
265
267
  });
266
268
  } catch (err) {
267
269
  console.warn(`[CachedDataLoader] Batch failed: ${err.message}. Legacy fallback.`);
268
- return this._loadSeriesLegacy(loaderMethod, dateStr, lookbackDays);
270
+ return this._loadSeriesLegacy(loaderMethod, dateStr, lookbackDays, ...args);
269
271
  }
270
272
  }
271
273
 
@@ -277,7 +279,7 @@ class CachedDataLoader {
277
279
  };
278
280
  }
279
281
 
280
- async _loadSeriesLegacy(loaderMethod, dateStr, lookbackDays) {
282
+ async _loadSeriesLegacy(loaderMethod, dateStr, lookbackDays, ...args) {
281
283
  const results = {};
282
284
  const promises = [];
283
285
  const endDate = new Date(dateStr);
@@ -286,7 +288,8 @@ class CachedDataLoader {
286
288
  const d = new Date(endDate);
287
289
  d.setUTCDate(d.getUTCDate() - i);
288
290
  const dStr = d.toISOString().slice(0, 10);
289
- promises.push(this[loaderMethod](dStr).then(data => data ? results[dStr] = data : null).catch(() => null));
291
+ // [FIX] Pass args (e.g. requiredUserTypes) to the loader method
292
+ promises.push(this[loaderMethod](dStr, ...args).then(data => data ? results[dStr] = data : null).catch(() => null));
290
293
  }
291
294
 
292
295
  await Promise.all(promises);
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * @fileoverview Executor for "Standard" (User-Level) calculations.
3
3
  * REFACTORED: Hoisted data loading, centralized Series/Root logic.
4
- * UPDATED: Added Master List Driver for PI calculations to ensure full coverage.
5
4
  */
6
5
  const { normalizeName, getEarliestDataDates } = require('../utils/utils');
7
- const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs, getHistoryPartRefs, loadFullDayMap } = require('../utils/data_loader');
6
+ const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs, getHistoryPartRefs } = require('../utils/data_loader');
8
7
  const { CachedDataLoader } = require('../data/CachedDataLoader');
9
8
  const { ContextFactory } = require('../context/ContextFactory');
10
9
  const { commitResults } = require('../persistence/ResultCommitter');
@@ -81,6 +80,8 @@ class StandardExecutor {
81
80
  const loader = new CachedDataLoader(config, deps);
82
81
 
83
82
  // --- 1. PRE-LOAD GLOBAL DATA (Hoisted) ---
83
+ // Load all "Singleton" datasets once (Ratings, Rankings, Series, Mappings)
84
+ // instead of checking/loading per-user inside the loop.
84
85
  const startSetup = performance.now();
85
86
 
86
87
  const [
@@ -116,37 +117,8 @@ class StandardExecutor {
116
117
  shardMap[normalizeName(c.manifest.name)] = 0;
117
118
  });
118
119
 
119
- // --- 3. DETERMINE ITERATION STRATEGY ---
120
- let tP_iter;
121
- const isPiOnly = requiredUserTypes && requiredUserTypes.length === 1 && requiredUserTypes[0] === 'POPULAR_INVESTOR';
122
-
123
- // STRATEGY A: MASTER LIST DRIVER (For PIs, ensuring we hit every ID in the list, even if missing today)
124
- if (isPiOnly && globalRoots.piMasterList && !targetCid) {
125
- logger.log('INFO', `[StandardExecutor] 🚀 Driving execution via Master List (${Object.keys(globalRoots.piMasterList).length} PIs)`);
126
-
127
- // Load Today's Map Fully (Small enough for PIs)
128
- const todayMap = await loadFullDayMap(config, deps, effPortRefs, dateStr);
129
-
130
- // Create a generator that yields chunks from the Master List keys
131
- tP_iter = (async function* () {
132
- const allCids = Object.keys(globalRoots.piMasterList);
133
- const batchSize = config.partRefBatchSize || 50;
134
- for (let i = 0; i < allCids.length; i += batchSize) {
135
- const batchCids = allCids.slice(i, i + batchSize);
136
- const chunk = {};
137
- batchCids.forEach(cid => {
138
- // If missing today, provide a stub so the Calc can look back in History
139
- chunk[cid] = todayMap[cid] || { _userType: 'POPULAR_INVESTOR', _isMissingToday: true };
140
- });
141
- yield chunk;
142
- }
143
- })();
144
- }
145
- // STRATEGY B: STANDARD STREAM (Iterate whatever is in Firestore/GCS for today)
146
- else {
147
- tP_iter = streamPortfolioData(config, deps, dateStr, effPortRefs, requiredUserTypes);
148
- }
149
-
120
+ // --- 3. STREAM EXECUTION ---
121
+ const tP_iter = streamPortfolioData(config, deps, dateStr, effPortRefs, requiredUserTypes);
150
122
  const yP_iter = (streamingCalcs.some(c => c.manifest.isHistorical) && rootData.yesterdayPortfolioRefs)
151
123
  ? streamPortfolioData(config, deps, new Date(new Date(dateStr).getTime() - 86400000).toISOString().slice(0, 10), rootData.yesterdayPortfolioRefs)
152
124
  : null;
@@ -175,7 +147,7 @@ class StandardExecutor {
175
147
  stats[normalizeName(calc.manifest.name)],
176
148
  earliestDates,
177
149
  seriesData,
178
- globalRoots
150
+ globalRoots // <--- PASSED DOWN
179
151
  )
180
152
  ));
181
153
 
@@ -196,8 +168,8 @@ class StandardExecutor {
196
168
  }
197
169
  }
198
170
  } finally {
199
- if (yP_iter?.return && typeof yP_iter.return === 'function') await yP_iter.return();
200
- if (tH_iter?.return && typeof tH_iter.return === 'function') await tH_iter.return();
171
+ if (yP_iter?.return) await yP_iter.return();
172
+ if (tH_iter?.return) await tH_iter.return();
201
173
  }
202
174
 
203
175
  const finalRes = await StandardExecutor.flushBuffer(state, dateStr, passName, config, deps, shardMap, stats, 'FINAL', skipStatusWrite, !hasFlushed);
@@ -207,7 +179,7 @@ class StandardExecutor {
207
179
  }
208
180
 
209
181
  // =========================================================================
210
- // PER-USER EXECUTION
182
+ // PER-USER EXECUTION (Pure Logic)
211
183
  // =========================================================================
212
184
  static async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps, config, deps, stats, earliestDates, seriesData, globalRoots) {
213
185
  const { logger } = deps;
@@ -217,21 +189,18 @@ class StandardExecutor {
217
189
  let success = 0, failures = 0;
218
190
 
219
191
  for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
192
+ // 1. Filter User
220
193
  if (metadata.targetCid && String(userId) !== String(metadata.targetCid)) { stats.skippedUsers++; continue; }
221
194
 
222
- // 2. Determine Type (Handle Stub for Missing Users)
195
+ // 2. Determine Type
223
196
  let actualType = todayPortfolio._userType;
224
197
  if (!actualType) {
225
- // If driving from Master List, we know they are PIs even if missing today
226
198
  const isRanked = globalRoots.rankings && globalRoots.rankings.some(r => String(r.CustomerId) === String(userId));
227
- if (todayPortfolio._isMissingToday && isRanked) {
228
- actualType = 'POPULAR_INVESTOR';
229
- } else {
230
- actualType = isRanked ? 'POPULAR_INVESTOR' : (todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL);
231
- }
199
+ actualType = isRanked ? 'POPULAR_INVESTOR' : (todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL);
232
200
  }
233
201
  if (targetUserType && targetUserType !== 'all' && targetUserType !== actualType) { stats.skippedUsers++; continue; }
234
202
 
203
+ // 3. Resolve User Specifics from Global Data
235
204
  const userRank = globalRoots.rankings?.find(r => String(r.CustomerId) === String(userId)) || null;
236
205
  const userRankYest = globalRoots.rankingsYesterday?.find(r => String(r.CustomerId) === String(userId)) || null;
237
206
  const userVerify = globalRoots.verifications?.[userId] || null;
@@ -242,21 +211,30 @@ class StandardExecutor {
242
211
  (actualType === 'SIGNED_IN_USER' ? globalRoots.social.signedIn[userId] : globalRoots.social.generic)) || {};
243
212
  }
244
213
 
214
+ // 4. Build Context
245
215
  const context = ContextFactory.buildPerUserContext({
246
- todayPortfolio: todayPortfolio._isMissingToday ? null : todayPortfolio,
216
+ todayPortfolio,
247
217
  yesterdayPortfolio: yesterdayPortfolioData?.[userId] || null,
248
218
  todayHistory: historyData?.[userId] || null,
249
219
  userId, userType: actualType, dateStr, metadata,
220
+
221
+ // Injected Global Data
250
222
  mappings: globalRoots.mappings,
251
223
  piMasterList: globalRoots.piMasterList,
252
224
  insights: globalRoots.insights,
253
225
  socialData: social ? { today: social } : null,
226
+
227
+ // Dependency Data
254
228
  computedDependencies: computedDeps,
255
229
  previousComputedDependencies: prevDeps,
256
230
  config, deps,
231
+
232
+ // Specific Lookups
257
233
  verification: userVerify,
258
234
  rankings: userRank,
259
235
  yesterdayRankings: userRankYest,
236
+
237
+ // Full Access (if needed by calc)
260
238
  allRankings: globalRoots.rankings,
261
239
  allRankingsYesterday: globalRoots.rankingsYesterday,
262
240
  allVerifications: globalRoots.verifications,
@@ -264,6 +242,7 @@ class StandardExecutor {
264
242
  pageViews: globalRoots.pageViews || {},
265
243
  watchlistMembership: globalRoots.watchlistMembership || {},
266
244
  alertHistory: globalRoots.alertHistory || {},
245
+
267
246
  seriesData
268
247
  });
269
248
 
@@ -285,12 +264,13 @@ class StandardExecutor {
285
264
  }
286
265
 
287
266
  // =========================================================================
288
- // FLUSH & MERGE
267
+ // HELPERS (Flush & Merge)
289
268
  // =========================================================================
290
269
  static async flushBuffer(state, dateStr, passName, config, deps, shardMap, stats, mode, skipStatus, isInitial) {
291
270
  const transformedState = {};
292
271
  for (const [name, inst] of Object.entries(state)) {
293
272
  let data = inst.results || {};
273
+ // Pivot user-date structure if needed
294
274
  const first = Object.keys(data)[0];
295
275
  if (first && data[first] && typeof data[first] === 'object' && /^\d{4}-\d{2}-\d{2}$/.test(Object.keys(data[first])[0])) {
296
276
  const pivoted = {};
@@ -303,7 +283,7 @@ class StandardExecutor {
303
283
  data = pivoted;
304
284
  }
305
285
  transformedState[name] = { manifest: inst.manifest, getResult: async () => data, _executionStats: stats[name] };
306
- inst.results = {};
286
+ inst.results = {}; // Clear buffer
307
287
  }
308
288
  const res = await commitResults(transformedState, dateStr, passName, config, deps, skipStatus, { flushMode: mode, shardIndexes: shardMap, isInitialWrite: isInitial });
309
289
  if (res.shardIndexes) Object.assign(shardMap, res.shardIndexes);
@@ -330,13 +310,31 @@ class StandardExecutor {
330
310
  }
331
311
  }
332
312
 
313
+ // =============================================================================
314
+ // SHARED LOADING HELPERS
315
+ // =============================================================================
316
+
317
+ /**
318
+ * Pre-loads all shared global datasets required by the active calculations.
319
+ * Returns a consolidated object of { ratings, rankings, insights, ... }
320
+ */
333
321
  async function loadGlobalRoots(loader, dateStr, calcs, deps) {
334
322
  const { logger } = deps;
335
323
  const roots = {};
324
+
325
+ // 1. Identify Requirements
336
326
  const reqs = {
337
- mappings: true, piMasterList: true, rankings: false, rankingsYesterday: false,
338
- verifications: false, insights: false, social: false, ratings: false,
339
- pageViews: false, watchlist: false, alerts: false
327
+ mappings: true,
328
+ piMasterList: true,
329
+ rankings: false,
330
+ rankingsYesterday: false,
331
+ verifications: false,
332
+ insights: false,
333
+ social: false,
334
+ ratings: false,
335
+ pageViews: false,
336
+ watchlist: false,
337
+ alerts: false
340
338
  };
341
339
 
342
340
  for (const c of calcs) {
@@ -352,6 +350,7 @@ async function loadGlobalRoots(loader, dateStr, calcs, deps) {
352
350
  if (deps.includes('alerts')) reqs.alerts = true;
353
351
  }
354
352
 
353
+ // 2. Fetch Helper
355
354
  const fetch = async (key, method, dateArg, optional = true) => {
356
355
  if (!reqs[key]) return;
357
356
  try {
@@ -363,8 +362,9 @@ async function loadGlobalRoots(loader, dateStr, calcs, deps) {
363
362
  }
364
363
  };
365
364
 
365
+ // 3. Execute Loads
366
366
  await Promise.all([
367
- fetch('mappings', 'loadMappings', null, false),
367
+ fetch('mappings', 'loadMappings', null, false), // Always required
368
368
  fetch('piMasterList', 'loadPIMasterList', null, false),
369
369
  fetch('rankings', 'loadRankings', dateStr),
370
370
  fetch('verifications', 'loadVerifications', dateStr),
@@ -381,14 +381,20 @@ async function loadGlobalRoots(loader, dateStr, calcs, deps) {
381
381
  await fetch('rankingsYesterday', 'loadRankings', prev);
382
382
  }
383
383
 
384
+ // Map internal names to match loadGlobalRoots structure if needed
384
385
  roots.watchlistMembership = roots.watchlist;
385
386
  roots.alertHistory = roots.alerts;
387
+
386
388
  return roots;
387
389
  }
388
390
 
389
391
  async function loadSeriesData(loader, dateStr, calcs, config, deps) {
390
392
  const rootReqs = {};
391
- const depReqs = {};
393
+ const depReqs = {}; // norm -> { days, originalName, category }
394
+
395
+ // [FIX] Calculate User Types here to pass to loadSeries
396
+ const requiredUserTypes = new Set(calcs.map(c => (c.userType || 'ALL').toUpperCase()));
397
+ const userTypeArray = requiredUserTypes.has('ALL') ? null : Array.from(requiredUserTypes);
392
398
 
393
399
  calcs.forEach(c => {
394
400
  if (c.manifest.rootDataSeries) {
@@ -409,21 +415,30 @@ async function loadSeriesData(loader, dateStr, calcs, config, deps) {
409
415
  const rootMap = {
410
416
  alerts: 'loadAlertHistory', insights: 'loadInsights', ratings: 'loadRatings',
411
417
  watchlist: 'loadWatchlistMembership', rankings: 'loadRankings',
418
+ // <--- ADDED MAPPING
412
419
  portfolios: 'loadPortfolios'
413
420
  };
414
421
 
415
422
  await Promise.all(Object.entries(rootReqs).map(async ([key, days]) => {
416
- if (rootMap[key]) series.root[key] = (await loader.loadSeries(rootMap[key], dateStr, days)).data;
423
+ if (rootMap[key]) {
424
+ // [FIX] Pass userTypeArray specifically if loading portfolios
425
+ const extraArgs = (key === 'portfolios') ? [userTypeArray] : [];
426
+ series.root[key] = (await loader.loadSeries(rootMap[key], dateStr, days, ...extraArgs)).data;
427
+ }
417
428
  }));
418
429
 
430
+ // Build lookup from ALL computations using config.calculations
419
431
  const depEntries = Object.values(depReqs);
420
432
  if (depEntries.length) {
421
433
  const depOriginalNames = depEntries.map(e => e.originalName);
422
434
  const maxDays = Math.max(...depEntries.map(e => e.days));
435
+
423
436
  const allManifests = getManifest(config.activeProductLines || [], getCalculations(config), deps);
424
437
  const lookup = Object.fromEntries(allManifests.map(m => [normalizeName(m.name), m.category]));
438
+
425
439
  series.results = await fetchResultSeries(dateStr, depOriginalNames, lookup, config, deps, maxDays);
426
440
  }
441
+
427
442
  return series;
428
443
  }
429
444
 
@@ -229,13 +229,14 @@ async function loadFullDayMap(config, deps, partRefs, dateString) {
229
229
  }
230
230
 
231
231
  /** Stage 3.5: Load Daily Portfolios (Wrapper for Series Loading) */
232
- async function loadDailyPortfolios(config, deps, dateString) {
232
+ async function loadDailyPortfolios(config, deps, dateString, requiredUserTypes = null) {
233
233
  // 1. GCS FAST PATH
234
234
  const cached = await tryLoadFromGCS(config, dateString, 'portfolios', deps.logger);
235
235
  if (cached) return cached;
236
236
 
237
237
  // 2. FIRESTORE FALLBACK
238
- const partRefs = await getPortfolioPartRefs(config, deps, dateString);
238
+ // [FIX] Now passing requiredUserTypes to prevent fetching all users (e.g. NormalUserPortfolios)
239
+ const partRefs = await getPortfolioPartRefs(config, deps, dateString, requiredUserTypes);
239
240
  if (partRefs.length === 0) return {};
240
241
  return loadDataByRefs(config, deps, partRefs);
241
242
  }
@@ -813,7 +814,7 @@ module.exports = {
813
814
  getPortfolioPartRefs,
814
815
  loadDataByRefs,
815
816
  loadFullDayMap,
816
- loadDailyPortfolios, // <--- EXPORTED NEW WRAPPER
817
+ loadDailyPortfolios,
817
818
  loadDailyInsights,
818
819
  loadDailySocialPostInsights,
819
820
  getHistoryPartRefs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.702",
3
+ "version": "1.0.704",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [