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
- // We need to construct this step-by-step, not use the template directly
137
- const { getCollectionPath } = collectionRegistry || {};
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
- // We need to construct this step-by-step, not use the template directly
313
- const { getCollectionPath } = collectionRegistry || {};
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
- // ALWAYS try proxy first, regardless of circuit breaker state
24
- // Circuit breaker only prevents retrying after it's open, but we always attempt proxy first
25
- let proxyAttempted = false;
26
- let proxyFailed = false;
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
- // Try proxy first (unless circuit breaker is open and we've already failed)
29
- if (shouldTryProxy()) {
30
- proxyAttempted = true;
28
+ if (headerManager) {
31
29
  try {
32
- const res = await proxyManager.fetch(url, options);
33
- if (res.ok) {
34
- recordProxyOutcome(true);
35
- logger.log('TRACE', `[${label}] Proxy fetch succeeded for ${url}`);
36
- return res;
37
- } else {
38
- // Log proxy failure with details
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
- recordProxyOutcome(false);
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
- } else {
50
- // Circuit breaker is open - log but still try proxy once more
51
- logger.log('INFO', `[${label}] Circuit breaker open (${getFailureCount()}/${getMaxFailures()} failures), but attempting proxy once more for ${url}`);
52
- proxyAttempted = true;
41
+ }
42
+
43
+ // Try proxy first (if circuit breaker allows)
44
+ if (shouldTryProxy()) {
53
45
  try {
54
- const res = await proxyManager.fetch(url, options);
46
+ const res = await proxyManager.fetch(url, finalOptions);
55
47
  if (res.ok) {
56
- // Success! Reset circuit breaker
57
48
  recordProxyOutcome(true);
58
- logger.log('INFO', `[${label}] Proxy succeeded despite circuit breaker being open. Resetting circuit breaker.`);
49
+ if (headerManager && headerId) {
50
+ headerManager.updatePerformance(headerId, true);
51
+ }
59
52
  return res;
60
53
  } else {
61
- const errorText = await res.text().catch(() => 'Unable to read response');
62
- logger.log('WARN', `[${label}] Proxy failed (circuit breaker open): Status ${res.status}. Response: ${errorText.substring(0, 200)}`);
63
- proxyFailed = true;
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
- logger.log('WARN', `[${label}] Proxy failed (circuit breaker open): ${e.message}`);
67
- proxyFailed = true;
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 after proxy fails
72
- if (proxyFailed || !proxyAttempted) {
73
- logger.log('INFO', `[${label}] Falling back to direct fetch for ${url}${proxyFailed ? ' (proxy failed)' : ''}`);
74
- const directFetch = typeof fetch !== 'undefined' ? fetch : require('node-fetch');
75
- const res = await directFetch(url, options);
76
- if (!res.ok) {
77
- const errorText = await res.text().catch(() => 'Unable to read response');
78
- logger.log('ERROR', `[${label}] Direct fetch failed for ${url}: Status ${res.status}. Response: ${errorText.substring(0, 200)}`);
79
- throw new Error(`Fetch failed: ${res.status} ${res.statusText} - ${errorText.substring(0, 100)}`);
80
- }
81
- return res;
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
- // Should not reach here, but just in case
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.544",
3
+ "version": "1.0.545",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [