bulltrackers-module 1.0.178 → 1.0.179

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.
@@ -3,7 +3,6 @@
3
3
  * (REFACTORED: Removed all concurrency from `handleUpdate` and `lookupUsernames`)
4
4
  * (REFACTORED: Added node-fetch fallback for all API calls)
5
5
  * (REFACTORED: Added verbose, user-centric logging for all operations)
6
- * (FIXED: Resolved ReferenceError 'instId is not defined' in final timestamp loops)
7
6
  */
8
7
 
9
8
  const { FieldValue } = require('@google-cloud/firestore');
@@ -16,83 +15,54 @@ async function lookupUsernames(cids, { logger, headerManager, proxyManager }, co
16
15
  if (!cids?.length) return [];
17
16
  logger.log('INFO', `[lookupUsernames] Looking up usernames for ${cids.length} CIDs.`);
18
17
 
19
- // --- Set concurrency to 1 to prevent AppScript rate limits. DO NOT CHANGE THIS.
18
+ // --- Set concurrency to 1 because appscript gets really fucked up with undocumented rate limits if we try spam it concurrently, a shame but that's life. DO NOT CHANGE THIS
20
19
  const limit = pLimit(1);
21
20
  const { USERNAME_LOOKUP_BATCH_SIZE, ETORO_API_RANKINGS_URL } = config;
22
21
  const batches = [];
23
- for (let i = 0; i < cids.length; i += USERNAME_LOOKUP_BATCH_SIZE) {
24
- batches.push(cids.slice(i, i + USERNAME_LOOKUP_BATCH_SIZE).map(Number));
25
- }
26
-
27
- const batchPromises = batches.map((batch, index) => limit(async () => {
28
- const batchId = `batch-${index + 1}`;
22
+ for (let i = 0; i < cids.length; i += USERNAME_LOOKUP_BATCH_SIZE) { batches.push(cids.slice(i, i + USERNAME_LOOKUP_BATCH_SIZE).map(Number)); }
23
+ const batchPromises = batches.map((batch, index) => limit(async () => { const batchId = `batch-${index + 1}`;
29
24
  logger.log('INFO', `[lookupUsernames/${batchId}] Processing batch of ${batch.length} CIDs...`);
30
25
  const header = await headerManager.selectHeader();
31
26
  if (!header) { logger.log('ERROR', `[lookupUsernames/${batchId}] Could not select a header.`); return null; }
32
-
33
27
  let wasSuccess = false;
34
28
  let proxyUsed = true;
35
29
  let response;
36
30
  const url = `${ETORO_API_RANKINGS_URL}?Period=LastTwoYears`;
37
31
  const options = { method: 'POST', headers: { ...header.header, 'Content-Type': 'application/json' }, body: JSON.stringify(batch) };
38
-
39
32
  try {
40
33
  logger.log('TRACE', `[lookupUsernames/${batchId}] Attempting fetch via AppScript proxy...`);
41
34
  response = await proxyManager.fetch(url, options);
42
35
  if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
43
- wasSuccess = true;
36
+ wasSuccess = true; // Yay we win
44
37
  logger.log('INFO', `[lookupUsernames/${batchId}] AppScript proxy fetch successful.`);
45
- } catch (proxyError) {
46
- logger.log('WARN', `[lookupUsernames/${batchId}] AppScript proxy fetch FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, { error: proxyError.message, source: 'AppScript' });
47
- proxyUsed = false;
48
- try {
49
- response = await fetch(url, options);
50
- if (!response.ok) {
51
- const errorText = await response.text();
52
- throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`);
53
- }
54
- logger.log('INFO', `[lookupUsernames/${batchId}] Direct node-fetch fallback successful.`);
55
- } catch (fallbackError) {
56
- logger.log('ERROR', `[lookupUsernames/${batchId}] Direct node-fetch fallback FAILED. Giving up on this batch.`, { error: fallbackError.message, source: 'eToro/Network' });
57
- return null;
38
+ } catch (proxyError) { logger.log('WARN', `[lookupUsernames/${batchId}] AppScript proxy fetch FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, { error: proxyError.message, source: 'AppScript' }); // SHIT we failed...
39
+ proxyUsed = false; // Don't penalize header for proxy failure
40
+ try { response = await fetch(url, options); // Ok let's try again with node, using GCP IP pools
41
+ if (!response.ok) { const errorText = await response.text(); throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`); }
42
+ logger.log('INFO', `[lookupUsernames/${batchId}] Direct node-fetch fallback successful.`); // Yay we win
43
+ } catch (fallbackError) { logger.log('ERROR', `[lookupUsernames/${batchId}] Direct node-fetch fallback FAILED. Giving up on this batch.`, { error: fallbackError.message, source: 'eToro/Network' }); // SHIT, we failed here too
44
+ return null; // Give up on this batch
58
45
  }
59
- } finally {
60
- if (proxyUsed) { headerManager.updatePerformance(header.id, wasSuccess); }
61
- }
62
-
63
- try {
64
- const data = await response.json();
65
- return data;
66
- } catch (parseError) {
67
- logger.log('ERROR', `[lookupUsernames/${batchId}] Failed to parse JSON response.`, { error: parseError.message });
68
- return null;
69
- }
70
- }));
46
+ } finally { if (proxyUsed) { headerManager.updatePerformance(header.id, wasSuccess); } } // If we used Appscript IP Pool and not GCP IP Pool, record performance
47
+ try { const data = await response.json(); return data;
48
+ } catch (parseError) { logger.log('ERROR', `[lookupUsernames/${batchId}] Failed to parse JSON response.`, { error: parseError.message }); return null; } }));
71
49
 
