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
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Social task handling for task engine
|
|
3
|
+
* Handles fetching social posts for instruments, PIs, and signed-in users
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { FieldValue, FieldPath } = require('@google-cloud/firestore');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const { getGcidForUser } = require('../../social-task-handler/helpers/handler_helpers');
|
|
9
|
+
|
|
10
|
+
const MAX_POSTS_TO_STORE = 30;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Handles social fetch task for instruments, PIs, or signed-in users
|
|
14
|
+
* @param {object} taskData - Contains { type, id, username, since }
|
|
15
|
+
* @param {object} config - Task engine configuration
|
|
16
|
+
* @param {object} dependencies - Contains db, logger, headerManager, proxyManager
|
|
17
|
+
* @param {object} batchCounterRef - Optional Firestore reference for batch counter
|
|
18
|
+
* @returns {Promise<void>}
|
|
19
|
+
*/
|
|
20
|
+
async function handleSocialFetch(taskData, config, dependencies, batchCounterRef = null) {
|
|
21
|
+
const { db, logger, headerManager, proxyManager } = dependencies;
|
|
22
|
+
const { type, id, username, since } = taskData;
|
|
23
|
+
const cid = id;
|
|
24
|
+
|
|
25
|
+
let fetchUrlBase = '';
|
|
26
|
+
let targetCollectionPath = '';
|
|
27
|
+
|
|
28
|
+
// 1. Target URL Construction (using GCID for Users)
|
|
29
|
+
try {
|
|
30
|
+
if (type === 'INSTRUMENT') {
|
|
31
|
+
const socialApiBaseUrl = config.social?.socialApiBaseUrl || 'https://www.etoro.com/api/edm-streams/v1/feed/instrument/';
|
|
32
|
+
fetchUrlBase = `${socialApiBaseUrl}${cid}`;
|
|
33
|
+
const socialInsightsCollection = config.social?.socialInsightsCollectionName || 'social_insights';
|
|
34
|
+
targetCollectionPath = `${socialInsightsCollection}/${new Date().toISOString().slice(0, 10)}/posts`;
|
|
35
|
+
} else if (type === 'POPULAR_INVESTOR' || type === 'SIGNED_IN_USER') {
|
|
36
|
+
// REVERSE LOOKUP: Convert CID to GCID
|
|
37
|
+
const gcid = await getGcidForUser(dependencies, config.social || {}, cid, username);
|
|
38
|
+
|
|
39
|
+
const userFeedApiUrl = config.social?.userFeedApiUrl || 'https://www.etoro.com/api/edm-streams/v1/feed/user/top/';
|
|
40
|
+
fetchUrlBase = `${userFeedApiUrl}${gcid}`;
|
|
41
|
+
targetCollectionPath = (type === 'SIGNED_IN_USER')
|
|
42
|
+
? `${config.social?.signedInUserSocialCollection || 'signed_in_users_social'}/${cid}/posts`
|
|
43
|
+
: `${config.social?.piSocialCollectionName || 'pi_social_posts'}/${cid}/posts`;
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
logger.log('ERROR', `[SocialFetch] Initialization failed for ${type} ${cid}.`, err);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const taskId = `social-${cid}-${Date.now()}`;
|
|
51
|
+
logger.log('INFO', `[SocialFetch/${taskId}] Starting fetch for ${type} ${cid}. Limit: ${MAX_POSTS_TO_STORE} posts.`);
|
|
52
|
+
|
|
53
|
+
let offset = 0;
|
|
54
|
+
const take = 10;
|
|
55
|
+
let totalSaved = 0;
|
|
56
|
+
let keepFetching = true;
|
|
57
|
+
const processedPostsCollection = config.social?.processedPostsCollectionName || 'social_processed_registry';
|
|
58
|
+
const processedRef = db.collection(processedPostsCollection);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
while (keepFetching && totalSaved < MAX_POSTS_TO_STORE) {
|
|
62
|
+
const clientRequestId = crypto.randomUUID();
|
|
63
|
+
const url = `${fetchUrlBase}?take=${take}&offset=${offset}&reactionsPageSize=20&badgesExperimentIsEnabled=false&client_request_id=${clientRequestId}`;
|
|
64
|
+
const selectedHeader = await headerManager.selectHeader();
|
|
65
|
+
|
|
66
|
+
const requestHeaders = {
|
|
67
|
+
'Accept': 'application/json',
|
|
68
|
+
'Referer': 'https://www.etoro.com/',
|
|
69
|
+
...selectedHeader.header
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
logger.log('INFO', `[SocialFetch/${taskId}] Requesting URL: ${url}`);
|
|
73
|
+
|
|
74
|
+
let response;
|
|
75
|
+
try {
|
|
76
|
+
response = await proxyManager.fetch(url, { headers: requestHeaders });
|
|
77
|
+
if (!response.ok) throw new Error(`Status ${response.status}`);
|
|
78
|
+
headerManager.updatePerformance(selectedHeader.id, true);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.log('WARN', `[SocialFetch/${taskId}] Fetch failed for ${url}: ${err.message}. Retrying direct.`);
|
|
81
|
+
try {
|
|
82
|
+
const directFetch = typeof fetch !== 'undefined' ? fetch : require('node-fetch');
|
|
83
|
+
response = await directFetch(url, { headers: requestHeaders });
|
|
84
|
+
if (!response.ok) throw new Error(`Direct status ${response.status}`);
|
|
85
|
+
headerManager.updatePerformance(selectedHeader.id, true);
|
|
86
|
+
} catch (directErr) {
|
|
87
|
+
logger.log('ERROR', `[SocialFetch/${taskId}] All fetch methods failed.`, { error: directErr.message });
|
|
88
|
+
headerManager.updatePerformance(selectedHeader.id, false);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const page = await response.json();
|
|
94
|
+
const discussions = page?.discussions || [];
|
|
95
|
+
logger.log('INFO', `[SocialFetch/${taskId}] API returned ${discussions.length} items for ${cid} at offset ${offset}.`);
|
|
96
|
+
|
|
97
|
+
if (discussions.length === 0) {
|
|
98
|
+
keepFetching = false;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const postIds = discussions.map(d => d.post?.id).filter(Boolean);
|
|
103
|
+
const existingIds = new Set();
|
|
104
|
+
if (postIds.length > 0) {
|
|
105
|
+
const existingDocs = await processedRef.where(FieldPath.documentId(), 'in', postIds).get();
|
|
106
|
+
existingDocs.forEach(d => existingIds.add(d.id));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Filter and prepare posts: only new posts in English
|
|
110
|
+
const newPostsWithDiscussion = discussions
|
|
111
|
+
.filter(d => {
|
|
112
|
+
const post = d.post;
|
|
113
|
+
if (!post || !post.id || !post.message?.text || existingIds.has(post.id)) return false;
|
|
114
|
+
const lang = (post.message.languageCode || 'unknown').toLowerCase();
|
|
115
|
+
return lang === 'en' || lang === 'english';
|
|
116
|
+
})
|
|
117
|
+
.slice(0, MAX_POSTS_TO_STORE - totalSaved);
|
|
118
|
+
|
|
119
|
+
if (newPostsWithDiscussion.length === 0) {
|
|
120
|
+
keepFetching = false;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Process posts in batches
|
|
125
|
+
const batch = db.batch();
|
|
126
|
+
let pageBatchCount = 0;
|
|
127
|
+
|
|
128
|
+
// Process first post with AI analysis (if available)
|
|
129
|
+
if (newPostsWithDiscussion.length > 0 && dependencies.geminiModel) {
|
|
130
|
+
const discussion = newPostsWithDiscussion[0];
|
|
131
|
+
const post = discussion.post;
|
|
132
|
+
const snippet = post.message.text.substring(0, 500);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const { getAdvancedAnalysisFromGemini } = require('../../social-task-handler/helpers/handler_helpers');
|
|
136
|
+
const aiResult = await getAdvancedAnalysisFromGemini(dependencies, snippet);
|
|
137
|
+
|
|
138
|
+
const docData = {
|
|
139
|
+
postId: post.id,
|
|
140
|
+
text: post.message.text,
|
|
141
|
+
ownerId: post.owner?.id,
|
|
142
|
+
username: post.owner?.username,
|
|
143
|
+
createdAt: post.created,
|
|
144
|
+
fetchedAt: FieldValue.serverTimestamp(),
|
|
145
|
+
snippet: snippet,
|
|
146
|
+
stats: {
|
|
147
|
+
likes: discussion.emotionsData?.like?.paging?.totalCount || 0,
|
|
148
|
+
comments: discussion.summary?.totalCommentsAndReplies || 0
|
|
149
|
+
},
|
|
150
|
+
aiAnalysis: aiResult,
|
|
151
|
+
tags: post.tags?.map(t => t.market?.symbolName).filter(Boolean) || []
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
batch.set(db.collection(targetCollectionPath).doc(post.id), docData);
|
|
155
|
+
batch.set(processedRef.doc(post.id), { processedAt: FieldValue.serverTimestamp() });
|
|
156
|
+
pageBatchCount++;
|
|
157
|
+
} catch (aiError) {
|
|
158
|
+
logger.log('WARN', `[SocialFetch/${taskId}] AI analysis failed, using default`, aiError);
|
|
159
|
+
// Fall through to default processing
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Process remaining posts without AI (or if AI failed)
|
|
164
|
+
const postsToProcess = newPostsWithDiscussion.slice(pageBatchCount > 0 ? 1 : 0);
|
|
165
|
+
for (const discussion of postsToProcess) {
|
|
166
|
+
if (totalSaved + pageBatchCount >= MAX_POSTS_TO_STORE) {
|
|
167
|
+
keepFetching = false;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const post = discussion.post;
|
|
172
|
+
const docData = {
|
|
173
|
+
postId: post.id,
|
|
174
|
+
text: post.message.text,
|
|
175
|
+
ownerId: post.owner?.id,
|
|
176
|
+
username: post.owner?.username,
|
|
177
|
+
createdAt: post.created,
|
|
178
|
+
fetchedAt: FieldValue.serverTimestamp(),
|
|
179
|
+
snippet: post.message.text.substring(0, 500),
|
|
180
|
+
stats: {
|
|
181
|
+
likes: discussion.emotionsData?.like?.paging?.totalCount || 0,
|
|
182
|
+
comments: discussion.summary?.totalCommentsAndReplies || 0
|
|
183
|
+
},
|
|
184
|
+
aiAnalysis: {
|
|
185
|
+
overallSentiment: "Neutral",
|
|
186
|
+
topics: [],
|
|
187
|
+
isSpam: false,
|
|
188
|
+
qualityScore: 0.5
|
|
189
|
+
},
|
|
190
|
+
tags: post.tags?.map(t => t.market?.symbolName).filter(Boolean) || []
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
batch.set(db.collection(targetCollectionPath).doc(post.id), docData);
|
|
194
|
+
batch.set(processedRef.doc(post.id), { processedAt: FieldValue.serverTimestamp() });
|
|
195
|
+
pageBatchCount++;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (pageBatchCount > 0) {
|
|
199
|
+
await batch.commit();
|
|
200
|
+
totalSaved += pageBatchCount;
|
|
201
|
+
}
|
|
202
|
+
offset += take;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Write date tracking document after successful fetch
|
|
206
|
+
if (totalSaved > 0 && (type === 'SIGNED_IN_USER' || type === 'POPULAR_INVESTOR')) {
|
|
207
|
+
const today = new Date().toISOString().split('T')[0];
|
|
208
|
+
let trackingCollectionPath;
|
|
209
|
+
|
|
210
|
+
if (type === 'SIGNED_IN_USER') {
|
|
211
|
+
trackingCollectionPath = config.social?.signedInUserSocialCollection || 'signed_in_users_social';
|
|
212
|
+
} else {
|
|
213
|
+
trackingCollectionPath = config.social?.piSocialCollectionName || 'pi_social_posts';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Write to a single tracking document at the root of the collection
|
|
217
|
+
const trackingDocRef = db.collection(trackingCollectionPath).doc('_dates');
|
|
218
|
+
const trackingData = {
|
|
219
|
+
[`fetchedDates.${today}`]: true,
|
|
220
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
await trackingDocRef.set(trackingData, { merge: true });
|
|
224
|
+
logger.log('INFO', `[SocialFetch/${taskId}] Updated date tracking for ${type}: ${today}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Decrement batch counter if it exists
|
|
228
|
+
if (batchCounterRef && (type === 'SIGNED_IN_USER' || type === 'POPULAR_INVESTOR')) {
|
|
229
|
+
try {
|
|
230
|
+
await batchCounterRef.update({
|
|
231
|
+
remainingTasks: FieldValue.increment(-1),
|
|
232
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Check if this was the last task
|
|
236
|
+
const counterDoc = await batchCounterRef.get();
|
|
237
|
+
if (counterDoc.exists) {
|
|
238
|
+
const counterData = counterDoc.data();
|
|
239
|
+
if (counterData.remainingTasks <= 0 && !counterData.rootDataIndexed) {
|
|
240
|
+
// All tasks complete - trigger root data indexer
|
|
241
|
+
const today = new Date().toISOString().split('T')[0];
|
|
242
|
+
logger.log('INFO', `[SocialFetch] All social batch tasks complete. Triggering root data indexer for ${today}...`);
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const { runRootDataIndexer } = require('../../root-data-indexer/index');
|
|
246
|
+
const rootDataIndexerConfig = config.rootDataIndexer || {};
|
|
247
|
+
const indexerConfig = {
|
|
248
|
+
...rootDataIndexerConfig,
|
|
249
|
+
targetDate: today
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
await runRootDataIndexer(indexerConfig, dependencies);
|
|
253
|
+
|
|
254
|
+
// Mark as indexed
|
|
255
|
+
await batchCounterRef.update({
|
|
256
|
+
rootDataIndexed: true,
|
|
257
|
+
rootDataIndexedAt: FieldValue.serverTimestamp()
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
logger.log('SUCCESS', `[SocialFetch] Root data indexer completed for ${today}`);
|
|
261
|
+
} catch (indexerError) {
|
|
262
|
+
logger.log('ERROR', `[SocialFetch] Failed to run root data indexer for ${today}`, indexerError);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} catch (counterError) {
|
|
267
|
+
logger.log('WARN', `[SocialFetch] Failed to update batch counter`, counterError);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
logger.log('SUCCESS', `[SocialFetch/${taskId}] Process complete for ${cid}. Total stored: ${totalSaved}/${MAX_POSTS_TO_STORE}`);
|
|
272
|
+
|
|
273
|
+
} catch (error) {
|
|
274
|
+
logger.log('ERROR', `[SocialFetch/${taskId}] Fatal error in processing.`, error);
|
|
275
|
+
throw error;
|
|
276
|
+
} finally {
|
|
277
|
+
await headerManager.flushPerformanceUpdates();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = { handleSocialFetch };
|
|
282
|
+
|
|
@@ -14,6 +14,7 @@ const { handleDiscover } = require('../helpers/discover_helpers');
|
|
|
14
14
|
const { handleVerify } = require('../helpers/verify_helpers');
|
|
15
15
|
const { handleUpdate } = require('../helpers/update_helpers');
|
|
16
16
|
const { handlePopularInvestorUpdate, handleOnDemandUserUpdate } = require('../helpers/popular_investor_helpers');
|
|
17
|
+
const { handleSocialFetch } = require('../helpers/social_helpers');
|
|
17
18
|
const pLimit = require('p-limit');
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -29,33 +30,37 @@ function parseTaskPayload(message, logger) {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
|
-
* Sorts tasks into update
|
|
33
|
+
* Sorts tasks into update, other (discover/verify/PI/OnDemand), and social.
|
|
33
34
|
*/
|
|
34
35
|
async function prepareTaskBatches(tasks, batchManager, logger) {
|
|
35
|
-
const tasksToRun = [], otherTasks = [];
|
|
36
|
+
const tasksToRun = [], otherTasks = [], socialTasks = [];
|
|
36
37
|
|
|
37
38
|
for (const task of tasks) {
|
|
38
39
|
if (task.type === 'update') {
|
|
39
40
|
// Standard portfolio updates (Normal/Speculator ONLY)
|
|
40
41
|
// NOTE: Popular Investors use type 'POPULAR_INVESTOR_UPDATE', not 'update'
|
|
41
42
|
tasksToRun.push(task);
|
|
43
|
+
} else if (task.type && task.type.startsWith('SOCIAL_')) {
|
|
44
|
+
// Social fetch tasks
|
|
45
|
+
socialTasks.push(task);
|
|
42
46
|
} else {
|
|
43
47
|
// Discover, Verify, Popular Investor (POPULAR_INVESTOR_UPDATE), Signed-In User (ON_DEMAND_USER_UPDATE)
|
|
44
48
|
otherTasks.push(task);
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
|
|
48
|
-
logger.log('INFO', `[TaskEngine] Task sorting: ${tasksToRun.length} UPDATE (normal/speculator), ${otherTasks.length} other (PI/signed-in/discover/verify)`);
|
|
52
|
+
logger.log('INFO', `[TaskEngine] Task sorting: ${tasksToRun.length} UPDATE (normal/speculator), ${otherTasks.length} other (PI/signed-in/discover/verify), ${socialTasks.length} social`);
|
|
49
53
|
|
|
50
|
-
return { tasksToRun, cidsToLookup: new Map(), otherTasks };
|
|
54
|
+
return { tasksToRun, cidsToLookup: new Map(), otherTasks, socialTasks };
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
/**
|
|
54
58
|
* Executes all tasks.
|
|
55
59
|
* (FIXED: Concurrency limit increased to 5)
|
|
60
|
+
* (NEW: Added batch counter support for root data indexing)
|
|
56
61
|
*/
|
|
57
|
-
async function executeTasks(tasksToRun, otherTasks, dependencies, config, taskId) {
|
|
58
|
-
const { logger, batchManager } = dependencies;
|
|
62
|
+
async function executeTasks(tasksToRun, otherTasks, dependencies, config, taskId, batchCounterRef = null, targetDate = null, socialTasks = [], socialCounterRef = null) {
|
|
63
|
+
const { logger, batchManager, db } = dependencies;
|
|
59
64
|
|
|
60
65
|
// [CRITICAL FIX] Increased from 1 to 5.
|
|
61
66
|
// A limit of 1 was causing timeouts on batches of 500 tasks (500s > 60s/540s timeout).
|
|
@@ -130,28 +135,103 @@ async function executeTasks(tasksToRun, otherTasks, dependencies, config, taskId
|
|
|
130
135
|
allTaskPromises.push(
|
|
131
136
|
limit(() =>
|
|
132
137
|
handleUpdate(task, subTaskId, dependencies, config)
|
|
133
|
-
.then(() =>
|
|
138
|
+
.then(async () => {
|
|
139
|
+
taskCounters.update++;
|
|
140
|
+
|
|
141
|
+
// Decrement batch counter if it exists
|
|
142
|
+
if (batchCounterRef) {
|
|
143
|
+
try {
|
|
144
|
+
await batchCounterRef.update({
|
|
145
|
+
remainingTasks: require('@google-cloud/firestore').FieldValue.increment(-1),
|
|
146
|
+
lastUpdated: require('@google-cloud/firestore').FieldValue.serverTimestamp()
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Check if this was the last task
|
|
150
|
+
const counterDoc = await batchCounterRef.get();
|
|
151
|
+
if (counterDoc.exists) {
|
|
152
|
+
const counterData = counterDoc.data();
|
|
153
|
+
if (counterData.remainingTasks <= 0 && !counterData.rootDataIndexed) {
|
|
154
|
+
// All tasks complete - trigger root data indexer
|
|
155
|
+
logger.log('INFO', `[TaskEngine/${taskId}] All batch tasks complete. Triggering root data indexer for ${targetDate}...`);
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const { runRootDataIndexer } = require('../../root-data-indexer/index');
|
|
159
|
+
const rootDataIndexerConfig = config.rootDataIndexer || {};
|
|
160
|
+
const indexerConfig = {
|
|
161
|
+
...rootDataIndexerConfig,
|
|
162
|
+
targetDate: targetDate
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
await runRootDataIndexer(indexerConfig, dependencies);
|
|
166
|
+
|
|
167
|
+
// Mark as indexed
|
|
168
|
+
await batchCounterRef.update({
|
|
169
|
+
rootDataIndexed: true,
|
|
170
|
+
rootDataIndexedAt: require('@google-cloud/firestore').FieldValue.serverTimestamp()
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
logger.log('SUCCESS', `[TaskEngine/${taskId}] Root data indexer completed for ${targetDate}`);
|
|
174
|
+
} catch (indexerError) {
|
|
175
|
+
logger.log('ERROR', `[TaskEngine/${taskId}] Failed to run root data indexer for ${targetDate}`, indexerError);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch (counterError) {
|
|
180
|
+
logger.log('WARN', `[TaskEngine/${taskId}] Failed to update batch counter`, counterError);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
})
|
|
134
184
|
.catch(err => {
|
|
135
185
|
logger.log('ERROR', `[TaskEngine/${taskId}] Error in handleUpdate for ${task.userId}`, { errorMessage: err.message });
|
|
136
186
|
taskCounters.failed++;
|
|
187
|
+
|
|
188
|
+
// Still decrement counter on failure to avoid blocking
|
|
189
|
+
if (batchCounterRef) {
|
|
190
|
+
batchCounterRef.update({
|
|
191
|
+
remainingTasks: require('@google-cloud/firestore').FieldValue.increment(-1)
|
|
192
|
+
}).catch(() => {});
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 3. Queue social tasks
|
|
200
|
+
for (const task of socialTasks) {
|
|
201
|
+
const taskData = task.data || task;
|
|
202
|
+
const socialType = task.type === 'SOCIAL_INSTRUMENT_FETCH' ? 'INSTRUMENT' :
|
|
203
|
+
task.type === 'SOCIAL_PI_FETCH' ? 'POPULAR_INVESTOR' : 'SIGNED_IN_USER';
|
|
204
|
+
|
|
205
|
+
allTaskPromises.push(
|
|
206
|
+
limit(() =>
|
|
207
|
+
handleSocialFetch({
|
|
208
|
+
type: socialType,
|
|
209
|
+
id: taskData.id,
|
|
210
|
+
username: taskData.username,
|
|
211
|
+
since: taskData.since
|
|
212
|
+
}, config, dependencies, socialCounterRef)
|
|
213
|
+
.then(() => taskCounters.social = (taskCounters.social || 0) + 1)
|
|
214
|
+
.catch(err => {
|
|
215
|
+
logger.log('ERROR', `[TaskEngine/${taskId}] Error in social fetch for ${taskData.id}`, { errorMessage: err.message });
|
|
216
|
+
taskCounters.failed++;
|
|
137
217
|
})
|
|
138
218
|
)
|
|
139
219
|
);
|
|
140
220
|
}
|
|
141
221
|
|
|
142
|
-
//
|
|
222
|
+
// 4. Wait for ALL tasks to complete
|
|
143
223
|
await Promise.all(allTaskPromises);
|
|
144
224
|
|
|
145
|
-
//
|
|
225
|
+
// 5. Flush any remaining data in the buffer
|
|
146
226
|
if (batchManager) {
|
|
147
227
|
logger.log('INFO', `[TaskEngine/${taskId}] Triggering final batch flush...`);
|
|
148
228
|
await batchManager.flushBatches();
|
|
149
229
|
}
|
|
150
230
|
|
|
151
|
-
//
|
|
231
|
+
// 6. Log final summary
|
|
152
232
|
logger.log(
|
|
153
233
|
taskCounters.failed > 0 ? 'WARN' : 'SUCCESS',
|
|
154
|
-
`[TaskEngine/${taskId}] Processed all tasks. Updates: ${taskCounters.update}, Discovers: ${taskCounters.discover}, Verifies: ${taskCounters.verify}, PI: ${taskCounters.popular_investor}, OnDemand: ${taskCounters.on_demand}, Unknown: ${taskCounters.unknown}, Failed: ${taskCounters.failed}.`
|
|
234
|
+
`[TaskEngine/${taskId}] Processed all tasks. Updates: ${taskCounters.update}, Discovers: ${taskCounters.discover}, Verifies: ${taskCounters.verify}, PI: ${taskCounters.popular_investor}, OnDemand: ${taskCounters.on_demand}, Social: ${taskCounters.social || 0}, Unknown: ${taskCounters.unknown}, Failed: ${taskCounters.failed}.`
|
|
155
235
|
);
|
|
156
236
|
}
|
|
157
237
|
|