bulltrackers-module 1.0.211 → 1.0.213

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.
@@ -1,188 +1,199 @@
1
- /**
2
- * FIXED: computation_controller.js
3
- * V4.2: Added InsightsExtractor and UserClassifier to Context
4
- */
5
-
6
- const { DataExtractor, HistoryExtractor, MathPrimitives, Aggregators, Validators, SCHEMAS, SignalPrimitives, DistributionAnalytics, TimeSeries, priceExtractor, InsightsExtractor, UserClassifier } = require('../layers/math_primitives');
7
- const { loadDailyInsights, loadDailySocialPostInsights, getRelevantShardRefs, getPriceShardRefs } = require('../utils/data_loader');
8
-
9
- class DataLoader {
10
- constructor(config, dependencies) {
11
- this.config = config;
12
- this.deps = dependencies;
13
- this.cache = { mappings: null, insights: new Map(), social: new Map(), prices: null };
14
- }
15
-
16
- // Helper to fix property access issues if any legacy code exists
17
- get mappings() { return this.cache.mappings; }
18
-
19
- async loadMappings() {
20
- if (this.cache.mappings) return this.cache.mappings;
21
- const { calculationUtils } = this.deps;
22
- this.cache.mappings = await calculationUtils.loadInstrumentMappings();
23
- return this.cache.mappings;
24
- }
25
- async loadInsights(dateStr) {
26
- if (this.cache.insights.has(dateStr)) return this.cache.insights.get(dateStr);
27
- const insights = await loadDailyInsights(this.config, this.deps, dateStr);
28
- this.cache.insights.set(dateStr, insights);
29
- return insights;
30
- }
31
- async loadSocial(dateStr) {
32
- if (this.cache.social.has(dateStr)) return this.cache.social.get(dateStr);
33
- const social = await loadDailySocialPostInsights(this.config, this.deps, dateStr);
34
- this.cache.social.set(dateStr, social);
35
- return social;
36
- }
37
-
38
- /**
39
- * NEW: Get references to all price shards without loading data.
40
- */
41
- async getPriceShardReferences() {
42
- return getPriceShardRefs(this.config, this.deps);
43
- }
44
-
45
- /**
46
- * NEW: Get specific shard references based on instrument IDs (Smart Lookup)
47
- */
48
- async getSpecificPriceShardReferences(targetInstrumentIds) {
49
- return getRelevantShardRefs(this.config, this.deps, targetInstrumentIds);
50
- }
51
-
52
- /**
53
- * NEW: Load a single price shard.
54
- */
55
- async loadPriceShard(docRef) {
56
- try {
57
- const snap = await docRef.get();
58
- if (!snap.exists) return {};
59
- return snap.data();
60
- } catch (e) {
61
- console.error(`Error loading shard ${docRef.path}:`, e);
62
- return {};
63
- }
64
- }
65
- }
66
-
67
- class ContextBuilder {
68
- static buildPerUserContext(options) {
69
- const { todayPortfolio, yesterdayPortfolio, todayHistory, yesterdayHistory, userId, userType, dateStr, metadata, mappings, insights, socialData, computedDependencies, previousComputedDependencies, config, deps } = options;
70
- return {
71
- user: { id: userId, type: userType, portfolio: { today: todayPortfolio, yesterday: yesterdayPortfolio }, history: { today: todayHistory, yesterday: yesterdayHistory } },
72
- date: { today: dateStr },
73
- insights: { today: insights?.today, yesterday: insights?.yesterday },
74
- social: { today: socialData?.today, yesterday: socialData?.yesterday },
75
- mappings: mappings || {},
76
- math: {
77
- extract: DataExtractor,
78
- history: HistoryExtractor,
79
- compute: MathPrimitives,
80
- aggregate: Aggregators,
81
- validate: Validators,
82
- signals: SignalPrimitives,
83
- schemas: SCHEMAS,
84
- distribution : DistributionAnalytics,
85
- TimeSeries: TimeSeries,
86
- priceExtractor : priceExtractor,
87
- insights: InsightsExtractor, // Mapped for new Meta calcs
88
- classifier: UserClassifier // Mapped for Smart/Dumb logic
89
- },
90
- computed: computedDependencies || {},
91
- previousComputed: previousComputedDependencies || {},
92
- meta: metadata, config, deps
93
- };
94
- }
95
-
96
- static buildMetaContext(options) {
97
- const { dateStr, metadata, mappings, insights, socialData, prices, computedDependencies, previousComputedDependencies, config, deps } = options;
98
- return {
99
- date: { today: dateStr },
100
- insights: { today: insights?.today, yesterday: insights?.yesterday },
101
- social: { today: socialData?.today, yesterday: socialData?.yesterday },
102
- prices: prices || {},
103
- mappings: mappings || {},
104
- math: {
105
- extract: DataExtractor,
106
- history: HistoryExtractor,
107
- compute: MathPrimitives,
108
- aggregate: Aggregators,
109
- validate: Validators,
110
- signals: SignalPrimitives,
111
- schemas: SCHEMAS,
112
- distribution: DistributionAnalytics,
113
- TimeSeries: TimeSeries,
114
- priceExtractor : priceExtractor,
115
- insights: InsightsExtractor, // Mapped for new Meta calcs
116
- classifier: UserClassifier // Mapped for Smart/Dumb logic
117
- },
118
- computed: computedDependencies || {},
119
- previousComputed: previousComputedDependencies || {},
120
- meta: metadata, config, deps
121
- };
122
- }
123
- }
124
-
125
- class ComputationExecutor {
126
- constructor(config, dependencies, dataLoader) {
127
- this.config = config;
128
- this.deps = dependencies;
129
- this.loader = dataLoader;
130
- }
131
-
132
- async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps) {
133
- const { logger } = this.deps;
134
- const targetUserType = metadata.userType;
135
- const mappings = await this.loader.loadMappings();
136
- const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
137
-
138
- for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
139
- const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
140
- const todayHistory = historyData ? historyData[userId] : null;
141
- const actualUserType = todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
142
- if (targetUserType !== 'all') {
143
- const mappedTarget = (targetUserType === 'speculator') ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
144
- if (mappedTarget !== actualUserType) continue;
145
- }
146
- const context = ContextBuilder.buildPerUserContext({ todayPortfolio, yesterdayPortfolio, todayHistory, userId, userType: actualUserType, dateStr, metadata, mappings, insights, computedDependencies: computedDeps, previousComputedDependencies: prevDeps, config: this.config, deps: this.deps });
147
- try { await calcInstance.process(context); } catch (e) { logger.log('WARN', `Calc ${metadata.name} failed for user ${userId}: ${e.message}`); }
148
- }
149
- }
150
-
151
- async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps) {
152
- const mappings = await this.loader.loadMappings();
153
- const { logger } = this.deps;
154
- const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
155
- const social = metadata.rootDataDependencies?.includes('social') ? { today: await this.loader.loadSocial(dateStr) } : null;
156
-
157
- if (metadata.rootDataDependencies?.includes('price')) {
158
- logger.log('INFO', `[Executor] Running Batched/Sharded Execution for ${metadata.name}`);
159
- const shardRefs = await this.loader.getPriceShardReferences();
160
- if (shardRefs.length === 0) { logger.log('WARN', '[Executor] No price shards found.'); return {}; }
161
- let processedCount = 0;
162
- for (const ref of shardRefs) {
163
- const shardData = await this.loader.loadPriceShard(ref);
164
- const partialContext = ContextBuilder.buildMetaContext({ dateStr, metadata, mappings, insights, socialData: social, prices: { history: shardData }, computedDependencies: computedDeps, previousComputedDependencies: prevDeps, config: this.config, deps: this.deps });
165
- await calcInstance.process(partialContext);
166
- partialContext.prices = null;
167
- processedCount++;
168
- if (processedCount % 10 === 0) { if (global.gc) { global.gc(); } }
169
- }
170
- logger.log('INFO', `[Executor] Finished Batched Execution for ${metadata.name} (${processedCount} shards).`);
171
- return calcInstance.getResult ? await calcInstance.getResult() : {};
172
- } else {
173
- const context = ContextBuilder.buildMetaContext({ dateStr, metadata, mappings, insights, socialData: social, prices: {}, computedDependencies: computedDeps, previousComputedDependencies: prevDeps, config: this.config, deps: this.deps });
174
- return await calcInstance.process(context);
175
- }
176
- }
177
- }
178
-
179
- class ComputationController {
180
- constructor(config, dependencies) {
181
- this.config = config;
182
- this.deps = dependencies;
183
- this.loader = new DataLoader(config, dependencies);
184
- this.executor = new ComputationExecutor(config, dependencies, this.loader);
185
- }
186
- }
187
-
188
- module.exports = { ComputationController };
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 };