bulltrackers-module 1.0.135 → 1.0.136

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.
@@ -260,25 +260,48 @@ async function getSpeculatorsToUpdate(dependencies, config) {
260
260
  const { dateThreshold, gracePeriodThreshold, speculatorBlocksCollectionName } = config;
261
261
  logger.log('INFO','[Core Utils] Getting speculators to update...');
262
262
  const updates = [];
263
+
264
+ // ⚠️ NEW: Collect per user first
265
+ const userMap = new Map(); // userId -> { instruments: Set }
266
+
263
267
  try {
264
268
  const blocksRef = db.collection(speculatorBlocksCollectionName);
265
269
  const snapshot = await blocksRef.get();
266
- if (snapshot.empty) { logger.log('INFO','[Core Utils] No speculator blocks found.'); return []; }
270
+ if (snapshot.empty) {
271
+ logger.log('INFO','[Core Utils] No speculator blocks found.');
272
+ return [];
273
+ }
274
+
267
275
  snapshot.forEach(doc => {
268
276
  const blockData = doc.data();
269
277
  for (const key in blockData) {
270
- if (!key.startsWith('users.')) continue;
271
- const userId = key.split('.')[1];
272
- if (!userId) continue;
273
- const userData = blockData[key];
274
- const lastVerified = userData.lastVerified?.toDate ? userData.lastVerified.toDate() : new Date(0);
275
- const lastHeld = userData.lastHeldSpeculatorAsset?.toDate ? userData.lastHeldSpeculatorAsset.toDate() : new Date(0);
276
- if (lastVerified < dateThreshold && lastHeld > gracePeriodThreshold) { if (userData.instruments && Array.isArray(userData.instruments)) { userData.instruments.forEach(instrumentId => { updates.push({ userId, instrumentId }); });
277
- }
278
- }
278
+ if (!key.startsWith('users.')) continue;
279
+ const userId = key.split('.')[1];
280
+ if (!userId) continue;
281
+ const userData = blockData[key];
282
+ const lastVerified = userData.lastVerified?.toDate ? userData.lastVerified.toDate() : new Date(0);
283
+ const lastHeld = userData.lastHeldSpeculatorAsset?.toDate ? userData.lastHeldSpeculatorAsset.toDate() : new Date(0);
284
+
285
+ if (lastVerified < dateThreshold && lastHeld > gracePeriodThreshold) {
286
+ if (!userMap.has(userId)) {
287
+ userMap.set(userId, new Set());
288
+ }
289
+ if (userData.instruments && Array.isArray(userData.instruments)) {
290
+ userData.instruments.forEach(id => userMap.get(userId).add(id));
291
+ }
292
+ }
279
293
  }
280
294
  });
281
- logger.log('INFO',`[Core Utils] Found ${updates.length} speculator user/instrument pairs to update.`);
295
+
296
+ // ⚠️ NEW: Return one task per user with ALL instruments
297
+ for (const [userId, instrumentSet] of userMap) {
298
+ updates.push({
299
+ userId,
300
+ instruments: Array.from(instrumentSet) // ⚠️ Array of all instruments
301
+ });
302
+ }
303
+
304
+ logger.log('INFO',`[Core Utils] Found ${updates.length} speculator users to update (covering ${[...userMap.values()].reduce((sum, set) => sum + set.size, 0)} total instruments).`);
282
305
  return updates;
283
306
  } catch (error) {
284
307
  logger.log('ERROR','[Core Utils] Error getting speculators to update', { errorMessage: error.message });
@@ -40,100 +40,104 @@ async function lookupUsernames(cids, { logger, headerManager, proxyManager }, {
40
40
 
41
41
  // --- START MODIFICATION: Added historyFetchedForUser argument ---
42
42
  async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager }, config, username, historyFetchedForUser) {
43
- // --- END MODIFICATION ---
44
-
45
- const { userId, instrumentId, userType } = task;
46
- const portfolioUrl = userType === 'speculator'
47
- ? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instrumentId}`
48
- : `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
43
+ const { userId, instruments, instrumentId, userType } = task; // ⚠️ Now supports both
44
+
45
+ // ⚠️ Support both old (instrumentId) and new (instruments array) format
46
+ const instrumentsToProcess = userType === 'speculator'
47
+ ? (instruments || [instrumentId]) // New format or fallback to old
48
+ : [undefined]; // Normal users don't have instruments
49
49
 
50
- // --- MODIFICATION: Moved historyUrl definition inside conditional ---
51
50
  const today = new Date().toISOString().slice(0, 10);
52
51
  const portfolioBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
53
-
54
- // --- MODIFICATION: Select history header only if needed ---
52
+
55
53
  let portfolioHeader = await headerManager.selectHeader();
56
- let historyHeader = null; // Will be selected if needed
54
+ let historyHeader = null;
57
55
  if (!portfolioHeader) throw new Error("Could not select portfolio header.");
58
-
59
- let wasPortfolioSuccess = false, wasHistorySuccess = false, isPrivate = false, fetchHistory = false; // <-- FIX: 'fetchHistory' declared here
60
-
56
+
57
+ let wasHistorySuccess = false, isPrivate = false;
58
+
61
59
  try {
62
- // --- START MODIFICATION: Build promises conditionally ---
60
+ // Fetch history ONCE per user
63
61
  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; // <-- FIX: This line is removed
62
+ let fetchHistory = false;
63
+
70
64
  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
75
- } else {
76
- fetchHistory = true; // <-- FIX: Assigns to outer scope variable
77
- historyFetchedForUser.add(userId); // Mark as fetched for this batch
65
+ historyHeader = await headerManager.selectHeader();
66
+ if (historyHeader) {
67
+ fetchHistory = true;
68
+ historyFetchedForUser.add(userId);
78
69
  const historyUrl = `${config.ETORO_API_USERSTATS_URL}${username}/trades/oneYearAgo?CopyAsAsset=true`;
79
70
  promisesToRun.push(proxyManager.fetch(historyUrl, { headers: historyHeader.header }));
80
71
  }
81
72
  }
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); }
94
- } else {
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 });
98
- }
99
-
100
- // --- Process History (results[1], if it exists) ---
101
- if (fetchHistory && historyRes) { // Check if we ran this promise
73
+
74
+ // Process history result (if fetched)
75
+ if (fetchHistory) {
76
+ const results = await Promise.allSettled(promisesToRun);
77
+ const historyRes = results[0];
102
78
  if (historyRes.status === 'fulfilled' && historyRes.value.ok) {
103
79
  const data = await historyRes.value.json();
104
80
  wasHistorySuccess = true;
105
81
  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 });
111
82
  }
