bulltrackers-module 1.0.544 → 1.0.545
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.
|
@@ -133,16 +133,8 @@ async function storeSignedInUserSocialPosts({ db, logger, collectionRegistry, ci
|
|
|
133
133
|
|
|
134
134
|
// 2. Store latest posts to user-centric collection (for fallback)
|
|
135
135
|
// Path structure: SignedInUsers/{cid}/posts/{postId}
|
|
136
|
-
//
|
|
137
|
-
const
|
|
138
|
-
if (!getCollectionPath) {
|
|
139
|
-
throw new Error('collectionRegistry.getCollectionPath is required');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Get the base path template to extract collection name
|
|
143
|
-
const pathTemplate = getCollectionPath('signedInUsers', 'socialPosts', { cid: String(cid), postId: 'placeholder' });
|
|
144
|
-
// Extract collection name (first part before /)
|
|
145
|
-
const collectionName = pathTemplate.split('/')[0]; // Should be 'SignedInUsers'
|
|
136
|
+
// Construct path directly - we know the structure
|
|
137
|
+
const collectionName = 'SignedInUsers';
|
|
146
138
|
|
|
147
139
|
// Store each post individually in user-centric collection
|
|
148
140
|
const batch = db.batch();
|
|
@@ -309,16 +301,8 @@ async function storePopularInvestorSocialPosts({ db, logger, collectionRegistry,
|
|
|
309
301
|
|
|
310
302
|
// 2. Store latest posts to user-centric collection (for fallback)
|
|
311
303
|
// Path structure: PopularInvestors/{piCid}/posts/{postId}
|
|
312
|
-
//
|
|
313
|
-
const
|
|
314
|
-
if (!getCollectionPath) {
|
|
315
|
-
throw new Error('collectionRegistry.getCollectionPath is required');
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Get the base path template to extract collection name
|
|
319
|
-
const pathTemplate = getCollectionPath('popularInvestors', 'socialPosts', { piCid: String(cid), postId: 'placeholder' });
|
|
320
|
-
// Extract collection name (first part before /)
|
|
321
|
-
const collectionName = pathTemplate.split('/')[0]; // Should be 'PopularInvestors'
|
|
304
|
+
// Construct path directly - we know the structure
|
|
305
|
+
const collectionName = 'PopularInvestors';
|
|
322
306
|
|
|
323
307
|
// Store each post individually in user-centric collection
|
|
324
308
|
const batch = db.batch();
|
|
@@ -19,70 +19,69 @@ const {
|
|
|
19
19
|
// 1. SHARED INTERNAL HELPERS (The "Engine")
|
|
20
20
|
// ==========================================
|
|
21
21
|
|
|
22
|
-
async function fetchWithRetry(url, options, proxyManager, logger, label) {
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
let
|
|
26
|
-
let
|
|
22
|
+
async function fetchWithRetry(url, options, proxyManager, logger, label, headerManager = null) {
|
|
23
|
+
// SIMPLIFIED: Always try proxy first, then fallback to direct
|
|
24
|
+
// Use header manager if provided
|
|
25
|
+
let finalOptions = { ...options };
|
|
26
|
+
let headerId = null;
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
if (shouldTryProxy()) {
|
|
30
|
-
proxyAttempted = true;
|
|
28
|
+
if (headerManager) {
|
|
31
29
|
try {
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const errorText = await res.text().catch(() => 'Unable to read response');
|
|
40
|
-
logger.log('WARN', `[${label}] Proxy returned status ${res.status} for ${url}. Response: ${errorText.substring(0, 200)}`);
|
|
41
|
-
recordProxyOutcome(false);
|
|
42
|
-
proxyFailed = true;
|
|
30
|
+
const headerResult = await headerManager.selectHeader();
|
|
31
|
+
if (headerResult && headerResult.header) {
|
|
32
|
+
finalOptions.headers = {
|
|
33
|
+
...(finalOptions.headers || {}),
|
|
34
|
+
...headerResult.header
|
|
35
|
+
};
|
|
36
|
+
headerId = headerResult.id;
|
|
43
37
|
}
|
|
44
38
|
} catch (e) {
|
|
45
|
-
|
|
46
|
-
logger.log('WARN', `[${label}] Proxy failed for ${url}: ${e.message}. Failures: ${getFailureCount()}/${getMaxFailures()}. Falling back to direct fetch.`);
|
|
47
|
-
proxyFailed = true;
|
|
39
|
+
logger.log('WARN', `[${label}] Failed to get header from headerManager: ${e.message}`);
|
|
48
40
|
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Try proxy first (if circuit breaker allows)
|
|
44
|
+
if (shouldTryProxy()) {
|
|
53
45
|
try {
|
|
54
|
-
const res = await proxyManager.fetch(url,
|
|
46
|
+
const res = await proxyManager.fetch(url, finalOptions);
|
|
55
47
|
if (res.ok) {
|
|
56
|
-
// Success! Reset circuit breaker
|
|
57
48
|
recordProxyOutcome(true);
|
|
58
|
-
|
|
49
|
+
if (headerManager && headerId) {
|
|
50
|
+
headerManager.updatePerformance(headerId, true);
|
|
51
|
+
}
|
|
59
52
|
return res;
|
|
60
53
|
} else {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
// Proxy returned non-OK status - record failure but don't throw yet
|
|
55
|
+
const status = res.status;
|
|
56
|
+
recordProxyOutcome(false);
|
|
57
|
+
logger.log('WARN', `[${label}] Proxy returned status ${status} for ${url}. Failures: ${getFailureCount()}/${getMaxFailures()}. Will try direct fetch.`);
|
|
58
|
+
// Continue to fallback - don't throw here
|
|
64
59
|
}
|
|
65
60
|
} catch (e) {
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
recordProxyOutcome(false);
|
|
62
|
+
logger.log('WARN', `[${label}] Proxy exception for ${url}: ${e.message}. Failures: ${getFailureCount()}/${getMaxFailures()}. Will try direct fetch.`);
|
|
63
|
+
// Continue to fallback - don't throw here
|
|
68
64
|
}
|
|
65
|
+
} else {
|
|
66
|
+
logger.log('INFO', `[${label}] Circuit breaker open (${getFailureCount()}/${getMaxFailures()} failures), skipping proxy for ${url}`);
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
// Fallback to direct fetch only
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
69
|
+
// Fallback to direct fetch only if proxy failed or circuit breaker is open
|
|
70
|
+
logger.log('INFO', `[${label}] Using direct fetch for ${url}`);
|
|
71
|
+
const directFetch = typeof fetch !== 'undefined' ? fetch : require('node-fetch');
|
|
72
|
+
const res = await directFetch(url, finalOptions);
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
const errorText = await res.text().catch(() => 'Unable to read response');
|
|
75
|
+
logger.log('ERROR', `[${label}] Direct fetch failed for ${url}: Status ${res.status}. Response: ${errorText.substring(0, 200)}`);
|
|
76
|
+
throw new Error(`Fetch failed: ${res.status} ${res.statusText} - ${errorText.substring(0, 100)}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Update header manager on success
|
|
80
|
+
if (headerManager && headerId) {
|
|
81
|
+
headerManager.updatePerformance(headerId, true);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
throw new Error(`[${label}] Unexpected state: proxy attempted but no response returned`);
|
|
84
|
+
return res;
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
async function updateLastUpdated(db, collectionRegistry, cid, userType, dataType, logger) {
|
|
@@ -97,13 +96,13 @@ async function updateLastUpdated(db, collectionRegistry, cid, userType, dataType
|
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
async function processPortfolio(context, config, taskData, isPI) {
|
|
100
|
-
const { db, logger, collectionRegistry, proxyManager } = context;
|
|
99
|
+
const { db, logger, collectionRegistry, proxyManager, headerManager } = context;
|
|
101
100
|
const { cid, username, uuid, today, requestOptions } = taskData;
|
|
102
101
|
const url = `${config.ETORO_API_PORTFOLIO_URL}?cid=${cid}&client_request_id=${uuid}`;
|
|
103
102
|
|
|
104
103
|
logger.log('INFO', `[Portfolio] Fetching ${isPI ? 'PI' : 'Signed-In User'} portfolio from: ${url}`);
|
|
105
104
|
|
|
106
|
-
const res = await fetchWithRetry(url, requestOptions, proxyManager, logger, 'Portfolio');
|
|
105
|
+
const res = await fetchWithRetry(url, requestOptions, proxyManager, logger, 'Portfolio', headerManager);
|
|
107
106
|
const data = await res.json();
|
|
108
107
|
data.fetchedAt = new Date();
|
|
109
108
|
data.username = username;
|
|
@@ -116,7 +115,7 @@ async function processPortfolio(context, config, taskData, isPI) {
|
|
|
116
115
|
for (const pos of topPositions) {
|
|
117
116
|
try {
|
|
118
117
|
const posUrl = `${config.ETORO_API_POSITIONS_URL}?cid=${cid}&InstrumentID=${pos.InstrumentID}&client_request_id=${uuid}`;
|
|
119
|
-
const deepRes = await fetchWithRetry(posUrl, requestOptions, proxyManager, logger, 'DeepPos');
|
|
118
|
+
const deepRes = await fetchWithRetry(posUrl, requestOptions, proxyManager, logger, 'DeepPos', headerManager);
|
|
120
119
|
deepPositions.push({ instrumentId: pos.InstrumentID, ...(await deepRes.json()) });
|
|
121
120
|
} catch (e) {} // Skip failed deep positions
|
|
122
121
|
}
|
|
@@ -133,12 +132,12 @@ async function processPortfolio(context, config, taskData, isPI) {
|
|
|
133
132
|
}
|
|
134
133
|
|
|
135
134
|
async function processHistory(context, config, taskData, isPI) {
|
|
136
|
-
const { db, logger, collectionRegistry, proxyManager } = context;
|
|
135
|
+
const { db, logger, collectionRegistry, proxyManager, headerManager } = context;
|
|
137
136
|
const { cid, uuid, today, requestOptions } = taskData;
|
|
138
137
|
const oneYearAgo = new Date(); oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
|
139
138
|
const url = `${config.ETORO_API_HISTORY_URL}?StartTime=${oneYearAgo.toISOString()}&PageNumber=1&ItemsPerPage=30000&PublicHistoryPortfolioFilter=&CID=${cid}&client_request_id=${uuid}`;
|
|
140
139
|
|
|
141
|
-
const res = await fetchWithRetry(url, requestOptions, proxyManager, logger, 'History');
|
|
140
|
+
const res = await fetchWithRetry(url, requestOptions, proxyManager, logger, 'History', headerManager);
|
|
142
141
|
const data = await res.json();
|
|
143
142
|
|
|
144
143
|
// Filter valid close reasons
|
|
@@ -154,7 +153,7 @@ async function processHistory(context, config, taskData, isPI) {
|
|
|
154
153
|
}
|
|
155
154
|
|
|
156
155
|
async function processSocial(context, config, taskData, isPI) {
|
|
157
|
-
const { db, logger, collectionRegistry, proxyManager } = context;
|
|
156
|
+
const { db, logger, collectionRegistry, proxyManager, headerManager } = context;
|
|
158
157
|
const { cid, username, uuid, today, requestOptions } = taskData;
|
|
159
158
|
const { getGcidForUser } = require('../../social-task-handler/helpers/handler_helpers');
|
|
160
159
|
|
|
@@ -162,7 +161,7 @@ async function processSocial(context, config, taskData, isPI) {
|
|
|
162
161
|
const gcid = await getGcidForUser(context, config.social || {}, cid, username);
|
|
163
162
|
const url = `${config.social?.userFeedApiUrl || 'https://www.etoro.com/api/edm-streams/v1/feed/user/top/'}${gcid}?take=10&client_request_id=${uuid}`;
|
|
164
163
|
|
|
165
|
-
const res = await fetchWithRetry(url, requestOptions, proxyManager, logger, 'Social');
|
|
164
|
+
const res = await fetchWithRetry(url, requestOptions, proxyManager, logger, 'Social', headerManager);
|
|
166
165
|
const data = await res.json();
|
|
167
166
|
const posts = (data.discussions || []).slice(0, 30).map(d => ({
|
|
168
167
|
id: d.post.id,
|