72
50
  const results = await Promise.allSettled(batchPromises);
73
- const allUsers = results
74
- .filter(r => r.status === 'fulfilled' && r.value && Array.isArray(r.value))
75
- .flatMap(r => r.value);
76
-
51
+ const allUsers = results .filter(r => r.status === 'fulfilled' && r.value && Array.isArray(r.value)) .flatMap(r => r.value);
77
52
  logger.log('INFO', `[lookupUsernames] Found ${allUsers.length} public users out of ${cids.length}.`);
78
53
  return allUsers;
79
54
  }
80
55
 
81
56
 
82
57
  /**
83
- * (REFACTORED: Fully sequential, verbose logging, node-fetch fallback, FIXED SCOPING)
58
+ * (REFACTORED: Fully sequential, verbose logging, node-fetch fallback)
84
59
  */
85
60
  async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager }, config, username) {
86
61
  const { userId, instruments, instrumentId, userType } = task;
87
-
88
- // For normal users, we pass [undefined] so the loop runs exactly once.
89
- // For speculators, we pass the list of instruments to fetch individually.
90
- const instrumentsToProcess = userType === 'speculator' ? (instruments || [instrumentId]) : [undefined];
91
-
62
+ const instrumentsToProcess = userType === 'speculator' ? (instruments || [instrumentId]) : [undefined];
92
63
  const today = new Date().toISOString().slice(0, 10);
93
64
  const portfolioBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
94
65
  let isPrivate = false;
95
-
96
66
  logger.log('INFO', `[handleUpdate/${userId}] Starting update task. Type: ${userType}. Instruments: ${instrumentsToProcess.join(', ')}`);
97
67
 
98
68
  // --- 1. Process History Fetch (Sequentially) ---
@@ -104,70 +74,52 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
104
74
  if (!batchManager.checkAndSetHistoryFetched(userId)) {
105
75
  logger.log('INFO', `[handleUpdate/${userId}] Attempting history fetch.`);
106
76
  historyHeader = await headerManager.selectHeader();
107
- if (!historyHeader) {
108
- logger.log('WARN', `[handleUpdate/${userId}] Could not select history header. Skipping history.`);
77
+ if (!historyHeader) { logger.log('WARN', `[handleUpdate/${userId}] Could not select history header. Skipping history.`);
109
78
  } else {
110
79
  const historyUrl = `${config.ETORO_API_USERSTATS_URL}${username}/trades/oneYearAgo?CopyAsAsset=true`;
111
80
  const options = { headers: historyHeader.header };
112
81
  let response;
113
-
114
- try {
115
- logger.log('TRACE', `[handleUpdate/${userId}] Attempting history fetch via AppScript proxy...`);
82
+ try { logger.log('TRACE', `[handleUpdate/${userId}] Attempting history fetch via AppScript proxy...`);
116
83
  response = await proxyManager.fetch(historyUrl, options);
117
- if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
118
- wasHistorySuccess = true;
84
+ if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`); // SHIT we failed here
85
+ wasHistorySuccess = true; // Appscript worked, we are very smart
86
+
119
87
  } catch (proxyError) {
120
- logger.log('WARN', `[handleUpdate/${userId}] History fetch via AppScript proxy FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, { error: proxyError.message, source: 'AppScript' });
88
+ logger.log('WARN', `[handleUpdate/${userId}] History fetch via AppScript proxy FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, { error: proxyError.message, source: 'AppScript' }); // SHIT we failed here
121
89
  proxyUsedForHistory = false;
122
- try {
123
- response = await fetch(historyUrl, options);
124
- if (!response.ok) {
125
- const errorText = await response.text();
126
- throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`);
127
- }
128
- wasHistorySuccess = true;
129
- } catch (fallbackError) {
130
- logger.log('ERROR', `[handleUpdate/${userId}] History fetch direct fallback FAILED.`, { error: fallbackError.message, source: 'eToro/Network' });
131
- wasHistorySuccess = false;
90
+ try { response = await fetch(historyUrl, options);
91
+ if (!response.ok) { const errorText = await response.text();
92
+ throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`); } // SHIT we failed here too
93
+ wasHistorySuccess = true; // Fallback succeeded, we are so smart
94
+
95
+ } catch (fallbackError) { logger.log('ERROR', `[handleUpdate/${userId}] History fetch direct fallback FAILED.`, { error: fallbackError.message, source: 'eToro/Network' }); // We are dumb, everything failed
96
+ wasHistorySuccess = false; // Nope we are dumb....
132
97
  }
133
98
  }
134
99
 
135
- if (wasHistorySuccess) {
136
- logger.log('INFO', `[handleUpdate/${userId}] History fetch successful.`);
100
+ if (wasHistorySuccess) { logger.log('INFO', `[handleUpdate/${userId}] History fetch successful.`); // Some method worked, we are very smart
137
101
  const data = await response.json();
138
- await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType);
139
- }
102
+ await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType); }
140
103
  }
141
- } else {
142
- logger.log('TRACE', `[handleUpdate/${userId}] History fetch skipped (already fetched by this instance).`);
143
- }
144
- } catch (err) {
145
- logger.log('ERROR', `[handleUpdate/${userId}] Unhandled error during history processing.`, { error: err.message });
146
- wasHistorySuccess = false;
147
- } finally {
148
- if (historyHeader && proxyUsedForHistory) { headerManager.updatePerformance(historyHeader.id, wasHistorySuccess); }
149
- }
104
+ } else { logger.log('TRACE', `[handleUpdate/${userId}] History fetch skipped (already fetched by this instance).`); }
105
+ } catch (err) { logger.log('ERROR', `[handleUpdate/${userId}] Unhandled error during history processing.`, { error: err.message }); wasHistorySuccess = false; // We fucked up.
106
+ } finally { if (historyHeader && proxyUsedForHistory) { headerManager.updatePerformance(historyHeader.id, wasHistorySuccess); } } // If we used appscript proxy, record performance, otherwise fuck off.
150
107
 
151
108
  // --- 2. Process Portfolio Fetches (Sequentially) ---
152
109
  logger.log('INFO', `[handleUpdate/${userId}] Starting ${instrumentsToProcess.length} sequential portfolio fetches.`);
153
110
 
154
- // Renamed loop variable to currentInstrumentId to prevent 'instId' reference errors later
155
- for (const currentInstrumentId of instrumentsToProcess) {
111
+ for (const instId of instrumentsToProcess) {
156
112
  if (isPrivate) {
157
113
  logger.log('INFO', `[handleUpdate/${userId}] Skipping remaining instruments because user was marked as private.`);
158
114
  break;
159
115
  }
160
116
 
161
117
  const portfolioHeader = await headerManager.selectHeader();
162
- if (!portfolioHeader) {
163
- logger.log('ERROR', `[handleUpdate/${userId}] Could not select portfolio header. Skipping instrument.`);
118
+ if (!portfolioHeader) { logger.log('ERROR', `[handleUpdate/${userId}] Could not select portfolio header for instId ${instId}. Skipping this instrument.`);
164
119
  continue;
165
120
  }
166
121
 
167
- // Only append InstrumentID if it is defined (i.e., for speculators)
168
- const portfolioUrl = userType === 'speculator' && currentInstrumentId
169
- ? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${currentInstrumentId}`
170
- : `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
122
+ const portfolioUrl = userType === 'speculator' ? `${config.ETORO_API_POSITIONS_URL}?cid=${userId}&InstrumentID=${instId}` : `${config.ETORO_API_PORTFOLIO_URL}?cid=${userId}`;
171
123
 
172
124
  const options = { headers: portfolioHeader.header };
173
125
  let response;
@@ -175,54 +127,48 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
175
127
  let proxyUsedForPortfolio = true;
176
128
 
177
129
  try {
178
- logger.log('TRACE', `[handleUpdate/${userId}] Attempting portfolio fetch via AppScript proxy...`);
130
+ // --- REFACTOR 3: ADD FALLBACK ---
131
+ logger.log('TRACE', `[handleUpdate/${userId}] Attempting portfolio fetch for instId ${instId} via AppScript proxy...`);
179
132
  response = await proxyManager.fetch(portfolioUrl, options);
180
- if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
181
- wasPortfolioSuccess = true;
133
+ if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`); // SHIT we failed here
134
+ wasPortfolioSuccess = true; // Oh we are smart, worked first time.
182
135
 
183
- } catch (proxyError) {
184
- logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch via AppScript proxy FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, { error: proxyError.message, source: 'AppScript' });
185
- proxyUsedForPortfolio = false;
136
+ } catch (proxyError) { // try fallback with local node fetch using GCP IP Pools
137
+ logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch for instId ${instId} via AppScript proxy FAILED. Error: ${proxyError.message}. Attempting direct node-fetch fallback.`, { error: proxyError.message, source: 'AppScript' });
138
+ proxyUsedForPortfolio = false; // We are not using Appscript proxy here as fallback is GCP based, so false
186
139
 
187
140
  try {
188
- response = await fetch(portfolioUrl, options);
141
+ response = await fetch(portfolioUrl, options); // Direct node-fetch
189
142
  if (!response.ok) {
190
143
  const errorText = await response.text();
191
- throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`);
144
+ throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`); // SHIT we failed here
192
145
  }
193
- wasPortfolioSuccess = true;
146
+ wasPortfolioSuccess = true; // Fallback succeeded we are so smart
147
+
194
148
  } catch (fallbackError) {
195
- logger.log('ERROR', `[handleUpdate/${userId}] Portfolio fetch direct fallback FAILED.`, { error: fallbackError.message, source: 'eToro/Network' });
149
+ logger.log('ERROR', `[handleUpdate/${userId}] Portfolio fetch for instId ${instId} direct fallback FAILED.`, { error: fallbackError.message, source: 'eToro/Network' });
196
150
  wasPortfolioSuccess = false;
197
151
  }
198
152
  }
199
153
 
154
+ // --- 4. Process Portfolio Result (with verbose, raw logging) ---
200
155
  if (wasPortfolioSuccess) {
201
156
  const body = await response.text();
202
- if (body.includes("user is PRIVATE")) {
203
- isPrivate = true;
204
- logger.log('WARN', `[handleUpdate/${userId}] User is PRIVATE. Marking for removal.`);
205
- break;
157
+ if (body.includes("user is PRIVATE")) { isPrivate = true; logger.log('WARN', `[handleUpdate/${userId}] User is PRIVATE. Marking for removal.`);
158
+ break; // Stop processing more portfolios for this private user
206
159
  }
207
160
 
208
161
  try {
209
162
  const portfolioJson = JSON.parse(body);
210
- // Pass currentInstrumentId to batchManager. For normal users it's undefined, which is fine.
211
- await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, portfolioJson, userType, currentInstrumentId);
212
-
213
- if (userType === 'speculator') {
214
- logger.log('INFO', `[handleUpdate/${userId}] Successfully processed portfolio for instId ${currentInstrumentId}.`);
215
- } else {
216
- logger.log('INFO', `[handleUpdate/${userId}] Successfully processed full portfolio (normal user).`);
217
- }
163
+ await batchManager.addToPortfolioBatch(userId, portfolioBlockId, today, portfolioJson, userType, instId);
164
+ if (userType === 'speculator') { logger.log('INFO', `[handleUpdate/${userId}] Successfully processed portfolio for instId ${instId}.`); // Only speculators have an instid, so this is conditional
165
+ } else { logger.log('INFO', `[handleUpdate/${userId}] Successfully processed full portfolio (normal user).`); } // Normal users
218
166
 
219
- } catch (parseError) {
220
- wasPortfolioSuccess = false;
221
- logger.log('ERROR', `[handleUpdate/${userId}] FAILED TO PARSE JSON RESPONSE. RAW BODY:`, { url: portfolioUrl, parseErrorMessage: parseError.message, rawResponseText: body });
167
+ } catch (parseError) { // Idk why this would happen, but if it does....log.
168
+ wasPortfolioSuccess = false; // Mark as failure
169
+ logger.log('ERROR', `[handleUpdate/${userId}] FAILED TO PARSE JSON RESPONSE. RAW BODY:`, { url: portfolioUrl, parseErrorMessage: parseError.message, rawResponseText: body }); // Return full response
222
170
  }
223
- } else {
224
- logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch failed. No response to process.`);
225
- }
171
+ } else { logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch failed for instId ${instId}. No response to process.`); }
226
172
 
227
173
  if (proxyUsedForPortfolio) { headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess); }
228
174
  }
@@ -230,32 +176,21 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
230
176
  // --- 5. Handle Private Users & Timestamps ---
231
177
  if (isPrivate) {
232
178
  logger.log('WARN', `[handleUpdate/${userId}] Removing private user from updates.`);
233
- // Iterate again using correct scoping
234
- for (const currentInstrumentId of instrumentsToProcess) {
235
- await batchManager.deleteFromTimestampBatch(userId, userType, currentInstrumentId);
236
- }
237
-
179
+ for (const instrumentId of instrumentsToProcess) { await batchManager.deleteFromTimestampBatch(userId, userType, instId); }
238
180
  const blockCountsRef = db.doc(config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS);
239
- for (const currentInstrumentId of instrumentsToProcess) {
240
- // Only update counts if we have a valid instrument ID (speculators)
241
- if (currentInstrumentId) {
242
- const incrementField = `counts.${currentInstrumentId}_${Math.floor(userId/1e6)*1e6}`;
243
- await blockCountsRef.set({ [incrementField]: FieldValue.increment(-1) }, { merge: true });
244
- }
181
+ for (const instrumentId of instrumentsToProcess) {
182
+ const incrementField = `counts.${instrumentId}_${Math.floor(userId/1e6)*1e6}`;
183
+ await blockCountsRef.set({ [incrementField]: FieldValue.increment(-1) }, { merge: true });
245
184
  }
246
185
  return;
247
186
  }
248
187
 
249
188
  // If not private, update all timestamps
250
- for (const currentInstrumentId of instrumentsToProcess) {
251
- await batchManager.updateUserTimestamp(userId, userType, currentInstrumentId);
252
- }
253
-
254
- if (userType === 'speculator') {
255
- await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6));
256
- }
189
+ for (const instrumentId of instrumentsToProcess) { await batchManager.updateUserTimestamp(userId, userType, instId); }
190
+ if (userType === 'speculator') { await batchManager.addSpeculatorTimestampFix(userId, String(Math.floor(userId/1e6)*1e6)); }
257
191
 
258
192
  logger.log('INFO', `[handleUpdate/${userId}] Update task finished successfully.`);
193
+ // 'finally' block for header flushing is handled by the main handler_creator.js
259
194
  }
260
195
 
261
196
  module.exports = { handleUpdate, lookupUsernames };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.178",
3
+ "version": "1.0.179",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [