bulltrackers-module 1.0.127 → 1.0.129

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.
@@ -1,26 +1,16 @@
1
1
  /**
2
2
  * @fileoverview Main pipe for the Task Engine.
3
- * REFACTORED: This file is now 'handler_creator.js' in name only.
4
- * It exports the main 'handleRequest' pipe function.
5
- *
6
- * --- MODIFICATION ---
7
- * The `handleRequest` function is now designed to receive a *batch*
8
- * of tasks (e.g., { tasks: [...] }) from the Dispatcher. It loops
9
- * through all tasks in a single function instance, allowing the
10
- * FirestoreBatchManager to accumulate state. It then explicitly
11
- * calls `batchManager.flushBatches()` in the `finally` block
12
- * to commit all changes.
13
- * --- V2 MODIFICATION ---
14
- * - Loads username map at start.
15
- * - Sorts tasks into "run now" (username known) and "lookup needed".
16
- * - Runs a batch lookup for missing usernames.
17
- * - Runs all update tasks *after* lookups are complete.
3
+ * REFACTORED: This function is the high-level "manual of processes"
4
+ * that orchestrates the task engine logic.
18
5
  */
19
6
 
20
- const { handleDiscover } = require('./helpers/discover_helpers');
21
- const { handleVerify } = require('./helpers/verify_helpers');
22
- // --- MODIFIED: Import both helpers from update_helpers ---
23
- const { handleUpdate, lookupUsernames } = require('./helpers/update_helpers');
7
+ // Import the new utility functions that contain the logic
8
+ const {
9
+ parseTaskPayload,
10
+ prepareTaskBatches,
11
+ runUsernameLookups,
12
+ executeTasks
13
+ } = require('./utils/task_engine_utils'); // <-- Import from our new utils file
24
14
 
25
15
  /**
26
16
  * Main pipe: pipe.taskEngine.handleRequest
@@ -31,131 +21,20 @@ const { handleUpdate, lookupUsernames } = require('./helpers/update_helpers');
31
21
  * @param {object} dependencies - Contains all clients: db, pubsub, logger, headerManager, proxyManager, batchManager.
32
22
  */
