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.
- 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/computation-system/workflows/data_feeder_pipeline.yaml +8 -47
- package/functions/etoro-price-fetcher/helpers/handler_helpers.js +24 -0
- package/functions/fetch-insights/helpers/handler_helpers.js +24 -0
- package/functions/fetch-popular-investors/helpers/fetch_helpers.js +31 -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
|
@@ -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
|
}
|
|
@@ -27,7 +27,9 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
27
27
|
throw new Error('taskData is required for POPULAR_INVESTOR_UPDATE');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const { cid, username, requestId, source } = taskData;
|
|
30
|
+
const { cid, username, requestId, source, metadata } = taskData;
|
|
31
|
+
// Extract targetCid from metadata if present (for optimization)
|
|
32
|
+
const targetCid = metadata?.targetCid || cid;
|
|
31
33
|
|
|
32
34
|
// Validate required fields
|
|
33
35
|
if (!cid && !username) {
|
|
@@ -388,39 +390,46 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
388
390
|
});
|
|
389
391
|
}
|
|
390
392
|
|
|
391
|
-
// Trigger
|
|
393
|
+
// Trigger computations with dependency chain resolution
|
|
392
394
|
const { pubsub } = dependencies;
|
|
393
395
|
if (pubsub) {
|
|
394
|
-
const
|
|
395
|
-
const topic = pubsub.topic(computationTopic);
|
|
396
|
-
const crypto = require('crypto');
|
|
396
|
+
const { triggerComputationWithDependencies } = require('../../computation-system/helpers/on_demand_helpers');
|
|
397
397
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
pass: '1', // Standard pass
|
|
403
|
-
dispatchId: crypto.randomUUID(),
|
|
404
|
-
triggerReason: 'on_demand_pi_fetch',
|
|
405
|
-
resources: 'standard',
|
|
406
|
-
metadata: {
|
|
407
|
-
onDemand: true,
|
|
408
|
-
requestId: requestId,
|
|
409
|
-
piCid: cid,
|
|
410
|
-
piUsername: username
|
|
411
|
-
},
|
|
412
|
-
traceContext: {
|
|
413
|
-
traceId: crypto.randomBytes(16).toString('hex'),
|
|
414
|
-
spanId: crypto.randomBytes(8).toString('hex'),
|
|
415
|
-
sampled: true
|
|
416
|
-
}
|
|
417
|
-
};
|
|
398
|
+
// For Popular Investors, we need:
|
|
399
|
+
// 1. PopularInvestorProfileMetrics (main profile)
|
|
400
|
+
// 2. SignedInUserPIPersonalizedMetrics (if PI is also a signed-in user)
|
|
401
|
+
const computationsToTrigger = ['PopularInvestorProfileMetrics'];
|
|
418
402
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
403
|
+
// Check if this PI is also a signed-in user
|
|
404
|
+
const signedInUserRef = db.collection('signed_in_users').doc(String(cid));
|
|
405
|
+
const signedInUserDoc = await signedInUserRef.get();
|
|
406
|
+
if (signedInUserDoc.exists) {
|
|
407
|
+
computationsToTrigger.push('SignedInUserPIPersonalizedMetrics');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const triggeredMessages = [];
|
|
411
|
+
for (const computation of computationsToTrigger) {
|
|
412
|
+
try {
|
|
413
|
+
const messages = await triggerComputationWithDependencies(
|
|
414
|
+
computation,
|
|
415
|
+
today,
|
|
416
|
+
dependencies,
|
|
417
|
+
config.computationSystem || {},
|
|
418
|
+
{
|
|
419
|
+
triggerReason: 'on_demand_pi_fetch',
|
|
420
|
+
requestId: requestId,
|
|
421
|
+
piCid: cid,
|
|
422
|
+
piUsername: username,
|
|
423
|
+
targetCid: targetCid // Pass targetCid for optimization
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
triggeredMessages.push(...messages);
|
|
427
|
+
} catch (compError) {
|
|
428
|
+
logger.log('ERROR', `[PI Update] Failed to trigger ${computation}`, compError);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
422
431
|
|
|
423
|
-
logger.log('INFO', `[PI Update] Triggered computation
|
|
432
|
+
logger.log('INFO', `[PI Update] Triggered ${triggeredMessages.length} computation messages for PI ${cid} (${username}) for date ${today}`);
|
|
424
433
|
} else {
|
|
425
434
|
logger.log('WARN', `[PI Update] PubSub not available, cannot trigger computation`);
|
|
426
435
|
}
|
|
@@ -478,7 +487,15 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
478
487
|
* Handles the On-Demand update for a Signed-In User.
|
|
479
488
|
*/
|
|
480
489
|
async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
481
|
-
const { cid, username, requestId, source } = taskData;
|
|
490
|
+
const { cid, username, requestId, source, metadata } = taskData;
|
|
491
|
+
const data = taskData.data || {}; // Extract data object
|
|
492
|
+
const includeSocial = data.includeSocial === true; // Check if social should be fetched
|
|
493
|
+
const since = data.since || new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString(); // Default to 7 days
|
|
494
|
+
const portfolioOnly = data.portfolioOnly === false ? false : true; // Default to true (fetch portfolio)
|
|
495
|
+
|
|
496
|
+
// Extract targetCid from metadata if present (for optimization)
|
|
497
|
+
const targetCid = metadata?.targetCid || cid;
|
|
498
|
+
const isNewUser = metadata?.isNewUser === true; // Flag for new user signup
|
|
482
499
|
|
|
483
500
|
// [FIX] Destructure dependencies first
|
|
484
501
|
const { logger, proxyManager, batchManager, headerManager, db } = dependencies;
|
|
@@ -539,63 +556,71 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
|
539
556
|
};
|
|
540
557
|
|
|
541
558
|
let fetchSuccess = false;
|
|
559
|
+
let portfolioFetched = false;
|
|
560
|
+
let historyFetched = false;
|
|
542
561
|
|
|
543
562
|
try {
|
|
544
|
-
// Portfolio Fetch
|
|
545
|
-
|
|
546
|
-
|
|
563
|
+
// Portfolio Fetch (only if portfolioOnly is true)
|
|
564
|
+
if (portfolioOnly) {
|
|
565
|
+
const portfolioUrl = `${ETORO_API_PORTFOLIO_URL}?cid=${cid}&client_request_id=${uuid}`;
|
|
566
|
+
logger.log('INFO', `[On-Demand DEBUG] Fetching Portfolio URL: ${portfolioUrl}`);
|
|
547
567
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
568
|
+
let portfolioRes = null;
|
|
569
|
+
let portfolioSuccess = false;
|
|
570
|
+
|
|
571
|
+
// [CIRCUIT BREAKER] Try proxy only if circuit is not open
|
|
572
|
+
if (shouldTryProxy()) {
|
|
573
|
+
try {
|
|
574
|
+
portfolioRes = await proxyManager.fetch(portfolioUrl, requestOptions);
|
|
575
|
+
if (portfolioRes.ok) {
|
|
576
|
+
portfolioSuccess = true;
|
|
577
|
+
recordProxyOutcome(true);
|
|
578
|
+
} else {
|
|
579
|
+
throw new Error(`Proxy returned non-ok status: ${portfolioRes.status}`);
|
|
580
|
+
}
|
|
581
|
+
} catch (proxyError) {
|
|
582
|
+
recordProxyOutcome(false);
|
|
583
|
+
logger.log('WARN', `[On-Demand] Proxy failed (${getFailureCount()}/${getMaxFailures()}). Error: ${proxyError.message}. Trying direct fetch...`);
|
|
560
584
|
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
logger.log('WARN', `[On-Demand] Proxy failed (${getFailureCount()}/${getMaxFailures()}). Error: ${proxyError.message}. Trying direct fetch...`);
|
|
585
|
+
} else {
|
|
586
|
+
logger.log('INFO', `[On-Demand] Circuit breaker open (${getFailureCount()}/${getMaxFailures()} failures). Skipping proxy, using direct fetch.`);
|
|
564
587
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
588
|
+
|
|
589
|
+
// Fallback to direct fetch if proxy failed or circuit is open
|
|
590
|
+
if (!portfolioSuccess) {
|
|
591
|
+
try {
|
|
592
|
+
const directFetch = typeof fetch !== 'undefined' ? fetch : require('node-fetch');
|
|
593
|
+
portfolioRes = await directFetch(portfolioUrl, requestOptions);
|
|
594
|
+
if (!portfolioRes.ok) {
|
|
595
|
+
throw new Error(`Failed to fetch portfolio for ${cid}: ${portfolioRes.status}`);
|
|
596
|
+
}
|
|
597
|
+
portfolioSuccess = true;
|
|
598
|
+
} catch (fetchErr) {
|
|
599
|
+
logger.log('ERROR', `[On-Demand] Direct fetch also failed for ${username}`, fetchErr);
|
|
600
|
+
throw new Error(`Failed to fetch portfolio for ${cid}. Direct: ${fetchErr.message}`);
|
|
576
601
|
}
|
|
577
|
-
portfolioSuccess = true;
|
|
578
|
-
} catch (fetchErr) {
|
|
579
|
-
logger.log('ERROR', `[On-Demand] Direct fetch also failed for ${username}`, fetchErr);
|
|
580
|
-
throw new Error(`Failed to fetch portfolio for ${cid}. Direct: ${fetchErr.message}`);
|
|
581
602
|
}
|
|
582
|
-
}
|
|
583
603
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
604
|
+
const portfolioData = await portfolioRes.json();
|
|
605
|
+
portfolioData.lastUpdated = new Date();
|
|
606
|
+
portfolioData.username = username;
|
|
607
|
+
|
|
608
|
+
fetchSuccess = true;
|
|
609
|
+
portfolioFetched = true;
|
|
589
610
|
|
|
590
|
-
|
|
611
|
+
await batchManager.addToPortfolioBatch(String(cid), blockId, today, portfolioData, 'signed_in_user');
|
|
612
|
+
} else {
|
|
613
|
+
logger.log('INFO', `[On-Demand Update] Skipping portfolio fetch (portfolioOnly=false) for ${username}`);
|
|
614
|
+
}
|
|
591
615
|
|
|
592
|
-
// History Fetch - Single Page with Large ItemsPerPage
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
616
|
+
// History Fetch - Single Page with Large ItemsPerPage (only if portfolioOnly is true)
|
|
617
|
+
if (portfolioOnly) {
|
|
618
|
+
const oneYearAgo = new Date();
|
|
619
|
+
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
|
620
|
+
const historyBaseUrl = ETORO_API_HISTORY_URL || 'https://www.etoro.com/sapi/trade-data-real/history/public/credit/flat';
|
|
621
|
+
const historyUrl = `${historyBaseUrl}?StartTime=${oneYearAgo.toISOString()}&PageNumber=1&ItemsPerPage=30000&PublicHistoryPortfolioFilter=&CID=${cid}&client_request_id=${uuid}`;
|
|
597
622
|
|
|
598
|
-
|
|
623
|
+
logger.log('INFO', `[On-Demand DEBUG] Fetching History URL: ${historyUrl}`);
|
|
599
624
|
|
|
600
625
|
let historyRes = null;
|
|
601
626
|
let historySuccess = false;
|
|
@@ -644,15 +669,103 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
|
644
669
|
p => VALID_REASONS.includes(p.CloseReason)
|
|
645
670
|
);
|
|
646
671
|
}
|
|
647
|
-
|
|
672
|
+
await batchManager.addToTradingHistoryBatch(String(cid), blockId, today, historyData, 'signed_in_user');
|
|
673
|
+
historyFetched = true;
|
|
674
|
+
} else {
|
|
675
|
+
logger.log('WARN', `[On-Demand Update] History fetch failed for ${username} (${historyRes.status})`);
|
|
676
|
+
}
|
|
648
677
|
} else {
|
|
649
|
-
logger.log('
|
|
678
|
+
logger.log('INFO', `[On-Demand Update] Skipping history fetch (portfolioOnly=false) for ${username}`);
|
|
650
679
|
}
|
|
651
680
|
|
|
652
|
-
|
|
681
|
+
// Fetch social data if requested (for user signup or explicit request)
|
|
682
|
+
let socialFetched = false;
|
|
683
|
+
if (includeSocial && username) {
|
|
684
|
+
try {
|
|
685
|
+
logger.log('INFO', `[On-Demand Update] Fetching social data for ${username} (${cid})...`);
|
|
686
|
+
|
|
687
|
+
// Import social helper functions
|
|
688
|
+
const { getGcidForUser } = require('../../social-task-handler/helpers/handler_helpers');
|
|
689
|
+
const { FieldValue, FieldPath } = require('@google-cloud/firestore');
|
|
690
|
+
|
|
691
|
+
// Get GCID for user
|
|
692
|
+
const gcid = await getGcidForUser(dependencies, config.social || {}, cid, username);
|
|
693
|
+
|
|
694
|
+
// Fetch social posts
|
|
695
|
+
const userFeedApiUrl = config.social?.userFeedApiUrl || 'https://www.etoro.com/api/edm-streams/v1/feed/user/top/';
|
|
696
|
+
const fetchUrl = `${userFeedApiUrl}${gcid}?take=10&offset=0&reactionsPageSize=20&badgesExperimentIsEnabled=false&client_request_id=${uuid}`;
|
|
697
|
+
|
|
698
|
+
const socialResponse = await proxyManager.fetch(fetchUrl, requestOptions);
|
|
699
|
+
if (socialResponse.ok) {
|
|
700
|
+
const socialData = await socialResponse.json();
|
|
701
|
+
const discussions = socialData?.discussions || [];
|
|
702
|
+
|
|
703
|
+
// Store social posts (simplified version - full logic would be in merged handler)
|
|
704
|
+
const signedInUserSocialCollection = config.social?.signedInUserSocialCollection || 'signed_in_users_social';
|
|
705
|
+
const processedPostsCollection = config.social?.processedPostsCollectionName || 'social_processed_registry';
|
|
706
|
+
const processedRef = db.collection(processedPostsCollection);
|
|
707
|
+
|
|
708
|
+
const MAX_POSTS = 30;
|
|
709
|
+
let saved = 0;
|
|
710
|
+
|
|
711
|
+
for (const discussion of discussions.slice(0, MAX_POSTS)) {
|
|
712
|
+
const post = discussion.post;
|
|
713
|
+
if (!post || !post.id) continue;
|
|
714
|
+
|
|
715
|
+
// Check if already processed
|
|
716
|
+
const existing = await processedRef.doc(post.id).get();
|
|
717
|
+
if (existing.exists) continue;
|
|
718
|
+
|
|
719
|
+
// Store post
|
|
720
|
+
const postData = {
|
|
721
|
+
postId: post.id,
|
|
722
|
+
text: post.message?.text || '',
|
|
723
|
+
ownerId: post.owner?.id,
|
|
724
|
+
username: post.owner?.username,
|
|
725
|
+
createdAt: post.created,
|
|
726
|
+
fetchedAt: FieldValue.serverTimestamp(),
|
|
727
|
+
snippet: (post.message?.text || '').substring(0, 500),
|
|
728
|
+
stats: {
|
|
729
|
+
likes: discussion.emotionsData?.like?.paging?.totalCount || 0,
|
|
730
|
+
comments: discussion.summary?.totalCommentsAndReplies || 0
|
|
731
|
+
},
|
|
732
|
+
aiAnalysis: {
|
|
733
|
+
overallSentiment: "Neutral",
|
|
734
|
+
topics: [],
|
|
735
|
+
isSpam: false,
|
|
736
|
+
qualityScore: 0.5
|
|
737
|
+
},
|
|
738
|
+
tags: post.tags?.map(t => t.market?.symbolName).filter(Boolean) || []
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
await db.collection(signedInUserSocialCollection).doc(String(cid)).collection('posts').doc(post.id).set(postData);
|
|
742
|
+
await processedRef.doc(post.id).set({ processedAt: FieldValue.serverTimestamp() });
|
|
743
|
+
saved++;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Update date tracking document
|
|
747
|
+
const trackingDocRef = db.collection(signedInUserSocialCollection).doc('_dates');
|
|
748
|
+
await trackingDocRef.set({
|
|
749
|
+
[`fetchedDates.${today}`]: true,
|
|
750
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
751
|
+
}, { merge: true });
|
|
752
|
+
|
|
753
|
+
socialFetched = true;
|
|
754
|
+
logger.log('INFO', `[On-Demand Update] Fetched and stored ${saved} social posts for ${username}`);
|
|
755
|
+
}
|
|
756
|
+
} catch (socialError) {
|
|
757
|
+
logger.log('WARN', `[On-Demand Update] Social fetch failed for ${username}`, socialError);
|
|
758
|
+
// Continue - social failure shouldn't block portfolio/history processing
|
|
759
|
+
}
|
|
760
|
+
}
|
|
653
761
|
|
|
654
|
-
|
|
655
|
-
|
|
762
|
+
logger.log('SUCCESS', `[On-Demand Update] Complete for ${username} (Portfolio: ${portfolioFetched ? '✓' : '✗'}, History: ${historyFetched ? '✓' : '✗'}, Social: ${socialFetched ? '✓' : '✗'})`);
|
|
763
|
+
|
|
764
|
+
// Update root data indexer after all data is fetched
|
|
765
|
+
// Then trigger computations for both sync requests AND user signups
|
|
766
|
+
const shouldTriggerComputations = (requestId && (source === 'on_demand_sync' || source === 'user_signup')) || isNewUser;
|
|
767
|
+
|
|
768
|
+
if (shouldTriggerComputations && db) {
|
|
656
769
|
try {
|
|
657
770
|
const requestRef = db.collection('user_sync_requests')
|
|
658
771
|
.doc(String(cid))
|
|
@@ -693,39 +806,44 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
|
693
806
|
});
|
|
694
807
|
}
|
|
695
808
|
|
|
696
|
-
// Trigger
|
|
809
|
+
// Trigger computations with dependency chain resolution
|
|
697
810
|
const { pubsub } = dependencies;
|
|
698
811
|
if (pubsub) {
|
|
699
|
-
const
|
|
700
|
-
const topic = pubsub.topic(computationTopic);
|
|
701
|
-
const crypto = require('crypto');
|
|
812
|
+
const { triggerComputationWithDependencies } = require('../../computation-system/helpers/on_demand_helpers');
|
|
702
813
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
onDemand: true,
|
|
713
|
-
requestId: requestId,
|
|
714
|
-
userCid: cid,
|
|
715
|
-
username: username
|
|
716
|
-
},
|
|
717
|
-
traceContext: {
|
|
718
|
-
traceId: crypto.randomBytes(16).toString('hex'),
|
|
719
|
-
spanId: crypto.randomBytes(8).toString('hex'),
|
|
720
|
-
sampled: true
|
|
721
|
-
}
|
|
722
|
-
};
|
|
814
|
+
// For signed-in users, we need to trigger multiple computations:
|
|
815
|
+
// 1. SignedInUserProfileMetrics (main profile)
|
|
816
|
+
// 2. SignedInUserCopiedList, SignedInUserCopiedPIs, SignedInUserPastCopies (copied data)
|
|
817
|
+
const computationsToTrigger = [
|
|
818
|
+
'SignedInUserProfileMetrics',
|
|
819
|
+
'SignedInUserCopiedList',
|
|
820
|
+
'SignedInUserCopiedPIs',
|
|
821
|
+
'SignedInUserPastCopies'
|
|
822
|
+
];
|
|
723
823
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
824
|
+
const triggeredMessages = [];
|
|
825
|
+
for (const computation of computationsToTrigger) {
|
|
826
|
+
try {
|
|
827
|
+
const messages = await triggerComputationWithDependencies(
|
|
828
|
+
computation,
|
|
829
|
+
today,
|
|
830
|
+
dependencies,
|
|
831
|
+
config.computationSystem || {},
|
|
832
|
+
{
|
|
833
|
+
triggerReason: source === 'user_signup' ? 'user_signup' : 'on_demand_sync',
|
|
834
|
+
requestId: requestId,
|
|
835
|
+
userCid: cid,
|
|
836
|
+
username: username,
|
|
837
|
+
targetCid: targetCid // Pass targetCid for optimization
|
|
838
|
+
}
|
|
839
|
+
);
|
|
840
|
+
triggeredMessages.push(...messages);
|
|
841
|
+
} catch (compError) {
|
|
842
|
+
logger.log('ERROR', `[On-Demand Update] Failed to trigger ${computation}`, compError);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
727
845
|
|
|
728
|
-
logger.log('INFO', `[On-Demand Update] Triggered computation
|
|
846
|
+
logger.log('INFO', `[On-Demand Update] Triggered ${triggeredMessages.length} computation messages for user ${cid} (${username}) for date ${today}`);
|
|
729
847
|
|
|
730
848
|
// Don't mark as completed yet - wait for computation to finish
|
|
731
849
|
// The status will remain 'computing' until computation completes
|