bulltrackers-module 1.0.122 → 1.0.123
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/task-engine/handler_creator.js +75 -21
- package/functions/task-engine/helpers/discover_helpers.js +11 -2
- package/functions/task-engine/helpers/update_helpers.js +143 -67
- package/functions/task-engine/helpers/verify_helpers.js +27 -2
- package/functions/task-engine/utils/firestore_batch_manager.js +137 -23
- package/package.json +1 -1
|
@@ -10,11 +10,17 @@
|
|
|
10
10
|
* FirestoreBatchManager to accumulate state. It then explicitly
|
|
11
11
|
* calls `batchManager.flushBatches()` in the `finally` block
|
|
12
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.
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
20
|
const { handleDiscover } = require('./helpers/discover_helpers');
|
|
16
21
|
const { handleVerify } = require('./helpers/verify_helpers');
|
|
17
|
-
|
|
22
|
+
// --- MODIFIED: Import both helpers from update_helpers ---
|
|
23
|
+
const { handleUpdate, lookupUsernames } = require('./helpers/update_helpers');
|
|
18
24
|
|
|
19
25
|
/**
|
|
20
26
|
* Main pipe: pipe.taskEngine.handleRequest
|
|
@@ -25,7 +31,6 @@ const { handleUpdate } = require('./helpers/update_helpers');
|
|
|
25
31
|
* @param {object} dependencies - Contains all clients: db, pubsub, logger, headerManager, proxyManager, batchManager.
|
|
26
32
|
*/
|
|
27
33
|
async function handleRequest(message, context, config, dependencies) {
|
|
28
|
-
// --- MODIFICATION: Destructure batchManager and headerManager ---
|
|
29
34
|
const { logger, batchManager, headerManager } = dependencies;
|
|
30
35
|
|
|
31
36
|
if (!message || !message.data) {
|
|
@@ -41,9 +46,6 @@ async function handleRequest(message, context, config, dependencies) {
|
|
|
41
46
|
return;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
// --- START MODIFICATION: Handle Batch Payload ---
|
|
45
|
-
|
|
46
|
-
// Check if this is a batched task payload
|
|
47
49
|
if (!payload.tasks || !Array.isArray(payload.tasks)) {
|
|
48
50
|
logger.log('ERROR', `[TaskEngine] Received invalid payload. Expected { tasks: [...] }`, { payload });
|
|
49
51
|
return;
|
|
@@ -58,48 +60,100 @@ async function handleRequest(message, context, config, dependencies) {
|
|
|
58
60
|
logger.log('INFO', `[TaskEngine/${taskId}] Received batch of ${payload.tasks.length} tasks.`);
|
|
59
61
|
|
|
60
62
|
try {
|
|
61
|
-
//
|
|
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
|
+
|
|
62
73
|
for (const task of payload.tasks) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.`);
|
|
66
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'}`;
|
|
67
119
|
const handlerFunction = {
|
|
68
120
|
discover: handleDiscover,
|
|
69
121
|
verify: handleVerify,
|
|
70
|
-
update: handleUpdate
|
|
71
122
|
}[task.type];
|
|
72
123
|
|
|
73
124
|
if (handlerFunction) {
|
|
74
|
-
// We await each one sequentially to manage API load,
|
|
75
|
-
// but they all use the *same* batchManager instance.
|
|
76
125
|
await handlerFunction(task, subTaskId, dependencies, config);
|
|
77
126
|
} else {
|
|
78
|
-
logger.log('ERROR', `[TaskEngine/${taskId}] Unknown task type in
|
|
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 });
|
|
79
139
|
}
|
|
80
140
|
}
|
|
81
141
|
|
|
82
|
-
logger.log('SUCCESS', `[TaskEngine/${taskId}] Processed
|
|
142
|
+
logger.log('SUCCESS', `[TaskEngine/${taskId}] Processed all tasks. Done.`);
|
|
143
|
+
// --- END V2 MODIFICATION ---
|
|
83
144
|
|
|
84
145
|
} catch (error) {
|
|
85
146
|
logger.log('ERROR', `[TaskEngine/${taskId}] Failed during batch processing.`, { errorMessage: error.message, errorStack: error.stack });
|
|
86
147
|
|
|
87
148
|
} finally {
|
|
88
149
|
try {
|
|
89
|
-
// --- MODIFICATION ---
|
|
90
|
-
// This is the most critical change.
|
|
91
|
-
// After the loop (or if an error occurs), explicitly flush
|
|
92
|
-
// all accumulated writes (portfolios, timestamps, etc.)
|
|
93
|
-
// from the batchManager.
|
|
94
150
|
logger.log('INFO', `[TaskEngine/${taskId}] Flushing all accumulated batches...`);
|
|
95
151
|
await batchManager.flushBatches();
|
|
96
152
|
logger.log('INFO', `[TaskEngine/${taskId}] Final batch and header flush complete.`);
|
|
97
|
-
// --- END MODIFICATION ---
|
|
98
153
|
} catch (flushError) {
|
|
99
154
|
logger.log('ERROR', `[TaskEngine/${taskId}] Error during final flush attempt.`, { error: flushError.message });
|
|
100
155
|
}
|
|
101
156
|
}
|
|
102
|
-
// --- END MODIFICATION ---
|
|
103
157
|
}
|
|
104
158
|
|
|
105
159
|
module.exports = {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Sub-pipe: pipe.taskEngine.handleDiscover
|
|
3
3
|
* REFACTORED: Now stateless and receives dependencies.
|
|
4
|
+
* --- MODIFIED: Passes username to the 'verify' task. ---
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -103,13 +104,21 @@ async function handleDiscover(task, taskId, dependencies, config) {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
if (finalActiveUsers.length > 0) {
|
|
107
|
+
// --- START MODIFICATION ---
|
|
108
|
+
// Include the UserName in the task payload for 'verify'
|
|
106
109
|
const verificationTask = {
|
|
107
110
|
type: 'verify',
|
|
108
|
-
users: finalActiveUsers.map(u => ({
|
|
111
|
+
users: finalActiveUsers.map(u => ({
|
|
112
|
+
cid: u.CID,
|
|
113
|
+
isBronze: u.Value.IsBronze,
|
|
114
|
+
username: u.Value.UserName // <-- ADD THIS
|
|
115
|
+
})),
|
|
109
116
|
blockId,
|
|
110
117
|
instrument,
|
|
111
118
|
userType
|
|
112
119
|
};
|
|
120
|
+
// --- END MODIFICATION ---
|
|
121
|
+
|
|
113
122
|
// Use pubsub from dependencies
|
|
114
123
|
await pubsub.topic(config.PUBSUB_TOPIC_USER_FETCH)
|
|
115
124
|
.publishMessage({ json: verificationTask });
|
|
@@ -122,4 +131,4 @@ async function handleDiscover(task, taskId, dependencies, config) {
|
|
|
122
131
|
}
|
|
123
132
|
}
|
|
124
133
|
|
|
125
|
-
module.exports = { handleDiscover };
|
|
134
|
+
module.exports = { handleDiscover };
|
|
@@ -2,49 +2,151 @@
|
|
|
2
2
|
* @fileoverview Sub-pipe: pipe.taskEngine.handleUpdate
|
|
3
3
|
* REFACTORED: Now stateless and receives dependencies.
|
|
4
4
|
* OPTIMIZED: Removed immediate batch commit for speculator timestamp fix.
|
|
5
|
+
* --- MODIFIED: Fetches portfolio AND trade history in parallel. ---
|
|
6
|
+
* --- MODIFIED: Includes helper to look up usernames from CIDs. ---
|
|
5
7
|
*/
|
|
6
8
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* --- NEW HELPER ---
|
|
12
|
+
* Fetches user details (including username) from the rankings API for a batch of CIDs.
|
|
13
|
+
* @param {Array<string>} cids - An array of user CIDs (as strings or numbers).
|
|
14
|
+
* @param {object} dependencies - Contains proxyManager, headerManager, logger.
|
|
15
|
+
* @param {object} config - The configuration object.
|
|
16
|
+
* @returns {Promise<Array<object>>} An array of the raw user objects from the API.
|
|
17
|
+
*/
|
|
18
|
+
async function lookupUsernames(cids, dependencies, config) {
|
|
19
|
+
const { logger, headerManager, proxyManager } = dependencies;
|
|
20
|
+
const { USERNAME_LOOKUP_BATCH_SIZE, ETORO_API_RANKINGS_URL } = config;
|
|
21
|
+
|
|
22
|
+
if (!cids || cids.length === 0) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
logger.log('INFO', `[lookupUsernames] Looking up usernames for ${cids.length} CIDs.`);
|
|
27
|
+
const allPublicUsers = [];
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < cids.length; i += USERNAME_LOOKUP_BATCH_SIZE) {
|
|
30
|
+
const batchCids = cids.slice(i, i + USERNAME_LOOKUP_BATCH_SIZE).map(Number);
|
|
31
|
+
|
|
32
|
+
const url = `${ETORO_API_RANKINGS_URL}?Period=LastTwoYears`;
|
|
33
|
+
const selectedHeader = await headerManager.selectHeader();
|
|
34
|
+
if (!selectedHeader) {
|
|
35
|
+
logger.log('ERROR', '[lookupUsernames] Could not select a header.');
|
|
36
|
+
continue; // Skip this batch
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let wasSuccess = false;
|
|
40
|
+
try {
|
|
41
|
+
const response = await proxyManager.fetch(url, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { ...selectedHeader.header, 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify(batchCids),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(`[lookupUsernames] API status ${response.status}`);
|
|
49
|
+
}
|
|
50
|
+
wasSuccess = true;
|
|
51
|
+
|
|
52
|
+
const publicUsers = await response.json();
|
|
53
|
+
if (Array.isArray(publicUsers)) {
|
|
54
|
+
allPublicUsers.push(...publicUsers);
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
logger.log('WARN', `[lookupUsernames] Failed to fetch batch of usernames.`, { errorMessage: error.message });
|
|
58
|
+
} finally {
|
|
59
|
+
if (selectedHeader) {
|
|
60
|
+
headerManager.updatePerformance(selectedHeader.id, wasSuccess);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
logger.log('INFO', `[lookupUsernames] Found ${allPublicUsers.length} public users out of ${cids.length}.`);
|
|
66
|
+
return allPublicUsers;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
8
70
|
/**
|
|
9
71
|
* Sub-pipe: pipe.taskEngine.handleUpdate
|
|
10
72
|
* @param {object} task The Pub/Sub task payload.
|
|
11
73
|
* @param {string} taskId A unique ID for logging.
|
|
12
74
|
* @param {object} dependencies - Contains db, pubsub, logger, headerManager, proxyManager, batchManager.
|
|
13
75
|
* @param {object} config The configuration object.
|
|
76
|
+
* @param {string} username - The user's eToro username.
|
|
14
77
|
*/
|
|
15
|
-
async function handleUpdate(task, taskId, dependencies, config) {
|
|
78
|
+
async function handleUpdate(task, taskId, dependencies, config, username) {
|
|
16
79
|
const { logger, headerManager, proxyManager, db, batchManager } = dependencies;
|
|
17
80
|
const { userId, instrumentId, userType } = task;
|
|
81
|
+
|
|
82
|
+
// --- START MODIFICATION: Define both URLs ---
|
|
83
|
+
const portfolioUrl = userType === 'speculator'
|
|
84
|
+
? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instrumentId}`
|
|
85
|
+
: `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
|
|
86
|
+
|
|
87
|
+
const historyUrl = `${config.ETORO_API_USERSTATS_URL}${username}/trades/oneYearAgo?CopyAsAsset=true`;
|
|
18
88
|
|
|
19
|
-
const
|
|
20
|
-
|
|
89
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
90
|
+
const portfolioStorageBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
|
|
91
|
+
|
|
92
|
+
let portfolioHeader = null;
|
|
93
|
+
let historyHeader = null;
|
|
94
|
+
let wasPortfolioSuccess = false;
|
|
95
|
+
let wasHistorySuccess = false;
|
|
96
|
+
let isPrivate = false;
|
|
21
97
|
|
|
22
|
-
let wasSuccess = false;
|
|
23
98
|
try {
|
|
24
|
-
|
|
25
|
-
? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instrumentId}`
|
|
26
|
-
: `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
|
|
27
|
-
|
|
28
|
-
logger.log('INFO', `[UPDATE] Fetching portfolio for user ${userId} (${userType} with url ${url})`);
|
|
29
|
-
|
|
30
|
-
// Use proxyManager from dependencies
|
|
31
|
-
const response = await proxyManager.fetch(url, { headers: selectedHeader.header });
|
|
99
|
+
logger.log('INFO', `[UPDATE] Fetching data for user ${userId} (${username}, ${userType})`);
|
|
32
100
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
101
|
+
// Select headers for both requests
|
|
102
|
+
portfolioHeader = await headerManager.selectHeader();
|
|
103
|
+
historyHeader = await headerManager.selectHeader();
|
|
104
|
+
if (!portfolioHeader || !historyHeader) {
|
|
105
|
+
throw new Error("Could not select headers for update requests.");
|
|
36
106
|
}
|
|
37
107
|
|
|
38
|
-
|
|
108
|
+
// --- Fetch both endpoints in parallel ---
|
|
109
|
+
const [portfolioResult, historyResult] = await Promise.allSettled([
|
|
110
|
+
proxyManager.fetch(portfolioUrl, { headers: portfolioHeader.header }),
|
|
111
|
+
proxyManager.fetch(historyUrl, { headers: historyHeader.header })
|
|
112
|
+
]);
|
|
39
113
|
|
|
40
|
-
|
|
41
|
-
|
|
114
|
+
// --- Process Portfolio Result ---
|
|
115
|
+
if (portfolioResult.status === 'fulfilled' && portfolioResult.value.ok) {
|
|
116
|
+
const responseBody = await portfolioResult.value.text();
|
|
42
117
|
|
|
43
|
-
//
|
|
118
|
+
// Check for private user
|
|
119
|
+
if (responseBody.includes("user is PRIVATE")) {
|
|
120
|
+
isPrivate = true;
|
|
121
|
+
} else {
|
|
122
|
+
wasPortfolioSuccess = true;
|
|
123
|
+
const portfolioData = JSON.parse(responseBody);
|
|
124
|
+
// Add to portfolio batch
|
|
125
|
+
await batchManager.addToPortfolioBatch(userId, portfolioStorageBlockId, today, portfolioData, userType, instrumentId);
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
// Log failure
|
|
129
|
+
const errorMsg = portfolioResult.status === 'rejected' ? portfolioResult.reason.message : `API status ${portfolioResult.value.status}`;
|
|
130
|
+
logger.log('WARN', `[UPDATE] Failed to fetch PORTFOLIO for user ${userId}.`, { error: errorMsg });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- Process History Result ---
|
|
134
|
+
if (historyResult.status === 'fulfilled' && historyResult.value.ok) {
|
|
135
|
+
const historyData = await historyResult.value.json();
|
|
136
|
+
wasHistorySuccess = true;
|
|
137
|
+
// Add to history batch
|
|
138
|
+
await batchManager.addToTradingHistoryBatch(userId, portfolioStorageBlockId, today, historyData, userType);
|
|
139
|
+
} else {
|
|
140
|
+
// Log failure
|
|
141
|
+
const errorMsg = historyResult.status === 'rejected' ? historyResult.reason.message : `API status ${historyResult.value.status}`;
|
|
142
|
+
logger.log('WARN', `[UPDATE] Failed to fetch HISTORY for user ${userId} (${username}).`, { error: errorMsg });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- Handle Private User (if detected) ---
|
|
146
|
+
if (isPrivate) {
|
|
147
|
+
logger.log('WARN', `User ${userId} is private. Removing from future updates and decrementing block count.`);
|
|
44
148
|
batchManager.deleteFromTimestampBatch(userId, userType, instrumentId);
|
|
45
149
|
|
|
46
|
-
// <<< START FIX for private user blockId format >>>
|
|
47
|
-
// The blockId format for Speculator Blocks is numeric (e.g., "1000000")
|
|
48
150
|
let blockId;
|
|
49
151
|
let incrementField;
|
|
50
152
|
|
|
@@ -52,68 +154,42 @@ async function handleUpdate(task, taskId, dependencies, config) {
|
|
|
52
154
|
blockId = String(Math.floor(parseInt(userId) / 1000000) * 1000000);
|
|
53
155
|
incrementField = `counts.${instrumentId}_${blockId}`;
|
|
54
156
|
} else {
|
|
55
|
-
|
|
56
|
-
// <<< START FIX for Normal User Block ID >>>
|
|
57
|
-
// BUG: This was calculating '19M', which is wrong for the counter.
|
|
58
|
-
// blockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
|
|
59
|
-
|
|
60
|
-
// FIX: Use the numeric '19000000' format, just like the orchestrator.
|
|
61
157
|
blockId = String(Math.floor(parseInt(userId) / 1000000) * 1000000);
|
|
62
|
-
// <<< END FIX for Normal User Block ID >>>
|
|
63
|
-
|
|
64
158
|
incrementField = `counts.${blockId}`;
|
|
65
159
|
}
|
|
66
160
|
|
|
67
|
-
// Use db from dependencies
|
|
68
161
|
const blockCountsRef = db.doc(userType === 'speculator'
|
|
69
162
|
? config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS
|
|
70
163
|
: config.FIRESTORE_DOC_BLOCK_COUNTS);
|
|
71
|
-
// <<< END FIX for private user blockId format >>>
|
|
72
164
|
|
|
73
165
|
// This is a single, immediate write, which is fine for this rare case.
|
|
166
|
+
// We use .set here because batchManager.flushBatches() might not run if this is the only user.
|
|
167
|
+
// To be safer, this should also be added to a batch, but this is an edge case.
|
|
74
168
|
await blockCountsRef.set({ [incrementField]: FieldValue.increment(-1) }, { merge: true });
|
|
75
|
-
|
|
76
169
|
return;
|
|
77
170
|
}
|
|
78
|
-
|
|
79
|
-
if (!response.ok) {
|
|
80
|
-
throw new Error(`API Error: Status ${response.status}`);
|
|
81
|
-
}
|
|
82
|
-
wasSuccess = true;
|
|
83
|
-
const portfolioData = JSON.parse(responseBody);
|
|
84
|
-
|
|
85
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
86
|
-
|
|
87
|
-
// <<< START OPTIMIZATION / FULL CODE FIX >>>
|
|
88
|
-
|
|
89
|
-
// BUG 1: The blockId format is different for portfolio storage ('1M')
|
|
90
|
-
// vs. orchestrator logic ('1000000').
|
|
91
|
-
const portfolioStorageBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
|
|
92
|
-
|
|
93
|
-
// This call is correct for storing portfolio data (which uses the 'M' format)
|
|
94
|
-
await batchManager.addToPortfolioBatch(userId, portfolioStorageBlockId, today, portfolioData, userType, instrumentId);
|
|
95
|
-
|
|
96
|
-
// This call is correct for 'normal' users.
|
|
97
|
-
await batchManager.updateUserTimestamp(userId, userType, instrumentId);
|
|
98
171
|
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
172
|
+
// --- If either fetch was successful, update timestamps ---
|
|
173
|
+
if (wasPortfolioSuccess || wasHistorySuccess) {
|
|
174
|
+
// This call is correct for 'normal' users.
|
|
175
|
+
await batchManager.updateUserTimestamp(userId, userType, instrumentId);
|
|
103
176
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
logger.log('INFO', `[UPDATE] Speculator timestamp fix for user ${userId} queued.`);
|
|
177
|
+
// This is the fix for speculator timestamps
|
|
178
|
+
if (userType === 'speculator') {
|
|
179
|
+
const orchestratorBlockId = String(Math.floor(parseInt(userId) / 1000000) * 1000000);
|
|
180
|
+
logger.log('INFO', `[UPDATE] Applying speculator timestamp fix for user ${userId} in block ${orchestratorBlockId}`);
|
|
181
|
+
await batchManager.addSpeculatorTimestampFix(userId, orchestratorBlockId);
|
|
182
|
+
}
|
|
111
183
|
}
|
|
112
|
-
|
|
113
|
-
|
|
184
|
+
|
|
114
185
|
} finally {
|
|
115
|
-
|
|
186
|
+
// Update performance for both headers used
|
|
187
|
+
if (portfolioHeader) headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess);
|
|
188
|
+
if (historyHeader) headerManager.updatePerformance(historyHeader.id, wasHistorySuccess);
|
|
116
189
|
}
|
|
117
190
|
}
|
|
118
191
|
|
|
119
|
-
module.exports = {
|
|
192
|
+
module.exports = {
|
|
193
|
+
handleUpdate,
|
|
194
|
+
lookupUsernames // <-- EXPORT NEW HELPER
|
|
195
|
+
};
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* @fileoverview Sub-pipe: pipe.taskEngine.handleVerify
|
|
3
3
|
* REFACTORED: Now stateless and receives dependencies.
|
|
4
4
|
* OPTIMIZED: Fetches all user portfolios in parallel to reduce function runtime.
|
|
5
|
+
* --- MODIFIED: Saves username to cid_username_map collection. ---
|
|
5
6
|
*/
|
|
6
7
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Internal helper to fetch and process a single user's portfolio.
|
|
10
|
-
*
|
|
11
|
-
* @param {object} user - The user object { cid, isBronze }
|
|
11
|
+
* @param {object} user - The user object { cid, isBronze, username }
|
|
12
12
|
* @param {object} dependencies - Contains proxyManager, headerManager
|
|
13
13
|
* @param {object} config - The configuration object
|
|
14
14
|
* @param {Set<number>} speculatorInstrumentSet - Pre-built Set of instrument IDs
|
|
@@ -46,6 +46,7 @@ async function fetchAndVerifyUser(user, dependencies, config, speculatorInstrume
|
|
|
46
46
|
type: 'speculator',
|
|
47
47
|
userId: userId,
|
|
48
48
|
isBronze: user.isBronze,
|
|
49
|
+
username: user.username, // <-- PASS USERNAME
|
|
49
50
|
updateData: {
|
|
50
51
|
instruments: matchingInstruments,
|
|
51
52
|
lastVerified: new Date(),
|
|
@@ -62,6 +63,7 @@ async function fetchAndVerifyUser(user, dependencies, config, speculatorInstrume
|
|
|
62
63
|
type: 'normal',
|
|
63
64
|
userId: userId,
|
|
64
65
|
isBronze: user.isBronze,
|
|
66
|
+
username: user.username, // <-- PASS USERNAME
|
|
65
67
|
updateData: {
|
|
66
68
|
lastVerified: new Date()
|
|
67
69
|
}
|
|
@@ -93,6 +95,10 @@ async function handleVerify(task, taskId, dependencies, config) {
|
|
|
93
95
|
const speculatorUpdates = {};
|
|
94
96
|
const normalUserUpdates = {};
|
|
95
97
|
const bronzeStateUpdates = {}; // This can be one object
|
|
98
|
+
|
|
99
|
+
// --- NEW: Object to store username updates ---
|
|
100
|
+
const usernameMapUpdates = {};
|
|
101
|
+
// --- END NEW ---
|
|
96
102
|
|
|
97
103
|
const speculatorInstrumentSet = new Set(config.SPECULATOR_INSTRUMENTS_ARRAY);
|
|
98
104
|
|
|
@@ -118,6 +124,13 @@ async function handleVerify(task, taskId, dependencies, config) {
|
|
|
118
124
|
|
|
119
125
|
const data = result.value;
|
|
120
126
|
|
|
127
|
+
// --- NEW: Capture username ---
|
|
128
|
+
if (data.username) {
|
|
129
|
+
// We use the CID as the key for the username map
|
|
130
|
+
usernameMapUpdates[data.userId] = { username: data.username };
|
|
131
|
+
}
|
|
132
|
+
// --- END NEW ---
|
|
133
|
+
|
|
121
134
|
if (data.type === 'speculator') {
|
|
122
135
|
speculatorUpdates[`users.${data.userId}`] = data.updateData;
|
|
123
136
|
bronzeStateUpdates[data.userId] = data.isBronze;
|
|
@@ -154,6 +167,18 @@ async function handleVerify(task, taskId, dependencies, config) {
|
|
|
154
167
|
}
|
|
155
168
|
}
|
|
156
169
|
|
|
170
|
+
// --- NEW: Add username map updates to the batch ---
|
|
171
|
+
// We will use a simple sharding strategy (e.g., by first letter of CID)
|
|
172
|
+
// For simplicity here, we'll write to one document.
|
|
173
|
+
// A better production strategy would shard.
|
|
174
|
+
if (Object.keys(usernameMapUpdates).length > 0) {
|
|
175
|
+
// Simple, non-scalable approach:
|
|
176
|
+
const mapDocRef = db.collection(config.FIRESTORE_COLLECTION_USERNAME_MAP).doc('cid_map_shard_1');
|
|
177
|
+
batch.set(mapDocRef, usernameMapUpdates, { merge: true });
|
|
178
|
+
logger.log('INFO', `[VERIFY] Staging ${Object.keys(usernameMapUpdates).length} username updates.`);
|
|
179
|
+
}
|
|
180
|
+
// --- END NEW ---
|
|
181
|
+
|
|
157
182
|
await batch.commit();
|
|
158
183
|
if(validUserCount > 0) {
|
|
159
184
|
logger.log('INFO', `[VERIFY] Verified and stored ${validUserCount} new ${userType} users.`);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @fileoverview Utility class to manage stateful Firestore write batches.
|
|
3
3
|
* REFACTORED: Renamed 'firestore' to 'db' for consistency.
|
|
4
4
|
* OPTIMIZED: Added logic to handle speculator timestamp fixes within the batch.
|
|
5
|
+
* --- MODIFIED: Added username map caching and trading history batching. ---
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
@@ -23,7 +24,18 @@ class FirestoreBatchManager {
|
|
|
23
24
|
this.portfolioBatch = {};
|
|
24
25
|
this.timestampBatch = {};
|
|
25
26
|
this.processedSpeculatorCids = new Set();
|
|
26
|
-
this.speculatorTimestampFixBatch = {};
|
|
27
|
+
this.speculatorTimestampFixBatch = {};
|
|
28
|
+
|
|
29
|
+
// --- NEW STATE ---
|
|
30
|
+
this.tradingHistoryBatch = {}; // For the new trade history data
|
|
31
|
+
this.usernameMap = new Map(); // In-memory cache: Map<cid, username>
|
|
32
|
+
this.usernameMapUpdates = {}; // Batched updates for Firestore: { [cid]: { username } }
|
|
33
|
+
this.usernameMapLastLoaded = 0;
|
|
34
|
+
this.usernameMapCollectionName = config.FIRESTORE_COLLECTION_USERNAME_MAP;
|
|
35
|
+
this.normalHistoryCollectionName = config.FIRESTORE_COLLECTION_NORMAL_HISTORY;
|
|
36
|
+
this.speculatorHistoryCollectionName = config.FIRESTORE_COLLECTION_SPECULATOR_HISTORY;
|
|
37
|
+
// --- END NEW STATE ---
|
|
38
|
+
|
|
27
39
|
this.batchTimeout = null;
|
|
28
40
|
|
|
29
41
|
this.logger.log('INFO', 'FirestoreBatchManager initialized.');
|
|
@@ -41,6 +53,82 @@ class FirestoreBatchManager {
|
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
55
|
|
|
56
|
+
// --- NEW: Load the username map into memory ---
|
|
57
|
+
async loadUsernameMap() {
|
|
58
|
+
// Cache map for 1 hour to reduce reads
|
|
59
|
+
if (Date.now() - this.usernameMapLastLoaded < 3600000) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.logger.log('INFO', '[BATCH] Refreshing username map from Firestore...');
|
|
64
|
+
this.usernameMap.clear();
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const snapshot = await this.db.collection(this.usernameMapCollectionName).get();
|
|
68
|
+
if (snapshot.empty) {
|
|
69
|
+
this.logger.log('WARN', '[BATCH] Username map collection is empty.');
|
|
70
|
+
} else {
|
|
71
|
+
snapshot.forEach(doc => {
|
|
72
|
+
const data = doc.data();
|
|
73
|
+
// Assumes doc.id is CID, or data is { [cid]: { username } }
|
|
74
|
+
for (const cid in data) {
|
|
75
|
+
if (data[cid] && data[cid].username) {
|
|
76
|
+
this.usernameMap.set(String(cid), data[cid].username);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
this.usernameMapLastLoaded = Date.now();
|
|
82
|
+
this.logger.log('INFO', `[BATCH] Loaded ${this.usernameMap.size} usernames into memory cache.`);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.logger.log('ERROR', '[BATCH] Failed to load username map.', { errorMessage: error.message });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- NEW: Get username from in-memory cache ---
|
|
89
|
+
getUsername(cid) {
|
|
90
|
+
return this.usernameMap.get(String(cid));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- NEW: Add a username to the cache and the write batch ---
|
|
94
|
+
addUsernameMapUpdate(cid, username) {
|
|
95
|
+
const cidStr = String(cid);
|
|
96
|
+
if (!username) return;
|
|
97
|
+
|
|
98
|
+
// Update in-memory cache immediately
|
|
99
|
+
this.usernameMap.set(cidStr, username);
|
|
100
|
+
|
|
101
|
+
// Add to Firestore write batch
|
|
102
|
+
// We'll write this to a single-field doc for simple merging
|
|
103
|
+
this.usernameMapUpdates[cidStr] = { username: username };
|
|
104
|
+
this.logger.log('TRACE', `[BATCH] Queued username update for ${cidStr}.`);
|
|
105
|
+
this._scheduleFlush();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- NEW: Add trading history to its own batch ---
|
|
109
|
+
async addToTradingHistoryBatch(userId, blockId, date, historyData, userType) {
|
|
110
|
+
const collection = userType === 'speculator'
|
|
111
|
+
? this.speculatorHistoryCollectionName
|
|
112
|
+
: this.normalHistoryCollectionName;
|
|
113
|
+
|
|
114
|
+
const basePath = `${collection}/${blockId}/snapshots/${date}`;
|
|
115
|
+
|
|
116
|
+
if (!this.tradingHistoryBatch[basePath]) {
|
|
117
|
+
this.tradingHistoryBatch[basePath] = {};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.tradingHistoryBatch[basePath][userId] = historyData;
|
|
121
|
+
|
|
122
|
+
// Trigger flush based on portfolio batch size, assuming they'll be roughly equal
|
|
123
|
+
const totalUsersInBatch = Object.values(this.portfolioBatch).reduce((sum, users) => sum + Object.keys(users).length, 0);
|
|
124
|
+
|
|
125
|
+
if (totalUsersInBatch >= this.config.TASK_ENGINE_MAX_BATCH_SIZE) {
|
|
126
|
+
await this.flushBatches();
|
|
127
|
+
} else {
|
|
128
|
+
this._scheduleFlush();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
44
132
|
/**
|
|
45
133
|
* Adds a portfolio to the batch.
|
|
46
134
|
* @param {string} userId
|
|
@@ -148,41 +236,56 @@ class FirestoreBatchManager {
|
|
|
148
236
|
// --- END NEW METHOD ---
|
|
149
237
|
|
|
150
238
|
/**
|
|
151
|
-
*
|
|
239
|
+
* Helper to flush a specific batch type (portfolio or history).
|
|
240
|
+
* @param {object} batchData - The batch object (e.g., this.portfolioBatch)
|
|
241
|
+
* @param {Firestore.Batch} firestoreBatch - The main Firestore batch object
|
|
242
|
+
* @param {string} logName - A name for logging (e.g., "Portfolio")
|
|
243
|
+
* @returns {number} The number of batch operations added.
|
|
152
244
|
*/
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const promises = [];
|
|
160
|
-
const firestoreBatch = this.db.batch(); // Use this.db
|
|
161
|
-
let batchOperationCount = 0;
|
|
162
|
-
|
|
163
|
-
for (const basePath in this.portfolioBatch) {
|
|
164
|
-
const userPortfolios = this.portfolioBatch[basePath];
|
|
245
|
+
_flushDataBatch(batchData, firestoreBatch, logName) {
|
|
246
|
+
let operationCount = 0;
|
|
247
|
+
for (const basePath in batchData) {
|
|
248
|
+
const userPortfolios = batchData[basePath];
|
|
165
249
|
const userIds = Object.keys(userPortfolios);
|
|
166
250
|
|
|
167
251
|
if (userIds.length > 0) {
|
|
168
252
|
for (let i = 0; i < userIds.length; i += this.config.TASK_ENGINE_MAX_USERS_PER_SHARD) {
|
|
169
253
|
const chunkUserIds = userIds.slice(i, i + this.config.TASK_ENGINE_MAX_USERS_PER_SHARD);
|
|
170
|
-
|
|
171
|
-
|
|
254
|
+
|
|
172
255
|
const chunkData = {};
|
|
173
256
|
chunkUserIds.forEach(userId => {
|
|
174
257
|
chunkData[userId] = userPortfolios[userId];
|
|
175
258
|
});
|
|
176
259
|
|
|
177
|
-
const docRef = this.db.collection(`${basePath}/parts`).doc(); //
|
|
178
|
-
firestoreBatch.set(docRef, chunkData);
|
|
179
|
-
|
|
260
|
+
const docRef = this.db.collection(`${basePath}/parts`).doc(); // Use random ID
|
|
261
|
+
firestoreBatch.set(docRef, chunkData);
|
|
262
|
+
operationCount++;
|
|
180
263
|
}
|
|
181
|
-
|
|
182
|
-
this.logger.log('INFO', `[BATCH] Staged ${userIds.length} users into ${Math.ceil(userIds.length / this.config.TASK_ENGINE_MAX_USERS_PER_SHARD)} unique shard documents for ${basePath}.`);
|
|
264
|
+
this.logger.log('INFO', `[BATCH] Staged ${userIds.length} users' ${logName} data into ${Math.ceil(userIds.length / this.config.TASK_ENGINE_MAX_USERS_PER_SHARD)} shards for ${basePath}.`);
|
|
183
265
|
}
|
|
184
|
-
delete
|
|
266
|
+
delete batchData[basePath];
|
|
185
267
|
}
|
|
268
|
+
return operationCount;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Flushes all pending writes to Firestore and updates header performance.
|
|
274
|
+
*/
|
|
275
|
+
async flushBatches() {
|
|
276
|
+
if (this.batchTimeout) {
|
|
277
|
+
clearTimeout(this.batchTimeout);
|
|
278
|
+
this.batchTimeout = null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const promises = [];
|
|
282
|
+
const firestoreBatch = this.db.batch(); // Use this.db
|
|
283
|
+
let batchOperationCount = 0;
|
|
284
|
+
|
|
285
|
+
// --- REFACTOR: Use helper for both batches ---
|
|
286
|
+
batchOperationCount += this._flushDataBatch(this.portfolioBatch, firestoreBatch, "Portfolio");
|
|
287
|
+
batchOperationCount += this._flushDataBatch(this.tradingHistoryBatch, firestoreBatch, "Trade History");
|
|
288
|
+
// --- END REFACTOR ---
|
|
186
289
|
|
|
187
290
|
for (const docPath in this.timestampBatch) {
|
|
188
291
|
if (Object.keys(this.timestampBatch[docPath]).length > 0) {
|
|
@@ -243,7 +346,18 @@ class FirestoreBatchManager {
|
|
|
243
346
|
}
|
|
244
347
|
delete this.speculatorTimestampFixBatch[docPath];
|
|
245
348
|
}
|
|
246
|
-
|
|
349
|
+
|
|
350
|
+
// --- NEW: Flush Username Map Updates ---
|
|
351
|
+
if (Object.keys(this.usernameMapUpdates).length > 0) {
|
|
352
|
+
this.logger.log('INFO', `[BATCH] Flushing ${Object.keys(this.usernameMapUpdates).length} username map updates.`);
|
|
353
|
+
// Simple sharding: just merge into one doc for now.
|
|
354
|
+
// A more robust solution would shard based on CID.
|
|
355
|
+
const mapDocRef = this.db.collection(this.usernameMapCollectionName).doc('cid_map_shard_1');
|
|
356
|
+
firestoreBatch.set(mapDocRef, this.usernameMapUpdates, { merge: true });
|
|
357
|
+
batchOperationCount++;
|
|
358
|
+
this.usernameMapUpdates = {}; // Clear the batch
|
|
359
|
+
}
|
|
360
|
+
// --- END NEW ---
|
|
247
361
|
|
|
248
362
|
if (batchOperationCount > 0) {
|
|
249
363
|
promises.push(firestoreBatch.commit());
|