33
23
  async function handleRequest(message, context, config, dependencies) {
34
- const { logger, batchManager, headerManager } = dependencies;
35
-
36
- if (!message || !message.data) {
37
- logger.log('ERROR', '[TaskEngine Module] Received invalid message structure.', { message });
38
- return;
39
- }
40
-
41
- let payload;
24
+ const { logger, batchManager } = dependencies;
25
+ const taskId = `batch-${context.eventId || Date.now()}`;
42
26
  try {
43
- payload = JSON.parse(Buffer.from(message.data, 'base64').toString('utf-8'));
44
- } catch (e) {
45
- logger.log('ERROR', '[TaskEngine Module] Failed to parse Pub/Sub message data.', { error: e.message, data: message.data });
46
- return;
47
- }
48
-
49
- if (!payload.tasks || !Array.isArray(payload.tasks)) {
50
- logger.log('ERROR', `[TaskEngine] Received invalid payload. Expected { tasks: [...] }`, { payload });
51
- return;
52
- }
53
-
54
- if (payload.tasks.length === 0) {
55
- logger.log('WARN', '[TaskEngine] Received message with an empty tasks array.');
56
- return;
57
- }
58
-
59
- const taskId = `batch-${payload.tasks[0]?.type || 'unknown'}-${Date.now()}`;
60
- logger.log('INFO', `[TaskEngine/${taskId}] Received batch of ${payload.tasks.length} tasks.`);
61
-
62
- try {
63
- // --- START V2 MODIFICATION ---
64
-
65
- // 1. Load the username map into the batch manager's memory
66
- await batchManager.loadUsernameMap();
67
-
68
- // 2. Sort tasks
69
- const tasksToRun = []; // { task, username }
70
- const cidsToLookup = new Map(); // Map<string, object>
71
- const otherTasks = []; // for 'discover' and 'verify'
72
-
73
- for (const task of payload.tasks) {
74
- if (task.type === 'update') {
75
- const username = batchManager.getUsername(task.userId);
76
- if (username) {
77
- tasksToRun.push({ task, username });
78
- } else {
79
- cidsToLookup.set(String(task.userId), task);
80
- }
81
- } else if (task.type === 'discover' || task.type === 'verify') {
82
- otherTasks.push(task);
83
- }
84
- }
85
-
86
- logger.log('INFO', `[TaskEngine/${taskId}] Usernames known for ${tasksToRun.length} users. Looking up ${cidsToLookup.size} users. Processing ${otherTasks.length} other tasks.`);
87
-
88
- // 3. Run username lookups if needed
89
- if (cidsToLookup.size > 0) {
90
- const foundUsers = await lookupUsernames(Array.from(cidsToLookup.keys()), dependencies, config);
91
-
92
- for (const user of foundUsers) {
93
- const cidStr = String(user.CID);
94
- const username = user.Value.UserName;
95
-
96
- // Add to batch manager to save this new mapping
97
- batchManager.addUsernameMapUpdate(cidStr, username);
98
-
99
- // Get the original task
100
- const task = cidsToLookup.get(cidStr);
101
- if (task) {
102
- // Add to the list of tasks to run
103
- tasksToRun.push({ task, username });
104
- // Remove from lookup map
105
- cidsToLookup.delete(cidStr);
106
- }
107
- }
108
-
109
- if (cidsToLookup.size > 0) {
110
- logger.log('WARN', `[TaskEngine/${taskId}] Could not find usernames for ${cidsToLookup.size} CIDs (likely private). They will be skipped.`, { skippedCids: Array.from(cidsToLookup.keys()) });
111
- }
112
- }
113
-
114
- // 4. Process all tasks
115
-
116
- // Process discover/verify tasks
117
- for (const task of otherTasks) {
118
- const subTaskId = `${task.type || 'unknown'}-${task.userType || 'unknown'}-${task.userId || task.cids?.[0] || 'sub'}`;
119
- const handlerFunction = {
120
- discover: handleDiscover,
121
- verify: handleVerify,
122
- }[task.type];
123
-
124
- if (handlerFunction) {
125
- await handlerFunction(task, subTaskId, dependencies, config);
126
- } else {
127
- logger.log('ERROR', `[TaskEngine/${taskId}] Unknown task type in 'otherTasks': ${task.type}`);
128
- }
129
- }
130
-
131
- // Process update tasks (now with usernames)
132
- for (const { task, username } of tasksToRun) {
133
- const subTaskId = `${task.type || 'unknown'}-${task.userType || 'unknown'}-${task.userId || 'sub'}`;
134
- try {
135
- // Pass the username to handleUpdate
136
- await handleUpdate(task, subTaskId, dependencies, config, username);
137
- } catch (updateError) {
138
- logger.log('ERROR', `[TaskEngine/${taskId}] Error in handleUpdate for user ${task.userId}`, { errorMessage: updateError.message, errorStack: updateError.stack });
139
- }
140
- }
141
-
142
- logger.log('SUCCESS', `[TaskEngine/${taskId}] Processed all tasks. Done.`);
143
- // --- END V2 MODIFICATION ---
144
-
145
- } catch (error) {
146
- logger.log('ERROR', `[TaskEngine/${taskId}] Failed during batch processing.`, { errorMessage: error.message, errorStack: error.stack });
147
-
27
+ const tasks = parseTaskPayload(message, logger);
28
+ if (!tasks) {return; }
29
+ logger.log('INFO', `[TaskEngine/${taskId}] Received batch of ${tasks.length} tasks.`);
30
+ const { tasksToRun, cidsToLookup, otherTasks } = await prepareTaskBatches(tasks, batchManager, logger);
31
+ await runUsernameLookups(tasksToRun, cidsToLookup, dependencies, config, batchManager, logger);
32
+ await executeTasks(tasksToRun, otherTasks, dependencies, config, taskId);
33
+ } catch (error) {logger.log('ERROR', `[TaskEngine/${taskId}] Failed during batch processing.`, { errorMessage: error.message, errorStack: error.stack });
148
34
  } finally {
149
- try {
150
- logger.log('INFO', `[TaskEngine/${taskId}] Flushing all accumulated batches...`);
151
- await batchManager.flushBatches();
152
- logger.log('INFO', `[TaskEngine/${taskId}] Final batch and header flush complete.`);
153
- } catch (flushError) {
154
- logger.log('ERROR', `[TaskEngine/${taskId}] Error during final flush attempt.`, { error: flushError.message });
155
- }
156
- }
35
+ try {logger.log('INFO', `[TaskEngine/${taskId}] Flushing all accumulated batches...`);
36
+ await batchManager.flushBatches();
37
+ logger.log('INFO', `[TaskEngine/${taskId}] Final batch and header flush complete.`);} catch (flushError) {logger.log('ERROR', `[TaskEngine/${taskId}] Error during final flush attempt.`, { error: flushError.message });}}
157
38
  }
158
39
 
