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.
- package/functions/computation-system/helpers/computation_pass_runner.js +20 -773
- package/functions/computation-system/helpers/orchestration_helpers.js +88 -867
- package/functions/computation-system/utils/data_loader.js +84 -151
- package/functions/computation-system/utils/utils.js +55 -98
- package/functions/orchestrator/helpers/discovery_helpers.js +40 -188
- package/functions/orchestrator/helpers/update_helpers.js +21 -61
- package/functions/orchestrator/index.js +42 -121
- package/functions/task-engine/handler_creator.js +22 -143
- package/functions/task-engine/helpers/discover_helpers.js +20 -90
- package/functions/task-engine/helpers/update_helpers.js +90 -185
- package/functions/task-engine/helpers/verify_helpers.js +43 -159
- package/functions/task-engine/utils/firestore_batch_manager.js +97 -290
- package/functions/task-engine/utils/task_engine_utils.js +99 -0
- package/package.json +1 -1
- package/functions/task-engine/utils/api_calls.js +0 -0
- package/functions/task-engine/utils/firestore_ops.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* FILENAME: CloudFunctions/NpmWrappers/bulltrackers-module/functions/task-engine/helpers/update_helpers.js
|
|
3
|
-
* (
|
|
3
|
+
* (MODIFIED: To conditionally fetch history API once per user per batch)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -9,227 +9,132 @@
|
|
|
9
9
|
* OPTIMIZED: Removed immediate batch commit for speculator timestamp fix.
|
|
10
10
|
* --- MODIFIED: Fetches portfolio AND trade history in parallel. ---
|
|
11
11
|
* --- MODIFIED: Includes helper to look up usernames from CIDs. ---
|
|
12
|
+
* --- MODIFIED: Conditionally fetches history only once per user per batch. ---
|
|
12
13
|
*/
|
|
13
14
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
16
|
+
async function lookupUsernames(cids, { logger, headerManager, proxyManager }, { USERNAME_LOOKUP_BATCH_SIZE, ETORO_API_RANKINGS_URL }) {
|
|
17
|
+
if (!cids?.length) return [];
|
|
31
18
|
logger.log('INFO', `[lookupUsernames] Looking up usernames for ${cids.length} CIDs.`);
|
|
32
|
-
const
|
|
33
|
-
|
|
19
|
+
const allUsers = [];
|
|
34
20
|
for (let i = 0; i < cids.length; i += USERNAME_LOOKUP_BATCH_SIZE) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (!selectedHeader) {
|
|
40
|
-
logger.log('ERROR', '[lookupUsernames] Could not select a header.');
|
|
41
|
-
continue; // Skip this batch
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let wasSuccess = false;
|
|
21
|
+
const batch = cids.slice(i, i + USERNAME_LOOKUP_BATCH_SIZE).map(Number);
|
|
22
|
+
const header = await headerManager.selectHeader();
|
|
23
|
+
if (!header) { logger.log('ERROR', '[lookupUsernames] Could not select a header.'); continue; }
|
|
24
|
+
let success = false;
|
|
45
25
|
try {
|
|
46
|
-
const
|
|
47
|
-
method: 'POST',
|
|
48
|
-
headers: { ...selectedHeader.header, 'Content-Type': 'application/json' },
|
|
49
|
-
body: JSON.stringify(batchCids),
|
|
26
|
+
const res = await proxyManager.fetch(`${ETORO_API_RANKINGS_URL}?Period=LastTwoYears`, {
|
|
27
|
+
method: 'POST', headers: { ...header.header, 'Content-Type': 'application/json' }, body: JSON.stringify(batch)
|
|
50
28
|
});
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
}
|
|
29
|
+
if (!res.ok) throw new Error(`API status ${res.status}`);
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
if (Array.isArray(data)) allUsers.push(...data);
|
|
32
|
+
success = true;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
logger.log('WARN', `[lookupUsernames] Failed batch`, { error: err.message });
|
|
35
|
+
} finally { headerManager.updatePerformance(header.id, success); }
|
|
68
36
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return allPublicUsers;
|
|
37
|
+
logger.log('INFO', `[lookupUsernames] Found ${allUsers.length} public users out of ${cids.length}.`);
|
|
38
|
+
return allUsers;
|
|
72
39
|
}
|
|
73
40
|
|
|
41
|
+
// --- START MODIFICATION: Added historyFetchedForUser argument ---
|
|
42
|
+
async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager }, config, username, historyFetchedForUser) {
|
|
43
|
+
// --- END MODIFICATION ---
|
|
74
44
|
|
|
75
|
-
/**
|
|
76
|
-
* Sub-pipe: pipe.taskEngine.handleUpdate
|
|
77
|
-
* @param {object} task The Pub/Sub task payload.
|
|
78
|
-
* @param {string} taskId A unique ID for logging.
|
|
79
|
-
* @param {object} dependencies - Contains db, pubsub, logger, headerManager, proxyManager, batchManager.
|
|
80
|
-
* @param {object} config The configuration object.
|
|
81
|
-
* @param {string} username - The user's eToro username.
|
|
82
|
-
*/
|
|
83
|
-
async function handleUpdate(task, taskId, dependencies, config, username) {
|
|
84
|
-
const { logger, headerManager, proxyManager, db, batchManager } = dependencies;
|
|
85
45
|
const { userId, instrumentId, userType } = task;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const portfolioUrl = userType === 'speculator'
|
|
89
|
-
? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instrumentId}`
|
|
46
|
+
const portfolioUrl = userType === 'speculator'
|
|
47
|
+
? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instrumentId}`
|
|
90
48
|
: `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
|
|
91
|
-
|
|
92
|
-
const historyUrl = `${config.ETORO_API_USERSTATS_URL}${username}/trades/oneYearAgo?CopyAsAsset=true`;
|
|
93
49
|
|
|
50
|
+
// --- MODIFICATION: Moved historyUrl definition inside conditional ---
|
|
94
51
|
const today = new Date().toISOString().slice(0, 10);
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
let portfolioHeader = null;
|
|
98
|
-
let historyHeader = null;
|
|
99
|
-
let wasPortfolioSuccess = false;
|
|
100
|
-
let wasHistorySuccess = false;
|
|
101
|
-
let isPrivate = false;
|
|
52
|
+
const portfolioBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
|
|
102
53
|
|
|
103
|
-
|
|
104
|
-
|
|
54
|
+
// --- MODIFICATION: Select history header only if needed ---
|
|
55
|
+
let portfolioHeader = await headerManager.selectHeader();
|
|
56
|
+
let historyHeader = null; // Will be selected if needed
|
|
57
|
+
if (!portfolioHeader) throw new Error("Could not select portfolio header.");
|
|
105
58
|
|
|
106
|
-
|
|
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.");
|
|
111
|
-
}
|
|
59
|
+
let wasPortfolioSuccess = false, wasHistorySuccess = false, isPrivate = false;
|
|
112
60
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
if (
|
|
125
|
-
|
|
61
|
+
try {
|
|
62
|
+
// --- START MODIFICATION: Build promises conditionally ---
|
|
63
|
+
const promisesToRun = [];
|
|
64
|
+
|
|
65
|
+
// 1. Always fetch portfolio
|
|
66
|
+
promisesToRun.push(proxyManager.fetch(portfolioUrl, { headers: portfolioHeader.header }));
|
|
67
|
+
|
|
68
|
+
// 2. Conditionally fetch history
|
|
69
|
+
let fetchHistory = false;
|
|
70
|
+
if (!historyFetchedForUser.has(userId)) {
|
|
71
|
+
historyHeader = await headerManager.selectHeader(); // Select header just-in-time
|
|
72
|
+
if (!historyHeader) {
|
|
73
|
+
logger.log('WARN', `[UPDATE] Could not select history header for ${userId}, skipping history fetch for this batch.`);
|
|
74
|
+
historyFetchedForUser.add(userId); // Add to set to prevent retries in this batch
|
|
126
75
|
} else {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
await batchManager.addToPortfolioBatch(userId, portfolioStorageBlockId, today, portfolioData, userType, instrumentId);
|
|
132
|
-
|
|
76
|
+
fetchHistory = true;
|
|
77
|
+
historyFetchedForUser.add(userId); // Mark as fetched for this batch
|
|
78
|
+
const historyUrl = `${config.ETORO_API_USERSTATS_URL}${username}/trades/oneYearAgo?CopyAsAsset=true`;
|
|
79
|
+
promisesToRun.push(proxyManager.fetch(historyUrl, { headers: historyHeader.header }));
|
|
133
80
|
}
|
|
81
|
+
}
|
|
82
|
+
// --- END MODIFICATION ---
|
|
83
|
+
|
|
84
|
+
// --- Run Promises ---
|
|
85
|
+
const results = await Promise.allSettled(promisesToRun);
|
|
86
|
+
const portfolioRes = results[0];
|
|
87
|
+
const historyRes = fetchHistory ? results[1] : null; // History is only index 1 if we fetched it
|
|
88
|
+
|
|
89
|
+
// --- Process Portfolio (results[0]) ---
|
|
90
|
+
if (portfolioRes.status === 'fulfilled' && portfolioRes.value.ok) {
|
|
91
|
+
const body = await portfolioRes.value.text();
|
|
92
|
+
if (body.includes("user is PRIVATE")) isPrivate = true;
|
|
93
|
+
else { wasPortfolioSuccess = true; await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, JSON.parse(body), userType, instrumentId); }
|
|
134
94
|
} else {
|
|
135
|
-
|
|
136
|
-
let
|
|
137
|
-
|
|
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
|
-
}
|
|
148
|
-
|
|
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 ---
|
|
95
|
+
let errMsg = portfolioRes.status === 'rejected' ? portfolioRes.reason.message : `API status ${portfolioRes.value.status}`;
|
|
96
|
+
let rawText = portfolioRes.value?.text ? await portfolioRes.value.text() : 'N/A';
|
|
97
|
+
logger.log('WARN', `[UPDATE] Portfolio fetch failed for ${userId}`, { error: errMsg, proxyResponse: rawText });
|
|
154
98
|
}
|
|
155
99
|
|
|
156
|
-
// --- Process History
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
}
|
|
100
|
+
// --- Process History (results[1], if it exists) ---
|
|
101
|
+
if (fetchHistory && historyRes) { // Check if we ran this promise
|
|
102
|
+
if (historyRes.status === 'fulfilled' && historyRes.value.ok) {
|
|
103
|
+
const data = await historyRes.value.json();
|
|
104
|
+
wasHistorySuccess = true;
|
|
105
|
+
await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType);
|
|
106
|
+
} else {
|
|
107
|
+
// History fetch failed
|
|
108
|
+
let errMsg = historyRes.status === 'rejected' ? historyRes.reason.message : `API status ${historyRes.value.status}`;
|
|
109
|
+
let rawText = historyRes.value?.text ? await historyRes.value.text() : 'N/A';
|
|
110
|
+
logger.log('WARN', `[UPDATE] History fetch failed for ${userId} (${username})`, { error: errMsg, proxyResponse: rawText });
|
|
176
111
|
}
|
|
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
112
|
}
|
|
113
|
+
// --- END MODIFICATION ---
|
|
184
114
|
|
|
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.`);
|
|
188
|
-
batchManager.deleteFromTimestampBatch(userId, userType, instrumentId);
|
|
189
115
|
|
|
190
|
-
|
|
191
|
-
|
|
116
|
+
// --- Private user handling ---
|
|
117
|
+
if (isPrivate) {
|
|
118
|
+
logger.log('WARN', `User ${userId} is private. Removing from updates.`);
|
|
119
|
+
await batchManager.deleteFromTimestampBatch(userId, userType, instrumentId);
|
|
192
120
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
incrementField = `counts.${instrumentId}_${blockId}`;
|
|
196
|
-
} else {
|
|
197
|
-
blockId = String(Math.floor(parseInt(userId) / 1000000) * 1000000);
|
|
198
|
-
incrementField = `counts.${blockId}`;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const blockCountsRef = db.doc(userType === 'speculator'
|
|
202
|
-
? config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS
|
|
203
|
-
: config.FIRESTORE_DOC_BLOCK_COUNTS);
|
|
204
|
-
|
|
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.
|
|
121
|
+
const blockCountsRef = db.doc(userType === 'speculator' ? config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS : config.FIRESTORE_DOC_BLOCK_COUNTS);
|
|
122
|
+
const incrementField = userType === 'speculator' ? `counts.${instrumentId}_${Math.floor(userId/1e6)*1e6}` : `counts.${Math.floor(userId/1e6)*1e6}`;
|
|
208
123
|
await blockCountsRef.set({ [incrementField]: FieldValue.increment(-1) }, { merge: true });
|
|
209
124
|
return;
|
|
210
125
|
}
|
|
211
126
|
|
|
212
|
-
// --- If either fetch was successful, update timestamps ---
|
|
213
127
|
if (wasPortfolioSuccess || wasHistorySuccess) {
|
|
214
|
-
// This call is correct for 'normal' users.
|
|
215
128
|
await batchManager.updateUserTimestamp(userId, userType, instrumentId);
|
|
216
|
-
|
|
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
|
-
}
|
|
129
|
+
if (userType === 'speculator') await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6));
|
|
223
130
|
}
|
|
224
|
-
|
|
225
131
|
} finally {
|
|
226
|
-
// Update performance for both headers used
|
|
227
132
|
if (portfolioHeader) headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess);
|
|
228
|
-
|
|
133
|
+
|
|
134
|
+
// --- MODIFICATION: Only update history header if we used it ---
|
|
135
|
+
if (fetchHistory && historyHeader) headerManager.updatePerformance(historyHeader.id, wasHistorySuccess);
|
|
136
|
+
// --- END MODIFICATION ---
|
|
229
137
|
}
|
|
230
138
|
}
|
|
231
139
|
|
|
232
|
-
module.exports = {
|
|
233
|
-
handleUpdate,
|
|
234
|
-
lookupUsernames // <-- EXPORT NEW HELPER
|
|
235
|
-
};
|
|
140
|
+
module.exports = { handleUpdate, lookupUsernames };
|
|
@@ -1,188 +1,72 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Sub-pipe: pipe.taskEngine.handleVerify
|
|
3
|
-
* REFACTORED: Now stateless and receives dependencies.
|
|
4
|
-
* OPTIMIZED: Fetches all user portfolios in parallel to reduce function runtime.
|
|
5
|
-
* --- MODIFIED: Saves username to cid_username_map collection. ---
|
|
6
|
-
*/
|
|
7
1
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
8
2
|
|
|
9
|
-
|
|
10
|
-
* Internal helper to fetch and process a single user's portfolio.
|
|
11
|
-
* @param {object} user - The user object { cid, isBronze, username }
|
|
12
|
-
* @param {object} dependencies - Contains proxyManager, headerManager
|
|
13
|
-
* @param {object} config - The configuration object
|
|
14
|
-
* @param {Set<number>} speculatorInstrumentSet - Pre-built Set of instrument IDs
|
|
15
|
-
* @returns {Promise<object|null>} A result object for batching, or null on failure.
|
|
16
|
-
*/
|
|
17
|
-
async function fetchAndVerifyUser(user, dependencies, config, speculatorInstrumentSet) {
|
|
18
|
-
const { logger, headerManager, proxyManager } = dependencies;
|
|
19
|
-
const userId = user.cid;
|
|
20
|
-
|
|
21
|
-
const portfolioUrl = `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
|
|
3
|
+
async function fetchAndVerifyUser(user, { logger, headerManager, proxyManager }, { userType, SPECULATOR_INSTRUMENTS_ARRAY }) {
|
|
22
4
|
const selectedHeader = await headerManager.selectHeader();
|
|
23
|
-
if (!selectedHeader) return null;
|
|
5
|
+
if (!selectedHeader) return null;
|
|
24
6
|
|
|
25
7
|
let wasSuccess = false;
|
|
26
8
|
try {
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
29
|
-
wasSuccess = false;
|
|
30
|
-
return null; // API error or private user
|
|
31
|
-
}
|
|
32
|
-
wasSuccess = true;
|
|
33
|
-
|
|
34
|
-
const portfolioData = await response.json();
|
|
35
|
-
|
|
36
|
-
// Process verification logic *within* the parallel task
|
|
37
|
-
if (config.userType === 'speculator') {
|
|
38
|
-
const matchingInstruments = portfolioData.AggregatedPositions
|
|
39
|
-
.map(p => p.InstrumentID)
|
|
40
|
-
.filter(id => speculatorInstrumentSet.has(id));
|
|
9
|
+
const res = await proxyManager.fetch(`${process.env.ETORO_API_PORTFOLIO_URL}?cid=${user.cid}`, { headers: selectedHeader.header });
|
|
10
|
+
if (!res.ok) return null;
|
|
41
11
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
username: user.username, // <-- PASS USERNAME
|
|
50
|
-
updateData: {
|
|
51
|
-
instruments: matchingInstruments,
|
|
52
|
-
lastVerified: new Date(),
|
|
53
|
-
lastHeldSpeculatorAsset: new Date()
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
} else {
|
|
57
|
-
logger.log('INFO', `[VERIFY] Speculator user ${userId} does not hold any speculator assets.`);
|
|
58
|
-
return null; // Valid fetch, but failed verification
|
|
59
|
-
}
|
|
60
|
-
} else { // Normal user verification
|
|
61
|
-
// Return data needed for the normal user batch update
|
|
62
|
-
return {
|
|
63
|
-
type: 'normal',
|
|
64
|
-
userId: userId,
|
|
65
|
-
isBronze: user.isBronze,
|
|
66
|
-
username: user.username, // <-- PASS USERNAME
|
|
67
|
-
updateData: {
|
|
68
|
-
lastVerified: new Date()
|
|
69
|
-
}
|
|
70
|
-
};
|
|
12
|
+
wasSuccess = true;
|
|
13
|
+
const portfolioData = await res.json();
|
|
14
|
+
if (userType === 'speculator') {
|
|
15
|
+
const instruments = portfolioData.AggregatedPositions.map(p => p.InstrumentID)
|
|
16
|
+
.filter(id => SPECULATOR_INSTRUMENTS_ARRAY.includes(id));
|
|
17
|
+
if (!instruments.length) return null;
|
|
18
|
+
return { type: 'speculator', userId: user.cid, isBronze: user.isBronze, username: user.username, updateData: { instruments, lastVerified: new Date(), lastHeldSpeculatorAsset: new Date() } };
|
|
71
19
|
}
|
|
72
|
-
|
|
73
|
-
|
|
20
|
+
return { type: 'normal', userId: user.cid, isBronze: user.isBronze, username: user.username, updateData: { lastVerified: new Date() } };
|
|
21
|
+
} catch (err) {
|
|
22
|
+
logger.log('WARN', `[VERIFY] Error processing user ${user.cid}`, { errorMessage: err.message });
|
|
74
23
|
return null;
|
|
75
24
|
} finally {
|
|
76
25
|
if (selectedHeader) headerManager.updatePerformance(selectedHeader.id, wasSuccess);
|
|
77
26
|
}
|
|
78
27
|
}
|
|
79
28
|
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Sub-pipe: pipe.taskEngine.handleVerify
|
|
83
|
-
* @param {object} task The Pub/Sub task payload.
|
|
84
|
-
* @param {string} taskId A unique ID for logging.
|
|
85
|
-
* @param {object} dependencies - Contains db, pubsub, logger, headerManager, proxyManager, batchManager.
|
|
86
|
-
* @param {object} config The configuration object.
|
|
87
|
-
*/
|
|
88
|
-
async function handleVerify(task, taskId, dependencies, config) {
|
|
89
|
-
const { logger, db } = dependencies;
|
|
29
|
+
async function handleVerify(task, taskId, { db, logger, ...dependencies }, config) {
|
|
90
30
|
const { users, blockId, instrument, userType } = task;
|
|
91
|
-
|
|
92
|
-
// Use db from dependencies
|
|
93
31
|
const batch = db.batch();
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
const normalUserUpdates = {};
|
|
97
|
-
const bronzeStateUpdates = {}; // This can be one object
|
|
98
|
-
|
|
99
|
-
// --- NEW: Object to store username updates ---
|
|
100
|
-
const usernameMapUpdates = {};
|
|
101
|
-
// --- END NEW ---
|
|
102
|
-
|
|
103
|
-
const speculatorInstrumentSet = new Set(config.SPECULATOR_INSTRUMENTS_ARRAY);
|
|
104
|
-
|
|
105
|
-
// --- OPTIMIZATION: Run all user fetches in parallel ---
|
|
106
|
-
const verificationPromises = users.map(user =>
|
|
107
|
-
fetchAndVerifyUser(
|
|
108
|
-
user,
|
|
109
|
-
dependencies,
|
|
110
|
-
{ ...config, userType: userType }, // Pass userType into the helper config
|
|
111
|
-
speculatorInstrumentSet
|
|
112
|
-
)
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
const results = await Promise.allSettled(verificationPromises);
|
|
116
|
-
// --- END OPTIMIZATION ---
|
|
117
|
-
|
|
118
|
-
// Process results (this is fast, no I/O)
|
|
119
|
-
results.forEach(result => {
|
|
120
|
-
if (result.status === 'rejected' || !result.value) {
|
|
121
|
-
// Log rejection reason if needed: result.reason
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
32
|
+
const speculatorUpdates = {}, normalUpdates = {}, bronzeStates = {}, usernameMap = {};
|
|
33
|
+
const specSet = new Set(config.SPECULATOR_INSTRUMENTS_ARRAY);
|
|
124
34
|
|
|
125
|
-
|
|
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 ---
|
|
35
|
+
const results = await Promise.allSettled(users.map(u => fetchAndVerifyUser(u, dependencies, { ...config, userType })));
|
|
133
36
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
bronzeStateUpdates[data.userId] = data.isBronze;
|
|
37
|
+
let validUserCount = 0;
|
|
38
|
+
results.forEach(r => {
|
|
39
|
+
if (r.status === 'fulfilled' && r.value) {
|
|
40
|
+
const d = r.value;
|
|
41
|
+
usernameMap[d.userId] = { username: d.username };
|
|
42
|
+
bronzeStates[d.userId] = d.isBronze;
|
|
141
43
|
validUserCount++;
|
|
44
|
+
if (d.type === 'speculator') speculatorUpdates[`users.${d.userId}`] = d.updateData;
|
|
45
|
+
else normalUpdates[`users.${d.userId}`] = d.updateData;
|
|
142
46
|
}
|
|
143
47
|
});
|
|
144
48
|
|
|
49
|
+
if (Object.keys(speculatorUpdates).length || Object.keys(normalUpdates).length) {
|
|
50
|
+
const blockRef = db.collection(userType === 'speculator' ? config.FIRESTORE_COLLECTION_SPECULATOR_BLOCKS : config.FIRESTORE_COLLECTION_NORMAL_PORTFOLIOS).doc(String(blockId));
|
|
51
|
+
batch.set(blockRef, userType === 'speculator' ? speculatorUpdates : normalUpdates, { merge: true });
|
|
52
|
+
const bronzeRef = db.collection(userType === 'speculator' ? config.FIRESTORE_COLLECTION_BRONZE_SPECULATORS : config.FIRESTORE_COLLECTION_BRONZE_NORMAL).doc(String(blockId));
|
|
53
|
+
batch.set(bronzeRef, bronzeStates, { merge: true });
|
|
145
54
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
batch.set(
|
|
150
|
-
const bronzeStateRef = db.collection(config.FIRESTORE_COLLECTION_BRONZE_SPECULATORS).doc(String(blockId));
|
|
151
|
-
batch.set(bronzeStateRef, bronzeStateUpdates, {merge: true});
|
|
152
|
-
|
|
153
|
-
} else {
|
|
154
|
-
const normalBlockRef = db.collection(config.FIRESTORE_COLLECTION_NORMAL_PORTFOLIOS).doc(String(blockId));
|
|
155
|
-
batch.set(normalBlockRef, normalUserUpdates, { merge: true });
|
|
156
|
-
const bronzeStateRef = db.collection(config.FIRESTORE_COLLECTION_BRONZE_NORMAL).doc(String(blockId));
|
|
157
|
-
batch.set(bronzeStateRef, bronzeStateUpdates, {merge: true});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (validUserCount > 0) {
|
|
161
|
-
const blockCountsRef = db.doc(userType === 'speculator'
|
|
162
|
-
? config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS
|
|
163
|
-
: config.FIRESTORE_DOC_BLOCK_COUNTS);
|
|
164
|
-
|
|
165
|
-
const incrementField = userType === 'speculator' ? `counts.${instrument}_${blockId}` : `counts.${blockId}`;
|
|
166
|
-
batch.set(blockCountsRef, { [incrementField]: FieldValue.increment(validUserCount) }, { merge: true });
|
|
55
|
+
if (validUserCount) {
|
|
56
|
+
const countsRef = db.doc(userType === 'speculator' ? config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS : config.FIRESTORE_DOC_BLOCK_COUNTS);
|
|
57
|
+
const field = userType === 'speculator' ? `counts.${instrument}_${blockId}` : `counts.${blockId}`;
|
|
58
|
+
batch.set(countsRef, { [field]: FieldValue.increment(validUserCount) }, { merge: true });
|
|
167
59
|
}
|
|
168
60
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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.`);
|
|
61
|
+
|
|
62
|
+
if (Object.keys(usernameMap).length) {
|
|
63
|
+
const mapRef = db.collection(config.FIRESTORE_COLLECTION_USERNAME_MAP).doc('cid_map_shard_1');
|
|
64
|
+
batch.set(mapRef, usernameMap, { merge: true });
|
|
65
|
+
logger.log('INFO', `[VERIFY] Staging ${Object.keys(usernameMap).length} username updates.`);
|
|
179
66
|
}
|
|
180
|
-
|
|
181
|
-
|
|
67
|
+
|
|
182
68
|
await batch.commit();
|
|
183
|
-
if(validUserCount
|
|
184
|
-
logger.log('INFO', `[VERIFY] Verified and stored ${validUserCount} new ${userType} users.`);
|
|
185
|
-
}
|
|
69
|
+
if (validUserCount) logger.log('INFO', `[VERIFY] Verified and stored ${validUserCount} new ${userType} users.`);
|
|
186
70
|
}
|
|
187
71
|
|
|
188
|
-
module.exports = { handleVerify };
|
|
72
|
+
module.exports = { handleVerify };
|