bulltrackers-module 1.0.17 → 1.0.19

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.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @fileoverview Core logic for fetching eToro instrument insights.
3
+ */
4
+
5
+ const { FieldValue } = require('@google-cloud/firestore');
6
+
7
+ /**
8
+ * Fetches insights data using header and proxy managers and stores it in Firestore.
9
+ *
10
+ * @param {object} firestore - An initialized Firestore client instance.
11
+ * @param {object} logger - A logger instance.
12
+ * @param {object} headerManager - An initialized IntelligentHeaderManager instance.
13
+ * @param {object} proxyManager - An initialized IntelligentProxyManager instance.
14
+ * @param {object} config - Configuration object.
15
+ * @param {string} config.etoroInsightsUrl - The URL for the eToro insights API.
16
+ * @param {string} config.insightsCollectionName - Firestore collection name for results.
17
+ * @returns {Promise<{success: boolean, message: string, instrumentCount?: number}>}
18
+ */
19
+ exports.fetchAndStoreInsights = async (firestore, logger, headerManager, proxyManager, config) => {
20
+ logger.log('INFO', '[FetchInsightsHelpers] Starting eToro insights data fetch...');
21
+ let selectedHeader = null;
22
+ let wasSuccessful = false;
23
+
24
+ try {
25
+ // Validate config
26
+ if (!config.etoroInsightsUrl || !config.insightsCollectionName) {
27
+ throw new Error("Missing required configuration: etoroInsightsUrl or insightsCollectionName.");
28
+ }
29
+
30
+ selectedHeader = await headerManager.selectHeader();
31
+ if (!selectedHeader) {
32
+ throw new Error("Could not select a header for the request.");
33
+ }
34
+
35
+ logger.log('INFO', `[FetchInsightsHelpers] Using header ID: ${selectedHeader.id}`, {
36
+ userAgent: selectedHeader.header['User-Agent'] // Access header data correctly
37
+ });
38
+
39
+ // Make the API request via the proxy manager
40
+ // Pass only the necessary parts of the selected header object
41
+ const fetchOptions = {
42
+ headers: selectedHeader.header, // Pass the actual header object
43
+ timeout: 30000 // 30 second timeout - Note: Proxy manager might have its own timeout logic
44
+ };
45
+ const { response } = await proxyManager.fetch(config.etoroInsightsUrl, fetchOptions);
46
+
47
+ if (!response.ok) {
48
+ const errorText = await response.text();
49
+ // No need to update header performance here, proxy manager handles failures implicitly if needed
50
+ throw new Error(`API request failed via proxy with status ${response.status}: ${errorText}`);
51
+ }
52
+
53
+ // If fetch succeeded via proxy, mark header as successful
54
+ wasSuccessful = true; // Mark success for header update
55
+
56
+ const insightsData = await response.json();
57
+
58
+ if (!Array.isArray(insightsData) || insightsData.length === 0) {
59
+ throw new Error('API returned empty or invalid data.');
60
+ }
61
+
62
+ // Store in Firestore
63
+ const today = new Date().toISOString().slice(0, 10);
64
+ const docRef = firestore.collection(config.insightsCollectionName).doc(today);
65
+
66
+ const firestorePayload = {
67
+ fetchedAt: FieldValue.serverTimestamp(),
68
+ instrumentCount: insightsData.length,
69
+ insights: insightsData
70
+ };
71
+
72
+ await docRef.set(firestorePayload);
73
+
74
+ const successMsg = `Successfully fetched and stored ${insightsData.length} instrument insights for ${today}.`;
75
+ logger.log('SUCCESS', `[FetchInsightsHelpers] ${successMsg}`, {
76
+ documentId: today,
77
+ instrumentCount: insightsData.length
78
+ });
79
+ return { success: true, message: successMsg, instrumentCount: insightsData.length };
80
+
81
+ } catch (error) {
82
+ logger.log('ERROR', '[FetchInsightsHelpers] Error fetching eToro insights', {
83
+ errorMessage: error.message,
84
+ errorStack: error.stack,
85
+ headerId: selectedHeader ? selectedHeader.id : 'not-selected'
86
+ });
87
+ // Let the calling function handle header update on failure
88
+ throw error; // Re-throw the error to be caught by the handler
89
+ } finally {
90
+ // Update header performance based on the success *of the target API call*
91
+ if (selectedHeader) {
92
+ await headerManager.updatePerformance(selectedHeader.id, wasSuccessful);
93
+ }
94
+ }
95
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @fileoverview Exports the FetchInsights handler creator.
3
+ */
4
+
5
+ const { Firestore } = require('@google-cloud/firestore');
6
+ const { logger } = require("sharedsetup")(__filename);
7
+ const { IntelligentHeaderManager, IntelligentProxyManager } = require('../core/utils');
8
+ const { fetchAndStoreInsights } = require('./helpers/handler_helpers');
9
+
10
+ /**
11
+ * Creates the Cloud Function handler for fetching eToro insights.
12
+ * @param {object} config - The configuration object loaded from the calling function's context.
13
+ * @returns {Function} The async Cloud Function handler (Express middleware format).
14
+ */
15
+ function createFetchInsightsHandler(config) {
16
+ // Initialize clients once when the function instance starts
17
+ const firestore = new Firestore();
18
+ const headerManager = new IntelligentHeaderManager(firestore, logger, config); // Pass dependencies
19
+ const proxyManager = new IntelligentProxyManager(firestore, logger, config); // Pass dependencies
20
+
21
+ return async (req, res) => {
22
+ logger.log('INFO', '🚀 Fetch eToro Insights triggered via module...');
23
+ try {
24
+ // Delegate all logic to the helper
25
+ const result = await fetchAndStoreInsights(firestore, logger, headerManager, proxyManager, config);
26
+
27
+ // Send success response
28
+ res.status(200).send(result.message);
29
+
30
+ } catch (error) {
31
+ logger.log('ERROR', 'FATAL Error in Fetch eToro Insights (Module)', { errorMessage: error.message, errorStack: error.stack });
32
+ // Header performance update on failure is handled within the helper's finally block if header was selected
33
+
34
+ // Ensure header performance flush happens even on fatal error before responding
35
+ try {
36
+ await headerManager.flushPerformanceUpdates();
37
+ } catch (flushError) {
38
+ logger.log('ERROR', 'Error flushing header performance during error handling', { flushError: flushError.message });
39
+ }
40
+
41
+ // Send error response
42
+ res.status(500).send(`An internal error occurred: ${error.message}`);
43
+ } finally {
44
+ // Always attempt to flush performance updates at the end of the function execution
45
+ try {
46
+ await headerManager.flushPerformanceUpdates();
47
+ } catch (flushError) {
48
+ logger.log('ERROR', 'Error flushing header performance in final finally block', { flushError: flushError.message });
49
+ }
50
+ }
51
+ };
52
+ }
53
+
54
+ module.exports = {
55
+ createFetchInsightsHandler,
56
+ helpers: { fetchAndStoreInsights } // Export helpers if needed directly
57
+ };
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @fileoverview Core logic for the speculator cleanup orchestrator.
3
+ */
4
+
5
+ const { FieldValue } = require('@google-cloud/firestore');
6
+
7
+ /**
8
+ * Cleans up stale entries from the pending speculators collection.
9
+ * @param {object} firestore - An initialized Firestore client.
10
+ * @param {object} logger - A logger instance.
11
+ * @param {object} config - Configuration object.
12
+ * @param {string} config.pendingSpeculatorsCollectionName - Name of the pending collection.
13
+ * @param {number} config.pendingGracePeriodHours - Hours before a pending entry is considered stale.
14
+ * @returns {Promise<{ batch: object, count: number }>} - The batch object with updates and the count of removed users.
15
+ */
16
+ async function cleanupPendingSpeculators(firestore, logger, config) {
17
+ logger.log('INFO', '[CleanupHelpers] Starting pending speculator cleanup...');
18
+ const batch = firestore.batch();
19
+ let stalePendingUsersRemoved = 0;
20
+ const pendingCollectionRef = firestore.collection(config.pendingSpeculatorsCollectionName);
21
+ const staleThreshold = new Date();
22
+ staleThreshold.setHours(staleThreshold.getHours() - config.pendingGracePeriodHours);
23
+
24
+ try {
25
+ const pendingSnapshot = await pendingCollectionRef.get();
26
+ if (pendingSnapshot.empty) {
27
+ logger.log('INFO', '[CleanupHelpers] Pending speculators collection is empty.');
28
+ return { batch, count: 0 };
29
+ }
30
+
31
+ for (const doc of pendingSnapshot.docs) {
32
+ const pendingData = doc.data().users || {};
33
+ const updates = {};
34
+ let updatesInDoc = 0;
35
+
36
+ for (const userId in pendingData) {
37
+ // Ensure timestamp exists and is a Firestore Timestamp before converting
38
+ const timestamp = pendingData[userId]?.toDate ? pendingData[userId].toDate() : null;
39
+ if (timestamp && timestamp < staleThreshold) {
40
+ updates[`users.${userId}`] = FieldValue.delete();
41
+ stalePendingUsersRemoved++;
42
+ updatesInDoc++;
43
+ }
44
+ }
45
+
46
+ if (updatesInDoc > 0) {
47
+ logger.log('TRACE', `[CleanupHelpers] Marking ${updatesInDoc} users for removal from pending doc ${doc.id}`);
48
+ batch.update(doc.ref, updates);
49
+ }
50
+ }
51
+ logger.log('INFO', `[CleanupHelpers] Marked ${stalePendingUsersRemoved} total stale pending users for removal.`);
52
+ } catch (error) {
53
+ logger.log('ERROR', '[CleanupHelpers] Error cleaning pending speculators', { errorMessage: error.message });
54
+ throw error; // Re-throw to be caught by the main handler
55
+ }
56
+ return { batch, count: stalePendingUsersRemoved };
57
+ }
58
+
59
+ /**
60
+ * Cleans up stale speculators from the main blocks based on inactivity grace period.
61
+ * @param {object} firestore - An initialized Firestore client.
62
+ * @param {object} logger - A logger instance.
63
+ * @param {object} config - Configuration object.
64
+ * @param {string} config.speculatorBlocksCollectionName - Name of the speculator blocks collection.
65
+ * @param {string} config.speculatorBlockCountsDocPath - Path to the block counts document.
66
+ * @param {number} config.activityGracePeriodDays - Days of inactivity before removal.
67
+ * @param {object} batch - The Firestore batch object to add updates to.
68
+ * @returns {Promise<{ batch: object, count: number }>} - The batch object with updates and the count of removed users.
69
+ */
70
+ async function cleanupStaleSpeculators(firestore, logger, config, batch) {
71
+ logger.log('INFO', '[CleanupHelpers] Starting stale speculator cleanup from blocks...');
72
+ let totalUsersRemoved = 0;
73
+ const blocksCollectionRef = firestore.collection(config.speculatorBlocksCollectionName);
74
+ const gracePeriodDate = new Date();
75
+ gracePeriodDate.setDate(gracePeriodDate.getDate() - config.activityGracePeriodDays);
76
+ const blockCountsUpdate = {}; // To track decrements per instrument/block
77
+
78
+ try {
79
+ const blocksSnapshot = await blocksCollectionRef.get();
80
+ if (blocksSnapshot.empty) {
81
+ logger.log('INFO', '[CleanupHelpers] Speculator blocks collection is empty.');
82
+ return { batch, count: 0 };
83
+ }
84
+
85
+ for (const doc of blocksSnapshot.docs) {
86
+ const blockId = doc.id; // e.g., "19000000"
87
+ const blockData = doc.data();
88
+ // Firestore map keys cannot contain '.', so the keys are likely 'users.123456'
89
+ const users = blockData.users || {};
90
+ let usersRemovedFromBlock = 0;
91
+ const updates = {}; // Updates specific to this block document
92
+
93
+ for (const userKey in users) { // userKey is like "users.123456"
94
+ // Extract CID correctly
95
+ const userId = userKey.split('.')[1];
96
+ if (!userId) continue; // Skip malformed keys
97
+
98
+ const userData = users[userKey];
99
+ // Ensure timestamp exists and is a Firestore Timestamp before converting
100
+ const lastHeldTimestamp = userData.lastHeldSpeculatorAsset?.toDate ? userData.lastHeldSpeculatorAsset.toDate() : null;
101
+
102
+ // Check if the user is stale based on the grace period
103
+ if (lastHeldTimestamp && lastHeldTimestamp < gracePeriodDate) {
104
+ updates[userKey] = FieldValue.delete(); // Delete the specific user field
105
+ usersRemovedFromBlock++;
106
+
107
+ // Decrement block counts for each instrument this user held in this block
108
+ if (userData.instruments && Array.isArray(userData.instruments)) {
109
+ userData.instruments.forEach(instrumentId => {
110
+ const instrumentBlockKey = `${instrumentId}_${blockId}`;
111
+ if (!blockCountsUpdate[instrumentBlockKey]) {
112
+ blockCountsUpdate[instrumentBlockKey] = 0;
113
+ }
114
+ blockCountsUpdate[instrumentBlockKey]--; // Decrement count
115
+ });
116
+ }
117
+ }
118
+ }
119
+
120
+ // If users were marked for removal in this block, add the update to the batch
121
+ if (usersRemovedFromBlock > 0) {
122
+ logger.log('TRACE', `[CleanupHelpers] Marking ${usersRemovedFromBlock} users for removal from block ${blockId}.`);
123
+ batch.update(doc.ref, updates);
124
+ totalUsersRemoved += usersRemovedFromBlock;
125
+ }
126
+ }
127
+
128
+ // After iterating all blocks, if any users were removed, add the block count decrements to the batch
129
+ if (totalUsersRemoved > 0 && Object.keys(blockCountsUpdate).length > 0) {
130
+ const countsRef = firestore.doc(config.speculatorBlockCountsDocPath);
131
+ const finalCountUpdates = {};
132
+ for (const key in blockCountsUpdate) {
133
+ // Use dot notation for nested map fields
134
+ finalCountUpdates[`counts.${key}`] = FieldValue.increment(blockCountsUpdate[key]);
135
+ }
136
+ logger.log('TRACE', '[CleanupHelpers] Staging block count decrements.', { updates: finalCountUpdates });
137
+ batch.set(countsRef, finalCountUpdates, { merge: true }); // Use set with merge: true
138
+ }
139
+
140
+ logger.log('INFO', `[CleanupHelpers] Marked ${totalUsersRemoved} total stale speculators for removal from blocks.`);
141
+
142
+ } catch (error) {
143
+ logger.log('ERROR', '[CleanupHelpers] Error cleaning stale speculators', { errorMessage: error.message });
144
+ throw error; // Re-throw
145
+ }
146
+
147
+ return { batch, count: totalUsersRemoved };
148
+ }
149
+
150
+ /**
151
+ * Orchestrates the cleanup process.
152
+ * @param {object} firestore - An initialized Firestore client.
153
+ * @param {object} logger - A logger instance.
154
+ * @param {object} config - Configuration object.
155
+ */
156
+ exports.runCleanup = async (firestore, logger, config) => {
157
+ logger.log('INFO', '[CleanupHelpers] Running cleanup orchestrator...');
158
+
159
+ try {
160
+ // Start cleanup for pending users
161
+ const { batch: batchAfterPending, count: pendingRemoved } = await cleanupPendingSpeculators(firestore, logger, config);
162
+
163
+ // Continue with the same batch for stale speculators
164
+ const { batch: finalBatch, count: staleRemoved } = await cleanupStaleSpeculators(firestore, logger, config, batchAfterPending);
165
+
166
+ // Commit the batch if any changes were made
167
+ if (pendingRemoved > 0 || staleRemoved > 0) {
168
+ await finalBatch.commit();
169
+ logger.log('SUCCESS', `[CleanupHelpers] Cleanup commit successful. Removed ${pendingRemoved} pending, ${staleRemoved} stale speculators.`);
170
+ return { pendingRemoved, staleRemoved };
171
+ } else {
172
+ logger.log('SUCCESS', '[CleanupHelpers] No stale users found in pending or blocks.');
173
+ return { pendingRemoved: 0, staleRemoved: 0 };
174
+ }
175
+ } catch (error) {
176
+ logger.log('ERROR', '[CleanupHelpers] FATAL error during cleanup orchestration', { errorMessage: error.message, errorStack: error.stack });
177
+ throw error; // Re-throw for the main handler
178
+ }
179
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @fileoverview Exports the SpeculatorCleanupOrchestrator handler creator.
3
+ */
4
+
5
+ const { Firestore } = require('@google-cloud/firestore');
6
+ const { logger } = require("sharedsetup")(__filename);
7
+ const { runCleanup } = require('./helpers/cleanup_helpers');
8
+
9
+ /**
10
+ * Creates the Cloud Function handler for speculator cleanup.
11
+ * @param {object} config - The configuration object loaded from the calling function's context.
12
+ * @returns {Function} The async Cloud Function handler (Express middleware format).
13
+ */
14
+ function createSpeculatorCleanupHandler(config) {
15
+ // Initialize Firestore client once when the function instance starts
16
+ const firestore = new Firestore();
17
+
18
+ return async (req, res) => {
19
+ logger.log('INFO', '🚀 Speculator Cleanup Orchestrator triggered via module...');
20
+ try {
21
+ // Validate essential config
22
+ if (!config || !config.speculatorBlocksCollectionName || !config.speculatorBlockCountsDocPath || !config.pendingSpeculatorsCollectionName) {
23
+ throw new Error("Speculator Cleanup Orchestrator received invalid configuration.");
24
+ }
25
+
26
+ // Delegate all logic to the helper
27
+ const { pendingRemoved, staleRemoved } = await runCleanup(firestore, logger, config);
28
+
29
+ // Send success response
30
+ res.status(200).send(`Cleanup complete via module. Removed ${staleRemoved} stale speculators, ${pendingRemoved} stale pending users.`);
31
+
32
+ } catch (error) {
33
+ logger.log('ERROR', 'FATAL Error in Speculator Cleanup Orchestrator (Module)', { errorMessage: error.message, errorStack: error.stack });
34
+ // Send error response
35
+ res.status(500).send("An internal cleanup error occurred (via module).");
36
+ }
37
+ };
38
+ }
39
+
40
+ module.exports = {
41
+ createSpeculatorCleanupHandler,
42
+ helpers: { runCleanup } // Export helpers if needed directly
43
+ };
package/index.js CHANGED
@@ -11,6 +11,8 @@ const ComputationSystem = require('./functions/computation-system');
11
11
  const GenericAPI = require('./functions/generic-api'); // <-- ADD THIS
12
12
  const Dispatcher = require('./functions/dispatcher'); // <-- ADD THIS
13
13
  const InvalidSpeculatorHandler = require('./functions/invalid-speculator-handler'); // <-- ADD THIS
14
+ const SpeculatorCleanupOrchestrator = require('./functions/speculator-cleanup-orchestrator'); // <-- ADD THIS
15
+ const FetchInsights = require('./functions/fetch-insights'); // <-- ADD THIS
14
16
 
15
17
  module.exports = {
16
18
  core,
@@ -20,4 +22,6 @@ module.exports = {
20
22
  GenericAPI, // <-- AND ADD THIS
21
23
  Dispatcher, // <-- AND ADD THIS
22
24
  InvalidSpeculatorHandler, // <-- AND ADD THIS
25
+ SpeculatorCleanupOrchestrator, // <-- AND ADD THIS
26
+ FetchInsights, // <-- AND ADD THIS
23
27
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -11,7 +11,9 @@
11
11
  "functions/computation-system/",
12
12
  "functions/generic-api/",
13
13
  "functions/dispatcher/",
14
- "functions/invalid-speculator-handler/"
14
+ "functions/invalid-speculator-handler/",
15
+ "functions/speculator-cleanup-orchestrator/",
16
+ "functions/fetch-insights/"
15
17
  ],
16
18
  "scripts": {
17
19
  "test": "echo \"Error: no test specified\" && exit 1"