159
- module.exports = {
160
- handleRequest,
161
- };
40
+ module.exports = {handleRequest};
@@ -1,8 +1,3 @@
1
- /*
2
- * FILENAME: CloudFunctions/NpmWrappers/bulltrackers-module/functions/task-engine/helpers/discover_helpers.js
3
- * (FIXED: Publishes 'verify' task in the correct batch format { tasks: [...] })
4
- */
5
-
6
1
  /**
7
2
  * @fileoverview Sub-pipe: pipe.taskEngine.handleDiscover
8
3
  * REFACTORED: Now stateless and receives dependencies.
@@ -19,120 +14,55 @@
19
14
  async function handleDiscover(task, taskId, dependencies, config) {
20
15
  const { logger, headerManager, proxyManager, pubsub, batchManager } = dependencies;
21
16
  const { cids, blockId, instrument, userType } = task;
22
-
23
17
  const url = `${config.ETORO_API_RANKINGS_URL}?Period=LastTwoYears`;
24
18
  const selectedHeader = await headerManager.selectHeader();
25
19
  if (!selectedHeader) throw new Error("Could not select a header.");
26
-
27
20
  let wasSuccess = false;
28
- try {
29
- if (userType === 'speculator') {
30
- // Use the batchManager from dependencies
31
- batchManager.addProcessedSpeculatorCids(cids);
32
- logger.log('INFO', `[DISCOVER] Added ${cids.length} speculator CIDs to the in-memory set to be flushed.`);
33
- }
34
-
35
- // Use the proxyManager from dependencies
36
- const response = await proxyManager.fetch(url, {
37
- method: 'POST',
38
- headers: { ...selectedHeader.header, 'Content-Type': 'application/json' },
39
- body: JSON.stringify(cids),
40
- });
41
- if (!response.ok) {
42
- throw new Error(`API status ${response.status}`);
43
- }
44
- wasSuccess = true;
45
21
 
22
+ // Step 1. Discover Speculators
23
+ try {if (userType === 'speculator') {batchManager.addProcessedSpeculatorCids(cids);logger.log('INFO', `[DISCOVER] Added ${cids.length} speculator CIDs to the in-memory set to be flushed.`);}
24
+ const response = await proxyManager.fetch(url, {method: 'POST',headers: { ...selectedHeader.header, 'Content-Type': 'application/json' },body: JSON.stringify(cids),});
25
+ if (!response.ok) {throw new Error(`API status ${response.status}`);}
26
+ wasSuccess = true;
46
27
  const publicUsers = await response.json();
47
28
  if (!Array.isArray(publicUsers)) return;
48
-
49
29
  const oneMonthAgo = new Date();
50
30
  oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
51
-
52
- const preliminaryActiveUsers = publicUsers.filter(user =>
53
- new Date(user.Value.LastActivity) > oneMonthAgo &&
54
- user.Value.DailyGain !== 0 &&
55
- user.Value.Exposure !== 0 &&
56
- user.Value.RiskScore !== 0
57
- );
58
-
31
+ // Step 2. We know a user is "active" if their risk score is NOT 0 and they logged in, in the last month. This covers the edge case that a user IS active but does NOT hold positions (cannot hold no positions and have 0 risk score)
32
+ const preliminaryActiveUsers = publicUsers.filter(user => new Date(user.Value.LastActivity) > oneMonthAgo &&user.Value.DailyGain !== 0 &&user.Value.Exposure !== 0 &&user.Value.RiskScore !== 0);
59
33
  let finalActiveUsers = [];
60
34
  const invalidCidsToLog = [];
61
-
62
- if (userType === 'speculator') {
63
- const publicUserCids = new Set(publicUsers.map(u => u.CID));
64
-
65
- if (publicUserCids.size > 0 && publicUserCids.size < cids.length) {
66
- const privateUserCids = cids.filter(cid => !publicUserCids.has(cid));
67
- invalidCidsToLog.push(...privateUserCids);
68
- }
69
-
35
+ // Step 3. Users that were passed INTO the discovery API but did not come OUT OF the discovery API, are private accounts, the API filters this for us, so mark them as such to avoid processing them again.
36
+ if (userType === 'speculator') {const publicUserCids = new Set(publicUsers.map(u => u.CID));
37
+ if (publicUserCids.size > 0 && publicUserCids.size < cids.length) {const privateUserCids = cids.filter(cid => !publicUserCids.has(cid));invalidCidsToLog.push(...privateUserCids);}
70
38
  const activeUserCids = new Set(preliminaryActiveUsers.map(u => u.CID));
71
- const inactiveUserCids = publicUsers
72
- .filter(u => !activeUserCids.has(u.CID))
73
- .map(u => u.CID);
39
+ const inactiveUserCids = publicUsers.filter(u => !activeUserCids.has(u.CID)).map(u => u.CID);
74
40
  invalidCidsToLog.push(...inactiveUserCids);
75
-
76
41
  logger.log('INFO', `[DISCOVER] Applying new speculator pre-filter to ${preliminaryActiveUsers.length} active users.`);
77
42
  const nonSpeculatorCids = [];
78
-
43
+ // Step 4. For the remaining active users, apply heuristic filters to weed out non-speculators.
79
44
  for (const user of preliminaryActiveUsers) {
80
45
  const v = user.Value;
81
46
  const totalLeverage = (v.MediumLeveragePct || 0) + (v.HighLeveragePct || 0);
82
-
83
- const isLikelySpeculator = (
84
- (v.Trades || 0) > 500 ||
85
- (v.TotalTradedInstruments || 0) > 50 ||
86
- totalLeverage > 50 ||
87
- (v.WeeklyDD || 0) < -25
88
- );
89
-
90
- if (isLikelySpeculator) {
91
- finalActiveUsers.push(user);
92
- } else {
93
- nonSpeculatorCids.push(user.CID);
94
- }
95
- }
96
-
47
+ const isLikelySpeculator = ((v.Trades || 0) > 500 ||(v.TotalTradedInstruments || 0) > 50 ||totalLeverage > 50 ||(v.WeeklyDD || 0) < -25);
48
+ if (isLikelySpeculator) {finalActiveUsers.push(user);} else {nonSpeculatorCids.push(user.CID);}}
97
49
  invalidCidsToLog.push(...nonSpeculatorCids);
98
50
  logger.log('INFO', `[DISCOVER] Pre-filter complete. ${finalActiveUsers.length} users passed. ${nonSpeculatorCids.length} users failed heuristic.`);
99
-
100
51
  if (invalidCidsToLog.length > 0) {
101
- // Use pubsub from dependencies
102
- await pubsub.topic(config.PUBSUB_TOPIC_INVALID_SPECULATOR_LOG)
103
- .publishMessage({ json: { invalidCids: invalidCidsToLog } });
52
+ await pubsub.topic(config.PUBSUB_TOPIC_INVALID_SPECULATOR_LOG).publishMessage({ json: { invalidCids: invalidCidsToLog } });
104
53
  logger.log('INFO', `[DISCOVER] Reported ${invalidCidsToLog.length} invalid (private, inactive, or failed heuristic) speculator IDs.`);
105
54
  }
106
-
107
- } else { // 'normal' users
55
+ // Step 5. Remaining users are public active non speculators
56
+ } else {
108
57
  finalActiveUsers = preliminaryActiveUsers;
109
58
  }
110
-
59
+ // Step 6. Publish 'verify' task for all active users found
111
60
  if (finalActiveUsers.length > 0) {
112
- // --- START MODIFICATION ---
113
- // Include the UserName in the task payload for 'verify'
114
- const verificationTask = {
115
- type: 'verify',
116
- users: finalActiveUsers.map(u => ({
117
- cid: u.CID,
118
- isBronze: u.Value.IsBronze,
119
- username: u.Value.UserName // <-- ADD THIS
120
- })),
121
- blockId,
122
- instrument,
123
- userType
124
- };
125
- // --- END MODIFICATION ---
126
-
127
- // --- FIX: WRAP IN BATCH FORMAT ---
128
- // Use pubsub from dependencies
129
- await pubsub.topic(config.PUBSUB_TOPIC_USER_FETCH)
130
- .publishMessage({ json: { tasks: [verificationTask] } });
61
+ const verificationTask = {type: 'verify',users: finalActiveUsers.map(u => ({ cid: u.CID, isBronze: u.Value.IsBronze,username: u.Value.UserName})), blockId, instrument, userType};
62
+ await pubsub.topic(config.PUBSUB_TOPIC_USER_FETCH).publishMessage({ json: { tasks: [verificationTask] } });
131
63
  logger.log('INFO', `[DISCOVER] Verification message published was : ${JSON.stringify({ tasks: [verificationTask] })} `);
132
- // --- END FIX ---
133
64
  logger.log('INFO', `[DISCOVER] Chaining to 'verify' task for ${finalActiveUsers.length} active users.`);
134
65
  }
135
-
136
66
  } finally {
137
67
  if (selectedHeader) headerManager.updatePerformance(selectedHeader.id, wasSuccess);
138
68
  }