bulltrackers-module 1.0.658 → 1.0.659
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.
- package/functions/computation-system/data/AvailabilityChecker.js +163 -317
- package/functions/computation-system/data/CachedDataLoader.js +158 -222
- package/functions/computation-system/data/DependencyFetcher.js +201 -406
- package/functions/computation-system/executors/MetaExecutor.js +176 -280
- package/functions/computation-system/executors/StandardExecutor.js +325 -383
- package/functions/computation-system/helpers/computation_dispatcher.js +294 -699
- package/functions/computation-system/helpers/computation_worker.js +3 -2
- package/functions/computation-system/legacy/AvailabilityCheckerOld.js +382 -0
- package/functions/computation-system/legacy/CachedDataLoaderOld.js +357 -0
- package/functions/computation-system/legacy/DependencyFetcherOld.js +478 -0
- package/functions/computation-system/legacy/MetaExecutorold.js +364 -0
- package/functions/computation-system/legacy/StandardExecutorold.js +476 -0
- package/functions/computation-system/legacy/computation_dispatcherold.js +944 -0
- package/functions/computation-system/persistence/ResultCommitter.js +137 -188
- package/functions/computation-system/services/SnapshotService.js +129 -0
- package/functions/computation-system/tools/BuildReporter.js +12 -7
- package/functions/computation-system/utils/data_loader.js +213 -238
- package/package.json +3 -2
- package/functions/computation-system/workflows/bulltrackers_pipeline.yaml +0 -163
- package/functions/computation-system/workflows/data_feeder_pipeline.yaml +0 -115
- package/functions/computation-system/workflows/datafeederpipelineinstructions.md +0 -30
- package/functions/computation-system/workflows/morning_prep_pipeline.yaml +0 -55
|
@@ -1,186 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Executor for "Meta" (global) calculations.
|
|
3
|
-
*
|
|
4
|
-
* UPDATED: Tracks processed shard/item counts.
|
|
5
|
-
* UPDATED: Sends 'isInitialWrite: true' for robust cleanup.
|
|
6
|
-
* UPDATED: Support for historical rankings in Meta Context.
|
|
7
|
-
* UPDATED: Added support for loading Series Data (Root & Results) for lookbacks.
|
|
8
|
-
* UPDATED: Builds Manifest Lookup for DependencyFetcher.
|
|
3
|
+
* REFACTORED: Applied DRY principles to Root Data and Series loading.
|
|
9
4
|
*/
|
|
10
5
|
const { normalizeName } = require('../utils/utils');
|
|
11
6
|
const { CachedDataLoader } = require('../data/CachedDataLoader');
|
|
12
7
|
const { ContextFactory } = require('../context/ContextFactory');
|
|
13
8
|
const { commitResults } = require('../persistence/ResultCommitter');
|
|
14
|
-
const { fetchResultSeries
|
|
15
|
-
const { getManifest }
|
|
9
|
+
const { fetchResultSeries } = require('../data/DependencyFetcher');
|
|
10
|
+
const { getManifest } = require('../topology/ManifestLoader');
|
|
16
11
|
|
|
17
12
|
class MetaExecutor {
|
|
13
|
+
|
|
14
|
+
// =========================================================================
|
|
15
|
+
// PRIMARY ENTRY POINT (Batch Execution)
|
|
16
|
+
// =========================================================================
|
|
18
17
|
static async run(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps) {
|
|
18
|
+
const { logger } = deps;
|
|
19
19
|
const dStr = date.toISOString().slice(0, 10);
|
|
20
|
-
const { logger, db } = deps;
|
|
21
20
|
const loader = new CachedDataLoader(config, deps);
|
|
22
21
|
|
|
23
|
-
//
|
|
24
|
-
// getManifest is typically cached, so this is cheap.
|
|
22
|
+
// 1. Setup Manifest Lookup
|
|
25
23
|
const allManifests = getManifest(config.productLines, config.calculationsDirectory, deps);
|
|
26
|
-
const manifestLookup =
|
|
27
|
-
allManifests.forEach(m => { manifestLookup[normalizeName(m.name)] = m.category; });
|
|
28
|
-
|
|
29
|
-
// [FIX] Check if any meta calculation needs history
|
|
30
|
-
const needsHistory = calcs.some(c => c.isHistorical);
|
|
31
|
-
let rankingsYesterday = null;
|
|
32
|
-
|
|
33
|
-
if (needsHistory) {
|
|
34
|
-
const prevDate = new Date(date);
|
|
35
|
-
prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
36
|
-
const prevStr = prevDate.toISOString().slice(0, 10);
|
|
37
|
-
logger.log('INFO', `[MetaExecutor] Loading historical rankings for ${prevStr}`);
|
|
38
|
-
rankingsYesterday = await loader.loadRankings(prevStr);
|
|
39
|
-
}
|
|
24
|
+
const manifestLookup = Object.fromEntries(allManifests.map(m => [normalizeName(m.name), m.category]));
|
|
40
25
|
|
|
41
|
-
//
|
|
26
|
+
// 2. Load Base Data (Always Required)
|
|
42
27
|
const [mappings, rankings, verifications] = await Promise.all([
|
|
43
28
|
loader.loadMappings(),
|
|
44
29
|
loader.loadRankings(dStr),
|
|
45
30
|
loader.loadVerifications()
|
|
46
31
|
]);
|
|
47
|
-
|
|
48
|
-
// [NEW] Load New Root Data Types for Profile Metrics (if any calc needs them)
|
|
49
|
-
const needsNewRootData = calcs.some(c => {
|
|
50
|
-
const deps = c.rootDataDependencies || [];
|
|
51
|
-
return deps.includes('ratings') || deps.includes('pageViews') ||
|
|
52
|
-
deps.includes('watchlist') || deps.includes('alerts');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
let ratings = null, pageViews = null, watchlistMembership = null, alertHistory = null;
|
|
56
|
-
if (needsNewRootData) {
|
|
57
|
-
const loadPromises = [];
|
|
58
|
-
|
|
59
|
-
// Ratings
|
|
60
|
-
if (calcs.some(c => c.rootDataDependencies?.includes('ratings'))) {
|
|
61
|
-
loadPromises.push(loader.loadRatings(dStr).then(r => { ratings = r; }).catch(e => {
|
|
62
|
-
const allAllowMissing = calcs.every(c => {
|
|
63
|
-
const needsRatings = c.rootDataDependencies?.includes('ratings');
|
|
64
|
-
return !needsRatings || c.canHaveMissingRoots === true;
|
|
65
|
-
});
|
|
66
|
-
if (allAllowMissing) { ratings = null; } else { throw e; }
|
|
67
|
-
}));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// PageViews
|
|
71
|
-
if (calcs.some(c => c.rootDataDependencies?.includes('pageViews'))) {
|
|
72
|
-
loadPromises.push(loader.loadPageViews(dStr).then(pv => { pageViews = pv; }).catch(e => {
|
|
73
|
-
const allAllowMissing = calcs.every(c => {
|
|
74
|
-
const needsPageViews = c.rootDataDependencies?.includes('pageViews');
|
|
75
|
-
return !needsPageViews || c.canHaveMissingRoots === true;
|
|
76
|
-
});
|
|
77
|
-
if (allAllowMissing) { pageViews = null; } else { throw e; }
|
|
78
|
-
}));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Watchlist
|
|
82
|
-
if (calcs.some(c => c.rootDataDependencies?.includes('watchlist'))) {
|
|
83
|
-
loadPromises.push(loader.loadWatchlistMembership(dStr).then(w => { watchlistMembership = w; }).catch(e => {
|
|
84
|
-
const allAllowMissing = calcs.every(c => {
|
|
85
|
-
const needsWatchlist = c.rootDataDependencies?.includes('watchlist');
|
|
86
|
-
return !needsWatchlist || c.canHaveMissingRoots === true;
|
|
87
|
-
});
|
|
88
|
-
if (allAllowMissing) { watchlistMembership = null; } else { throw e; }
|
|
89
|
-
}));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Alerts
|
|
93
|
-
if (calcs.some(c => c.rootDataDependencies?.includes('alerts'))) {
|
|
94
|
-
loadPromises.push(loader.loadAlertHistory(dStr).then(a => { alertHistory = a; }).catch(e => {
|
|
95
|
-
const allAllowMissing = calcs.every(c => {
|
|
96
|
-
const needsAlerts = c.rootDataDependencies?.includes('alerts');
|
|
97
|
-
return !needsAlerts || c.canHaveMissingRoots === true;
|
|
98
|
-
});
|
|
99
|
-
if (allAllowMissing) { alertHistory = null; } else { throw e; }
|
|
100
|
-
}));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
await Promise.all(loadPromises);
|
|
104
|
-
|
|
105
|
-
// [FIX] Enforce canHaveMissingRoots - validate after loading
|
|
106
|
-
for (const c of calcs) {
|
|
107
|
-
const deps = c.rootDataDependencies || [];
|
|
108
|
-
const canSkip = c.canHaveMissingRoots === true;
|
|
109
|
-
const isMissing = (key, val) => deps.includes(key) && (val === null || val === undefined);
|
|
110
|
-
|
|
111
|
-
if (!canSkip) {
|
|
112
|
-
if (isMissing('ratings', ratings)) throw new Error(`[MetaExecutor] Missing required root 'ratings' for ${c.name}`);
|
|
113
|
-
if (isMissing('pageViews', pageViews)) throw new Error(`[MetaExecutor] Missing required root 'pageViews' for ${c.name}`);
|
|
114
|
-
if (isMissing('watchlist', watchlistMembership)) throw new Error(`[MetaExecutor] Missing required root 'watchlist' for ${c.name}`);
|
|
115
|
-
if (isMissing('alerts', alertHistory)) throw new Error(`[MetaExecutor] Missing required root 'alerts' for ${c.name}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// --- [NEW] Series / Lookback Loading Logic ---
|
|
121
|
-
const rootSeriesRequests = {};
|
|
122
|
-
const dependencySeriesRequests = {};
|
|
123
32
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (!rootSeriesRequests[type] || days > rootSeriesRequests[type]) {
|
|
130
|
-
rootSeriesRequests[type] = days;
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
if (c.dependencySeries) {
|
|
135
|
-
Object.entries(c.dependencySeries).forEach(([depName, conf]) => {
|
|
136
|
-
const days = typeof conf === 'object' ? conf.lookback : conf;
|
|
137
|
-
const normalized = normalizeName(depName);
|
|
138
|
-
if (!dependencySeriesRequests[normalized] || days > dependencySeriesRequests[normalized]) {
|
|
139
|
-
dependencySeriesRequests[normalized] = days;
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const seriesData = { root: {}, results: {} };
|
|
146
|
-
|
|
147
|
-
// 2. Load Root Series
|
|
148
|
-
for (const [type, days] of Object.entries(rootSeriesRequests)) {
|
|
149
|
-
let loaderMethod = null;
|
|
150
|
-
if (type === 'alerts') loaderMethod = 'loadAlertHistory';
|
|
151
|
-
else if (type === 'insights') loaderMethod = 'loadInsights';
|
|
152
|
-
else if (type === 'ratings') loaderMethod = 'loadRatings';
|
|
153
|
-
else if (type === 'watchlist') loaderMethod = 'loadWatchlistMembership';
|
|
154
|
-
// [CRITICAL UPDATE] Add rankings support for Meta lookbacks
|
|
155
|
-
else if (type === 'rankings') loaderMethod = 'loadRankings';
|
|
156
|
-
|
|
157
|
-
if (loaderMethod) {
|
|
158
|
-
logger.log('INFO', `[MetaExecutor] Loading ${days}-day series for Root Data '${type}'...`);
|
|
159
|
-
const series = await loader.loadSeries(loaderMethod, dStr, days);
|
|
160
|
-
seriesData.root[type] = series.data;
|
|
161
|
-
}
|
|
33
|
+
// 3. Load Historical Rankings (if needed)
|
|
34
|
+
let rankingsYesterday = null;
|
|
35
|
+
if (calcs.some(c => c.isHistorical)) {
|
|
36
|
+
const prevStr = new Date(date.getTime() - 86400000).toISOString().slice(0, 10);
|
|
37
|
+
rankingsYesterday = await loader.loadRankings(prevStr);
|
|
162
38
|
}
|
|
163
39
|
|
|
164
|
-
//
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
const maxDays = Math.max(...Object.values(dependencySeriesRequests));
|
|
168
|
-
logger.log('INFO', `[MetaExecutor] Loading up to ${maxDays}-day series for Dependencies: ${calcNamesToFetch.join(', ')}`);
|
|
169
|
-
|
|
170
|
-
// Pass manifestLookup to fetcher
|
|
171
|
-
const resultsSeries = await fetchResultSeries(dStr, calcNamesToFetch, manifestLookup, config, deps, maxDays);
|
|
172
|
-
seriesData.results = resultsSeries;
|
|
173
|
-
}
|
|
40
|
+
// 4. Load Variable Root Data & Series (Refactored Helpers)
|
|
41
|
+
const variableRoots = await loadVariableRootData(loader, dStr, calcs, logger);
|
|
42
|
+
const seriesData = await loadSeriesData(loader, dStr, calcs, manifestLookup, config, deps);
|
|
174
43
|
|
|
44
|
+
// 5. Execution Loop
|
|
175
45
|
const state = {};
|
|
176
46
|
for (const c of calcs) {
|
|
177
47
|
const inst = new c.class();
|
|
178
48
|
inst.manifest = c;
|
|
179
49
|
|
|
180
|
-
// DEBUG: Log what dependencies were fetched
|
|
181
|
-
const fetchedDepKeys = Object.keys(fetchedDeps || {});
|
|
182
|
-
logger.log('INFO', `[MetaExecutor] 📦 Fetched dependencies available: ${fetchedDepKeys.length > 0 ? fetchedDepKeys.join(', ') : 'NONE'}`);
|
|
183
|
-
|
|
184
50
|
const context = ContextFactory.buildMetaContext({
|
|
185
51
|
dateStr: dStr,
|
|
186
52
|
metadata: c,
|
|
@@ -193,172 +59,202 @@ class MetaExecutor {
|
|
|
193
59
|
allRankings: rankings,
|
|
194
60
|
allRankingsYesterday: rankingsYesterday,
|
|
195
61
|
allVerifications: verifications,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
watchlistMembership: watchlistMembership || {},
|
|
199
|
-
alertHistory: alertHistory || {},
|
|
62
|
+
// Spread variable roots directly (ratings, pageViews, etc.)
|
|
63
|
+
...variableRoots,
|
|
200
64
|
seriesData
|
|
201
65
|
});
|
|
202
66
|
|
|
203
|
-
// DEBUG: Log dependency availability
|
|
204
|
-
// CRITICAL: Use class.getDependencies() to get original case-sensitive names
|
|
205
|
-
const depNames = (c.class && typeof c.class.getDependencies === 'function')
|
|
206
|
-
? c.class.getDependencies()
|
|
207
|
-
: (c.getDependencies ? c.getDependencies() : (c.dependencies || []));
|
|
208
|
-
depNames.forEach(depName => {
|
|
209
|
-
const depData = context.computed[depName];
|
|
210
|
-
if (depData) {
|
|
211
|
-
const keys = Object.keys(depData);
|
|
212
|
-
logger.log('INFO', `[MetaExecutor] ✅ Dependency '${depName}' available for ${c.name}. Keys: ${keys.length} (sample: ${keys.slice(0, 5).join(', ')})`);
|
|
213
|
-
} else {
|
|
214
|
-
logger.log('ERROR', `[MetaExecutor] ❌ Dependency '${depName}' MISSING for ${c.name} in context.computed`);
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
|
|
218
67
|
try {
|
|
219
68
|
const result = await inst.process(context);
|
|
220
69
|
|
|
221
|
-
//
|
|
222
|
-
if (
|
|
223
|
-
const resultKeys = Object.keys(result);
|
|
224
|
-
logger.log('INFO', `[MetaExecutor] ✅ ${c.name} computed result. Keys: ${resultKeys.length} (sample: ${resultKeys.slice(0, 10).join(', ')})`);
|
|
225
|
-
if (resultKeys.length === 0) {
|
|
226
|
-
logger.log('WARN', `[MetaExecutor] ⚠️ ${c.name} returned EMPTY result object!`);
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
logger.log('WARN', `[MetaExecutor] ⚠️ ${c.name} returned non-object result: ${typeof result} - ${result}`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// CRITICAL FIX: Do NOT overwrite this.results - the computation already sets it correctly
|
|
233
|
-
// The computation's process() method sets this.results, and getResult() returns it
|
|
234
|
-
// We only use the return value for logging/debugging
|
|
235
|
-
// inst.results should already be set by the computation's process() method
|
|
236
|
-
if (!inst.results) {
|
|
237
|
-
logger.log('WARN', `[MetaExecutor] ⚠️ ${c.name} did not set this.results - using return value as fallback`);
|
|
238
|
-
inst.results = result;
|
|
239
|
-
}
|
|
70
|
+
// Fallback if the computation didn't set this.results internally
|
|
71
|
+
if (!inst.results) inst.results = result;
|
|
240
72
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const finalKeys = Object.keys(finalResult);
|
|
245
|
-
logger.log('INFO', `[MetaExecutor] ✅ ${c.name} getResult() will return ${finalKeys.length} keys`);
|
|
246
|
-
} else {
|
|
247
|
-
logger.log('WARN', `[MetaExecutor] ⚠️ ${c.name} getResult() returns: ${typeof finalResult}`);
|
|
73
|
+
// Debug logging condensed
|
|
74
|
+
if (inst.results && Object.keys(inst.results).length === 0) {
|
|
75
|
+
logger.log('WARN', `[MetaExecutor] ⚠️ ${c.name} produced EMPTY results.`);
|
|
248
76
|
}
|
|
249
77
|
|
|
250
78
|
state[c.name] = inst;
|
|
251
79
|
} catch (e) {
|
|
252
|
-
logger.log('ERROR', `
|
|
80
|
+
logger.log('ERROR', `[MetaExecutor] ❌ ${c.name} failed: ${e.message}`);
|
|
253
81
|
}
|
|
254
82
|
}
|
|
255
83
|
|
|
256
|
-
//
|
|
84
|
+
// Force 'isInitialWrite: true' for robust cleanup of old keys
|
|
257
85
|
return await commitResults(state, dStr, passName, config, deps, false, { isInitialWrite: true });
|
|
258
86
|
}
|
|
259
87
|
|
|
88
|
+
// =========================================================================
|
|
89
|
+
// SINGLE EXECUTION (Sharded/On-Demand)
|
|
90
|
+
// =========================================================================
|
|
260
91
|
static async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps, config, deps, loader) {
|
|
261
|
-
const mappings = await loader.loadMappings();
|
|
262
92
|
const { logger } = deps;
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
93
|
+
const calcs = [metadata]; // Treat single as list for helpers
|
|
94
|
+
|
|
95
|
+
// 1. Load Data using Shared Helpers
|
|
96
|
+
const [mappings, rankings, variableRoots, seriesData] = await Promise.all([
|
|
97
|
+
loader.loadMappings(),
|
|
98
|
+
loader.loadRankings(dateStr),
|
|
99
|
+
loadVariableRootData(loader, dateStr, calcs, logger),
|
|
100
|
+
loadSeriesData(loader, dateStr, calcs, {}, config, deps) // Empty lookup fine for single usage usually
|
|
101
|
+
]);
|
|
102
|
+
|
|
268
103
|
let rankingsYesterday = null;
|
|
269
104
|
if (metadata.isHistorical) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const prevStr = prevDate.toISOString().slice(0, 10);
|
|
273
|
-
rankingsYesterday = await loader.loadRankings(prevStr);
|
|
105
|
+
const prevStr = new Date(new Date(dateStr).getTime() - 86400000).toISOString().slice(0, 10);
|
|
106
|
+
rankingsYesterday = await loader.loadRankings(prevStr);
|
|
274
107
|
}
|
|
275
108
|
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
let ratings = null;
|
|
280
|
-
if (metadata.rootDataDependencies?.includes('ratings')) {
|
|
281
|
-
try { ratings = await loader.loadRatings(dateStr); }
|
|
282
|
-
catch (e) { if (!allowMissing) throw e; ratings = null; }
|
|
283
|
-
if (!ratings && !allowMissing) throw new Error(`Missing ratings`);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
let pageViews = null;
|
|
287
|
-
if (metadata.rootDataDependencies?.includes('pageViews')) {
|
|
288
|
-
try { pageViews = await loader.loadPageViews(dateStr); }
|
|
289
|
-
catch (e) { if (!allowMissing) throw e; pageViews = null; }
|
|
290
|
-
if (!pageViews && !allowMissing) throw new Error(`Missing pageViews`);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
let watchlistMembership = null;
|
|
294
|
-
if (metadata.rootDataDependencies?.includes('watchlist')) {
|
|
295
|
-
try { watchlistMembership = await loader.loadWatchlistMembership(dateStr); }
|
|
296
|
-
catch (e) { if (!allowMissing) throw e; watchlistMembership = null; }
|
|
297
|
-
if (!watchlistMembership && !allowMissing) throw new Error(`Missing watchlist`);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
let alertHistory = null;
|
|
301
|
-
if (metadata.rootDataDependencies?.includes('alerts')) {
|
|
302
|
-
try { alertHistory = await loader.loadAlertHistory(dateStr); }
|
|
303
|
-
catch (e) { if (!allowMissing) throw e; alertHistory = null; }
|
|
304
|
-
if (!alertHistory && !allowMissing) throw new Error(`Missing alerts`);
|
|
305
|
-
}
|
|
109
|
+
const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await loader.loadInsights(dateStr) } : null;
|
|
110
|
+
const social = metadata.rootDataDependencies?.includes('social') ? { today: await loader.loadSocial(dateStr) } : null;
|
|
306
111
|
|
|
307
|
-
|
|
112
|
+
// 2. Build Context Base
|
|
113
|
+
const contextBase = {
|
|
114
|
+
dateStr, metadata, mappings, insights, socialData: social,
|
|
115
|
+
computedDependencies: computedDeps,
|
|
116
|
+
previousComputedDependencies: prevDeps, config, deps,
|
|
117
|
+
allRankings: rankings,
|
|
118
|
+
allRankingsYesterday: rankingsYesterday,
|
|
119
|
+
...variableRoots,
|
|
120
|
+
seriesData
|
|
121
|
+
};
|
|
308
122
|
|
|
123
|
+
// 3. Sharded Execution (Price) or Standard
|
|
309
124
|
if (metadata.rootDataDependencies?.includes('price')) {
|
|
310
125
|
logger.log('INFO', `[Executor] Running Batched/Sharded Execution for ${metadata.name}`);
|
|
311
126
|
const shardRefs = await loader.getPriceShardReferences();
|
|
312
|
-
if (shardRefs.length
|
|
127
|
+
if (!shardRefs.length) {
|
|
128
|
+
logger.log('WARN', '[Executor] No price shards found.');
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
313
131
|
|
|
314
|
-
|
|
132
|
+
const stats = { processedShards: 0, processedItems: 0 };
|
|
315
133
|
for (const ref of shardRefs) {
|
|
316
134
|
const shardData = await loader.loadPriceShard(ref);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
prices: { history: shardData }
|
|
320
|
-
|
|
321
|
-
allRankings: rankings,
|
|
322
|
-
allRankingsYesterday: rankingsYesterday,
|
|
323
|
-
ratings: ratings || {},
|
|
324
|
-
pageViews: pageViews || {},
|
|
325
|
-
watchlistMembership: watchlistMembership || {},
|
|
326
|
-
alertHistory: alertHistory || {},
|
|
327
|
-
seriesData
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
await calcInstance.process(partialContext);
|
|
331
|
-
partialContext.prices = null;
|
|
332
|
-
processedCount++;
|
|
135
|
+
await calcInstance.process(ContextFactory.buildMetaContext({
|
|
136
|
+
...contextBase,
|
|
137
|
+
prices: { history: shardData }
|
|
138
|
+
}));
|
|
333
139
|
|
|
334
140
|
stats.processedShards++;
|
|
335
141
|
stats.processedItems += Object.keys(shardData).length;
|
|
336
142
|
}
|
|
337
|
-
logger.log('INFO', `[Executor] Finished Batched Execution for ${metadata.name} (${processedCount} shards).`);
|
|
338
|
-
|
|
339
143
|
calcInstance._executionStats = stats;
|
|
340
144
|
return calcInstance.getResult ? await calcInstance.getResult() : {};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
ratings: ratings || {},
|
|
349
|
-
pageViews: pageViews || {},
|
|
350
|
-
watchlistMembership: watchlistMembership || {},
|
|
351
|
-
alertHistory: alertHistory || {},
|
|
352
|
-
seriesData
|
|
353
|
-
});
|
|
354
|
-
const res = await calcInstance.process(context);
|
|
355
|
-
|
|
356
|
-
stats.processedItems = 1;
|
|
357
|
-
calcInstance._executionStats = stats;
|
|
358
|
-
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
const res = await calcInstance.process(ContextFactory.buildMetaContext({
|
|
148
|
+
...contextBase,
|
|
149
|
+
prices: {}
|
|
150
|
+
}));
|
|
151
|
+
calcInstance._executionStats = { processedItems: 1 };
|
|
359
152
|
return res;
|
|
360
153
|
}
|
|
361
154
|
}
|
|
362
155
|
}
|
|
363
156
|
|
|
157
|
+
// =============================================================================
|
|
158
|
+
// INTERNAL HELPERS
|
|
159
|
+
// =============================================================================
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Loads variable root data types (Ratings, PageViews, Watchlist, Alerts)
|
|
163
|
+
* based on calculation requirements. Handles strict vs. optional failures.
|
|
164
|
+
*/
|
|
165
|
+
async function loadVariableRootData(loader, dateStr, calcs, logger) {
|
|
166
|
+
const requirements = {};
|
|
167
|
+
const results = { ratings: null, pageViews: null, watchlistMembership: null, alertHistory: null };
|
|
168
|
+
|
|
169
|
+
// Map internal key to Loader Method
|
|
170
|
+
const loaderMap = {
|
|
171
|
+
ratings: { method: 'loadRatings', resultKey: 'ratings' },
|
|
172
|
+
pageViews: { method: 'loadPageViews', resultKey: 'pageViews' },
|
|
173
|
+
watchlist: { method: 'loadWatchlistMembership', resultKey: 'watchlistMembership' },
|
|
174
|
+
alerts: { method: 'loadAlertHistory', resultKey: 'alertHistory' }
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// 1. Analyze Requirements
|
|
178
|
+
for (const c of calcs) {
|
|
179
|
+
const deps = c.rootDataDependencies || [];
|
|
180
|
+
const strict = c.canHaveMissingRoots !== true;
|
|
181
|
+
deps.forEach(d => {
|
|
182
|
+
if (loaderMap[d]) {
|
|
183
|
+
if (!requirements[d]) requirements[d] = { strict: false };
|
|
184
|
+
if (strict) requirements[d].strict = true;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 2. Fetch Data
|
|
190
|
+
const promises = Object.entries(requirements).map(async ([key, req]) => {
|
|
191
|
+
const { method, resultKey } = loaderMap[key];
|
|
192
|
+
try {
|
|
193
|
+
results[resultKey] = await loader[method](dateStr);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
if (req.strict) throw new Error(`[MetaExecutor] Missing required root '${key}': ${e.message}`);
|
|
196
|
+
logger.log('WARN', `[MetaExecutor] Missing optional root '${key}'.`);
|
|
197
|
+
results[resultKey] = null;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await Promise.all(promises);
|
|
202
|
+
return results;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Loads time-series data for both Root inputs and Computation results.
|
|
207
|
+
*/
|
|
208
|
+
async function loadSeriesData(loader, dateStr, calcs, manifestLookup, config, deps) {
|
|
209
|
+
const rootRequests = {};
|
|
210
|
+
const depRequests = {};
|
|
211
|
+
|
|
212
|
+
// 1. Aggregate Lookback Depths
|
|
213
|
+
for (const c of calcs) {
|
|
214
|
+
if (c.rootDataSeries) {
|
|
215
|
+
Object.entries(c.rootDataSeries).forEach(([type, val]) => {
|
|
216
|
+
const days = typeof val === 'object' ? val.lookback : val;
|
|
217
|
+
rootRequests[type] = Math.max(rootRequests[type] || 0, days);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (c.dependencySeries) {
|
|
221
|
+
Object.entries(c.dependencySeries).forEach(([name, val]) => {
|
|
222
|
+
const days = typeof val === 'object' ? val.lookback : val;
|
|
223
|
+
const norm = normalizeName(name);
|
|
224
|
+
depRequests[norm] = Math.max(depRequests[norm] || 0, days);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const seriesData = { root: {}, results: {} };
|
|
230
|
+
|
|
231
|
+
// 2. Fetch Root Series
|
|
232
|
+
const rootLoaders = {
|
|
233
|
+
alerts: 'loadAlertHistory',
|
|
234
|
+
insights: 'loadInsights',
|
|
235
|
+
ratings: 'loadRatings',
|
|
236
|
+
watchlist: 'loadWatchlistMembership',
|
|
237
|
+
rankings: 'loadRankings'
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const rootPromises = Object.entries(rootRequests).map(async ([type, days]) => {
|
|
241
|
+
if (rootLoaders[type]) {
|
|
242
|
+
deps.logger.log('INFO', `[MetaExecutor] Loading ${days}-day series for Root '${type}'`);
|
|
243
|
+
const res = await loader.loadSeries(rootLoaders[type], dateStr, days);
|
|
244
|
+
seriesData.root[type] = res.data;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// 3. Fetch Dependency Series
|
|
249
|
+
const depNames = Object.keys(depRequests);
|
|
250
|
+
if (depNames.length > 0) {
|
|
251
|
+
const maxDays = Math.max(...Object.values(depRequests));
|
|
252
|
+
deps.logger.log('INFO', `[MetaExecutor] Loading up to ${maxDays}-day series for Dependencies: ${depNames.join(', ')}`);
|
|
253
|
+
seriesData.results = await fetchResultSeries(dateStr, depNames, manifestLookup, config, deps, maxDays);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await Promise.all(rootPromises);
|
|
257
|
+
return seriesData;
|
|
258
|
+
}
|
|
259
|
+
|
|
364
260
|
module.exports = { MetaExecutor };
|