bulltrackers-module 1.0.17 → 1.0.18

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,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,7 @@ 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
14
15
 
15
16
  module.exports = {
16
17
  core,
@@ -20,4 +21,5 @@ module.exports = {
20
21
  GenericAPI, // <-- AND ADD THIS
21
22
  Dispatcher, // <-- AND ADD THIS
22
23
  InvalidSpeculatorHandler, // <-- AND ADD THIS
24
+ SpeculatorCleanupOrchestrator // <-- AND ADD THIS
23
25
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -11,7 +11,8 @@
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/"
15
16
  ],
16
17
  "scripts": {
17
18
  "test": "echo \"Error: no test specified\" && exit 1"