bulltrackers-module 1.0.176 → 1.0.178
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/controllers/computation_controller.js +1 -15
- package/functions/computation-system/helpers/computation_manifest_builder.js +4 -10
- package/functions/computation-system/helpers/computation_pass_runner.js +2 -8
- package/functions/task-engine/helpers/update_helpers.js +132 -67
- package/package.json +1 -1
|
@@ -147,28 +147,17 @@ class ComputationExecutor {
|
|
|
147
147
|
*/
|
|
148
148
|
async executePerUser(calcInstance, metadata, dateStr, portfolioData, yesterdayPortfolioData, historyData, computedDeps, prevDeps) {
|
|
149
149
|
const { logger } = this.deps;
|
|
150
|
-
|
|
151
|
-
// Fix for the 'all' userType discrepancy:
|
|
152
|
-
const targetUserType = metadata.userType; // 'all', 'normal', or 'speculator'
|
|
153
|
-
|
|
150
|
+
const targetUserType = metadata.userType;
|
|
154
151
|
const mappings = await this.loader.loadMappings();
|
|
155
152
|
const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
|
|
156
|
-
|
|
157
153
|
for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
|
|
158
|
-
// 1. Get Yesterday's Portfolio (if available)
|
|
159
154
|
const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
|
|
160
|
-
|
|
161
|
-
// 2. Get Today's Trading History (if available)
|
|
162
155
|
const todayHistory = historyData ? historyData[userId] : null;
|
|
163
|
-
|
|
164
156
|
const actualUserType = todayPortfolio.PublicPositions ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
|
|
165
|
-
|
|
166
|
-
// Filtering Logic
|
|
167
157
|
if (targetUserType !== 'all') {
|
|
168
158
|
const mappedTarget = (targetUserType === 'speculator') ? SCHEMAS.USER_TYPES.SPECULATOR : SCHEMAS.USER_TYPES.NORMAL;
|
|
169
159
|
if (mappedTarget !== actualUserType) continue;
|
|
170
160
|
}
|
|
171
|
-
|
|
172
161
|
const context = ContextBuilder.buildPerUserContext({
|
|
173
162
|
todayPortfolio, yesterdayPortfolio,
|
|
174
163
|
todayHistory,
|
|
@@ -177,7 +166,6 @@ class ComputationExecutor {
|
|
|
177
166
|
previousComputedDependencies: prevDeps,
|
|
178
167
|
config: this.config, deps: this.deps
|
|
179
168
|
});
|
|
180
|
-
|
|
181
169
|
try { await calcInstance.process(context); }
|
|
182
170
|
catch (e) { logger.log('WARN', `Calc ${metadata.name} failed for user ${userId}: ${e.message}`); }
|
|
183
171
|
}
|
|
@@ -187,14 +175,12 @@ class ComputationExecutor {
|
|
|
187
175
|
const mappings = await this.loader.loadMappings();
|
|
188
176
|
const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await this.loader.loadInsights(dateStr) } : null;
|
|
189
177
|
const social = metadata.rootDataDependencies?.includes('social') ? { today: await this.loader.loadSocial(dateStr) } : null;
|
|
190
|
-
|
|
191
178
|
const context = ContextBuilder.buildMetaContext({
|
|
192
179
|
dateStr, metadata, mappings, insights, socialData: social,
|
|
193
180
|
computedDependencies: computedDeps,
|
|
194
181
|
previousComputedDependencies: prevDeps,
|
|
195
182
|
config: this.config, deps: this.deps
|
|
196
183
|
});
|
|
197
|
-
|
|
198
184
|
return await calcInstance.process(context);
|
|
199
185
|
}
|
|
200
186
|
}
|
|
@@ -11,15 +11,6 @@
|
|
|
11
11
|
* It has removed all logic for deprecated folder structures (e.g., /historical/).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
//Hacky solution to force tmp writes TODO : This Tmp write is really dodgy, not ideal but works, consider less hacky solutions to writing to filesystem
|
|
16
|
-
process.env.TMPDIR = '/tmp';
|
|
17
|
-
process.env.TMP = '/tmp';
|
|
18
|
-
process.env.TEMP = '/tmp';
|
|
19
|
-
const os = require('os');
|
|
20
|
-
|
|
21
|
-
const path = require('path');
|
|
22
|
-
|
|
23
14
|
/* --------------------------------------------------
|
|
24
15
|
* Pretty Console Helpers
|
|
25
16
|
* -------------------------------------------------- */
|
|
@@ -54,7 +45,7 @@ function suggestClosest(name, candidates, n = 3) {
|
|
|
54
45
|
return dp[m][n];
|
|
55
46
|
};
|
|
56
47
|
const scores = candidates.map(c => [c, levenshtein(name, c)]);
|
|
57
|
-
scores.sort((a, b)
|
|
48
|
+
scores.sort((a, b) => a[1] - b[1]);
|
|
58
49
|
return scores.slice(0, n).map(s => s[0]);
|
|
59
50
|
}
|
|
60
51
|
|
|
@@ -206,17 +197,20 @@ function buildManifest(productLinesToRun = [], calculations) {
|
|
|
206
197
|
const sortedManifest = [];
|
|
207
198
|
const queue = [];
|
|
208
199
|
let maxPass = 0;
|
|
200
|
+
|
|
209
201
|
for (const [name, degree] of filteredInDegree) { if (degree === 0) { queue.push(name); filteredManifestMap.get(name).pass = 1; maxPass = 1; } }
|
|
210
202
|
queue.sort();
|
|
211
203
|
while (queue.length) {
|
|
212
204
|
const currentName = queue.shift();
|
|
213
205
|
const currentEntry = filteredManifestMap.get(currentName);
|
|
214
206
|
sortedManifest.push(currentEntry);
|
|
207
|
+
|
|
215
208
|
for (const neighborName of (filteredReverseAdjacency.get(currentName) || [])) { const newDegree = filteredInDegree.get(neighborName) - 1; filteredInDegree.set(neighborName, newDegree);
|
|
216
209
|
const neighborEntry = filteredManifestMap.get(neighborName);
|
|
217
210
|
if (neighborEntry.pass <= currentEntry.pass) { neighborEntry.pass = currentEntry.pass + 1; if (neighborEntry.pass > maxPass) maxPass = neighborEntry.pass; }
|
|
218
211
|
if (newDegree === 0) { queue.push(neighborName); } }
|
|
219
212
|
queue.sort(); }
|
|
213
|
+
|
|
220
214
|
if (sortedManifest.length !== filteredManifestMap.size) {
|
|
221
215
|
log.divider('Circular Dependency Detected');
|
|
222
216
|
const cycles = findCycles(filteredManifestMap, adjacency);
|
|
@@ -62,8 +62,6 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
62
62
|
// Process a single date and RETURN updates (do not write)
|
|
63
63
|
const processDate = async (dateStr) => {
|
|
64
64
|
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
65
|
-
|
|
66
|
-
// Filter using in-memory status
|
|
67
65
|
const standardToRun = standardCalcs.filter(c => shouldRun(c, dateStr));
|
|
68
66
|
const metaToRun = metaCalcs.filter(c => shouldRun(c, dateStr));
|
|
69
67
|
|
|
@@ -84,21 +82,17 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
84
82
|
try {
|
|
85
83
|
const calcsRunning = [...finalStandardToRun, ...finalMetaToRun];
|
|
86
84
|
const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
85
|
+
const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
86
|
+
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
90
87
|
const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
|
|
91
|
-
|
|
92
88
|
if (finalStandardToRun.length) {
|
|
93
89
|
const updates = await runStandardComputationPass(dateToProcess, finalStandardToRun, `Pass ${passToRun} (Std)`, config, dependencies, rootData, existingResults, previousResults, true); // skipStatusWrite=true
|
|
94
90
|
Object.assign(dateUpdates, updates);
|
|
95
91
|
}
|
|
96
|
-
|
|
97
92
|
if (finalMetaToRun.length) {
|
|
98
93
|
const updates = await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, true); // skipStatusWrite=true
|
|
99
94
|
Object.assign(dateUpdates, updates);
|
|
100
95
|
}
|
|
101
|
-
|
|
102
96
|
} catch (err) {
|
|
103
97
|
logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message });
|
|
104
98
|
// Mark failures
|
|
@@ -3,6 +3,7 @@
|
|
|
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)
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
@@ -15,54 +16,83 @@ async function lookupUsernames(cids, { logger, headerManager, proxyManager }, co
|
|
|
15
16
|
if (!cids?.length) return [];
|
|
16
17
|
logger.log('INFO', `[lookupUsernames] Looking up usernames for ${cids.length} CIDs.`);
|
|
17
18
|
|
|
18
|
-
// --- Set concurrency to 1
|
|
19
|
+
// --- Set concurrency to 1 to prevent AppScript rate limits. DO NOT CHANGE THIS.
|
|
19
20
|
const limit = pLimit(1);
|
|
20
21
|
const { USERNAME_LOOKUP_BATCH_SIZE, ETORO_API_RANKINGS_URL } = config;
|
|
21
22
|
const batches = [];
|
|
22
|
-
for (let i = 0; i < cids.length; i += USERNAME_LOOKUP_BATCH_SIZE) {
|
|
23
|
-
|
|
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}`;
|
|
24
29
|
logger.log('INFO', `[lookupUsernames/${batchId}] Processing batch of ${batch.length} CIDs...`);
|
|
25
30
|
const header = await headerManager.selectHeader();
|
|
26
31
|
if (!header) { logger.log('ERROR', `[lookupUsernames/${batchId}] Could not select a header.`); return null; }
|
|
32
|
+
|
|
27
33
|
let wasSuccess = false;
|
|
28
34
|
let proxyUsed = true;
|
|
29
35
|
let response;
|
|
30
36
|
const url = `${ETORO_API_RANKINGS_URL}?Period=LastTwoYears`;
|
|
31
37
|
const options = { method: 'POST', headers: { ...header.header, 'Content-Type': 'application/json' }, body: JSON.stringify(batch) };
|
|
38
|
+
|
|
32
39
|
try {
|
|
33
40
|
logger.log('TRACE', `[lookupUsernames/${batchId}] Attempting fetch via AppScript proxy...`);
|
|
34
41
|
response = await proxyManager.fetch(url, options);
|
|
35
42
|
if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
|
|
36
|
-
wasSuccess = true;
|
|
43
|
+
wasSuccess = true;
|
|
37
44
|
logger.log('INFO', `[lookupUsernames/${batchId}] AppScript proxy fetch successful.`);
|
|
38
|
-
} catch (proxyError) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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;
|
|
45
58
|
}
|
|
46
|
-
} finally {
|
|
47
|
-
|
|
48
|
-
}
|
|
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
|
+
}));
|
|
49
71
|
|
|
50
72
|
const results = await Promise.allSettled(batchPromises);
|
|
51
|
-
const allUsers = results
|
|
73
|
+
const allUsers = results
|
|
74
|
+
.filter(r => r.status === 'fulfilled' && r.value && Array.isArray(r.value))
|
|
75
|
+
.flatMap(r => r.value);
|
|
76
|
+
|
|
52
77
|
logger.log('INFO', `[lookupUsernames] Found ${allUsers.length} public users out of ${cids.length}.`);
|
|
53
78
|
return allUsers;
|
|
54
79
|
}
|
|
55
80
|
|
|
56
81
|
|
|
57
82
|
/**
|
|
58
|
-
* (REFACTORED: Fully sequential, verbose logging, node-fetch fallback)
|
|
83
|
+
* (REFACTORED: Fully sequential, verbose logging, node-fetch fallback, FIXED SCOPING)
|
|
59
84
|
*/
|
|
60
85
|
async function handleUpdate(task, taskId, { logger, headerManager, proxyManager, db, batchManager }, config, username) {
|
|
61
86
|
const { userId, instruments, instrumentId, userType } = task;
|
|
62
|
-
|
|
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
|
+
|
|
63
92
|
const today = new Date().toISOString().slice(0, 10);
|
|
64
93
|
const portfolioBlockId = `${Math.floor(parseInt(userId) / 1000000)}M`;
|
|
65
94
|
let isPrivate = false;
|
|
95
|
+
|
|
66
96
|
logger.log('INFO', `[handleUpdate/${userId}] Starting update task. Type: ${userType}. Instruments: ${instrumentsToProcess.join(', ')}`);
|
|
67
97
|
|
|
68
98
|
// --- 1. Process History Fetch (Sequentially) ---
|
|
@@ -74,52 +104,70 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
|
|
|
74
104
|
if (!batchManager.checkAndSetHistoryFetched(userId)) {
|
|
75
105
|
logger.log('INFO', `[handleUpdate/${userId}] Attempting history fetch.`);
|
|
76
106
|
historyHeader = await headerManager.selectHeader();
|
|
77
|
-
if (!historyHeader) {
|
|
107
|
+
if (!historyHeader) {
|
|
108
|
+
logger.log('WARN', `[handleUpdate/${userId}] Could not select history header. Skipping history.`);
|
|
78
109
|
} else {
|
|
79
110
|
const historyUrl = `${config.ETORO_API_USERSTATS_URL}${username}/trades/oneYearAgo?CopyAsAsset=true`;
|
|
80
111
|
const options = { headers: historyHeader.header };
|
|
81
112
|
let response;
|
|
82
|
-
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
logger.log('TRACE', `[handleUpdate/${userId}] Attempting history fetch via AppScript proxy...`);
|
|
83
116
|
response = await proxyManager.fetch(historyUrl, options);
|
|
84
|
-
if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
|
|
85
|
-
wasHistorySuccess = true;
|
|
86
|
-
|
|
117
|
+
if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
|
|
118
|
+
wasHistorySuccess = true;
|
|
87
119
|
} catch (proxyError) {
|
|
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' });
|
|
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' });
|
|
89
121
|
proxyUsedForHistory = false;
|
|
90
|
-
try {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
wasHistorySuccess =
|
|
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;
|
|
97
132
|
}
|
|
98
133
|
}
|
|
99
134
|
|
|
100
|
-
if (wasHistorySuccess) {
|
|
135
|
+
if (wasHistorySuccess) {
|
|
136
|
+
logger.log('INFO', `[handleUpdate/${userId}] History fetch successful.`);
|
|
101
137
|
const data = await response.json();
|
|
102
|
-
await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType);
|
|
138
|
+
await batchManager.addToTradingHistoryBatch(userId, portfolioBlockId, today, data, userType);
|
|
139
|
+
}
|
|
103
140
|
}
|
|
104
|
-
} else {
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
}
|
|
107
150
|
|
|
108
151
|
// --- 2. Process Portfolio Fetches (Sequentially) ---
|
|
109
152
|
logger.log('INFO', `[handleUpdate/${userId}] Starting ${instrumentsToProcess.length} sequential portfolio fetches.`);
|
|
110
153
|
|
|
111
|
-
|
|
154
|
+
// Renamed loop variable to currentInstrumentId to prevent 'instId' reference errors later
|
|
155
|
+
for (const currentInstrumentId of instrumentsToProcess) {
|
|
112
156
|
if (isPrivate) {
|
|
113
157
|
logger.log('INFO', `[handleUpdate/${userId}] Skipping remaining instruments because user was marked as private.`);
|
|
114
158
|
break;
|
|
115
159
|
}
|
|
116
160
|
|
|
117
161
|
const portfolioHeader = await headerManager.selectHeader();
|
|
118
|
-
if (!portfolioHeader) {
|
|
162
|
+
if (!portfolioHeader) {
|
|
163
|
+
logger.log('ERROR', `[handleUpdate/${userId}] Could not select portfolio header. Skipping instrument.`);
|
|
119
164
|
continue;
|
|
120
165
|
}
|
|
121
166
|
|
|
122
|
-
|
|
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}`;
|
|
123
171
|
|
|
124
172
|
const options = { headers: portfolioHeader.header };
|
|
125
173
|
let response;
|
|
@@ -127,48 +175,54 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
|
|
|
127
175
|
let proxyUsedForPortfolio = true;
|
|
128
176
|
|
|
129
177
|
try {
|
|
130
|
-
|
|
131
|
-
logger.log('TRACE', `[handleUpdate/${userId}] Attempting portfolio fetch for instId ${instId} via AppScript proxy...`);
|
|
178
|
+
logger.log('TRACE', `[handleUpdate/${userId}] Attempting portfolio fetch via AppScript proxy...`);
|
|
132
179
|
response = await proxyManager.fetch(portfolioUrl, options);
|
|
133
|
-
if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
|
|
134
|
-
wasPortfolioSuccess = true;
|
|
180
|
+
if (!response.ok) throw new Error(`AppScript proxy failed with status ${response.status}`);
|
|
181
|
+
wasPortfolioSuccess = true;
|
|
135
182
|
|
|
136
|
-
} catch (proxyError) {
|
|
137
|
-
logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch
|
|
138
|
-
proxyUsedForPortfolio = false;
|
|
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;
|
|
139
186
|
|
|
140
187
|
try {
|
|
141
|
-
response = await fetch(portfolioUrl, options);
|
|
188
|
+
response = await fetch(portfolioUrl, options);
|
|
142
189
|
if (!response.ok) {
|
|
143
190
|
const errorText = await response.text();
|
|
144
|
-
throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`);
|
|
191
|
+
throw new Error(`Direct fetch failed with status ${response.status}. Response: ${errorText.substring(0, 200)}`);
|
|
145
192
|
}
|
|
146
|
-
wasPortfolioSuccess = true;
|
|
147
|
-
|
|
193
|
+
wasPortfolioSuccess = true;
|
|
148
194
|
} catch (fallbackError) {
|
|
149
|
-
logger.log('ERROR', `[handleUpdate/${userId}] Portfolio fetch
|
|
195
|
+
logger.log('ERROR', `[handleUpdate/${userId}] Portfolio fetch direct fallback FAILED.`, { error: fallbackError.message, source: 'eToro/Network' });
|
|
150
196
|
wasPortfolioSuccess = false;
|
|
151
197
|
}
|
|
152
198
|
}
|
|
153
199
|
|
|
154
|
-
// --- 4. Process Portfolio Result (with verbose, raw logging) ---
|
|
155
200
|
if (wasPortfolioSuccess) {
|
|
156
201
|
const body = await response.text();
|
|
157
|
-
if (body.includes("user is PRIVATE")) {
|
|
158
|
-
|
|
202
|
+
if (body.includes("user is PRIVATE")) {
|
|
203
|
+
isPrivate = true;
|
|
204
|
+
logger.log('WARN', `[handleUpdate/${userId}] User is PRIVATE. Marking for removal.`);
|
|
205
|
+
break;
|
|
159
206
|
}
|
|
160
207
|
|
|
161
208
|
try {
|
|
162
209
|
const portfolioJson = JSON.parse(body);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
}
|
|
166
218
|
|
|
167
|
-
} catch (parseError) {
|
|
168
|
-
wasPortfolioSuccess = false;
|
|
169
|
-
logger.log('ERROR', `[handleUpdate/${userId}] FAILED TO PARSE JSON RESPONSE. RAW BODY:`, { url: portfolioUrl, parseErrorMessage: parseError.message, rawResponseText: body });
|
|
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 });
|
|
170
222
|
}
|
|
171
|
-
} else {
|
|
223
|
+
} else {
|
|
224
|
+
logger.log('WARN', `[handleUpdate/${userId}] Portfolio fetch failed. No response to process.`);
|
|
225
|
+
}
|
|
172
226
|
|
|
173
227
|
if (proxyUsedForPortfolio) { headerManager.updatePerformance(portfolioHeader.id, wasPortfolioSuccess); }
|
|
174
228
|
}
|
|
@@ -176,21 +230,32 @@ async function handleUpdate(task, taskId, { logger, headerManager, proxyManager,
|
|
|
176
230
|
// --- 5. Handle Private Users & Timestamps ---
|
|
177
231
|
if (isPrivate) {
|
|
178
232
|
logger.log('WARN', `[handleUpdate/${userId}] Removing private user from updates.`);
|
|
179
|
-
|
|
233
|
+
// Iterate again using correct scoping
|
|
234
|
+
for (const currentInstrumentId of instrumentsToProcess) {
|
|
235
|
+
await batchManager.deleteFromTimestampBatch(userId, userType, currentInstrumentId);
|
|
236
|
+
}
|
|
237
|
+
|
|
180
238
|
const blockCountsRef = db.doc(config.FIRESTORE_DOC_SPECULATOR_BLOCK_COUNTS);
|
|
181
|
-
for (const
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
}
|
|
184
245
|
}
|
|
185
246
|
return;
|
|
186
247
|
}
|
|
187
248
|
|
|
188
249
|
// If not private, update all timestamps
|
|
189
|
-
for (const
|
|
190
|
-
|
|
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
|
+
}
|
|
191
257
|
|
|
192
258
|
logger.log('INFO', `[handleUpdate/${userId}] Update task finished successfully.`);
|
|
193
|
-
// 'finally' block for header flushing is handled by the main handler_creator.js
|
|
194
259
|
}
|
|
195
260
|
|
|
196
261
|
module.exports = { handleUpdate, lookupUsernames };
|