bulltrackers-module 1.0.211 → 1.0.212
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/controllers/computation_controller.js +199 -188
- package/functions/computation-system/helpers/computation_dispatcher.js +90 -90
- package/functions/computation-system/helpers/computation_manifest_builder.js +323 -283
- package/functions/computation-system/helpers/computation_pass_runner.js +185 -157
- package/functions/computation-system/helpers/computation_worker.js +85 -85
- package/functions/computation-system/helpers/orchestration_helpers.js +542 -558
- package/functions/computation-system/layers/extractors.js +415 -0
- package/functions/computation-system/layers/index.js +40 -0
- package/functions/computation-system/layers/math_primitives.js +743 -743
- package/functions/computation-system/layers/mathematics.js +397 -0
- package/functions/computation-system/layers/profiling.js +287 -0
- package/functions/computation-system/layers/validators.js +170 -0
- package/functions/computation-system/utils/schema_capture.js +63 -63
- package/functions/computation-system/utils/utils.js +22 -1
- package/package.json +1 -1
|
@@ -1,188 +1,199 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FIXED: computation_controller.js
|
|
3
|
-
*
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
logger.log('
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
1
|
+
/**
|
|
2
|
+
* FIXED: computation_controller.js
|
|
3
|
+
* V5.0: Dynamic Layer Loading via Barrel File
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Load all layers dynamically from the index
|
|
7
|
+
const mathLayer = require('../layers/index');
|
|
8
|
+
|
|
9
|
+
const { loadDailyInsights, loadDailySocialPostInsights, getRelevantShardRefs, getPriceShardRefs } = require('../utils/data_loader');
|
|
10
|
+
|
|
11
|
+
// Legacy Keys Mapping (Ensures backward compatibility with existing Calculations)
|
|
12
|
+
// Maps the new modular class names to the property names expected by existing code (e.g. math.extract)
|
|
13
|
+
const LEGACY_MAPPING = {
|
|
14
|
+
DataExtractor: 'extract',
|
|
15
|
+
HistoryExtractor: 'history',
|
|
16
|
+
MathPrimitives: 'compute',
|
|
17
|
+
Aggregators: 'aggregate',
|
|
18
|
+
Validators: 'validate',
|
|
19
|
+
SignalPrimitives: 'signals',
|
|
20
|
+
SCHEMAS: 'schemas',
|
|
21
|
+
DistributionAnalytics: 'distribution',
|
|
22
|
+
TimeSeries: 'TimeSeries',
|
|
23
|
+
priceExtractor: 'priceExtractor',
|
|
24
|
+
InsightsExtractor: 'insights',
|
|
25
|
+
UserClassifier: 'classifier'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
class DataLoader {
|
|
29
|
+
constructor(config, dependencies) {
|
|
30
|
+
this.config = config;
|
|
31
|
+
this.deps = dependencies;
|
|
32
|
+
this.cache = { mappings: null, insights: new Map(), social: new Map(), prices: null };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get mappings() { return this.cache.mappings; }
|
|
36
|
+
|
|
37
|
+
async loadMappings() {
|
|
38
|
+
if (this.cache.mappings) return this.cache.mappings;
|
|
39
|
+
const { calculationUtils } = this.deps;
|
|
40
|
+
this.cache.mappings = await calculationUtils.loadInstrumentMappings();
|
|
41
|
+
return this.cache.mappings;
|
|
42
|
+
}
|
|
43
|
+
async loadInsights(dateStr) {
|
|
44
|
+
if (this.cache.insights.has(dateStr)) return this.cache.insights.get(dateStr);
|
|
45
|
+
const insights = await loadDailyInsights(this.config, this.deps, dateStr);
|
|
46
|
+
this.cache.insights.set(dateStr, insights);
|
|
47
|
+
return insights;
|
|
48
|
+
}
|
|
49
|
+
async loadSocial(dateStr) {
|
|
50
|
+
if (this.cache.social.has(dateStr)) return this.cache.social.get(dateStr);
|
|
51
|
+
const social = await loadDailySocialPostInsights(this.config, this.deps, dateStr);
|
|
52
|
+
this.cache.social.set(dateStr, social);
|
|
53
|
+
return social;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getPriceShardReferences() {
|
|
57
|
+
return getPriceShardRefs(this.config, this.deps);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getSpecificPriceShardReferences(targetInstrumentIds) {
|
|
61
|
+
return getRelevantShardRefs(this.config, this.deps, targetInstrumentIds);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async loadPriceShard(docRef) {
|
|
65
|
+
try {
|
|
66
|
+
const snap = await docRef.get();
|
|
67
|
+
if (!snap.exists) return {};
|
|
68
|
+
return snap.data();
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error(`Error loading shard ${docRef.path}:`, e);
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class ContextBuilder {
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* dynamically constructs the 'math' object.
|
|
80
|
+
* 1. Iterates over all exports from layers/index.js
|
|
81
|
+
* 2. Maps standard classes to legacy keys (extract, compute, etc.)
|
|
82
|
+
* 3. Adds ALL classes by their actual name to support new features automatically.
|
|
83
|
+
*/
|
|
84
|
+
static buildMathContext() {
|
|
85
|
+
const mathContext = {};
|
|
86
|
+
|
|
87
|
+
// 1. Auto-discover and map
|
|
88
|
+
for (const [key, value] of Object.entries(mathLayer)) {
|
|
89
|
+
// Add by actual name (e.g. math.NewFeature)
|
|
90
|
+
mathContext[key] = value;
|
|
91
|
+
|
|
92
|
+
// Map to legacy key if exists (e.g. math.extract)
|
|
93
|
+
const legacyKey = LEGACY_MAPPING[key];
|
|
94
|
+
if (legacyKey) {
|
|
95
|
+
mathContext[legacyKey] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return mathContext;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static buildPerUserContext(options) {
|
|
103
|
+
const { todayPortfolio, yesterdayPortfolio, todayHistory, yesterdayHistory, userId, userType, dateStr, metadata, mappings, insights, socialData, computedDependencies, previousComputedDependencies, config, deps } = options;
|
|
104
|
+
return {
|
|
105
|
+
user: { id: userId, type: userType, portfolio: { today: todayPortfolio, yesterday: yesterdayPortfolio }, history: { today: todayHistory, yesterday: yesterdayHistory } },
|
|
106
|
+
date: { today: dateStr },
|
|
107
|
+
insights: { today: insights?.today, yesterday: insights?.yesterday },
|
|
108
|
+
social: { today: socialData?.today, yesterday: socialData?.yesterday },
|
|
109
|
+
mappings: mappings || {},
|
|
110
|
+
math: ContextBuilder.buildMathContext(), // DYNAMIC LOAD
|
|
111
|
+
computed: computedDependencies || {},
|
|
112
|
+
previousComputed: previousComputedDependencies || {},
|
|
113
|
+
meta: metadata, config, deps
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static buildMetaContext(options) {
|
|
118
|
+
const { dateStr, metadata, mappings, insights, socialData, prices, computedDependencies, previousComputedDependencies, config, deps } = options;
|
|
119
|
+
return {
|
|
120
|
+
date: { today: dateStr },
|
|
121
|
+
insights: { today: insights?.today, yesterday: insights?.yesterday },
|
|
122
|
+
social: { today: socialData?.today, yesterday: socialData?.yesterday },
|
|
123
|
+
prices: prices || {},
|
|
124
|
+
mappings: mappings || {},
|
|
125
|
+
math: ContextBuilder.buildMathContext(), // DYNAMIC LOAD
|
|
126
|
+
computed: computedDependencies || {},
|
|
127
|
+
previousComputed: previousComputedDependencies || {},
|
|
128
|
+
meta: metadata, config, deps
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
class ComputationExecutor {
|
|
134
|
+
constructor(config, dependencies, dataLoader) {
|
|
135
|
+
this.config = config;
|
|
136
|
+
this.deps = dependencies;
|
|
137
|
+
this.loader = dataLoader;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps) {
|
|
141
|
+
const { logger } = this.deps;
|
|
142
|
+
const targetUserType = metadata.userType;
|
|
143
|
+
const mappings = await this.loader.loadMappings();
|
|
144
|
+
const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
|
|
145
|
+
|
|
146
|
+
// Access SCHEMAS dynamically from the loaded layer
|
|
147
|
+
const SCHEMAS = mathLayer.SCHEMAS;
|
|
148
|
+
|
|
149
|
+
for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
|
|
150
|
+
const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
|
|
151
|
+
const todayHistory = historyData ? historyData[userId] : null;
|
|
152
|
+
const actualUserType = todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
|
|
153
|
+
if (targetUserType !== 'all') {
|
|
154
|
+
const mappedTarget = (targetUserType === 'speculator') ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
|
|
155
|
+
if (mappedTarget !== actualUserType) continue;
|
|
156
|
+
}
|
|
157
|
+
const context = ContextBuilder.buildPerUserContext({ todayPortfolio, yesterdayPortfolio, todayHistory, userId, userType: actualUserType, dateStr, metadata, mappings, insights, computedDependencies: computedDeps, previousComputedDependencies: prevDeps, config: this.config, deps: this.deps });
|
|
158
|
+
try { await calcInstance.process(context); } catch (e) { logger.log('WARN', `Calc ${metadata.name} failed for user ${userId}: ${e.message}`); }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps) {
|
|
163
|
+
const mappings = await this.loader.loadMappings();
|
|
164
|
+
const { logger } = this.deps;
|
|
165
|
+
const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
|
|
166
|
+
const social = metadata.rootDataDependencies?.includes('social') ? { today: await this.loader.loadSocial(dateStr) } : null;
|
|
167
|
+
|
|
168
|
+
if (metadata.rootDataDependencies?.includes('price')) {
|
|
169
|
+
logger.log('INFO', `[Executor] Running Batched/Sharded Execution for ${metadata.name}`);
|
|
170
|
+
const shardRefs = await this.loader.getPriceShardReferences();
|
|
171
|
+
if (shardRefs.length === 0) { logger.log('WARN', '[Executor] No price shards found.'); return {}; }
|
|
172
|
+
let processedCount = 0;
|
|
173
|
+
for (const ref of shardRefs) {
|
|
174
|
+
const shardData = await this.loader.loadPriceShard(ref);
|
|
175
|
+
const partialContext = ContextBuilder.buildMetaContext({ dateStr, metadata, mappings, insights, socialData: social, prices: { history: shardData }, computedDependencies: computedDeps, previousComputedDependencies: prevDeps, config: this.config, deps: this.deps });
|
|
176
|
+
await calcInstance.process(partialContext);
|
|
177
|
+
partialContext.prices = null;
|
|
178
|
+
processedCount++;
|
|
179
|
+
if (processedCount % 10 === 0) { if (global.gc) { global.gc(); } }
|
|
180
|
+
}
|
|
181
|
+
logger.log('INFO', `[Executor] Finished Batched Execution for ${metadata.name} (${processedCount} shards).`);
|
|
182
|
+
return calcInstance.getResult ? await calcInstance.getResult() : {};
|
|
183
|
+
} else {
|
|
184
|
+
const context = ContextBuilder.buildMetaContext({ dateStr, metadata, mappings, insights, socialData: social, prices: {}, computedDependencies: computedDeps, previousComputedDependencies: prevDeps, config: this.config, deps: this.deps });
|
|
185
|
+
return await calcInstance.process(context);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
class ComputationController {
|
|
191
|
+
constructor(config, dependencies) {
|
|
192
|
+
this.config = config;
|
|
193
|
+
this.deps = dependencies;
|
|
194
|
+
this.loader = new DataLoader(config, dependencies);
|
|
195
|
+
this.executor = new ComputationExecutor(config, dependencies, this.loader);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = { ComputationController };
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FILENAME: bulltrackers-module/functions/computation-system/helpers/computation_dispatcher.js
|
|
3
|
-
* PURPOSE: Dispatches computation tasks to Pub/Sub for scalable execution.
|
|
4
|
-
* FIXED: Instantiates PubSubUtils locally to ensure valid logger/dependencies are used.
|
|
5
|
-
* IMPROVED: Logging now explicitly lists the calculations being scheduled.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const { getExpectedDateStrings } = require('../utils/utils.js');
|
|
9
|
-
const { groupByPass } = require('./orchestration_helpers.js');
|
|
10
|
-
const { PubSubUtils } = require('../../core/utils/pubsub_utils');
|
|
11
|
-
|
|
12
|
-
const TOPIC_NAME = 'computation-tasks';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Dispatches computation tasks for a specific pass.
|
|
16
|
-
* Instead of running them, it queues them in Pub/Sub.
|
|
17
|
-
*/
|
|
18
|
-
async function dispatchComputationPass(config, dependencies, computationManifest) {
|
|
19
|
-
const { logger } = dependencies;
|
|
20
|
-
|
|
21
|
-
// Create fresh PubSubUtils instance
|
|
22
|
-
const pubsubUtils = new PubSubUtils(dependencies);
|
|
23
|
-
|
|
24
|
-
const passToRun = String(config.COMPUTATION_PASS_TO_RUN);
|
|
25
|
-
|
|
26
|
-
if (!passToRun) {
|
|
27
|
-
return logger.log('ERROR', '[Dispatcher] No pass defined (COMPUTATION_PASS_TO_RUN). Aborting.');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// 1. Validate Pass Existence
|
|
31
|
-
const passes = groupByPass(computationManifest);
|
|
32
|
-
const calcsInThisPass = passes[passToRun] || [];
|
|
33
|
-
|
|
34
|
-
if (!calcsInThisPass.length) {
|
|
35
|
-
return logger.log('WARN', `[Dispatcher] No calcs for Pass ${passToRun}. Exiting.`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const calcNames = calcsInThisPass.map(c => c.name).join(', ');
|
|
39
|
-
logger.log('INFO', `🚀 [Dispatcher] Preparing PASS ${passToRun}.`);
|
|
40
|
-
logger.log('INFO', `[Dispatcher] Included Calculations: [${calcNames}]`);
|
|
41
|
-
|
|
42
|
-
// 2. Determine Date Range
|
|
43
|
-
// Hardcoded earliest dates - keep synced with PassRunner for now
|
|
44
|
-
const earliestDates = {
|
|
45
|
-
portfolio: new Date('2025-09-25T00:00:00Z'),
|
|
46
|
-
history: new Date('2025-11-05T00:00:00Z'),
|
|
47
|
-
social: new Date('2025-10-30T00:00:00Z'),
|
|
48
|
-
insights: new Date('2025-08-26T00:00:00Z'),
|
|
49
|
-
price: new Date('2025-08-01T00:00:00Z')
|
|
50
|
-
};
|
|
51
|
-
const passEarliestDate = Object.values(earliestDates).reduce((a, b) => a < b ? a : b);
|
|
52
|
-
const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
|
|
53
|
-
|
|
54
|
-
const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
|
|
55
|
-
|
|
56
|
-
logger.log('INFO', `[Dispatcher] Dispatches checks for ${allExpectedDates.length} dates (${allExpectedDates[0]} to ${allExpectedDates[allExpectedDates.length - 1]}). Workers will validate dependencies.`);
|
|
57
|
-
|
|
58
|
-
// 3. Dispatch Messages
|
|
59
|
-
let dispatchedCount = 0;
|
|
60
|
-
const BATCH_SIZE = 50;
|
|
61
|
-
|
|
62
|
-
// We can publish in parallel batches
|
|
63
|
-
const chunks = [];
|
|
64
|
-
for (let i = 0; i < allExpectedDates.length; i += BATCH_SIZE) {
|
|
65
|
-
chunks.push(allExpectedDates.slice(i, i + BATCH_SIZE));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
for (const chunk of chunks) {
|
|
69
|
-
const messages = chunk.map(dateStr => ({
|
|
70
|
-
json: {
|
|
71
|
-
action: 'RUN_COMPUTATION_DATE',
|
|
72
|
-
date: dateStr,
|
|
73
|
-
pass: passToRun,
|
|
74
|
-
timestamp: Date.now()
|
|
75
|
-
}
|
|
76
|
-
}));
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
await pubsubUtils.publishMessageBatch(TOPIC_NAME, messages);
|
|
80
|
-
dispatchedCount += messages.length;
|
|
81
|
-
logger.log('INFO', `[Dispatcher] Dispatched batch of ${messages.length} tasks.`);
|
|
82
|
-
} catch (err) {
|
|
83
|
-
logger.log('ERROR', `[Dispatcher] Failed to dispatch batch: ${err.message}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
logger.log('INFO', `[Dispatcher] Finished. Dispatched ${dispatchedCount} checks for Pass ${passToRun}.`);
|
|
88
|
-
return { dispatched: dispatchedCount };
|
|
89
|
-
}
|
|
90
|
-
|
|
1
|
+
/**
|
|
2
|
+
* FILENAME: bulltrackers-module/functions/computation-system/helpers/computation_dispatcher.js
|
|
3
|
+
* PURPOSE: Dispatches computation tasks to Pub/Sub for scalable execution.
|
|
4
|
+
* FIXED: Instantiates PubSubUtils locally to ensure valid logger/dependencies are used.
|
|
5
|
+
* IMPROVED: Logging now explicitly lists the calculations being scheduled.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { getExpectedDateStrings } = require('../utils/utils.js');
|
|
9
|
+
const { groupByPass } = require('./orchestration_helpers.js');
|
|
10
|
+
const { PubSubUtils } = require('../../core/utils/pubsub_utils');
|
|
11
|
+
|
|
12
|
+
const TOPIC_NAME = 'computation-tasks';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Dispatches computation tasks for a specific pass.
|
|
16
|
+
* Instead of running them, it queues them in Pub/Sub.
|
|
17
|
+
*/
|
|
18
|
+
async function dispatchComputationPass(config, dependencies, computationManifest) {
|
|
19
|
+
const { logger } = dependencies;
|
|
20
|
+
|
|
21
|
+
// Create fresh PubSubUtils instance
|
|
22
|
+
const pubsubUtils = new PubSubUtils(dependencies);
|
|
23
|
+
|
|
24
|
+
const passToRun = String(config.COMPUTATION_PASS_TO_RUN);
|
|
25
|
+
|
|
26
|
+
if (!passToRun) {
|
|
27
|
+
return logger.log('ERROR', '[Dispatcher] No pass defined (COMPUTATION_PASS_TO_RUN). Aborting.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 1. Validate Pass Existence
|
|
31
|
+
const passes = groupByPass(computationManifest);
|
|
32
|
+
const calcsInThisPass = passes[passToRun] || [];
|
|
33
|
+
|
|
34
|
+
if (!calcsInThisPass.length) {
|
|
35
|
+
return logger.log('WARN', `[Dispatcher] No calcs for Pass ${passToRun}. Exiting.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const calcNames = calcsInThisPass.map(c => c.name).join(', ');
|
|
39
|
+
logger.log('INFO', `🚀 [Dispatcher] Preparing PASS ${passToRun}.`);
|
|
40
|
+
logger.log('INFO', `[Dispatcher] Included Calculations: [${calcNames}]`);
|
|
41
|
+
|
|
42
|
+
// 2. Determine Date Range
|
|
43
|
+
// Hardcoded earliest dates - keep synced with PassRunner for now
|
|
44
|
+
const earliestDates = {
|
|
45
|
+
portfolio: new Date('2025-09-25T00:00:00Z'),
|
|
46
|
+
history: new Date('2025-11-05T00:00:00Z'),
|
|
47
|
+
social: new Date('2025-10-30T00:00:00Z'),
|
|
48
|
+
insights: new Date('2025-08-26T00:00:00Z'),
|
|
49
|
+
price: new Date('2025-08-01T00:00:00Z')
|
|
50
|
+
};
|
|
51
|
+
const passEarliestDate = Object.values(earliestDates).reduce((a, b) => a < b ? a : b);
|
|
52
|
+
const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
|
|
53
|
+
|
|
54
|
+
const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
|
|
55
|
+
|
|
56
|
+
logger.log('INFO', `[Dispatcher] Dispatches checks for ${allExpectedDates.length} dates (${allExpectedDates[0]} to ${allExpectedDates[allExpectedDates.length - 1]}). Workers will validate dependencies.`);
|
|
57
|
+
|
|
58
|
+
// 3. Dispatch Messages
|
|
59
|
+
let dispatchedCount = 0;
|
|
60
|
+
const BATCH_SIZE = 50;
|
|
61
|
+
|
|
62
|
+
// We can publish in parallel batches
|
|
63
|
+
const chunks = [];
|
|
64
|
+
for (let i = 0; i < allExpectedDates.length; i += BATCH_SIZE) {
|
|
65
|
+
chunks.push(allExpectedDates.slice(i, i + BATCH_SIZE));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const chunk of chunks) {
|
|
69
|
+
const messages = chunk.map(dateStr => ({
|
|
70
|
+
json: {
|
|
71
|
+
action: 'RUN_COMPUTATION_DATE',
|
|
72
|
+
date: dateStr,
|
|
73
|
+
pass: passToRun,
|
|
74
|
+
timestamp: Date.now()
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await pubsubUtils.publishMessageBatch(TOPIC_NAME, messages);
|
|
80
|
+
dispatchedCount += messages.length;
|
|
81
|
+
logger.log('INFO', `[Dispatcher] Dispatched batch of ${messages.length} tasks.`);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logger.log('ERROR', `[Dispatcher] Failed to dispatch batch: ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
logger.log('INFO', `[Dispatcher] Finished. Dispatched ${dispatchedCount} checks for Pass ${passToRun}.`);
|
|
88
|
+
return { dispatched: dispatchedCount };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
91
|
module.exports = { dispatchComputationPass };
|