112
83
  }
113
- // --- END MODIFICATION ---
114
-
115
-
116
- // --- Private user handling ---
84
+
85
+ // Now fetch portfolio for EACH instrument (speculators) or once (normal)
86
+ for (const instrumentId of instrumentsToProcess) {
87
+ const portfolioUrl = userType === 'speculator'
88
+ ? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instrumentId}`
89
+ : `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
90
+
91
+ let wasPortfolioSuccess = false;
92
+
93
+ const portfolioRes = await proxyManager.fetch(portfolioUrl, { headers: portfolioHeader.header });
94
+
95
+ if (portfolioRes.ok) {
96
+ const body = await portfolioRes.text();
97
+ if (body.includes("user is PRIVATE")) {
98
+ isPrivate = true;
99
+ break; // Stop processing this user
100
+ } else {
101
+ wasPortfolioSuccess = true;
102
+ await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, JSON.parse(body), userType, instrumentId);
103
+ }
104
+ }
105
+
106
+ headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess);
107
+
108
+ // Re-select header for next instrument
109
+ if (instrumentsToProcess.length > 1 && instrumentId !== instrumentsToProcess[instrumentsToProcess.length - 1]) {
110
+ portfolioHeader = await headerManager.selectHeader();
111
+ }
112
+ }
113
+
114
+ // Handle private user
117
115
  if (isPrivate) {
118
116
  logger.log('WARN', `User ${userId} is private. Removing from updates.`);
119
- await batchManager.deleteFromTimestampBatch(userId, userType, instrumentId);
120
-
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}`;
123
- await blockCountsRef.set({ [incrementField]: FieldValue.increment(-1) }, { merge: true });
117
+ // Delete for ALL instruments
118
+ for (const instrumentId of instrumentsToProcess) {
119
+ await batchManager.deleteFromTimestampBatch(userId, userType, instrumentId);
120
+ }
121
+ const blockCountsRef = db.doc(config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS);
122
+ for (const instrumentId of instrumentsToProcess) {
123
+ const incrementField = `counts.${instrumentId}_${Math.floor(userId/1e6)*1e6}`;
124
+ await blockCountsRef.set({ [incrementField]: FieldValue.increment(-1) }, { merge: true });
125
+ }
124
126
  return;
125
127
  }
126
-
127
- if (wasPortfolioSuccess || wasHistorySuccess) {
128
+
129
+ // Update timestamps
130
+ for (const instrumentId of instrumentsToProcess) {
128
131
  await batchManager.updateUserTimestamp(userId, userType, instrumentId);
129
- if (userType === 'speculator') await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6));
130
132
  }
131
- } finally {
132
- if (portfolioHeader) headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess);
133
+ if (userType === 'speculator') {
134
+ await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6));
135
+ }
133
136
 
134
- // --- MODIFICATION: Only update history header if we used it ---
135
- if (fetchHistory && historyHeader) headerManager.updatePerformance(historyHeader.id, wasHistorySuccess); // <-- FIX: This line now works
136
- // --- END MODIFICATION ---
137
+ } finally {
138
+ if (historyHeader && fetchHistory) {
139
+ headerManager.updatePerformance(historyHeader.id, wasHistorySuccess);
140
+ }
137
141
  }
138
142
  }
139
143
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.135",
3
+ "version": "1.0.136",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [