bulltrackers-module 1.0.658 → 1.0.660

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.
Files changed (22) hide show
  1. package/functions/computation-system/data/AvailabilityChecker.js +163 -317
  2. package/functions/computation-system/data/CachedDataLoader.js +158 -222
  3. package/functions/computation-system/data/DependencyFetcher.js +201 -406
  4. package/functions/computation-system/executors/MetaExecutor.js +176 -280
  5. package/functions/computation-system/executors/StandardExecutor.js +325 -383
  6. package/functions/computation-system/helpers/computation_dispatcher.js +306 -701
  7. package/functions/computation-system/helpers/computation_worker.js +3 -2
  8. package/functions/computation-system/legacy/AvailabilityCheckerOld.js +382 -0
  9. package/functions/computation-system/legacy/CachedDataLoaderOld.js +357 -0
  10. package/functions/computation-system/legacy/DependencyFetcherOld.js +478 -0
  11. package/functions/computation-system/legacy/MetaExecutorold.js +364 -0
  12. package/functions/computation-system/legacy/StandardExecutorold.js +476 -0
  13. package/functions/computation-system/legacy/computation_dispatcherold.js +944 -0
  14. package/functions/computation-system/persistence/ResultCommitter.js +137 -188
  15. package/functions/computation-system/services/SnapshotService.js +129 -0
  16. package/functions/computation-system/tools/BuildReporter.js +12 -7
  17. package/functions/computation-system/utils/data_loader.js +213 -238
  18. package/package.json +3 -2
  19. package/functions/computation-system/workflows/bulltrackers_pipeline.yaml +0 -163
  20. package/functions/computation-system/workflows/data_feeder_pipeline.yaml +0 -115
  21. package/functions/computation-system/workflows/datafeederpipelineinstructions.md +0 -30
  22. package/functions/computation-system/workflows/morning_prep_pipeline.yaml +0 -55
@@ -1,457 +1,241 @@
1
- const { normalizeName, getEarliestDataDates } = require('../utils/utils');
1
+ /**
2
+ * @fileoverview Executor for "Standard" (User-Level) calculations.
3
+ * REFACTORED: Hoisted data loading, centralized Series/Root logic.
4
+ */
5
+ const { normalizeName, getEarliestDataDates } = require('../utils/utils');
2
6
  const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs, getHistoryPartRefs } = require('../utils/data_loader');
3
- const { CachedDataLoader } = require('../data/CachedDataLoader');
4
- const { ContextFactory } = require('../context/ContextFactory');
5
- const { commitResults } = require('../persistence/ResultCommitter');
6
- const { fetchResultSeries } = require('../data/DependencyFetcher');
7
- const { getManifest } = require('../topology/ManifestLoader');
8
- const mathLayer = require('../layers/index');
9
- const { performance } = require('perf_hooks');
10
- const v8 = require('v8');
7
+ const { CachedDataLoader } = require('../data/CachedDataLoader');
8
+ const { ContextFactory } = require('../context/ContextFactory');
9
+ const { commitResults } = require('../persistence/ResultCommitter');
10
+ const { fetchResultSeries } = require('../data/DependencyFetcher');
11
+ const { getManifest } = require('../topology/ManifestLoader');
12
+ const mathLayer = require('../layers/index');
13
+ const { performance } = require('perf_hooks');
14
+ const v8 = require('v8');
11
15
 
