bulltrackers-module 1.0.474 → 1.0.475

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.
@@ -168,12 +168,17 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
168
168
  return report;
169
169
  }
170
170
 
171
- async function executeDispatchTask(dateStr, pass, targetComputation, config, dependencies, computationManifest, previousCategory = null, dependencyResultHashes = {}) {
171
+ async function executeDispatchTask(dateStr, pass, targetComputation, config, dependencies, computationManifest, previousCategory = null, dependencyResultHashes = {}, metadata = {}) {
172
172
  const { logger } = dependencies;
173
173
  const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
174
174
  const calcManifest = manifestMap.get(normalizeName(targetComputation));
175
175
 
176
176
  if (!calcManifest) throw new Error(`Calc '${targetComputation}' not found.`);
177
+
178
+ // Merge runtime metadata (like targetCid) into the manifest
179
+ // This allows the Executor to access it via 'calcInstance.manifest'
180
+ Object.assign(calcManifest, metadata);
181
+
177
182
  calcManifest.dependencyResultHashes = dependencyResultHashes;
178
183
 
179
184
  const rootData = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
@@ -235,6 +235,14 @@ class StandardExecutor {
235
235
  let chunkFailures = 0;
236
236
 
237
237
  for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
238
+ // --- OPTIMIZATION: TARGET SPECIFIC USER ---
239
+ // If the request contains a targetCid, skip all other users immediately
240
+ if (metadata.targetCid && String(userId) !== String(metadata.targetCid)) {
241
+ if (stats) stats.skippedUsers++;
242
+ continue;
243
+ }
244
+ // ------------------------------------------
245
+
238
246
  const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
239
247
  const todayHistory = historyData ? historyData[userId] : null;
240
248
 
@@ -93,7 +93,7 @@ function startMemoryHeartbeat(db, ledgerPath, workerId, computationName, traceId
93
93
  /**
94
94
  * STRICT IDEMPOTENCY GATE
95
95
  */
96
- async function checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId) {
96
+ async function checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId, onDemand = false) {
97
97
  const docRef = db.doc(ledgerPath);
98
98
 
99
99
  try {
@@ -103,25 +103,27 @@ async function checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerI
103
103
  if (doc.exists) {
104
104
  const data = doc.data();
105
105
 
106
- // [BUG] This blocks legitimate re-runs initiated by the Dispatcher
107
- /* if (['COMPLETED', 'FAILED', 'CRASH'].includes(data.status)) {
108
- return { shouldRun: false, reason: `Task already in terminal state: ${data.status}` };
109
- }
110
- */
111
-
112
- // [FIX] Only block if it's the EXACT SAME Dispatch ID (Duplicate Delivery)
113
- if (['COMPLETED', 'FAILED', 'CRASH'].includes(data.status)) {
114
- if (data.dispatchId === dispatchId) {
115
- return { shouldRun: false, reason: `Task already in terminal state: ${data.status}` };
106
+ // [ON-DEMAND OVERRIDE] If this is an on-demand request, force re-run even if completed
107
+ if (onDemand && ['COMPLETED', 'FAILED', 'CRASH'].includes(data.status)) {
108
+ // Force overwrite - on-demand requests should always run
109
+ // Log will be handled by caller
110
+ // Continue to claim lease below
111
+ } else {
112
+ // [FIX] Only block if it's the EXACT SAME Dispatch ID (Duplicate Delivery)
113
+ if (['COMPLETED', 'FAILED', 'CRASH'].includes(data.status)) {
114
+ if (data.dispatchId === dispatchId) {
115
+ return { shouldRun: false, reason: `Task already in terminal state: ${data.status}` };
116
+ }
117
+ // If dispatchId differs, we allow the overwrite (Re-Run).
118
+ // The Dispatcher is the authority; if it sent a message, we run it.
116
119
  }
117
- // If dispatchId differs, we allow the overwrite (Re-Run).
118
- // The Dispatcher is the authority; if it sent a message, we run it.
119
120
  }
120
121
 
121
122
  if (data.status === 'IN_PROGRESS' && data.dispatchId === dispatchId) {
122
123
  return { shouldRun: false, reason: 'Duplicate delivery: Task already IN_PROGRESS with same ID.' };
123
124
  }
124
- if (data.status === 'IN_PROGRESS') {
125
+ if (data.status === 'IN_PROGRESS' && !onDemand) {
126
+ // On-demand can break locks if needed, but regular requests should wait
125
127
  return { shouldRun: false, reason: 'Collision: Task currently IN_PROGRESS by another worker.' };
126
128
  }
127
129
  }
@@ -130,7 +132,8 @@ async function checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerI
130
132
  status: 'IN_PROGRESS',
131
133
  workerId: workerId,
132
134
  dispatchId: dispatchId || 'unknown',
133
- startedAt: new Date()
135
+ startedAt: new Date(),
136
+ onDemand: onDemand || false
134
137
  };
135
138
 
136
139
  t.set(docRef, lease, { merge: true });
@@ -151,8 +154,9 @@ async function handleComputationTask(message, config, dependencies) {
151
154
 
152
155
  if (!data || data.action !== 'RUN_COMPUTATION_DATE') return;
153
156
 
154
- const { date, pass, computation, previousCategory, triggerReason, dispatchId, dependencyResultHashes, resources, traceContext } = data;
157
+ const { date, pass, computation, previousCategory, triggerReason, dispatchId, dependencyResultHashes, resources, traceContext, metadata } = data;
155
158
  const resourceTier = resources || 'standard';
159
+ const onDemand = metadata?.onDemand === true || false; // Extract on-demand flag
156
160
  const ledgerPath = `computation_audit_ledger/${date}/passes/${pass}/tasks/${computation}`;
157
161
  const workerId = process.env.K_REVISION || os.hostname();
158
162
 
@@ -174,12 +178,16 @@ async function handleComputationTask(message, config, dependencies) {
174
178
  const runDeps = { ...dependencies, logger };
175
179
  const db = dependencies.db;
176
180
 
177
- // --- STEP 1: IDEMPOTENCY CHECK ---
178
- const gate = await checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId);
181
+ // --- STEP 1: IDEMPOTENCY CHECK (with on-demand override) ---
182
+ const gate = await checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId, onDemand);
179
183
  if (!gate.shouldRun) {
180
184
  logger.log('WARN', `[Worker] 🛑 Idempotency Gate: Skipping ${computation}. Reason: ${gate.reason}`);
181
185
  return;
182
186
  }
187
+
188
+ if (onDemand) {
189
+ logger.log('INFO', `[Worker] 🔄 On-demand request: Forcing re-run of ${computation} for ${date}`);
190
+ }
183
191
 
184
192
  logger.log('INFO', `[Worker] 📥 Task: ${computation} (${date}) [Tier: ${resourceTier}] [ID: ${dispatchId}]`);
185
193
 
@@ -195,7 +203,8 @@ async function handleComputationTask(message, config, dependencies) {
195
203
 
196
204
  const result = await executeDispatchTask(
197
205
  date, pass, computation, config, runDeps,
198
- manifest, previousCategory, dependencyResultHashes
206
+ manifest, previousCategory, dependencyResultHashes,
207
+ metadata // Pass metadata (including targetCid) to orchestrator
199
208
  );
200
209
 
201
210
  heartbeats.stop();
@@ -0,0 +1,151 @@
1
+ /**
2
+ * @fileoverview Helpers for on-demand computation requests
3
+ * Handles dependency chain resolution and ordered triggering
4
+ */
5
+
6
+ const { getManifest } = require('../topology/ManifestLoader');
7
+ const { normalizeName } = require('../utils/utils');
8
+
9
+ // Import calculations package (matching computation_worker.js pattern)
10
+ let calculationPackage;
11
+ try {
12
+ calculationPackage = require('aiden-shared-calculations-unified');
13
+ } catch (e) {
14
+ throw new Error(`Failed to load calculations package: ${e.message}`);
15
+ }
16
+ const calculations = calculationPackage.calculations;
17
+
18
+ /**
19
+ * Resolves all dependencies for a given computation and returns them grouped by pass
20
+ * @param {string} computationName - The target computation name
21
+ * @param {Array} manifest - The computation manifest
22
+ * @returns {Array<{pass: number, computations: Array<string>}>} Dependencies grouped by pass
23
+ */
24
+ function resolveDependencyChain(computationName, manifest) {
25
+ const manifestMap = new Map(manifest.map(c => [normalizeName(c.name), c]));
26
+ const normalizedTarget = normalizeName(computationName);
27
+
28
+ const targetCalc = manifestMap.get(normalizedTarget);
29
+ if (!targetCalc) {
30
+ throw new Error(`Computation ${computationName} not found in manifest`);
31
+ }
32
+
33
+ // Build adjacency list (dependencies map)
34
+ const adjacency = new Map();
35
+ for (const calc of manifest) {
36
+ const normName = normalizeName(calc.name);
37
+ adjacency.set(normName, (calc.dependencies || []).map(d => normalizeName(d)));
38
+ }
39
+
40
+ // Get all dependencies recursively
41
+ const required = new Set([normalizedTarget]);
42
+ const queue = [normalizedTarget];
43
+
44
+ while (queue.length > 0) {
45
+ const calcName = queue.shift();
46
+ const dependencies = adjacency.get(calcName) || [];
47
+ for (const dep of dependencies) {
48
+ if (!required.has(dep)) {
49
+ required.add(dep);
50
+ queue.push(dep);
51
+ }
52
+ }
53
+ }
54
+
55
+ // Group by pass
56
+ const byPass = new Map();
57
+ for (const calcName of required) {
58
+ const calc = manifestMap.get(calcName);
59
+ if (calc) {
60
+ const pass = calc.pass || 1;
61
+ if (!byPass.has(pass)) {
62
+ byPass.set(pass, []);
63
+ }
64
+ byPass.get(pass).push(calc.name); // Use original name, not normalized
65
+ }
66
+ }
67
+
68
+ // Convert to sorted array
69
+ const passes = Array.from(byPass.entries())
70
+ .sort((a, b) => a[0] - b[0])
71
+ .map(([pass, computations]) => ({ pass, computations }));
72
+
73
+ return passes;
74
+ }
75
+
76
+ /**
77
+ * Triggers computations for a given date, handling dependencies in order
78
+ * @param {string} targetComputation - The target computation to run
79
+ * @param {string} date - The date string (YYYY-MM-DD)
80
+ * @param {object} dependencies - Contains pubsub, logger, etc.
81
+ * @param {object} config - Computation system config
82
+ * @param {object} metadata - Additional metadata for the computation request
83
+ * @returns {Promise<Array>} Array of triggered computation messages
84
+ */
85
+ async function triggerComputationWithDependencies(targetComputation, date, dependencies, config, metadata = {}) {
86
+ const { pubsub, logger } = dependencies;
87
+ const computationTopic = config.computationTopicStandard || 'computation-tasks';
88
+ const topic = pubsub.topic(computationTopic);
89
+ const crypto = require('crypto');
90
+
91
+ // Get manifest to resolve dependencies
92
+ const manifest = getManifest(config.activeProductLines || [], calculations);
93
+
94
+ // Resolve dependency chain
95
+ const dependencyPasses = resolveDependencyChain(targetComputation, manifest);
96
+
97
+ logger.log('INFO', `[On-Demand] Resolved dependency chain for ${targetComputation}:`, {
98
+ totalPasses: dependencyPasses.length,
99
+ passes: dependencyPasses.map(p => `Pass ${p.pass}: ${p.computations.length} computations`)
100
+ });
101
+
102
+ const triggeredMessages = [];
103
+
104
+ // Trigger each pass in order
105
+ for (const passGroup of dependencyPasses) {
106
+ const { pass, computations } = passGroup;
107
+
108
+ // Trigger all computations in this pass
109
+ for (const computation of computations) {
110
+ const dispatchId = crypto.randomUUID();
111
+ const isTarget = normalizeName(computation) === normalizeName(targetComputation);
112
+
113
+ const computationMessage = {
114
+ action: 'RUN_COMPUTATION_DATE',
115
+ computation: computation,
116
+ date: date,
117
+ pass: String(pass),
118
+ dispatchId: dispatchId,
119
+ triggerReason: metadata.triggerReason || 'on_demand',
120
+ resources: metadata.resources || 'standard',
121
+ metadata: {
122
+ ...metadata,
123
+ onDemand: true,
124
+ isTargetComputation: isTarget,
125
+ targetCid: metadata.targetCid || null // Pass through targetCid for optimization
126
+ },
127
+ traceContext: {
128
+ traceId: crypto.randomBytes(16).toString('hex'),
129
+ spanId: crypto.randomBytes(8).toString('hex'),
130
+ sampled: true
131
+ }
132
+ };
133
+
134
+ await topic.publishMessage({
135
+ data: Buffer.from(JSON.stringify(computationMessage))
136
+ });
137
+
138
+ triggeredMessages.push(computationMessage);
139
+
140
+ logger.log('INFO', `[On-Demand] Triggered ${computation} (Pass ${pass})${isTarget ? ' [TARGET]' : ' [DEPENDENCY]'}`);
141
+ }
142
+ }
143
+
144
+ return triggeredMessages;
145
+ }
146
+
147
+ module.exports = {
148
+ resolveDependencyChain,
149
+ triggerComputationWithDependencies
150
+ };
151
+
@@ -94,6 +94,30 @@ exports.fetchAndStorePrices = async (config, dependencies) => {
94
94
 
95
95
  logger.log('INFO', `[PriceFetcherHelpers] Wrote price date tracking document for ${today} with ${priceDatesArray.length} dates (from August 2025 onwards)`);
96
96
 
97
+ // Update root data indexer for today's date after price data is stored
98
+ try {
99
+ const { runRootDataIndexer } = require('../../root-data-indexer/index');
100
+ const rootDataIndexerConfig = config.rootDataIndexer || {
101
+ availabilityCollection: 'system_root_data_index',
102
+ earliestDate: '2025-08-01',
103
+ collections: {
104
+ prices: priceCollectionName
105
+ }
106
+ };
107
+
108
+ const indexerConfig = {
109
+ ...rootDataIndexerConfig,
110
+ targetDate: today // Index only today's date for speed
111
+ };
112
+
113
+ logger.log('INFO', `[PriceFetcherHelpers] Triggering root data indexer for date ${today} after price data storage...`);
114
+ await runRootDataIndexer(indexerConfig, dependencies);
115
+ logger.log('INFO', `[PriceFetcherHelpers] Root data indexer completed for date ${today}`);
116
+ } catch (indexerError) {
117
+ logger.log('ERROR', `[PriceFetcherHelpers] Failed to run root data indexer for ${today}`, indexerError);
118
+ // Continue - price data is stored, indexer failure is non-critical
119
+ }
120
+
97
121
  const successMessage = `Successfully processed and saved daily prices for ${results.length} instruments to ${batchPromises.length} shards.`;
98
122
  logger.log('SUCCESS', `[PriceFetcherHelpers] ${successMessage}`);
99
123
  return { success: true, message: successMessage, instrumentsProcessed: results.length };
@@ -118,6 +118,30 @@ exports.fetchAndStoreInsights = async (config, dependencies) => {
118
118
 
119
119
  await docRef.set(firestorePayload);
120
120
 
121
+ // Update root data indexer for today's date after insights data is stored
122
+ try {
123
+ const { runRootDataIndexer } = require('../../root-data-indexer/index');
124
+ const rootDataIndexerConfig = config.rootDataIndexer || {
125
+ availabilityCollection: 'system_root_data_index',
126
+ earliestDate: '2025-08-01',
127
+ collections: {
128
+ insights: config.insightsCollectionName
129
+ }
130
+ };
131
+
132
+ const indexerConfig = {
133
+ ...rootDataIndexerConfig,
134
+ targetDate: today // Index only today's date for speed
135
+ };
136
+
137
+ logger.log('INFO', `[FetchInsightsHelpers] Triggering root data indexer for date ${today} after insights data storage...`);
138
+ await runRootDataIndexer(indexerConfig, dependencies);
139
+ logger.log('INFO', `[FetchInsightsHelpers] Root data indexer completed for date ${today}`);
140
+ } catch (indexerError) {
141
+ logger.log('ERROR', `[FetchInsightsHelpers] Failed to run root data indexer for ${today}`, indexerError);
142
+ // Continue - insights data is stored, indexer failure is non-critical
143
+ }
144
+
121
145
  const successMsg = `Successfully fetched and stored ${insightsData.length} instrument insights for ${today}.`;
122
146
  logger.log('SUCCESS', `[FetchInsightsHelpers] ${successMsg}`, { documentId: today, instrumentCount: insightsData.length });
123
147
  return { success: true, message: successMsg, instrumentCount: insightsData.length };
@@ -149,6 +149,7 @@ async function requestPiFetch(req, res, dependencies, config) {
149
149
  actualRequestedBy: Number(userCid), // Track actual developer CID
150
150
  metadata: {
151
151
  onDemand: true,
152
+ targetCid: piCidNum, // Target specific user for optimization
152
153
  requestedAt: now.toISOString(),
153
154
  isImpersonating: isImpersonating || false
154
155
  }
@@ -163,6 +163,7 @@ async function requestUserSync(req, res, dependencies, config) {
163
163
  effectiveRequestedBy: effectiveCid,
164
164
  metadata: {
165
165
  onDemand: true,
166
+ targetCid: targetCidNum, // Target specific user for optimization
166
167
  requestedAt: now.toISOString(),
167
168
  isImpersonating: isImpersonating || false
168
169
  }
@@ -163,31 +163,55 @@ async function finalizeVerification(req, res, dependencies, config) {
163
163
  }, { merge: true });
164
164
 
165
165
  // 3. Trigger Downstream Systems via Pub/Sub
166
+ // Send unified request to task engine that handles both portfolio/history AND social data
166
167
  const pubsubUtils = new PubSubUtils(dependencies);
167
-
168
- // TRIGGER 1: Portfolio (Only if Public)
168
+
169
+ // Create a unified on-demand request that includes both portfolio and social
170
+ // The task engine will process both, update root data, then trigger computations
171
+ const unifiedTask = {
172
+ type: 'ON_DEMAND_USER_UPDATE', // Matches Task Engine handler
173
+ data: {
174
+ cid: realCID,
175
+ username: profileData.username,
176
+ source: 'user_signup', // Mark as signup to ensure computations are triggered
177
+ includeSocial: true, // Flag to include social data fetch
178
+ since: new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString() // Last 7 days for social
179
+ },
180
+ metadata: {
181
+ onDemand: true,
182
+ targetCid: realCID, // Optimization: only process this user
183
+ isNewUser: true, // Flag to add to daily update queue
184
+ requestedAt: new Date().toISOString()
185
+ }
186
+ };
187
+
188
+ // Only trigger if user is public (has portfolio data)
169
189
  if (!isOptOut) {
170
- const portfolioTask = {
171
- type: 'ON_DEMAND_USER_UPDATE', // Matches Task Engine handler
190
+ await pubsubUtils.publish(pubsubTopicUserFetch, unifiedTask);
191
+ logger.log('INFO', `[Verification] Triggered unified data fetch (portfolio + social) for ${username} (${realCID})`);
192
+ } else {
193
+ // For private users, still fetch social data but no portfolio
194
+ const socialOnlyTask = {
195
+ type: 'ON_DEMAND_USER_UPDATE',
172
196
  data: {
173
197
  cid: realCID,
174
- username: profileData.username
198
+ username: profileData.username,
199
+ source: 'user_signup',
200
+ includeSocial: true,
201
+ portfolioOnly: false, // Skip portfolio for private users
202
+ since: new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString()
203
+ },
204
+ metadata: {
205
+ onDemand: true,
206
+ targetCid: realCID,
207
+ isNewUser: true,
208
+ requestedAt: new Date().toISOString()
175
209
  }
176
210
  };
177
- await pubsubUtils.publish(pubsubTopicUserFetch, portfolioTask);
178
- logger.log('INFO', `[Verification] Triggered Portfolio Fetch for ${username}`);
211
+ await pubsubUtils.publish(pubsubTopicUserFetch, socialOnlyTask);
212
+ logger.log('INFO', `[Verification] Triggered social-only fetch for private user ${username} (${realCID})`);
179
213
  }
180
214
 
181
- // TRIGGER 2: Social Posts (Always, assuming social is visible or standard handling applies)
182
- const socialTask = {
183
- type: 'SIGNED_IN_USER', // Matches Social Task Handler
184
- id: String(realCID),
185
- username: profileData.username,
186
- since: new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString() // Last 7 days initial fetch
187
- };
188
- await pubsubUtils.publish(pubsubTopicSocialFetch, socialTask);
189
- logger.log('INFO', `[Verification] Triggered Social Fetch for ${username}`);
190
-
191
215
  return res.status(200).json({
192
216
  success: true,
193
217
  message: "Account verified successfully. Data ingestion started.",
@@ -78,15 +78,56 @@ exports.runSocialOrchestrator = async (config, dependencies) => {
78
78
  return { success: true, message: "No tasks to run." };
79
79
  }
80
80
 
81
- // Use batch publish (assuming pubsubUtils handles batching logic internally or via array)
81
+ // Publish to task engine (not social task handler)
82
+ // The task engine will now handle social posts along with portfolio/history
83
+ const taskEngineTopicName = config.taskEngineTopicName || process.env.PUBSUB_TOPIC_USER_FETCH || 'etoro-user-fetch-topic';
84
+
85
+ // Convert social tasks to task engine format
86
+ const taskEngineTasks = tasks.map(task => ({
87
+ type: task.type === 'INSTRUMENT' ? 'SOCIAL_INSTRUMENT_FETCH' :
88
+ task.type === 'POPULAR_INVESTOR' ? 'SOCIAL_PI_FETCH' :
89
+ 'SOCIAL_SIGNED_IN_USER_FETCH',
90
+ data: {
91
+ id: task.id,
92
+ username: task.username,
93
+ since: task.since,
94
+ type: task.type
95
+ }
96
+ }));
97
+
98
+ // Initialize batch counter for social tasks
99
+ const userTasksCount = tasks.filter(t => t.type === 'POPULAR_INVESTOR' || t.type === 'SIGNED_IN_USER').length;
100
+ let socialCounterRef = null;
101
+
102
+ if (userTasksCount > 0) {
103
+ try {
104
+ const counterId = `social-${today}-${Date.now()}`;
105
+ socialCounterRef = db.collection('social_batch_counters').doc(counterId);
106
+ await socialCounterRef.set({
107
+ totalTasks: userTasksCount,
108
+ remainingTasks: userTasksCount,
109
+ startedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp(),
110
+ date: today
111
+ });
112
+ logger.log('INFO', `[SocialOrchestrator] Initialized social batch counter: ${userTasksCount} user tasks for ${today}`);
113
+ } catch (counterError) {
114
+ logger.log('WARN', `[SocialOrchestrator] Failed to initialize social batch counter`, counterError);
115
+ }
116
+ }
117
+
118
+ // Use batch publish to task engine
82
119
  await pubsubUtils.batchPublishTasks(dependencies, {
83
- topicName: socialFetchTaskTopicName,
84
- tasks: tasks,
85
- taskType: 'social-fetch-task'
120
+ topicName: taskEngineTopicName,
121
+ tasks: taskEngineTasks,
122
+ taskType: 'social-fetch-task',
123
+ metadata: {
124
+ batchCounterRef: socialCounterRef?.path || null,
125
+ targetDate: today
126
+ }
86
127
  });
87
128
 
88
- logger.log('SUCCESS', `[SocialOrchestrator] Published ${tasks.length} total social fetch tasks.`);
89
- return { success: true, tasksQueued: tasks.length };
129
+ logger.log('SUCCESS', `[SocialOrchestrator] Published ${taskEngineTasks.length} total social fetch tasks to task engine.`);
130
+ return { success: true, tasksQueued: taskEngineTasks.length, batchCounterRef: socialCounterRef?.path || null };
90
131
 
91
132
  } catch (error) {
92
133
  logger.log('ERROR', '[SocialOrchestrator] Fatal error.', error);
@@ -162,7 +162,7 @@ async function getAdvancedAnalysisFromGemini(dependencies, snippet) {
162
162
  /**
163
163
  * Main pipe (Task Handler): pipe.maintenance.handleSocialTask
164
164
  */
165
- exports.handleSocialTask = async (message, context, config, dependencies) => {
165
+ async function handleSocialTask(message, context, config, dependencies) {
166
166
  const { db, logger, headerManager, proxyManager } = dependencies;
167
167
  let task;
168
168
  try {
@@ -383,4 +383,6 @@ exports.handleSocialTask = async (message, context, config, dependencies) => {
383
383
  } finally {
384
384
  await headerManager.flushPerformanceUpdates();
385
385
  }
386
- };
386
+ }
387
+
388
+ module.exports = { handleSocialTask, getGcidForUser };
@@ -7,14 +7,21 @@ const { handleDiscover } = require('./helpers/discover_helpers');
7
7
  const { handleVerify } = require('./helpers/verify_helpers');
8
8
  const { handleUpdate } = require('./helpers/update_helpers');
9
9
  const { handlePopularInvestorUpdate, handleOnDemandUserUpdate } = require('./helpers/popular_investor_helpers');
10
+ const { handleSocialFetch } = require('./helpers/social_helpers');
10
11
 
11
12
  // IMPORT THE UTILS TO HANDLE BATCHES
12
13
  const { executeTasks, prepareTaskBatches } = require('./utils/task_engine_utils');
13
14
 
14
15
  async function handleRequest(message, context, configObj, dependencies) {
15
- // Support both old format (single config) and new format (object with taskEngine and rootDataIndexer)
16
+ // Support both old format (single config) and new format (object with taskEngine, rootDataIndexer, and social)
16
17
  const config = configObj.taskEngine || configObj; // Backward compatibility
17
18
  const rootDataIndexerConfig = configObj.rootDataIndexer;
19
+ const socialConfig = configObj.social;
20
+
21
+ // Merge social config into main config for easy access
22
+ if (socialConfig) {
23
+ config.social = socialConfig;
24
+ }
18
25
  const { logger, batchManager, db } = dependencies;
19
26
 
20
27
  // [CRITICAL FIX] Max Age increased to 25m to match the larger dedup window.
@@ -118,10 +125,55 @@ async function handleRequest(message, context, configObj, dependencies) {
118
125
  });
119
126
 
120
127
  const taskId = context.eventId || 'batch-' + Date.now();
128
+ const today = new Date().toISOString().split('T')[0];
129
+ const { db } = dependencies;
130
+
131
+ // Initialize counter for batch processing (only for update tasks that need root data indexing)
132
+ const updateTasksCount = payload.tasks.filter(t => t.type === 'update').length;
133
+ let batchCounterRef = null;
134
+
135
+ if (updateTasksCount > 0) {
136
+ try {
137
+ batchCounterRef = db.collection('task_engine_batch_counters').doc(`${today}-${taskId}`);
138
+ await batchCounterRef.set({
139
+ totalTasks: updateTasksCount,
140
+ remainingTasks: updateTasksCount,
141
+ startedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp(),
142
+ taskId: taskId
143
+ });
144
+ logger.log('INFO', `[TaskEngine] Initialized batch counter: ${updateTasksCount} update tasks for ${today}`);
145
+ } catch (counterError) {
146
+ logger.log('WARN', `[TaskEngine] Failed to initialize batch counter`, counterError);
147
+ }
148
+ }
121
149
 
122
150
  try {
123
- const { tasksToRun, otherTasks } = await prepareTaskBatches(payload.tasks, null, logger);
124
- await executeTasks(tasksToRun, otherTasks, dependencies, configObj, taskId);
151
+ const { tasksToRun, otherTasks, socialTasks } = await prepareTaskBatches(payload.tasks, null, logger);
152
+
153
+ // Initialize social batch counter if we have social tasks
154
+ let socialCounterRef = null;
155
+ const userSocialTasksCount = socialTasks.filter(t =>
156
+ t.type === 'SOCIAL_PI_FETCH' || t.type === 'SOCIAL_SIGNED_IN_USER_FETCH'
157
+ ).length;
158
+
159
+ if (userSocialTasksCount > 0) {
160
+ try {
161
+ const socialCounterId = `social-${today}-${taskId}`;
162
+ socialCounterRef = db.collection('social_batch_counters').doc(socialCounterId);
163
+ await socialCounterRef.set({
164
+ totalTasks: userSocialTasksCount,
165
+ remainingTasks: userSocialTasksCount,
166
+ startedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp(),
167
+ taskId: taskId,
168
+ date: today
169
+ });
170
+ logger.log('INFO', `[TaskEngine] Initialized social batch counter: ${userSocialTasksCount} user social tasks for ${today}`);
171
+ } catch (counterError) {
172
+ logger.log('WARN', `[TaskEngine] Failed to initialize social batch counter`, counterError);
173
+ }
174
+ }
175
+
176
+ await executeTasks(tasksToRun, otherTasks, dependencies, configObj, taskId, batchCounterRef, today, socialTasks, socialCounterRef);
125
177
  } catch (batchError) {
126
178
  logger.log('ERROR', `[TaskEngine] Error processing batch. Message will be acknowledged to prevent retry loop.`, {
127
179
  error: batchError.message,
@@ -186,6 +238,24 @@ async function handleRequest(message, context, configObj, dependencies) {
186
238
  }
187
239
  await handleOnDemandUserUpdate(onDemandData, configObj, dependencies);
188
240
  break;
241
+ case 'SOCIAL_INSTRUMENT_FETCH':
242
+ case 'SOCIAL_PI_FETCH':
243
+ case 'SOCIAL_SIGNED_IN_USER_FETCH':
244
+ const socialData = data || payload;
245
+ if (!socialData.id) {
246
+ logger.log('ERROR', `[TaskEngine] Social fetch task missing required field 'id'`, { data: socialData });
247
+ return;
248
+ }
249
+ // Map task type to social handler type
250
+ const socialType = type === 'SOCIAL_INSTRUMENT_FETCH' ? 'INSTRUMENT' :
251
+ type === 'SOCIAL_PI_FETCH' ? 'POPULAR_INVESTOR' : 'SIGNED_IN_USER';
252
+ await handleSocialFetch({
253
+ type: socialType,
254
+ id: socialData.id,
255
+ username: socialData.username,
256
+ since: socialData.since
257
+ }, config, dependencies);
258
+ break;
189
259
  default:
190
260
  logger.log('WARN', `[TaskEngine] Unknown task type: ${type}`);
191
261
  }