bulltrackers-module 1.0.153 → 1.0.154
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,8 @@
|
|
|
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
|
+
* (MODIFIED: `lookupUsernames` runs batches in parallel)
|
|
5
|
+
* (MODIFIED: `handleUpdate` fetches history and all portfolios in parallel)
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
/**
|
|
@@ -12,70 +14,108 @@
|
|
|
12
14
|
* --- MODIFIED: Conditionally fetches history only once per user per batch. ---
|
|
13
15
|
*/
|
|
14
16
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
17
|
+
const pLimit = require('p-limit'); // <--- IMPORT p-limit
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
/**
|
|
20
|
+
* (MODIFIED: Runs lookup batches in parallel)
|
|
21
|
+
*/
|
|
22
|
+
async function lookupUsernames(cids, { logger, headerManager, proxyManager }, config) {
|
|
17
23
|
if (!cids?.length) return [];
|
|
18
24
|
logger.log('INFO', `[lookupUsernames] Looking up usernames for ${cids.length} CIDs.`);
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
const limit = pLimit(config.USERNAME_LOOKUP_CONCURRENCY || 5);
|
|
26
|
+
const { USERNAME_LOOKUP_BATCH_SIZE, ETORO_API_RANKINGS_URL } = config;
|
|
27
|
+
const batches = [];
|
|
28
|
+
for (let i = 0; i < cids.length; i += USERNAME_LOOKUP_BATCH_SIZE) { batches.push(cids.slice(i, i + USERNAME_LOOKUP_BATCH_SIZE).map(Number)); }
|
|
29
|
+
|
|
30
|
+
const batchPromises = batches.map(batch => limit(async () => {
|
|
22
31
|
const header = await headerManager.selectHeader();
|
|
23
|
-
if (!header) { logger.log('ERROR', '[lookupUsernames] Could not select a header.');
|
|
32
|
+
if (!header) { logger.log('ERROR', '[lookupUsernames] Could not select a header.'); return null; }
|
|
24
33
|
let success = false;
|
|
25
34
|
try {
|
|
26
35
|
const res = await proxyManager.fetch(`${ETORO_API_RANKINGS_URL}?Period=LastTwoYears`, { method: 'POST', headers: { ...header.header, 'Content-Type': 'application/json' }, body: JSON.stringify(batch) });
|
|
27
36
|
if (!res.ok) throw new Error(`API status ${res.status}`);
|
|
28
37
|
const data = await res.json();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
} finally { headerManager.updatePerformance(header.id, success); }
|
|
34
|
-
|
|
38
|
+
success = true;
|
|
39
|
+
logger.log('DEBUG', 'Looked up usernames', { batch: batch.slice(0, 5) });
|
|
40
|
+
return data;
|
|
41
|
+
} catch (err) { logger.log('WARN', `[lookupUsernames] Failed batch`, { error: err.message }); return null;
|
|
42
|
+
} finally { headerManager.updatePerformance(header.id, success); } }));
|
|
43
|
+
const results = await Promise.allSettled(batchPromises);
|
|
44
|
+
const allUsers = results.filter(r => r.status === 'fulfilled' && r.value && Array.isArray(r.value)).flatMap(r => r.value);
|
|
35
45
|
logger.log('INFO', `[lookupUsernames] Found ${allUsers.length} public users out of ${cids.length}.`);
|
|
36
46
|
return allUsers;
|
|
37
47
|
}
|
|
38
48
|
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* (MODIFIED: Fetches history and all portfolios in parallel)
|
|
52
|
+
*/
|
|
39
53
|
async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager }, config, username, historyFetchedForUser) {
|
|
40
54
|
const { userId, instruments, instrumentId, userType } = task;
|
|
41
55
|
const instrumentsToProcess = userType === 'speculator' ? (instruments || [instrumentId]) : [undefined];
|
|
42
56
|
const today = new Date().toISOString().slice(0, 10);
|
|
43
57
|
const portfolioBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
|
|
44
|
-
|
|
58
|
+
|
|
45
59
|
let historyHeader = null;
|
|
46
|
-
|
|
47
|
-
let
|
|
48
|
-
let
|
|
60
|
+
let wasHistorySuccess = false;
|
|
61
|
+
let historyFetchPromise = null;
|
|
62
|
+
let isPrivate = false;
|
|
63
|
+
|
|
49
64
|
try {
|
|
50
|
-
|
|
65
|
+
// --- 1. Prepare History Fetch (if needed) ---
|
|
51
66
|
if (!historyFetchedForUser.has(userId)) {
|
|
52
67
|
historyHeader = await headerManager.selectHeader();
|
|
53
|
-
if (historyHeader) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
if (historyHeader) { historyFetchedForUser.add(userId);
|
|
69
|
+
const historyUrl = `${config.ETORO_API_USERSTATS_URL}${username}/trades/oneYearAgo?CopyAsAsset=true`;
|
|
70
|
+
historyFetchPromise = proxyManager.fetch(historyUrl, { headers: historyHeader.header }); } }
|
|
71
|
+
|
|
72
|
+
// --- 2. Prepare All Portfolio Fetches ---
|
|
73
|
+
const portfolioRequests = [];
|
|
74
|
+
for (const instId of instrumentsToProcess) {
|
|
75
|
+
const portfolioHeader = await headerManager.selectHeader();
|
|
76
|
+
if (!portfolioHeader) throw new Error(`Could not select portfolio header for ${userId}`);
|
|
77
|
+
const portfolioUrl = userType === 'speculator' ? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instId}` : `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
|
|
78
|
+
portfolioRequests.push({ instrumentId: instId, url: portfolioUrl, header: portfolioHeader, promise: proxyManager.fetch(portfolioUrl, { headers: portfolioHeader.header }) }); }
|
|
79
|
+
|
|
80
|
+
// --- 3. Execute All API Calls in Parallel ---
|
|
81
|
+
const allPromises = [ ...(historyFetchPromise ? [historyFetchPromise] : []), ...portfolioRequests.map(r => r.promise) ];
|
|
82
|
+
const allResults = await Promise.allSettled(allPromises);
|
|
83
|
+
|
|
84
|
+
// --- 4. Process History Result ---
|
|
85
|
+
let resultIndex = 0;
|
|
86
|
+
if (historyFetchPromise) {
|
|
87
|
+
const historyRes = allResults[resultIndex++];
|
|
57
88
|
if (historyRes.status === 'fulfilled' && historyRes.value.ok) { const data = await historyRes.value.json(); wasHistorySuccess = true; await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType); } }
|
|
58
|
-
|
|
59
|
-
|
|
89
|
+
|
|
90
|
+
// --- 5. Process Portfolio Results ---
|
|
91
|
+
for (let i = 0; i < portfolioRequests.length; i++) {
|
|
92
|
+
const requestInfo = portfolioRequests[i];
|
|
93
|
+
const portfolioRes = allResults[resultIndex++];
|
|
60
94
|
let wasPortfolioSuccess = false;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
headerManager.updatePerformance(
|
|
68
|
-
|
|
69
|
-
|
|
95
|
+
if (portfolioRes.status === 'fulfilled' && portfolioRes.value.ok) {
|
|
96
|
+
const body = await portfolioRes.value.text();
|
|
97
|
+
if (body.includes("user is PRIVATE")) { isPrivate = true; logger.log('WARN', `User ${userId} is private. Removing from updates.`); break;
|
|
98
|
+
} else { wasPortfolioSuccess = true; await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, JSON.parse(body), userType, requestInfo.instrumentId); }
|
|
99
|
+
logger.log('DEBUG', 'Processing portfolio for user', { userId, portfolioUrl: requestInfo.url });
|
|
100
|
+
} else { logger.log('WARN', `Failed to fetch portfolio`, { userId, url: requestInfo.url, error: portfolioRes.reason || `status ${portfolioRes.value?.status}` }); }
|
|
101
|
+
headerManager.updatePerformance(requestInfo.header.id, wasPortfolioSuccess);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- 6. Handle Private Users & Timestamps ---
|
|
70
105
|
if (isPrivate) {
|
|
71
106
|
logger.log('WARN', `User ${userId} is private. Removing from updates.`);
|
|
72
107
|
for (const instrumentId of instrumentsToProcess) { await batchManager.deleteFromTimestampBatch(userId, userType, instrumentId); }
|
|
73
108
|
const blockCountsRef = db.doc(config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS);
|
|
74
|
-
for (const instrumentId of instrumentsToProcess) {
|
|
75
|
-
|
|
109
|
+
for (const instrumentId of instrumentsToProcess) {
|
|
110
|
+
const incrementField = `counts.${instrumentId}_${Math.floor(userId/1e6)*1e6}`;
|
|
111
|
+
await blockCountsRef.set({ [incrementField]: FieldValue.increment(-1) }, { merge: true }); }
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
76
115
|
for (const instrumentId of instrumentsToProcess) { await batchManager.updateUserTimestamp(userId, userType, instrumentId); }
|
|
77
116
|
if (userType === 'speculator') { await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6)); }
|
|
78
|
-
|
|
117
|
+
|
|
118
|
+
} finally { if (historyHeader) { headerManager.updatePerformance(historyHeader.id, wasHistorySuccess); } }
|
|
79
119
|
}
|
|
80
120
|
|
|
81
121
|
module.exports = { handleUpdate, lookupUsernames };
|
|
@@ -29,7 +29,22 @@ class FirestoreBatchManager {
|
|
|
29
29
|
|
|
30
30
|
_getUsernameShardId(cid) { return `cid_map_shard_${Math.floor(parseInt(cid) / 10000) % 10}`; }
|
|
31
31
|
|
|
32
|
-
_scheduleFlush() { if (!this.batchTimeout) this.batchTimeout = setTimeout(() => this.flushBatches(), this.config.TASK_ENGINE_FLUSH_INTERVAL_MS); }
|
|
32
|
+
// _scheduleFlush() { if (!this.batchTimeout) this.batchTimeout = setTimeout(() => this.flushBatches(), this.config.TASK_ENGINE_FLUSH_INTERVAL_MS); } Old version
|
|
33
|
+
|
|
34
|
+
_scheduleFlush() {
|
|
35
|
+
const totalOps = this._estimateBatchSize();
|
|
36
|
+
if (totalOps >= 400) { this.flushBatches(); return; }
|
|
37
|
+
if (!this.batchTimeout) { this.batchTimeout = setTimeout(() => this.flushBatches(), this.config.TASK_ENGINE_FLUSH_INTERVAL_MS); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_estimateBatchSize() {
|
|
41
|
+
let ops = 0;
|
|
42
|
+
ops += Object.keys(this.portfolioBatch).length;
|
|
43
|
+
ops += Object.keys(this.tradingHistoryBatch).length;
|
|
44
|
+
ops += Object.keys(this.timestampBatch).length;
|
|
45
|
+
ops += Object.keys(this.speculatorTimestampFixBatch).length;
|
|
46
|
+
return ops;
|
|
47
|
+
}
|
|
33
48
|
|
|
34
49
|
async loadUsernameMap() {
|
|
35
50
|
if (Date.now() - this.usernameMapLastLoaded < 3600000) return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* FILENAME: CloudFunctions/NpmWrappers/bulltrackers-module/functions/task-engine/utils/task_engine_utils.js
|
|
3
3
|
* (MODIFIED: To pass down a Set to track history fetches)
|
|
4
|
+
* (MODIFIED: To run all update tasks in parallel with a concurrency limit)
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
const { handleDiscover } = require('../helpers/discover_helpers');
|
|
13
14
|
const { handleVerify } = require('../helpers/verify_helpers');
|
|
14
15
|
const { handleUpdate, lookupUsernames } = require('../helpers/update_helpers');
|
|
16
|
+
const pLimit = require('p-limit');
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Parses Pub/Sub message into task array.
|
|
@@ -60,11 +62,15 @@ async function executeTasks(tasksToRun, otherTasks, dependencies, config, taskId
|
|
|
60
62
|
if (handler) try { await handler(task, subTaskId, dependencies, config); }
|
|
61
63
|
catch (err) { logger.log('ERROR', `[TaskEngine/${taskId}] Error in ${task.type} for ${subTaskId}`, { errorMessage: err.message }); }
|
|
62
64
|
else logger.log('ERROR', `[TaskEngine/${taskId}] Unknown task type: ${task.type}`); }
|
|
63
|
-
|
|
65
|
+
const limit = pLimit(config.TASK_ENGINE_CONCURRENCY || 10);
|
|
66
|
+
let successCount = 0;
|
|
67
|
+
let errorCount = 0;
|
|
68
|
+
const updatePromises = tasksToRun.map(({ task, username }) => {
|
|
64
69
|
const subTaskId = `${task.type}-${task.userType || 'unknown'}-${task.userId}`;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
return limit(() => handleUpdate(task, subTaskId, dependencies, config, username, historyFetchedForUser) .catch(err => { logger.log('ERROR', `[TaskEngine/${taskId}] Error in handleUpdate for ${task.userId}`, { errorMessage: err.message }); throw err; }) ); });
|
|
71
|
+
const results = await Promise.allSettled(updatePromises);
|
|
72
|
+
results.forEach(result => { if (result.status === 'fulfilled') { successCount++; } else { errorCount++; } });
|
|
73
|
+
logger.log( errorCount > 0 ? 'WARN' : 'SUCCESS', `[TaskEngine/${taskId}] Processed all ${tasksToRun.length} update tasks. Success: ${successCount}, Failed: ${errorCount}.` );
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
module.exports = { parseTaskPayload, prepareTaskBatches, runUsernameLookups, executeTasks };
|