bulltrackers-module 1.0.122 → 1.0.124
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/orchestrator/helpers/discovery_helpers.js +45 -11
- package/functions/task-engine/handler_creator.js +75 -21
- package/functions/task-engine/helpers/discover_helpers.js +20 -4
- package/functions/task-engine/helpers/update_helpers.js +183 -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
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* FILENAME: CloudFunctions/NpmWrappers/bulltrackers-module/functions/orchestrator/helpers/discovery_helpers.js
|
|
3
|
+
* (FIXED: Publishes 'discover' tasks in the correct batch format { tasks: [...] })
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* @fileoverview Orchestrator's discovery sub-pipes.
|
|
3
8
|
* REFACTORED: All functions are now stateless and receive dependencies.
|
|
@@ -164,11 +169,11 @@ async function getDiscoveryCandidates(userType, blocksToFill, config, dependenci
|
|
|
164
169
|
* @returns {Promise<void>}
|
|
165
170
|
*/
|
|
166
171
|
async function dispatchDiscovery(userType, candidates, config, dependencies) {
|
|
167
|
-
const { logger, firestoreUtils, pubsubUtils } = dependencies;
|
|
172
|
+
const { logger, firestoreUtils, pubsubUtils, pubsub } = dependencies; // <-- Add pubsub
|
|
168
173
|
const {
|
|
169
174
|
topicName,
|
|
170
175
|
dispatchBatchSize,
|
|
171
|
-
pubsubBatchSize,
|
|
176
|
+
pubsubBatchSize, // This is no longer used by this function
|
|
172
177
|
pendingSpecCollection,
|
|
173
178
|
pendingMaxFieldsPerDoc,
|
|
174
179
|
pendingMaxWritesPerBatch
|
|
@@ -208,14 +213,43 @@ async function dispatchDiscovery(userType, candidates, config, dependencies) {
|
|
|
208
213
|
}
|
|
209
214
|
}
|
|
210
215
|
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
// --- START FIX: RE-IMPLEMENT BATCHING LOGIC ---
|
|
217
|
+
const topic = pubsub.topic(topicName);
|
|
218
|
+
let totalCidsPublished = 0;
|
|
219
|
+
let messagesPublished = 0;
|
|
220
|
+
|
|
221
|
+
// 'tasks' is an array of 'discover' task objects. We group them into
|
|
222
|
+
// batches and publish each batch as a single message.
|
|
223
|
+
// We use `dispatchBatchSize` here to determine how many 'discover' tasks go into one message.
|
|
224
|
+
// This value should probably be small, like 1-10.
|
|
225
|
+
|
|
226
|
+
// Let's re-use `dispatchBatchSize` as the number of *tasks* per message.
|
|
227
|
+
// If `tasks.length` is 200 and `dispatchBatchSize` is 50, this will create 4 messages.
|
|
228
|
+
// This seems correct.
|
|
229
|
+
|
|
230
|
+
for (let i = 0; i < tasks.length; i += dispatchBatchSize) {
|
|
231
|
+
// This batch contains multiple 'discover' tasks
|
|
232
|
+
const batchOfTasks = tasks.slice(i, i + dispatchBatchSize);
|
|
233
|
+
|
|
234
|
+
// Wrap this batch in the new payload format
|
|
235
|
+
const messagePayload = { tasks: batchOfTasks };
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
await topic.publishMessage({ json: messagePayload });
|
|
239
|
+
|
|
240
|
+
// Log the number of CIDs in this message
|
|
241
|
+
const cidsInThisMessage = batchOfTasks.reduce((acc, task) => acc + task.cids.length, 0);
|
|
242
|
+
totalCidsPublished += cidsInThisMessage;
|
|
243
|
+
messagesPublished++;
|
|
244
|
+
|
|
245
|
+
logger.log('INFO', `[Orchestrator Helpers] Dispatched batch ${messagesPublished} with ${batchOfTasks.length} discover tasks (${cidsInThisMessage} CIDs) as 1 Pub/Sub message.`);
|
|
246
|
+
} catch (publishError) {
|
|
247
|
+
logger.log('ERROR', `[Orchestrator Helpers] Failed to publish discover batch ${messagesPublished + 1}.`, { error: publishError.message });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
logger.log('SUCCESS', `[Orchestrator Helpers] Dispatched ${totalCidsPublished} CIDs in ${tasks.length} tasks, grouped into ${messagesPublished} Pub/Sub messages for ${userType} discovery.`);
|
|
252
|
+
// --- END FIX ---
|
|
219
253
|
}
|
|
220
254
|
|
|
221
255
|
|
|
@@ -223,4 +257,4 @@ module.exports = {
|
|
|
223
257
|
checkDiscoveryNeed,
|
|
224
258
|
getDiscoveryCandidates,
|
|
225
259
|
dispatchDiscovery,
|
|
226
|
-
};
|
|
260
|
+
};
|
|
@@ -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,12 @@
|
|
|
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
|
+
|
|
1
6
|
/**
|
|
2
7
|
* @fileoverview Sub-pipe: pipe.taskEngine.handleDiscover
|
|
3
8
|
* REFACTORED: Now stateless and receives dependencies.
|
|
9
|
+
* --- MODIFIED: Passes username to the 'verify' task. ---
|
|
4
10
|
*/
|
|
5
11
|
|
|
6
12
|
/**
|
|
@@ -103,17 +109,27 @@ async function handleDiscover(task, taskId, dependencies, config) {
|
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
if (finalActiveUsers.length > 0) {
|
|
112
|
+
// --- START MODIFICATION ---
|
|
113
|
+
// Include the UserName in the task payload for 'verify'
|
|
106
114
|
const verificationTask = {
|
|
107
115
|
type: 'verify',
|
|
108
|
-
users: finalActiveUsers.map(u => ({
|
|
116
|
+
users: finalActiveUsers.map(u => ({
|
|
117
|
+
cid: u.CID,
|
|
118
|
+
isBronze: u.Value.IsBronze,
|
|
119
|
+
username: u.Value.UserName // <-- ADD THIS
|
|
120
|
+
})),
|
|
109
121
|
blockId,
|
|
110
122
|
instrument,
|
|
111
123
|
userType
|
|
112
124
|
};
|
|
125
|
+
// --- END MODIFICATION ---
|
|
126
|
+
|
|
127
|
+
// --- FIX: WRAP IN BATCH FORMAT ---
|
|
113
128
|
// Use pubsub from dependencies
|
|
114
129
|
await pubsub.topic(config.PUBSUB_TOPIC_USER_FETCH)
|
|
115
|
-
.publishMessage({ json: verificationTask });
|
|
116
|
-
logger.log('INFO', `[DISCOVER] Verification message published was : ${JSON.stringify(verificationTask)} `);
|
|
130
|
+
.publishMessage({ json: { tasks: [verificationTask] } });
|
|
131
|
+
logger.log('INFO', `[DISCOVER] Verification message published was : ${JSON.stringify({ tasks: [verificationTask] })} `);
|
|
132
|
+
// --- END FIX ---
|
|
117
133
|
logger.log('INFO', `[DISCOVER] Chaining to 'verify' task for ${finalActiveUsers.length} active users.`);
|
|
118
134
|
}
|
|
119
135
|
|
|
@@ -122,4 +138,4 @@ async function handleDiscover(task, taskId, dependencies, config) {
|
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
140
|
|
|
125
|
-
module.exports = { handleDiscover };
|
|
141
|
+
module.exports = { handleDiscover };
|
|
@@ -1,50 +1,192 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* FILENAME: CloudFunctions/NpmWrappers/bulltrackers-module/functions/task-engine/helpers/update_helpers.js
|
|
3
|
+
* (FIXED: Enhanced error logging for portfolio and history fetches)
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* @fileoverview Sub-pipe: pipe.taskEngine.handleUpdate
|
|
3
8
|
* REFACTORED: Now stateless and receives dependencies.
|
|
4
9
|
* OPTIMIZED: Removed immediate batch commit for speculator timestamp fix.
|
|
10
|
+
* --- MODIFIED: Fetches portfolio AND trade history in parallel. ---
|
|
11
|
+
* --- MODIFIED: Includes helper to look up usernames from CIDs. ---
|
|
5
12
|
*/
|
|
6
13
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
7
14
|
|
|
15
|
+
/**
|
|
16
|
+
* --- NEW HELPER ---
|
|
17
|
+
* Fetches user details (including username) from the rankings API for a batch of CIDs.
|
|
18
|
+
* @param {Array<string>} cids - An array of user CIDs (as strings or numbers).
|
|
19
|
+
* @param {object} dependencies - Contains proxyManager, headerManager, logger.
|
|
20
|
+
* @param {object} config - The configuration object.
|
|
21
|
+
* @returns {Promise<Array<object>>} An array of the raw user objects from the API.
|
|
22
|
+
*/
|
|
23
|
+
async function lookupUsernames(cids, dependencies, config) {
|
|
24
|
+
const { logger, headerManager, proxyManager } = dependencies;
|
|
25
|
+
const { USERNAME_LOOKUP_BATCH_SIZE, ETORO_API_RANKINGS_URL } = config;
|
|
26
|
+
|
|
27
|
+
if (!cids || cids.length === 0) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
logger.log('INFO', `[lookupUsernames] Looking up usernames for ${cids.length} CIDs.`);
|
|
32
|
+
const allPublicUsers = [];
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < cids.length; i += USERNAME_LOOKUP_BATCH_SIZE) {
|
|
35
|
+
const batchCids = cids.slice(i, i + USERNAME_LOOKUP_BATCH_SIZE).map(Number);
|
|
36
|
+
|
|
37
|
+
const url = `${ETORO_API_RANKINGS_URL}?Period=LastTwoYears`;
|
|
38
|
+
const selectedHeader = await headerManager.selectHeader();
|
|
39
|
+
if (!selectedHeader) {
|
|
40
|
+
logger.log('ERROR', '[lookupUsernames] Could not select a header.');
|
|
41
|
+
continue; // Skip this batch
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let wasSuccess = false;
|
|
45
|
+
try {
|
|
46
|
+
const response = await proxyManager.fetch(url, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { ...selectedHeader.header, 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify(batchCids),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new Error(`[lookupUsernames] API status ${response.status}`);
|
|
54
|
+
}
|
|
55
|
+
wasSuccess = true;
|
|
56
|
+
|
|
57
|
+
const publicUsers = await response.json();
|
|
58
|
+
if (Array.isArray(publicUsers)) {
|
|
59
|
+
allPublicUsers.push(...publicUsers);
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.log('WARN', `[lookupUsernames] Failed to fetch batch of usernames.`, { errorMessage: error.message });
|
|
63
|
+
} finally {
|
|
64
|
+
if (selectedHeader) {
|
|
65
|
+
headerManager.updatePerformance(selectedHeader.id, wasSuccess);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
logger.log('INFO', `[lookupUsernames] Found ${allPublicUsers.length} public users out of ${cids.length}.`);
|
|
71
|
+
return allPublicUsers;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
8
75
|
/**
|
|
9
76
|
* Sub-pipe: pipe.taskEngine.handleUpdate
|
|
10
77
|
* @param {object} task The Pub/Sub task payload.
|
|
11
78
|
* @param {string} taskId A unique ID for logging.
|
|
12
79
|
* @param {object} dependencies - Contains db, pubsub, logger, headerManager, proxyManager, batchManager.
|
|
13
80
|
* @param {object} config The configuration object.
|
|
81
|
+
* @param {string} username - The user's eToro username.
|
|
14
82
|
*/
|
|
15
|
-
async function handleUpdate(task, taskId, dependencies, config) {
|
|
83
|
+
async function handleUpdate(task, taskId, dependencies, config, username) {
|
|
16
84
|
const { logger, headerManager, proxyManager, db, batchManager } = dependencies;
|
|
17
85
|
const { userId, instrumentId, userType } = task;
|
|
86
|
+
|
|
87
|
+
// --- START MODIFICATION: Define both URLs ---
|
|
88
|
+
const portfolioUrl = userType === 'speculator'
|
|
89
|
+
? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instrumentId}`
|
|
90
|
+
: `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
|
|
91
|
+
|
|
92
|
+
const historyUrl = `${config.ETORO_API_USERSTATS_URL}${username}/trades/oneYearAgo?CopyAsAsset=true`;
|
|
18
93
|
|
|
19
|
-
const
|
|
20
|
-
|
|
94
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
95
|
+
const portfolioStorageBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
|
|
96
|
+
|
|
97
|
+
let portfolioHeader = null;
|
|
98
|
+
let historyHeader = null;
|
|
99
|
+
let wasPortfolioSuccess = false;
|
|
100
|
+
let wasHistorySuccess = false;
|
|
101
|
+
let isPrivate = false;
|
|
21
102
|
|
|
22
|
-
let wasSuccess = false;
|
|
23
103
|
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 });
|
|
104
|
+
logger.log('INFO', `[UPDATE] Fetching data for user ${userId} (${username}, ${userType})`);
|
|
32
105
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
106
|
+
// Select headers for both requests
|
|
107
|
+
portfolioHeader = await headerManager.selectHeader();
|
|
108
|
+
historyHeader = await headerManager.selectHeader();
|
|
109
|
+
if (!portfolioHeader || !historyHeader) {
|
|
110
|
+
throw new Error("Could not select headers for update requests.");
|
|
36
111
|
}
|
|
37
112
|
|
|
38
|
-
|
|
113
|
+
// --- Fetch both endpoints in parallel ---
|
|
114
|
+
const [portfolioResult, historyResult] = await Promise.allSettled([
|
|
115
|
+
proxyManager.fetch(portfolioUrl, { headers: portfolioHeader.header }),
|
|
116
|
+
proxyManager.fetch(historyUrl, { headers: historyHeader.header })
|
|
117
|
+
]);
|
|
39
118
|
|
|
40
|
-
|
|
41
|
-
|
|
119
|
+
// --- Process Portfolio Result ---
|
|
120
|
+
if (portfolioResult.status === 'fulfilled' && portfolioResult.value.ok) {
|
|
121
|
+
const responseBody = await portfolioResult.value.text();
|
|
122
|
+
|
|
123
|
+
// Check for private user
|
|
124
|
+
if (responseBody.includes("user is PRIVATE")) {
|
|
125
|
+
isPrivate = true;
|
|
126
|
+
} else {
|
|
127
|
+
wasPortfolioSuccess = true;
|
|
128
|
+
const portfolioData = JSON.parse(responseBody);
|
|
129
|
+
// Add to portfolio batch
|
|
130
|
+
console.log('DEBUG', `[UPDATE] SUCCESS Adding portfolio data for user ${userId} to batch.`);
|
|
131
|
+
await batchManager.addToPortfolioBatch(userId, portfolioStorageBlockId, today, portfolioData, userType, instrumentId);
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
// --- FIX: ENHANCED LOGGING ---
|
|
136
|
+
let errorMsg = 'Unknown fetch error';
|
|
137
|
+
let rawErrorText = 'N/A';
|
|
138
|
+
|
|
139
|
+
if (portfolioResult.status === 'rejected') {
|
|
140
|
+
errorMsg = portfolioResult.reason.message;
|
|
141
|
+
} else if (portfolioResult.value) {
|
|
142
|
+
errorMsg = portfolioResult.value.error?.message || `API status ${portfolioResult.value.status}`;
|
|
143
|
+
// Get the raw text from the proxy manager's response
|
|
144
|
+
if (typeof portfolioResult.value.text === 'function') {
|
|
145
|
+
rawErrorText = await portfolioResult.value.text();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
42
148
|
|
|
43
|
-
|
|
149
|
+
logger.log('WARN', `[UPDATE] Failed to fetch PORTFOLIO for user ${userId}.`, {
|
|
150
|
+
error: errorMsg,
|
|
151
|
+
proxyResponse: rawErrorText // This will contain "urlfetch" or other quota messages
|
|
152
|
+
});
|
|
153
|
+
// --- END FIX ---
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- Process History Result ---
|
|
157
|
+
if (historyResult.status === 'fulfilled' && historyResult.value.ok) {
|
|
158
|
+
const historyData = await historyResult.value.json();
|
|
159
|
+
wasHistorySuccess = true;
|
|
160
|
+
// Add to history batch
|
|
161
|
+
console.log('DEBUG', `[UPDATE] SUCCESS Adding history data for user ${userId} to batch.`);
|
|
162
|
+
await batchManager.addToTradingHistoryBatch(userId, portfolioStorageBlockId, today, historyData, userType);
|
|
163
|
+
} else {
|
|
164
|
+
// --- FIX: ENHANCED LOGGING ---
|
|
165
|
+
let errorMsg = 'Unknown fetch error';
|
|
166
|
+
let rawErrorText = 'N/A';
|
|
167
|
+
|
|
168
|
+
if (historyResult.status === 'rejected') {
|
|
169
|
+
errorMsg = historyResult.reason.message;
|
|
170
|
+
} else if (historyResult.value) {
|
|
171
|
+
errorMsg = historyResult.value.error?.message || `API status ${historyResult.value.status}`;
|
|
172
|
+
// Get the raw text from the proxy manager's response
|
|
173
|
+
if (typeof historyResult.value.text === 'function') {
|
|
174
|
+
rawErrorText = await historyResult.value.text();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
logger.log('WARN', `[UPDATE] Failed to fetch HISTORY for user ${userId} (${username}).`, {
|
|
179
|
+
error: errorMsg,
|
|
180
|
+
proxyResponse: rawErrorText // This will contain "urlfetch" or other quota messages
|
|
181
|
+
});
|
|
182
|
+
// --- END FIX ---
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- Handle Private User (if detected) ---
|
|
186
|
+
if (isPrivate) {
|
|
187
|
+
logger.log('WARN', `User ${userId} is private. Removing from future updates and decrementing block count.`);
|
|
44
188
|
batchManager.deleteFromTimestampBatch(userId, userType, instrumentId);
|
|
45
189
|
|
|
46
|
-
// <<< START FIX for private user blockId format >>>
|
|
47
|
-
// The blockId format for Speculator Blocks is numeric (e.g., "1000000")
|
|
48
190
|
let blockId;
|
|
49
191
|
let incrementField;
|
|
50
192
|
|
|
@@ -52,68 +194,42 @@ async function handleUpdate(task, taskId, dependencies, config) {
|
|
|
52
194
|
blockId = String(Math.floor(parseInt(userId) / 1000000) * 1000000);
|
|
53
195
|
incrementField = `counts.${instrumentId}_${blockId}`;
|
|
54
196
|
} 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
197
|
blockId = String(Math.floor(parseInt(userId) / 1000000) * 1000000);
|
|
62
|
-
// <<< END FIX for Normal User Block ID >>>
|
|
63
|
-
|
|
64
198
|
incrementField = `counts.${blockId}`;
|
|
65
199
|
}
|
|
66
200
|
|
|
67
|
-
// Use db from dependencies
|
|
68
201
|
const blockCountsRef = db.doc(userType === 'speculator'
|
|
69
202
|
? config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS
|
|
70
203
|
: config.FIRESTORE_DOC_BLOCK_COUNTS);
|
|
71
|
-
// <<< END FIX for private user blockId format >>>
|
|
72
204
|
|
|
73
205
|
// This is a single, immediate write, which is fine for this rare case.
|
|
206
|
+
// We use .set here because batchManager.flushBatches() might not run if this is the only user.
|
|
207
|
+
// To be safer, this should also be added to a batch, but this is an edge case.
|
|
74
208
|
await blockCountsRef.set({ [incrementField]: FieldValue.increment(-1) }, { merge: true });
|
|
75
|
-
|
|
76
209
|
return;
|
|
77
210
|
}
|
|
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
211
|
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
212
|
+
// --- If either fetch was successful, update timestamps ---
|
|
213
|
+
if (wasPortfolioSuccess || wasHistorySuccess) {
|
|
214
|
+
// This call is correct for 'normal' users.
|
|
215
|
+
await batchManager.updateUserTimestamp(userId, userType, instrumentId);
|
|
103
216
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
logger.log('INFO', `[UPDATE] Speculator timestamp fix for user ${userId} queued.`);
|
|
217
|
+
// This is the fix for speculator timestamps
|
|
218
|
+
if (userType === 'speculator') {
|
|
219
|
+
const orchestratorBlockId = String(Math.floor(parseInt(userId) / 1000000) * 1000000);
|
|
220
|
+
logger.log('INFO', `[UPDATE] Applying speculator timestamp fix for user ${userId} in block ${orchestratorBlockId}`);
|
|
221
|
+
await batchManager.addSpeculatorTimestampFix(userId, orchestratorBlockId);
|
|
222
|
+
}
|
|
111
223
|
}
|
|
112
|
-
|
|
113
|
-
|
|
224
|
+
|
|
114
225
|
} finally {
|
|
115
|
-
|
|
226
|
+
// Update performance for both headers used
|
|
227
|
+
if (portfolioHeader) headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess);
|
|
228
|
+
if (historyHeader) headerManager.updatePerformance(historyHeader.id, wasHistorySuccess);
|
|
116
229
|
}
|
|
117
230
|
}
|
|
118
231
|
|
|
119
|
-
module.exports = {
|
|
232
|
+
module.exports = {
|
|
233
|
+
handleUpdate,
|
|
234
|
+
lookupUsernames // <-- EXPORT NEW HELPER
|
|
235
|
+
};
|
|
@@ -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());
|