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.
|
|
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"
|