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.
- package/functions/computation-system/WorkflowOrchestrator.js +6 -1
- package/functions/computation-system/executors/StandardExecutor.js +8 -0
- package/functions/computation-system/helpers/computation_worker.js +28 -19
- package/functions/computation-system/helpers/on_demand_helpers.js +151 -0
- package/functions/etoro-price-fetcher/helpers/handler_helpers.js +24 -0
- package/functions/fetch-insights/helpers/handler_helpers.js +24 -0
- package/functions/generic-api/user-api/helpers/on_demand_fetch_helpers.js +1 -0
- package/functions/generic-api/user-api/helpers/user_sync_helpers.js +1 -0
- package/functions/generic-api/user-api/helpers/verification_helpers.js +41 -17
- package/functions/social-orchestrator/helpers/orchestrator_helpers.js +47 -6
- package/functions/social-task-handler/helpers/handler_helpers.js +4 -2
- package/functions/task-engine/handler_creator.js +73 -3
- package/functions/task-engine/helpers/popular_investor_helpers.js +227 -109
- package/functions/task-engine/helpers/social_helpers.js +282 -0
- package/functions/task-engine/utils/task_engine_utils.js +91 -11
- package/package.json +1 -1
|
@@ -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
|
-
// [
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
171
|
-
|
|
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,
|
|
178
|
-
logger.log('INFO', `[Verification] Triggered
|
|
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
|
-
//
|
|
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:
|
|
84
|
-
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 ${
|
|
89
|
-
return { success: true, tasksQueued:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|