12
16
  class StandardExecutor {
17
+
18
+ // =========================================================================
19
+ // ENTRY POINT
20
+ // =========================================================================
13
21
  static async run(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps, skipStatusWrite = false) {
14
- const dStr = date.toISOString().slice(0, 10);
15
- const logger = deps.logger;
16
-
17
- // Determine required user types for this batch of calculations
18
- const requiredUserTypes = new Set();
19
- calcs.forEach(c => {
20
- const type = (c.userType || 'ALL').toUpperCase();
21
- requiredUserTypes.add(type);
22
- });
22
+ const dStr = date.toISOString().slice(0, 10);
23
+ const { logger } = deps;
24
+
25
+ // 1. Setup Scope (User Types & Target CID)
26
+ const requiredUserTypes = new Set(calcs.map(c => (c.userType || 'ALL').toUpperCase()));
23
27
  const userTypeArray = requiredUserTypes.has('ALL') ? null : Array.from(requiredUserTypes);
24
28
 
25
- // [OPTIMIZATION] Check for Target CID in manifests (On-Demand Optimization)
26
29
  const targetCid = calcs.find(c => c.targetCid)?.targetCid || calcs.find(c => c.manifest?.targetCid)?.manifest?.targetCid;
27
- if (targetCid) {
28
- logger.log('INFO', `[StandardExecutor] Running in Targeted Mode for CID: ${targetCid}`);
29
- }
30
+ if (targetCid) logger.log('INFO', `[StandardExecutor] 🎯 Targeted Mode: CID ${targetCid}`);
30
31
 
32
+ // 2. Prepare History Context (Refs)
31
33
  const fullRoot = { ...rootData };
32
34
  if (calcs.some(c => c.isHistorical)) {
33
- const prev = new Date(date); prev.setUTCDate(prev.getUTCDate() - 1);
34
- const prevStr = prev.toISOString().slice(0, 10);
35
-
36
- // Fetch yesterday's refs
35
+ const prevStr = new Date(date.getTime() - 86400000).toISOString().slice(0, 10);
37
36
  let yRefs = await getPortfolioPartRefs(config, deps, prevStr, userTypeArray);
38
37
 
39
- // [OPTIMIZATION] Filter Yesterday's Refs if targetCid is set
40
38
  if (targetCid && yRefs) {
41
- const originalCount = yRefs.length;
42
39
  yRefs = yRefs.filter(r => !r.cid || String(r.cid) === String(targetCid));
43
- logger.log('INFO', `[StandardExecutor] Filtered Yesterday's Refs: ${originalCount} -> ${yRefs.length}`);
44
40
  }
45
-
46
41
  fullRoot.yesterdayPortfolioRefs = yRefs;
47
42
  }
48
43
 
44
+ // 3. Initialize Instances
49
45
  const state = {};
50
- for (const c of calcs) {
51
- try {
52
- const inst = new c.class();
53
- inst.manifest = c;
54
- inst.results = {};
55
- state[normalizeName(c.name)] = inst;
56
- logger.log('INFO', `${c.name} calculation running for ${dStr}`);
57
- } catch (e) { logger.log('WARN', `Failed to init ${c.name}`); }
46
+ for (const c of calcs) {
47
+ try {
48
+ const inst = new c.class();
49
+ inst.manifest = c;
50
+ inst.results = {};
51
+ state[normalizeName(c.name)] = inst;
52
+ } catch (e) { logger.log('WARN', `Failed to init ${c.name}`); }
58
53
  }
59
54
 
60
55
  return await StandardExecutor.streamAndProcess(
61
- dStr, state, passName, config, deps, fullRoot,
62
- rootData.portfolioRefs, rootData.historyRefs,
63
- fetchedDeps, previousFetchedDeps, skipStatusWrite,
56
+ dStr, state, passName, config, deps, fullRoot,
57
+ rootData.portfolioRefs, rootData.historyRefs,
58
+ fetchedDeps, previousFetchedDeps, skipStatusWrite,
64
59
  userTypeArray, targetCid
65
60
  );
66
61
  }
67
62
 
63
+ // =========================================================================
64
+ // STREAMING LOOP
65
+ // =========================================================================
68
66
  static async streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps, previousFetchedDeps, skipStatusWrite, requiredUserTypes = null, targetCid = null) {
69
67
  const { logger } = deps;
70
- const calcs = Object.values(state).filter(c => c && c.manifest);
68
+ const calcs = Object.values(state).filter(c => c?.manifest);
71
69
  const streamingCalcs = calcs.filter(c => c.manifest.rootDataDependencies.includes('portfolio') || c.manifest.rootDataDependencies.includes('history'));
72
70
 
73
- if (streamingCalcs.length === 0) return { successUpdates: {}, failureReport: [] };
74
-
75
- // --- 0. [NEW] Build Global Manifest Lookup ---
76
- const allManifests = getManifest(config.productLines, config.calculationsDirectory, deps);
77
- const manifestLookup = {};
78
- allManifests.forEach(m => { manifestLookup[normalizeName(m.name)] = m.category; });
71
+ if (!streamingCalcs.length) return { successUpdates: {}, failureReport: [] };
79
72
 
80
- // --- 1. Resolve and Filter Portfolio Refs (Today) ---
81
- let effectivePortfolioRefs = portfolioRefs;
82
- if (!effectivePortfolioRefs) {
83
- effectivePortfolioRefs = await getPortfolioPartRefs(config, deps, dateStr, requiredUserTypes);
84
- }
85
- if (targetCid && effectivePortfolioRefs) {
86
- effectivePortfolioRefs = effectivePortfolioRefs.filter(r => !r.cid || String(r.cid) === String(targetCid));
87
- }
88
-
89
- // --- 2. Resolve and Filter History Refs ---
90
- let effectiveHistoryRefs = historyRefs;
91
- const needsTradingHistory = streamingCalcs.some(c => c.manifest.rootDataDependencies.includes('history'));
92
-
93
- if (needsTradingHistory) {
94
- if (!effectiveHistoryRefs) {
95
- effectiveHistoryRefs = await getHistoryPartRefs(config, deps, dateStr, requiredUserTypes);
96
- }
97
- if (targetCid && effectiveHistoryRefs) {
98
- effectiveHistoryRefs = effectiveHistoryRefs.filter(r => !r.cid || String(r.cid) === String(targetCid));
99
- }
100
- }
73
+ const loader = new CachedDataLoader(config, deps);
101
74
 
102
- let totalReadOps = (effectivePortfolioRefs?.length || 0) + (effectiveHistoryRefs?.length || 0);
103
- if (rootData.yesterdayPortfolioRefs) totalReadOps += rootData.yesterdayPortfolioRefs.length;
104
- totalReadOps += 2;
105
-
106
- const readOpsPerCalc = Math.ceil(totalReadOps / streamingCalcs.length);
107
-
108
- const executionStats = {};
109
- const shardIndexMap = {};
110
- const aggregatedSuccess = {};
111
- const aggregatedFailures = [];
112
- const errorStats = { count: 0, total: 0 };
113
-
114
- Object.keys(state).forEach(name => {
115
- executionStats[name] = {
116
- processedUsers: 0, skippedUsers: 0, timings: { setup: 0, stream: 0, processing: 0 }
117
- };
118
- shardIndexMap[name] = 0;
119
- });
120
-
121
- let hasFlushed = false;
122
- const cachedLoader = new CachedDataLoader(config, deps);
75
+ // --- 1. PRE-LOAD GLOBAL DATA (Hoisted) ---
76
+ // Load all "Singleton" datasets once (Ratings, Rankings, Series, Mappings)
77
+ // instead of checking/loading per-user inside the loop.
123
78
  const startSetup = performance.now();
124
79
 
125
- // [OPTIMIZATION] Hoist Static Data Load out of User Loop
126
- const mappings = await cachedLoader.loadMappings();
127
- // Pre-load Master List to cache it once
128
- const piMasterList = await cachedLoader.loadPIMasterList();
80
+ const [
81
+ globalRoots,
82
+ seriesData,
83
+ earliestDates
84
+ ] = await Promise.all([
85
+ loadGlobalRoots(loader, dateStr, streamingCalcs, deps),
86
+ loadSeriesData(loader, dateStr, streamingCalcs, config, deps),
87
+ streamingCalcs.some(c => c.manifest.requiresEarliestDataDate) ? getEarliestDataDates(config, deps) : null
88
+ ]);
129
89
 
130
90
  const setupDuration = performance.now() - startSetup;
131
- Object.keys(executionStats).forEach(name => executionStats[name].timings.setup += setupDuration);
132
91
 
133
- // --- [NEW] Series / Lookback Loading Logic ---
134
- const rootSeriesRequests = {};
135
- const dependencySeriesRequests = {};
92
+ // --- 2. PREPARE REFS & STATS ---
93
+ let effPortRefs = portfolioRefs || await getPortfolioPartRefs(config, deps, dateStr, requiredUserTypes);
94
+ let effHistRefs = historyRefs;
136
95
 
137
- // 1. Identify requirements from manifests
138
- streamingCalcs.forEach(c => {
139
- // Check for Root Data Series (e.g., { insights: 7, alerts: 30 })
140
- if (c.manifest.rootDataSeries) {
141
- Object.entries(c.manifest.rootDataSeries).forEach(([type, conf]) => {
142
- const days = typeof conf === 'object' ? conf.lookback : conf;
143
- if (!rootSeriesRequests[type] || days > rootSeriesRequests[type]) {
144
- rootSeriesRequests[type] = days;
145
- }
146
- });
147
- }
148
- // Check for Computation Result Series (e.g., { 'RiskScore': 7 })
149
- if (c.manifest.dependencySeries) {
150
- Object.entries(c.manifest.dependencySeries).forEach(([depName, conf]) => {
151
- const days = typeof conf === 'object' ? conf.lookback : conf;
152
- const normalized = normalizeName(depName);
153
- if (!dependencySeriesRequests[normalized] || days > dependencySeriesRequests[normalized]) {
154
- dependencySeriesRequests[normalized] = days;
155
- }
156
- });
157
- }
158
- });
159
-
160
- const seriesData = { root: {}, results: {} };
161
-
162
- // 2. Load Root Series
163
- for (const [type, days] of Object.entries(rootSeriesRequests)) {
164
- let loaderMethod = null;
165
- if (type === 'alerts') loaderMethod = 'loadAlertHistory';
166
- else if (type === 'insights') loaderMethod = 'loadInsights';
167
- else if (type === 'ratings') loaderMethod = 'loadRatings';
168
- else if (type === 'watchlist') loaderMethod = 'loadWatchlistMembership';
169
- // [CRITICAL UPDATE] Add rankings support for AUM lookback
170
- else if (type === 'rankings') loaderMethod = 'loadRankings';
171
-
172
- if (loaderMethod) {
173
- logger.log('INFO', `[StandardExecutor] Loading ${days}-day series for Root Data '${type}'...`);
174
- // Assume CachedDataLoader has loadSeries method (added in previous step)
175
- const series = await cachedLoader.loadSeries(loaderMethod, dateStr, days);
176
- seriesData.root[type] = series.data; // map of date -> data
177
- }
178
- }
96
+ if (targetCid) effPortRefs = effPortRefs?.filter(r => !r.cid || String(r.cid) === String(targetCid));
179
97
 
180
- // 3. Load Computation Result Series
181
- const calcNamesToFetch = Object.keys(dependencySeriesRequests);
182
- if (calcNamesToFetch.length > 0) {
183
- const maxDays = Math.max(...Object.values(dependencySeriesRequests));
184
- logger.log('INFO', `[StandardExecutor] Loading up to ${maxDays}-day series for Dependencies: ${calcNamesToFetch.join(', ')}`);
185
-
186
- // Pass manifestLookup to fetcher
187
- const resultsSeries = await fetchResultSeries(dateStr, calcNamesToFetch, manifestLookup, config, deps, maxDays);
188
- seriesData.results = resultsSeries;
98
+ const needsHistory = streamingCalcs.some(c => c.manifest.rootDataDependencies.includes('history'));
99
+ if (needsHistory) {
100
+ effHistRefs = effHistRefs || await getHistoryPartRefs(config, deps, dateStr, requiredUserTypes);
101
+ if (targetCid) effHistRefs = effHistRefs?.filter(r => !r.cid || String(r.cid) === String(targetCid));
189
102
  }
190
- // ---------------------------------------------
191
103
 
192
- const prevDate = new Date(dateStr + 'T00:00:00Z'); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
193
- const prevDateStr = prevDate.toISOString().slice(0, 10);
194
-
195
- let earliestDates = null;
196
- if (streamingCalcs.some(c => c.manifest.requiresEarliestDataDate)) {
197
- earliestDates = await getEarliestDataDates(config, deps);
198
- }
104
+ const stats = {};
105
+ const shardMap = {};
106
+ calcs.forEach(c => {
107
+ stats[normalizeName(c.manifest.name)] = {
108
+ processedUsers: 0, skippedUsers: 0, timings: { setup: setupDuration, stream: 0, processing: 0 }
109
+ };
110
+ shardMap[normalizeName(c.manifest.name)] = 0;
111
+ });
199
112
 
200
- const tP_iter = streamPortfolioData(config, deps, dateStr, effectivePortfolioRefs, requiredUserTypes);
201
-
202
- const needsYesterdayPortfolio = streamingCalcs.some(c => c.manifest.isHistorical);
203
- const yP_iter = (needsYesterdayPortfolio && rootData.yesterdayPortfolioRefs) ? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs) : null;
204
-
205
- const tH_iter = (needsTradingHistory) ? streamHistoryData(config, deps, dateStr, effectiveHistoryRefs, requiredUserTypes) : null;
113
+ // --- 3. STREAM EXECUTION ---
114
+ const tP_iter = streamPortfolioData(config, deps, dateStr, effPortRefs, requiredUserTypes);
115
+ const yP_iter = (streamingCalcs.some(c => c.manifest.isHistorical) && rootData.yesterdayPortfolioRefs)
116
+ ? streamPortfolioData(config, deps, new Date(new Date(dateStr).getTime() - 86400000).toISOString().slice(0, 10), rootData.yesterdayPortfolioRefs)
117
+ : null;
118
+ const tH_iter = needsHistory ? streamHistoryData(config, deps, dateStr, effHistRefs, requiredUserTypes) : null;
206
119
 
207
120
  let yP_chunk = {}, tH_chunk = {};
208
- let usersSinceLastFlush = 0;
121
+ let usersSinceFlush = 0;
122
+ let hasFlushed = false;
123
+ const aggregatedSuccess = {};
124
+ const aggregatedFailures = [];
209
125
 
210
126
  try {
211
127
  for await (const tP_chunk of tP_iter) {
212
- const startStream = performance.now();
128
+ const t0 = performance.now();
213
129
  if (yP_iter) yP_chunk = (await yP_iter.next()).value || {};
214
130
  if (tH_iter) tH_chunk = (await tH_iter.next()).value || {};
215
- const streamDuration = performance.now() - startStream;
216
- Object.keys(executionStats).forEach(name => executionStats[name].timings.stream += streamDuration);
131
+ const tStream = performance.now() - t0;
217
132
 
218
- const chunkSize = Object.keys(tP_chunk).length;
219
- const startProcessing = performance.now();
133
+ const t1 = performance.now();
220
134
 
221
- const batchResults = await Promise.all(streamingCalcs.map(calc =>
135
+ // Parallelize Calc Execution for this Chunk
136
+ await Promise.all(streamingCalcs.map(calc =>
222
137
  StandardExecutor.executePerUser(
223
- calc, calc.manifest, dateStr, tP_chunk, yP_chunk, tH_chunk,
224
- fetchedDeps, previousFetchedDeps, config, deps, cachedLoader,
225
- executionStats[normalizeName(calc.manifest.name)],
138
+ calc, calc.manifest, dateStr, tP_chunk, yP_chunk, tH_chunk,
139
+ fetchedDeps, previousFetchedDeps, config, deps,
140
+ stats[normalizeName(calc.manifest.name)],
226
141
  earliestDates,
227
142
  seriesData,
228
- // [NEW] Pass Hoisted Data
229
- mappings,
230
- piMasterList
231
- )
143
+ globalRoots // <--- PASSED DOWN
144
+ )
232
145
  ));
233
146
 
234
- const procDuration = performance.now() - startProcessing;
235
- Object.keys(executionStats).forEach(name => executionStats[name].timings.processing += procDuration);
236
-
237
- batchResults.forEach(r => { errorStats.total += (r.success + r.failures); errorStats.count += r.failures; });
238
- if (errorStats.total > 100 && (errorStats.count / errorStats.total) > 0.10) { throw new Error(`[Circuit Breaker] High failure rate detected.`); }
147
+ const tProc = performance.now() - t1;
148
+ streamingCalcs.forEach(c => {
149
+ const s = stats[normalizeName(c.manifest.name)];
150
+ s.timings.stream += tStream;
151
+ s.timings.processing += tProc;
152
+ });
239
153
 
240
- usersSinceLastFlush += chunkSize;
241
- const heapStats = v8.getHeapStatistics();
242
- if (usersSinceLastFlush >= 500 || (heapStats.used_heap_size / heapStats.heap_size_limit) > 0.70) {
243
- const flushResult = await StandardExecutor.flushBuffer(state, dateStr, passName, config, deps, shardIndexMap, executionStats, 'INTERMEDIATE', true, !hasFlushed);
154
+ usersSinceFlush += Object.keys(tP_chunk).length;
155
+ const heap = v8.getHeapStatistics();
156
+ if (usersSinceFlush >= 500 || (heap.used_heap_size / heap.heap_size_limit) > 0.7) {
157
+ const res = await StandardExecutor.flushBuffer(state, dateStr, passName, config, deps, shardMap, stats, 'INTERMEDIATE', true, !hasFlushed);
158
+ StandardExecutor.mergeReports(aggregatedSuccess, aggregatedFailures, res);
244
159
  hasFlushed = true;
245
- StandardExecutor.mergeReports(aggregatedSuccess, aggregatedFailures, flushResult);
246
- usersSinceLastFlush = 0;
160
+ usersSinceFlush = 0;
247
161
  }
248
162
  }
249
163
  } finally {
250
- if (yP_iter && yP_iter.return) await yP_iter.return();
251
- if (tH_iter && tH_iter.return) await tH_iter.return();
164
+ if (yP_iter?.return) await yP_iter.return();
165
+ if (tH_iter?.return) await tH_iter.return();
252
166
  }
253
167
 
254
- const finalResult = await StandardExecutor.flushBuffer(state, dateStr, passName, config, deps, shardIndexMap, executionStats, 'FINAL', skipStatusWrite, !hasFlushed);
255
- StandardExecutor.mergeReports(aggregatedSuccess, aggregatedFailures, finalResult);
256
-
257
- Object.values(aggregatedSuccess).forEach(update => {
258
- if (!update.metrics.io) update.metrics.io = { reads: 0, writes: 0, deletes: 0 };
259
- update.metrics.io.reads = readOpsPerCalc;
260
- });
168
+ const finalRes = await StandardExecutor.flushBuffer(state, dateStr, passName, config, deps, shardMap, stats, 'FINAL', skipStatusWrite, !hasFlushed);
169
+ StandardExecutor.mergeReports(aggregatedSuccess, aggregatedFailures, finalRes);
261
170
 
262
171
  return { successUpdates: aggregatedSuccess, failureReport: aggregatedFailures };
263
172
  }
264
-
265
- static async flushBuffer(state, dateStr, passName, config, deps, shardIndexMap, executionStats, mode, skipStatusWrite, isInitialWrite = false) {
266
- const { logger } = deps;
267
- const transformedState = {};
268
- for (const [name, inst] of Object.entries(state)) {
269
- const rawResult = inst.results || {};
270
- const firstUser = Object.keys(rawResult)[0];
271
- let dataToCommit = rawResult;
272
-
273
- if (firstUser && rawResult[firstUser] && typeof rawResult[firstUser] === 'object') {
274
- const innerKeys = Object.keys(rawResult[firstUser]);
275
- if (innerKeys.length > 0 && innerKeys.every(k => /^\d{4}-\d{2}-\d{2}$/.test(k))) {
276
- const transposed = {};
277
- for (const [userId, dateMap] of Object.entries(rawResult)) {
278
- for (const [dateKey, dailyData] of Object.entries(dateMap)) {
279
- if (!transposed[dateKey]) transposed[dateKey] = {};
280
- transposed[dateKey][userId] = dailyData;
281
- }
282
- }
283
- dataToCommit = transposed;
284
- }
285
- }
286
173
 
287
- transformedState[name] = {
288
- manifest: inst.manifest,
289
- getResult: async () => dataToCommit,
290
- _executionStats: executionStats[name]
291
- };
292
- inst.results = {};
293
- }
294
-
295
- const result = await commitResults(transformedState, dateStr, passName, config, deps, skipStatusWrite, {
296
- flushMode: mode, shardIndexes: shardIndexMap, isInitialWrite: isInitialWrite
297
- });
298
-
299
- if (result.shardIndexes) Object.assign(shardIndexMap, result.shardIndexes);
300
- return result;
301
- }
302
-
303
- static mergeReports(successAcc, failureAcc, newResult) {
304
- if (!newResult) return;
305
- for (const [name, update] of Object.entries(newResult.successUpdates)) {
306
- if (!successAcc[name]) {
307
- successAcc[name] = update;
308
- } else {
309
- successAcc[name].metrics.storage.sizeBytes += (update.metrics.storage.sizeBytes || 0);
310
- successAcc[name].metrics.storage.keys += (update.metrics.storage.keys || 0);
311
- successAcc[name].metrics.storage.shardCount = Math.max(successAcc[name].metrics.storage.shardCount, update.metrics.storage.shardCount || 1);
312
-
313
- if (update.metrics.io) {
314
- if (!successAcc[name].metrics.io) successAcc[name].metrics.io = { writes: 0, deletes: 0, reads: 0 };
315
- successAcc[name].metrics.io.writes += (update.metrics.io.writes || 0);
316
- successAcc[name].metrics.io.deletes += (update.metrics.io.deletes || 0);
317
- }
318
-
319
- if (update.metrics?.execution?.timings) {
320
- if (!successAcc[name].metrics.execution) successAcc[name].metrics.execution = { timings: { setup:0, stream:0, processing:0 }};
321
- const tDest = successAcc[name].metrics.execution.timings;
322
- const tSrc = update.metrics.execution.timings;
323
- tDest.setup += (tSrc.setup || 0);
324
- tDest.stream += (tSrc.stream || 0);
325
- tDest.processing += (tSrc.processing || 0);
326
- }
327
- successAcc[name].hash = update.hash;
328
- }
329
- }
330
- if (newResult.failureReport) failureAcc.push(...newResult.failureReport);
331
- }
332
-
333
- static async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps, config, deps, loader, stats, earliestDates, seriesData = {}, mappings = null, piMasterList = null) {
174
+ // =========================================================================
175
+ // PER-USER EXECUTION (Pure Logic)
176
+ // =========================================================================
177
+ static async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps, config, deps, stats, earliestDates, seriesData, globalRoots) {
334
178
  const { logger } = deps;
335
- const targetUserType = metadata.userType;
336
-
337
- // [OPTIMIZATION] Use passed mappings/list if available, else load (fallback)
338
- const mappingsToUse = mappings || await loader.loadMappings();
339
- const piMasterListToUse = piMasterList || await loader.loadPIMasterList();
340
-
179
+ const targetUserType = metadata.userType;
341
180
  const SCHEMAS = mathLayer.SCHEMAS;
342
-
343
- // 1. Load Root Data (CachedLoader handles memoization for these)
344
- const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await loader.loadInsights(dateStr) } : null;
345
- const verifications = metadata.rootDataDependencies?.includes('verification') ? await loader.loadVerifications() : null;
346
- const rankings = metadata.rootDataDependencies?.includes('rankings') ? await loader.loadRankings(dateStr) : null;
347
-
348
- let yesterdayRankings = null;
349
- if (metadata.rootDataDependencies?.includes('rankings') && metadata.isHistorical) {
350
- const prevDate = new Date(dateStr); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
351
- const prevStr = prevDate.toISOString().slice(0, 10);
352
- yesterdayRankings = await loader.loadRankings(prevStr);
353
- }
354
-
355
- const socialContainer = metadata.rootDataDependencies?.includes('social') ? await loader.loadSocial(dateStr) : null;
356
181
 
357
- const allowMissing = metadata.canHaveMissingRoots === true;
358
-
359
- // Helper to safely load roots
360
- const safeLoad = async (method, name) => {
361
- if (!metadata.rootDataDependencies?.includes(name)) return null;
362
- try {
363
- return await loader[method](dateStr);
364
- } catch (e) {
365
- if (!allowMissing) throw new Error(`[StandardExecutor] Required root '${name}' failed: ${e.message}`);
366
- return null;
367
- }
368
- };
369
-
370
- const ratings = await safeLoad('loadRatings', 'ratings');
371
- const pageViews = await safeLoad('loadPageViews', 'pageViews');
372
- const watchlistMembership = await safeLoad('loadWatchlistMembership', 'watchlist');
373
- const alertHistory = await safeLoad('loadAlertHistory', 'alerts');
374
-
375
- if (!allowMissing) {
376
- if (metadata.rootDataDependencies?.includes('ratings') && !ratings) throw new Error("Missing ratings");
377
- if (metadata.rootDataDependencies?.includes('pageViews') && !pageViews) throw new Error("Missing pageViews");
378
- if (metadata.rootDataDependencies?.includes('watchlist') && !watchlistMembership) throw new Error("Missing watchlist");
379
- if (metadata.rootDataDependencies?.includes('alerts') && !alertHistory) throw new Error("Missing alerts");
380
- }
381
-
382
- let chunkSuccess = 0;
383
- let chunkFailures = 0;
182
+ let success = 0, failures = 0;
384
183
 
385
184
  for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
386
- // --- OPTIMIZATION: TARGET SPECIFIC USER ---
387
- // If the request contains a targetCid, skip all other users immediately
388
- if (metadata.targetCid && String(userId) !== String(metadata.targetCid)) {
389
- if (stats) stats.skippedUsers++;
390
- continue;
391
- }
392
- // ------------------------------------------
185
+ // 1. Filter User
186
+ if (metadata.targetCid && String(userId) !== String(metadata.targetCid)) { stats.skippedUsers++; continue; }
393
187
 
394
- const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
395
- const todayHistory = historyData ? historyData[userId] : null;
396
-
397
- let actualUserType = todayPortfolio._userType;
398
- if (!actualUserType) {
399
- if (todayPortfolio.PublicPositions) {
400
- const isRanked = rankings && rankings.some(r => String(r.CustomerId) === String(userId));
401
- actualUserType = isRanked ? 'POPULAR_INVESTOR' : SCHEMAS.USER_TYPES.SPECULATOR;
402
- } else {
403
- actualUserType = SCHEMAS.USER_TYPES.NORMAL;
404
- }
405
- }
406
-
407
- if (targetUserType && targetUserType !== 'all') {
408
- if (targetUserType !== actualUserType) {
409
- if (stats) stats.skippedUsers++;
410
- continue;
411
- }
188
+ // 2. Determine Type
189
+ let actualType = todayPortfolio._userType;
190
+ if (!actualType) {
191
+ const isRanked = globalRoots.rankings && globalRoots.rankings.some(r => String(r.CustomerId) === String(userId));
192
+ actualType = isRanked ? 'POPULAR_INVESTOR' : (todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL);
412
193
  }
413
-
414
- const userVerification = verifications ? verifications[userId] : null;
415
-
416
- const userRanking = rankings ? (rankings.find(r => String(r.CustomerId) === String(userId)) || null) : null;
417
- const userRankingYesterday = yesterdayRankings ? (yesterdayRankings.find(r => String(r.CustomerId) === String(userId)) || null) : null;
418
-
419
- let effectiveSocialData = null;
420
- if (socialContainer) {
421
- if (actualUserType === 'POPULAR_INVESTOR') {
422
- effectiveSocialData = socialContainer.pi[userId] || {};
423
- } else if (actualUserType === 'SIGNED_IN_USER') {
424
- effectiveSocialData = socialContainer.signedIn[userId] || {};
425
- } else {
426
- effectiveSocialData = socialContainer.generic || {};
427
- }
194
+ if (targetUserType && targetUserType !== 'all' && targetUserType !== actualType) { stats.skippedUsers++; continue; }
195
+
196
+ // 3. Resolve User Specifics from Global Data
197
+ const userRank = globalRoots.rankings?.find(r => String(r.CustomerId) === String(userId)) || null;
198
+ const userRankYest = globalRoots.rankingsYesterday?.find(r => String(r.CustomerId) === String(userId)) || null;
199
+ const userVerify = globalRoots.verifications?.[userId] || null;
200
+
201
+ let social = null;
202
+ if (globalRoots.social) {
203
+ social = (actualType === 'POPULAR_INVESTOR' ? globalRoots.social.pi[userId] :
204
+ (actualType === 'SIGNED_IN_USER' ? globalRoots.social.signedIn[userId] : globalRoots.social.generic)) || {};
428
205
  }
429
206
 
430
- const context = ContextFactory.buildPerUserContext({
431
- todayPortfolio, yesterdayPortfolio, todayHistory, userId,
432
- userType: actualUserType, dateStr, metadata,
433
- mappings: mappingsToUse,
434
- insights,
435
- socialData: effectiveSocialData ? { today: effectiveSocialData } : null,
436
- computedDependencies: computedDeps, previousComputedDependencies: prevDeps,
437
- config, deps,
438
- verification: userVerification,
207
+ // 4. Build Context
208
+ const context = ContextFactory.buildPerUserContext({
209
+ todayPortfolio,
210
+ yesterdayPortfolio: yesterdayPortfolioData?.[userId] || null,
211
+ todayHistory: historyData?.[userId] || null,
212
+ userId, userType: actualType, dateStr, metadata,
439
213
 
440
- rankings: userRanking,
441
- yesterdayRankings: userRankingYesterday,
214
+ // Injected Global Data
215
+ mappings: globalRoots.mappings,
216
+ piMasterList: globalRoots.piMasterList,
217
+ insights: globalRoots.insights,
218
+ socialData: social ? { today: social } : null,
442
219
 
443
- allRankings: rankings,
444
- allRankingsYesterday: yesterdayRankings,
220
+ // Dependency Data
221
+ computedDependencies: computedDeps,
222
+ previousComputedDependencies: prevDeps,
223
+ config, deps,
445
224
 
446
- allVerifications: verifications,
225
+ // Specific Lookups
226
+ verification: userVerify,
227
+ rankings: userRank,
228
+ yesterdayRankings: userRankYest,
229
+
230
+ // Full Access (if needed by calc)
231
+ allRankings: globalRoots.rankings,
232
+ allRankingsYesterday: globalRoots.rankingsYesterday,
233
+ allVerifications: globalRoots.verifications,
234
+ ratings: globalRoots.ratings || {},
235
+ pageViews: globalRoots.pageViews || {},
236
+ watchlistMembership: globalRoots.watchlistMembership || {},
237
+ alertHistory: globalRoots.alertHistory || {},
447
238
 
448
- ratings: ratings || {},
449
- pageViews: pageViews || {},
450
- watchlistMembership: watchlistMembership || {},
451
- alertHistory: alertHistory || {},
452
-
453
- piMasterList: piMasterListToUse,
454
- // [NEW] Pass Series Data
455
239
  seriesData
456
240
  });
457
241
 
@@ -459,18 +243,176 @@ class StandardExecutor {
459
243
  if (!context.system) context.system = {};
460
244
  context.system.earliestHistoryDate = earliestDates.history;
461
245
  }
462
-
463
- try {
464
- await calcInstance.process(context);
465
- if (stats) stats.processedUsers++;
466
- chunkSuccess++;
467
- } catch (e) {
468
- logger.log('WARN', `Calc ${metadata.name} failed for user ${userId}: ${e.message}`);
469
- chunkFailures++;
246
+
247
+ try {
248
+ await calcInstance.process(context);
249
+ stats.processedUsers++;
250
+ success++;
251
+ } catch (e) {
252
+ logger.log('WARN', `Calc ${metadata.name} failed for user ${userId}: ${e.message}`);
253
+ failures++;
470
254
  }
471
255
  }
472
- return { success: chunkSuccess, failures: chunkFailures };
256
+ return { success, failures };
473
257
  }
258
+
259
+ // =========================================================================
260
+ // HELPERS (Flush & Merge)
261
+ // =========================================================================
262
+ static async flushBuffer(state, dateStr, passName, config, deps, shardMap, stats, mode, skipStatus, isInitial) {
263
+ const transformedState = {};
264
+ for (const [name, inst] of Object.entries(state)) {
265
+ let data = inst.results || {};
266
+ // Pivot user-date structure if needed
267
+ const first = Object.keys(data)[0];
268
+ if (first && data[first] && typeof data[first] === 'object' && /^\d{4}-\d{2}-\d{2}$/.test(Object.keys(data[first])[0])) {
269
+ const pivoted = {};
270
+ for (const [uid, dMap] of Object.entries(data)) {
271
+ for (const [dKey, val] of Object.entries(dMap)) {
272
+ if (!pivoted[dKey]) pivoted[dKey] = {};
273
+ pivoted[dKey][uid] = val;
274
+ }
275
+ }
276
+ data = pivoted;
277
+ }
278
+ transformedState[name] = { manifest: inst.manifest, getResult: async () => data, _executionStats: stats[name] };
279
+ inst.results = {}; // Clear buffer
280
+ }
281
+ const res = await commitResults(transformedState, dateStr, passName, config, deps, skipStatus, { flushMode: mode, shardIndexes: shardMap, isInitialWrite: isInitial });
282
+ if (res.shardIndexes) Object.assign(shardMap, res.shardIndexes);
283
+ return res;
284
+ }
285
+
286
+ static mergeReports(success, failure, result) {
287
+ if (!result) return;
288
+ if (result.failureReport) failure.push(...result.failureReport);
289
+ for (const [name, update] of Object.entries(result.successUpdates)) {
290
+ if (!success[name]) success[name] = update;
291
+ else {
292
+ const m = success[name].metrics;
293
+ const u = update.metrics;
294
+ m.storage.sizeBytes += u.storage.sizeBytes;
295
+ m.storage.keys += u.storage.keys;
296
+ m.storage.shardCount = Math.max(m.storage.shardCount, u.storage.shardCount);
297
+ if (u.execution) {
298
+ m.execution.timings.stream += u.execution.timings.stream;
299
+ m.execution.timings.processing += u.execution.timings.processing;
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ // =============================================================================
307
+ // SHARED LOADING HELPERS
308
+ // =============================================================================
309
+
310
+ /**
311
+ * Pre-loads all shared global datasets required by the active calculations.
312
+ * Returns a consolidated object of { ratings, rankings, insights, ... }
313
+ */
314
+ async function loadGlobalRoots(loader, dateStr, calcs, deps) {
315
+ const { logger } = deps;
316
+ const roots = {};
317
+
318
+ // 1. Identify Requirements
319
+ const reqs = {
320
+ mappings: true,
321
+ piMasterList: true,
322
+ rankings: false,
323
+ rankingsYesterday: false,
324
+ verifications: false,
325
+ insights: false,
326
+ social: false,
327
+ ratings: false,
328
+ pageViews: false,
329
+ watchlist: false,
330
+ alerts: false
331
+ };
332
+
333
+ for (const c of calcs) {
334
+ const deps = c.manifest.rootDataDependencies || [];
335
+ if (deps.includes('rankings')) reqs.rankings = true;
336
+ if (deps.includes('rankings') && c.manifest.isHistorical) reqs.rankingsYesterday = true;
337
+ if (deps.includes('verification')) reqs.verifications = true;
338
+ if (deps.includes('insights')) reqs.insights = true;
339
+ if (deps.includes('social')) reqs.social = true;
340
+ if (deps.includes('ratings')) reqs.ratings = true;
341
+ if (deps.includes('pageViews')) reqs.pageViews = true;
342
+ if (deps.includes('watchlist')) reqs.watchlist = true;
343
+ if (deps.includes('alerts')) reqs.alerts = true;
344
+ }
345
+
346
+ // 2. Fetch Helper
347
+ const fetch = async (key, method, dateArg, optional = true) => {
348
+ if (!reqs[key]) return;
349
+ try {
350
+ roots[key] = await loader[method](dateArg);
351
+ } catch (e) {
352
+ if (!optional) throw e;
353
+ logger.log('WARN', `[StandardExecutor] Optional root '${key}' failed to load: ${e.message}`);
354
+ roots[key] = null;
355
+ }
356
+ };
357
+
358
+ // 3. Execute Loads
359
+ await Promise.all([
360
+ fetch('mappings', 'loadMappings', null, false), // Always required
361
+ fetch('piMasterList', 'loadPIMasterList', null, false),
362
+ fetch('rankings', 'loadRankings', dateStr),
363
+ fetch('verifications', 'loadVerifications', dateStr),
364
+ fetch('insights', 'loadInsights', dateStr),
365
+ fetch('social', 'loadSocial', dateStr),
366
+ fetch('ratings', 'loadRatings', dateStr),
367
+ fetch('pageViews', 'loadPageViews', dateStr),
368
+ fetch('watchlist', 'loadWatchlistMembership', dateStr),
369
+ fetch('alerts', 'loadAlertHistory', dateStr)
370
+ ]);
371
+
372
+ if (reqs.rankingsYesterday) {
373
+ const prev = new Date(new Date(dateStr).getTime() - 86400000).toISOString().slice(0, 10);
374
+ await fetch('rankingsYesterday', 'loadRankings', prev);
375
+ }
376
+
377
+ // Map internal names to match loadGlobalRoots structure if needed
378
+ roots.watchlistMembership = roots.watchlist;
379
+ roots.alertHistory = roots.alerts;
380
+
381
+ return roots;
382
+ }
383
+
384
+ async function loadSeriesData(loader, dateStr, calcs, config, deps) {
385
+ const rootReqs = {};
386
+ const depReqs = {};
387
+
388
+ calcs.forEach(c => {
389
+ if (c.manifest.rootDataSeries) {
390
+ Object.entries(c.manifest.rootDataSeries).forEach(([k, v]) => rootReqs[k] = Math.max(rootReqs[k]||0, v.lookback||v));
391
+ }
392
+ if (c.manifest.dependencySeries) {
393
+ Object.entries(c.manifest.dependencySeries).forEach(([k, v]) => depReqs[normalizeName(k)] = Math.max(depReqs[normalizeName(k)]||0, v.lookback||v));
394
+ }
395
+ });
396
+
397
+ const series = { root: {}, results: {} };
398
+ const rootMap = {
399
+ alerts: 'loadAlertHistory', insights: 'loadInsights', ratings: 'loadRatings',
400
+ watchlist: 'loadWatchlistMembership', rankings: 'loadRankings'
401
+ };
402
+
403
+ await Promise.all(Object.entries(rootReqs).map(async ([key, days]) => {
404
+ if (rootMap[key]) series.root[key] = (await loader.loadSeries(rootMap[key], dateStr, days)).data;
405
+ }));
406
+
407
+ const depNames = Object.keys(depReqs);
408
+ if (depNames.length) {
409
+ // Construct manifest lookup on the fly for fetched names
410
+ const allManifests = getManifest(config.productLines, config.calculationsDirectory, deps);
411
+ const lookup = Object.fromEntries(allManifests.map(m => [normalizeName(m.name), m.category]));
412
+ series.results = await fetchResultSeries(dateStr, depNames, lookup, config, deps, Math.max(...Object.values(depReqs)));
413
+ }
414
+
415
+ return series;
474
416
  }
475
417
 
476
418
  module.exports = { StandardExecutor };