bulltrackers-module 1.0.474 → 1.0.476

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
+
@@ -1,6 +1,7 @@
1
- # Data Feeder Pipeline (V3.1 - Syntax Fixed)
1
+ # Data Feeder Pipeline (V3.2 - Automatic Root Data Indexing)
2
2
  # Starts at 22:00 UTC via Cloud Scheduler.
3
- # Fixes: Split assign/call steps and corrected assign syntax.
3
+ # UPDATED: Removed intermediate root data indexer calls - each fetcher now automatically triggers indexing.
4
+ # Only the global verification run at midnight remains.
4
5
 
5
6
  main:
6
7
  params: [input]
@@ -45,17 +46,7 @@ main:
45
46
  - wait_10_after_price:
46
47
  call: sys.sleep
47
48
  args: { seconds: 600 } # 10 Minutes
48
-
49
- # FIX 1: Split assign and call
50
- - prepare_index_price:
51
- assign:
52
- - today: '${text.split(time.format(sys.now()), "T")[0]}'
53
- - index_today_after_price:
54
- call: http.post
55
- args:
56
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
57
- body: { targetDate: '${today}' }
58
- auth: { type: OIDC }
49
+ # NOTE: Price fetcher now automatically triggers root data indexer after completion
59
50
 
60
51
  - wait_10_before_insights:
61
52
  call: sys.sleep
@@ -78,17 +69,7 @@ main:
78
69
  - wait_10_after_insights:
79
70
  call: sys.sleep
80
71
  args: { seconds: 600 }
81
-
82
- # FIX 2: Split assign and call
83
- - prepare_index_insights:
84
- assign:
85
- - today: '${text.split(time.format(sys.now()), "T")[0]}'
86
- - index_today_after_insights:
87
- call: http.post
88
- args:
89
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
90
- body: { targetDate: '${today}' }
91
- auth: { type: OIDC }
72
+ # NOTE: Insights fetcher now automatically triggers root data indexer after completion
92
73
 
93
74
  # ==========================================
94
75
  # PHASE 2: WAIT FOR MIDNIGHT
@@ -124,17 +105,7 @@ main:
124
105
  - wait_10_after_rankings:
125
106
  call: sys.sleep
126
107
  args: { seconds: 600 }
127
-
128
- # FIX 3: Split assign and call
129
- - prepare_index_rankings:
130
- assign:
131
- - today: '${text.split(time.format(sys.now()), "T")[0]}'
132
- - index_today_after_rankings:
133
- call: http.post
134
- args:
135
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
136
- body: { targetDate: '${today}' }
137
- auth: { type: OIDC }
108
+ # NOTE: Popular investor rankings fetcher now automatically triggers root data indexer after completion
138
109
 
139
110
  - phase_0000_social:
140
111
  try:
@@ -158,7 +129,7 @@ main:
158
129
  call: http.post
159
130
  args:
160
131
  url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
161
- # No targetDate = Global Run
132
+ # No targetDate = Global verification run (all intermediate indexing is now automatic)
162
133
  auth: { type: OIDC }
163
134
 
164
135
  # ==========================================
@@ -194,17 +165,7 @@ main:
194
165
  - wait_10_in_loop:
195
166
  call: sys.sleep
196
167
  args: { seconds: 600 }
197
-
198
- # FIX 4: Split assign and call
199
- - prepare_index_loop:
200
- assign:
201
- - today: '${text.split(time.format(sys.now()), "T")[0]}'
202
- - index_today_in_loop:
203
- call: http.post
204
- args:
205
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
206
- body: { targetDate: '${today}' }
207
- auth: { type: OIDC }
168
+ # NOTE: Social tasks are handled by task engine, which automatically triggers root data indexer after batch completion
208
169
 
209
170
  # FIX 5: Correct assign syntax (must be a list)
210
171
  - increment_loop:
@@ -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 };
@@ -113,6 +113,37 @@ async function fetchAndStorePopularInvestors(config, dependencies) {
113
113
  });
114
114
 
115
115
  logger.log('SUCCESS', `[PopularInvestorFetch] Stored ${data.TotalRows} rankings into ${rankingsCollectionName}/${today}`);
116
+
117
+ // Update root data indexer for today's date after rankings data is stored
118
+ try {
119
+ const { runRootDataIndexer } = require('../../root-data-indexer/index');
120
+ // Access rootDataIndexer from config (passed from index.js) or use defaults
121
+ // Using bracket notation to avoid TypeScript errors
122
+ let rootDataIndexerConfig;
123
+ if (config && typeof config === 'object' && config['rootDataIndexer']) {
124
+ rootDataIndexerConfig = config['rootDataIndexer'];
125
+ } else {
126
+ rootDataIndexerConfig = {
127
+ availabilityCollection: 'system_root_data_index',
128
+ earliestDate: '2025-08-01',
129
+ collections: {
130
+ piRankings: rankingsCollectionName
131
+ }
132
+ };
133
+ }
134
+
135
+ const indexerConfig = Object.assign({}, rootDataIndexerConfig, {
136
+ targetDate: today // Index only today's date for speed
137
+ });
138
+
139
+ logger.log('INFO', `[PopularInvestorFetch] Triggering root data indexer for date ${today} after rankings data storage...`);
140
+ await runRootDataIndexer(indexerConfig, dependencies);
141
+ logger.log('INFO', `[PopularInvestorFetch] Root data indexer completed for date ${today}`);
142
+ } catch (indexerError) {
143
+ logger.log('ERROR', `[PopularInvestorFetch] Failed to run root data indexer for ${today}`, indexerError);
144
+ // Continue - rankings data is stored, indexer failure is non-critical
145
+ }
146
+
116
147
  return { success: true, count: data.TotalRows };
117
148
 
118
149
  } catch (dbError) {
@@ -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 };