bulltrackers-module 1.0.180 → 1.0.182
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.
|
@@ -6,13 +6,15 @@ const {
|
|
|
6
6
|
groupByPass,
|
|
7
7
|
checkRootDataAvailability,
|
|
8
8
|
fetchExistingResults,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
fetchComputationStatus,
|
|
10
|
+
updateComputationStatus,
|
|
11
11
|
runStandardComputationPass,
|
|
12
12
|
runMetaComputationPass,
|
|
13
13
|
checkRootDependencies
|
|
14
14
|
} = require('./orchestration_helpers.js');
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
const { getExpectedDateStrings, normalizeName } = require('../utils/utils.js');
|
|
17
|
+
|
|
16
18
|
const PARALLEL_BATCH_SIZE = 7;
|
|
17
19
|
|
|
18
20
|
async function runComputationPass(config, dependencies, computationManifest) {
|
|
@@ -21,7 +23,7 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
21
23
|
if (!passToRun)
|
|
22
24
|
return logger.log('ERROR', '[PassRunner] No pass defined. Aborting.');
|
|
23
25
|
|
|
24
|
-
logger.log('INFO', `🚀 Starting PASS ${passToRun}
|
|
26
|
+
logger.log('INFO', `🚀 Starting PASS ${passToRun} (Targeting /computation_status/{YYYY-MM-DD})...`);
|
|
25
27
|
|
|
26
28
|
// Hardcoded earliest dates
|
|
27
29
|
const earliestDates = { portfolio: new Date('2025-09-25T00:00:00Z'), history: new Date('2025-11-05T00:00:00Z'), social: new Date('2025-10-30T00:00:00Z'), insights: new Date('2025-08-26T00:00:00Z') };
|
|
@@ -40,36 +42,47 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
40
42
|
const standardCalcs = calcsInThisPass.filter(c => c.type === 'standard');
|
|
41
43
|
const metaCalcs = calcsInThisPass.filter(c => c.type === 'meta');
|
|
42
44
|
|
|
43
|
-
//
|
|
44
|
-
// Returns { "2023-10-27": { calcA: true, calcB: false }, ... }
|
|
45
|
-
const globalStatusData = await fetchGlobalComputationStatus(config, dependencies);
|
|
46
|
-
|
|
47
|
-
// Helper: Check status using in-memory data
|
|
48
|
-
const shouldRun = (calc, dateStr) => {
|
|
49
|
-
const dailyStatus = globalStatusData[dateStr] || {};
|
|
50
|
-
|
|
51
|
-
// 1. If explicitly TRUE, ignore.
|
|
52
|
-
if (dailyStatus[calc.name] === true) return false;
|
|
53
|
-
|
|
54
|
-
// 2. Check dependencies (using same in-memory status)
|
|
55
|
-
if (calc.dependencies && calc.dependencies.length > 0) {
|
|
56
|
-
const depsMet = calc.dependencies.every(depName => dailyStatus[depName] === true);
|
|
57
|
-
if (!depsMet) return false;
|
|
58
|
-
}
|
|
59
|
-
return true;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// Process a single date and RETURN updates (do not write)
|
|
45
|
+
// Process a single date
|
|
63
46
|
const processDate = async (dateStr) => {
|
|
64
47
|
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
65
|
-
|
|
66
|
-
|
|
48
|
+
|
|
49
|
+
// 1. Fetch Status for THIS specific date only
|
|
50
|
+
// This ensures Pass 2 sees exactly what Pass 1 wrote for this date.
|
|
51
|
+
const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
|
|
52
|
+
|
|
53
|
+
// Helper: Check status using the fetched daily data
|
|
54
|
+
const shouldRun = (calc) => {
|
|
55
|
+
const cName = normalizeName(calc.name);
|
|
56
|
+
|
|
57
|
+
// A. If recorded as TRUE -> Ignore (already ran)
|
|
58
|
+
if (dailyStatus[cName] === true) return false;
|
|
59
|
+
|
|
60
|
+
// B. If recorded as FALSE or UNDEFINED -> Run it (retry or new)
|
|
61
|
+
// But first, check if we have the necessary data dependencies.
|
|
62
|
+
|
|
63
|
+
if (calc.dependencies && calc.dependencies.length > 0) {
|
|
64
|
+
// Check if prerequisites (from previous passes on THIS date) are complete
|
|
65
|
+
const missing = calc.dependencies.filter(depName => dailyStatus[normalizeName(depName)] !== true);
|
|
66
|
+
if (missing.length > 0) {
|
|
67
|
+
// Dependency missing: cannot run yet.
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If we are here, status is false/undefined AND dependencies are met.
|
|
73
|
+
return true;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const standardToRun = standardCalcs.filter(shouldRun);
|
|
77
|
+
const metaToRun = metaCalcs.filter(shouldRun);
|
|
67
78
|
|
|
68
|
-
if (!standardToRun.length && !metaToRun.length) return null; // No work
|
|
79
|
+
if (!standardToRun.length && !metaToRun.length) return null; // No work for this date
|
|
69
80
|
|
|
81
|
+
// 2. Check Root Data Availability (Portfolio, History, etc.)
|
|
70
82
|
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
|
|
71
83
|
if (!rootData) return null;
|
|
72
84
|
|
|
85
|
+
// 3. Filter again based on Root Data availability
|
|
73
86
|
const finalStandardToRun = standardToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
|
|
74
87
|
const finalMetaToRun = metaToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
|
|
75
88
|
|
|
@@ -85,52 +98,35 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
85
98
|
const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
86
99
|
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
87
100
|
const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
|
|
101
|
+
|
|
102
|
+
// Note: We use skipStatusWrite=true because we want to batch write the status at the end of this function
|
|
88
103
|
if (finalStandardToRun.length) {
|
|
89
|
-
const updates = await runStandardComputationPass(dateToProcess, finalStandardToRun, `Pass ${passToRun} (Std)`, config, dependencies, rootData, existingResults, previousResults, true);
|
|
104
|
+
const updates = await runStandardComputationPass(dateToProcess, finalStandardToRun, `Pass ${passToRun} (Std)`, config, dependencies, rootData, existingResults, previousResults, true);
|
|
90
105
|
Object.assign(dateUpdates, updates);
|
|
91
106
|
}
|
|
92
107
|
if (finalMetaToRun.length) {
|
|
93
|
-
const updates = await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, true);
|
|
108
|
+
const updates = await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, true);
|
|
94
109
|
Object.assign(dateUpdates, updates);
|
|
95
110
|
}
|
|
96
111
|
} catch (err) {
|
|
97
112
|
logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message });
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
[...finalStandardToRun, ...finalMetaToRun].forEach(c => dateUpdates[normalizeName(c.name)] = false);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 4. Write "true" or "false" results for THIS specific date immediately
|
|
117
|
+
if (Object.keys(dateUpdates).length > 0) {
|
|
118
|
+
await updateComputationStatus(dateStr, dateUpdates, config, dependencies);
|
|
100
119
|
}
|
|
101
120
|
|
|
102
|
-
// Return the updates for this date
|
|
103
121
|
return { date: dateStr, updates: dateUpdates };
|
|
104
122
|
};
|
|
105
123
|
|
|
106
124
|
// Batch process dates
|
|
107
125
|
for (let i = 0; i < allExpectedDates.length; i += PARALLEL_BATCH_SIZE) {
|
|
108
126
|
const batch = allExpectedDates.slice(i, i + PARALLEL_BATCH_SIZE);
|
|
109
|
-
|
|
110
|
-
// Run batch in parallel
|
|
111
|
-
const results = await Promise.all(batch.map(processDate));
|
|
112
|
-
|
|
113
|
-
// Aggregate updates from the batch
|
|
114
|
-
const batchUpdates = {};
|
|
115
|
-
let hasUpdates = false;
|
|
116
|
-
|
|
117
|
-
results.forEach(res => {
|
|
118
|
-
if (res && res.updates && Object.keys(res.updates).length > 0) {
|
|
119
|
-
batchUpdates[res.date] = res.updates;
|
|
120
|
-
hasUpdates = true;
|
|
121
|
-
|
|
122
|
-
// Also update our local in-memory copy so subsequent logic in this run sees it (though passes usually rely on prev days)
|
|
123
|
-
if (!globalStatusData[res.date]) globalStatusData[res.date] = {};
|
|
124
|
-
Object.assign(globalStatusData[res.date], res.updates);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Write status ONCE per batch
|
|
129
|
-
if (hasUpdates) {
|
|
130
|
-
await updateGlobalComputationStatus(batchUpdates, config, dependencies);
|
|
131
|
-
logger.log('INFO', `[PassRunner] Batched status update for ${Object.keys(batchUpdates).length} dates.`);
|
|
132
|
-
}
|
|
127
|
+
await Promise.all(batch.map(processDate));
|
|
133
128
|
}
|
|
129
|
+
|
|
134
130
|
logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
|
|
135
131
|
}
|
|
136
132
|
|
|
@@ -1,251 +1,219 @@
|
|
|
1
|
-
/**
|
|
2
|
-
@fileoverview Utility class to manage stateful Firestore write batches.
|
|
3
|
-
|
|
1
|
+
/** @fileoverview Utility class to manage stateful Firestore write batches.
|
|
4
2
|
REFACTORED: Renamed 'firestore' to 'db' for consistency.
|
|
5
|
-
|
|
6
3
|
OPTIMIZED: Added logic to handle speculator timestamp fixes within the batch.
|
|
7
|
-
|
|
8
4
|
--- MODIFIED: Added username map caching and trading history batching. ---
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
--- FIXED: Implemented deterministic sharding (part_N) to prevent document fragmentation. --- **/
|
|
5
|
+
--- FIXED: Implemented Modulo Sharding to pack sparse IDs into dense documents. ---
|
|
6
|
+
--- FIXED: Removed aggressive auto-flush timeout to prevent cost explosion. ---
|
|
7
|
+
**/
|
|
13
8
|
|
|
14
9
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
15
10
|
|
|
16
|
-
class FirestoreBatchManager {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
logger.log('INFO', 'FirestoreBatchManager initialized.');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/* * NEW: Checks if a user's history has been fetched in the last 10 minutes.
|
|
38
|
-
* If not, it logs them as fetched and returns false (to trigger a fetch).
|
|
39
|
-
* @param {string} userId
|
|
40
|
-
* @returns {boolean} True if already fetched, false if not.
|
|
41
|
-
*/
|
|
42
|
-
checkAndSetHistoryFetched(userId) {
|
|
43
|
-
// Check if the cache is stale
|
|
44
|
-
if (Date.now() - this.historyCacheTimestamp > this.HISTORY_CACHE_TTL_MS) {
|
|
45
|
-
this.logger.log('INFO', '[BATCH] History fetch cache (10m TTL) expired. Clearing set.');
|
|
46
|
-
this.historyFetchedUserIds.clear();
|
|
11
|
+
class FirestoreBatchManager {
|
|
12
|
+
constructor(db, headerManager, logger, config) {
|
|
13
|
+
this.db = db;
|
|
14
|
+
this.headerManager = headerManager;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.portfolioBatch = {};
|
|
18
|
+
this.timestampBatch = {};
|
|
19
|
+
this.tradingHistoryBatch = {};
|
|
20
|
+
this.speculatorTimestampFixBatch = {};
|
|
21
|
+
|
|
22
|
+
// Username map cache
|
|
23
|
+
this.usernameMap = new Map();
|
|
24
|
+
this.usernameMapUpdates = {};
|
|
25
|
+
this.usernameMapLastLoaded = 0;
|
|
26
|
+
|
|
27
|
+
// History fetch cache (NEW)
|
|
28
|
+
this.historyFetchedUserIds = new Set();
|
|
47
29
|
this.historyCacheTimestamp = Date.now();
|
|
48
|
-
|
|
30
|
+
this.HISTORY_CACHE_TTL_MS = config.HISTORY_CACHE_TTL_MS || 600000;
|
|
49
31
|
|
|
50
|
-
|
|
51
|
-
|
|
32
|
+
this.processedSpeculatorCids = new Set();
|
|
33
|
+
this.usernameMapCollectionName = config.FIRESTORE_COLLECTION_USERNAME_MAP;
|
|
34
|
+
this.normalHistoryCollectionName = config.FIRESTORE_COLLECTION_NORMAL_HISTORY;
|
|
35
|
+
this.speculatorHistoryCollectionName = config.FIRESTORE_COLLECTION_SPECULATOR_HISTORY;
|
|
36
|
+
this.batchTimeout = null;
|
|
37
|
+
|
|
38
|
+
logger.log('INFO', 'FirestoreBatchManager initialized.');
|
|
52
39
|
}
|
|
53
40
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
41
|
+
checkAndSetHistoryFetched(userId) {
|
|
42
|
+
if (Date.now() - this.historyCacheTimestamp > this.HISTORY_CACHE_TTL_MS) {
|
|
43
|
+
this.logger.log('INFO', '[BATCH] History fetch cache (10m TTL) expired. Clearing set.');
|
|
44
|
+
this.historyFetchedUserIds.clear();
|
|
45
|
+
this.historyCacheTimestamp = Date.now();
|
|
46
|
+
}
|
|
47
|
+
if (this.historyFetchedUserIds.has(userId)) { return true; }
|
|
48
|
+
this.historyFetchedUserIds.add(userId);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
58
51
|
|
|
59
|
-
_getUsernameShardId(cid) { return `cid_map_shard_${Math.floor(parseInt(cid) / 10000) % 10}`; }
|
|
52
|
+
_getUsernameShardId(cid) { return `cid_map_shard_${Math.floor(parseInt(cid) / 10000) % 10}`; }
|
|
60
53
|
|
|
61
|
-
|
|
54
|
+
_scheduleFlush() {
|
|
55
|
+
const maxBatch = this.config.TASK_ENGINE_MAX_BATCH_SIZE ? Number(this.config.TASK_ENGINE_MAX_BATCH_SIZE) : 400;
|
|
56
|
+
const totalOps = this._estimateBatchSize();
|
|
57
|
+
if (totalOps >= maxBatch) { this.flushBatches(); return; }
|
|
58
|
+
}
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
_estimateBatchSize() {
|
|
61
|
+
let ops = 0;
|
|
62
|
+
ops += Object.keys(this.portfolioBatch).length;
|
|
63
|
+
ops += Object.keys(this.tradingHistoryBatch).length;
|
|
64
|
+
ops += Object.keys(this.timestampBatch).length;
|
|
65
|
+
ops += Object.keys(this.speculatorTimestampFixBatch).length;
|
|
66
|
+
return ops;
|
|
67
|
+
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
async loadUsernameMap() {
|
|
70
|
+
if (Date.now() - this.usernameMapLastLoaded < 3600000) return;
|
|
71
|
+
this.usernameMap.clear();
|
|
72
|
+
this.logger.log('INFO', '[BATCH] Refreshing username map from Firestore...');
|
|
73
|
+
try {
|
|
74
|
+
const snapshot = await this.db.collection(this.usernameMapCollectionName).get();
|
|
75
|
+
snapshot.forEach(doc => { const data = doc.data(); for (const cid in data) if (data[cid]?.username) this.usernameMap.set(String(cid), data[cid].username); });
|
|
76
|
+
this.usernameMapLastLoaded = Date.now();
|
|
77
|
+
this.logger.log('INFO', `[BATCH] Loaded ${this.usernameMap.size} usernames.`);
|
|
78
|
+
} catch (e) { this.logger.log('ERROR', '[BATCH] Failed to load username map.', { errorMessage: e.message }); }
|
|
79
|
+
}
|
|
77
80
|
|
|
78
|
-
|
|
79
|
-
if (Date.now() - this.usernameMapLastLoaded < 3600000) return;
|
|
80
|
-
this.usernameMap.clear();
|
|
81
|
-
this.logger.log('INFO', '[BATCH] Refreshing username map from Firestore...');
|
|
82
|
-
try {
|
|
83
|
-
const snapshot = await this.db.collection(this.usernameMapCollectionName).get();
|
|
84
|
-
snapshot.forEach(doc => { const data = doc.data(); for (const cid in data) if (data[cid]?.username) this.usernameMap.set(String(cid), data[cid].username); });
|
|
85
|
-
this.usernameMapLastLoaded = Date.now();
|
|
86
|
-
this.logger.log('INFO', `[BATCH] Loaded ${this.usernameMap.size} usernames.`);
|
|
87
|
-
} catch (e) { this.logger.log('ERROR', '[BATCH] Failed to load username map.', { errorMessage: e.message }); }
|
|
88
|
-
}
|
|
81
|
+
getUsername(cid) { return this.usernameMap.get(String(cid)); }
|
|
89
82
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.logger.log('TRACE', `[BATCH] Queued username update for ${cidStr} in ${shardId}.`);
|
|
100
|
-
this._scheduleFlush();
|
|
101
|
-
}
|
|
83
|
+
addUsernameMapUpdate(cid, username) {
|
|
84
|
+
if (!username) return;
|
|
85
|
+
const cidStr = String(cid);
|
|
86
|
+
this.usernameMap.set(cidStr, username);
|
|
87
|
+
const shardId = this._getUsernameShardId(cidStr);
|
|
88
|
+
if (!this.usernameMapUpdates[shardId]) { this.usernameMapUpdates[shardId] = {}; }
|
|
89
|
+
this.usernameMapUpdates[shardId][cidStr] = { username };
|
|
90
|
+
this._scheduleFlush();
|
|
91
|
+
}
|
|
102
92
|
|
|
103
|
-
async addToTradingHistoryBatch(userId, blockId, date, historyData, userType) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
93
|
+
async addToTradingHistoryBatch(userId, blockId, date, historyData, userType) {
|
|
94
|
+
const collection = userType === 'speculator' ? this.speculatorHistoryCollectionName : this.normalHistoryCollectionName;
|
|
95
|
+
const path = `${collection}/${blockId}/snapshots/${date}`;
|
|
96
|
+
this.tradingHistoryBatch[path] ??= {};
|
|
97
|
+
this.tradingHistoryBatch[path][userId] = historyData;
|
|
98
|
+
this._scheduleFlush();
|
|
99
|
+
}
|
|
110
100
|
|
|
111
|
-
async addToPortfolioBatch(userId, blockId, date, portfolioData, userType) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
101
|
+
async addToPortfolioBatch(userId, blockId, date, portfolioData, userType) {
|
|
102
|
+
const collection = userType === 'speculator' ? this.config.FIRESTORE_COLLECTION_SPECULATOR_PORTFOLIOS : this.config.FIRESTORE_COLLECTION_NORMAL_PORTFOLIOS;
|
|
103
|
+
const path = `${collection}/${blockId}/snapshots/${date}`;
|
|
104
|
+
this.portfolioBatch[path] ??= {};
|
|
105
|
+
this.portfolioBatch[path][userId] = portfolioData;
|
|
106
|
+
this._scheduleFlush();
|
|
107
|
+
}
|
|
118
108
|
|
|
119
|
-
async updateUserTimestamp(userId, userType, instrumentId = null) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
109
|
+
async updateUserTimestamp(userId, userType, instrumentId = null) {
|
|
110
|
+
const collection = userType === 'speculator' ? this.config.FIRESTORE_COLLECTION_SPECULATOR_PORTFOLIOS : this.config.FIRESTORE_COLLECTION_NORMAL_PORTFOLIOS;
|
|
111
|
+
const docPath = `${collection}/${userType === 'speculator' ? 'speculators' : 'normal'}`;
|
|
112
|
+
this.timestampBatch[docPath] ??= {};
|
|
113
|
+
const key = userType === 'speculator' ? `${userId}_${instrumentId}` : userId;
|
|
114
|
+
this.timestampBatch[docPath][key] = new Date();
|
|
115
|
+
this._scheduleFlush();
|
|
116
|
+
}
|
|
127
117
|
|
|
128
|
-
deleteFromTimestampBatch(userId, userType, instrumentId) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
118
|
+
deleteFromTimestampBatch(userId, userType, instrumentId) {
|
|
119
|
+
const collection = userType === 'speculator' ? this.config.FIRESTORE_COLLECTION_SPECULATOR_PORTFOLIOS : this.config.FIRESTORE_COLLECTION_NORMAL_PORTFOLIOS;
|
|
120
|
+
const docPath = `${collection}/${userType === 'speculator' ? 'speculators' : 'normal'}`;
|
|
121
|
+
if (this.timestampBatch[docPath]) { const key = userType === 'speculator' ? `${userId}_${instrumentId}` : userId; delete this.timestampBatch[docPath][key]; }
|
|
122
|
+
}
|
|
133
123
|
|
|
134
|
-
addProcessedSpeculatorCids(cids) { cids.forEach(cid => this.processedSpeculatorCids.add(cid)); }
|
|
124
|
+
addProcessedSpeculatorCids(cids) { cids.forEach(cid => this.processedSpeculatorCids.add(cid)); }
|
|
135
125
|
|
|
136
|
-
async addSpeculatorTimestampFix(userId, orchestratorBlockId) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
126
|
+
async addSpeculatorTimestampFix(userId, orchestratorBlockId) {
|
|
127
|
+
const docPath = `${this.config.FIRESTORE_COLLECTION_SPECULATOR_BLOCKS}/${orchestratorBlockId}`;
|
|
128
|
+
this.speculatorTimestampFixBatch[docPath] ??= {};
|
|
129
|
+
this.speculatorTimestampFixBatch[docPath][`users.${userId}.lastVerified`] = new Date();
|
|
130
|
+
this.speculatorTimestampFixBatch[docPath][`users.${userId}.lastHeldSpeculatorAsset`] = new Date();
|
|
131
|
+
this._scheduleFlush();
|
|
132
|
+
}
|
|
144
133
|
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
_flushDataBatch(batchData, firestoreBatch, logName) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// Users are already partitioned into 1M blocks (basePath has blockId).
|
|
173
|
-
// Within a block (0-999,999), we want shards of size SHARD_CAPACITY.
|
|
174
|
-
// shardIndex = floor((cid % 1,000,000) / SHARD_CAPACITY)
|
|
175
|
-
const blockOffset = cid % 1000000;
|
|
176
|
-
const shardIndex = Math.floor(blockOffset / SHARD_CAPACITY);
|
|
177
|
-
shardId = `part_${shardIndex}`;
|
|
178
|
-
} else {
|
|
179
|
-
// Fallback for non-numeric IDs (unlikely for CIDs)
|
|
180
|
-
shardId = 'part_misc';
|
|
134
|
+
/**
|
|
135
|
+
* --- REFACTORED: Modulo Sharding ---
|
|
136
|
+
* Fixes the issue where sparse IDs created fragmented documents.
|
|
137
|
+
* We now calculate the number of shards needed to hold the target population
|
|
138
|
+
* and bucket users into them using modulo arithmetic.
|
|
139
|
+
*/
|
|
140
|
+
_flushDataBatch(batchData, firestoreBatch, logName) {
|
|
141
|
+
let count = 0;
|
|
142
|
+
const TARGET_USERS = this.config.DISCOVERY_ORCHESTRATOR_TARGET_USERS_PER_BLOCK ? Number(this.config.DISCOVERY_ORCHESTRATOR_TARGET_USERS_PER_BLOCK) : 1500;
|
|
143
|
+
const SHARD_CAPACITY = this.config.TASK_ENGINE_MAX_USERS_PER_SHARD ? Number(this.config.TASK_ENGINE_MAX_USERS_PER_SHARD) : 200;
|
|
144
|
+
const TOTAL_SHARDS = Math.max(1, Math.ceil(TARGET_USERS / SHARD_CAPACITY));
|
|
145
|
+
for (const basePath in batchData) {
|
|
146
|
+
const users = batchData[basePath];
|
|
147
|
+
const userIds = Object.keys(users);
|
|
148
|
+
if (!userIds.length) continue;
|
|
149
|
+
const updatesByShard = {};
|
|
150
|
+
for (const userId of userIds) {
|
|
151
|
+
const cid = parseInt(userId, 10);
|
|
152
|
+
let shardId;
|
|
153
|
+
|
|
154
|
+
if (!isNaN(cid)) {
|
|
155
|
+
const shardIndex = cid % TOTAL_SHARDS;
|
|
156
|
+
shardId = `part_${shardIndex}`;
|
|
157
|
+
} else { shardId = 'part_misc'; }
|
|
158
|
+
|
|
159
|
+
if (!updatesByShard[shardId]) { updatesByShard[shardId] = {}; }
|
|
160
|
+
updatesByShard[shardId][userId] = users[userId];
|
|
181
161
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
162
|
+
for (const shardId in updatesByShard) {
|
|
163
|
+
const chunkData = updatesByShard[shardId];
|
|
164
|
+
const docRef = this.db.collection(`${basePath}/parts`).doc(shardId);
|
|
165
|
+
firestoreBatch.set(docRef, chunkData, { merge: true });
|
|
166
|
+
count++;
|
|
185
167
|
}
|
|
186
|
-
|
|
168
|
+
this.logger.log('INFO', `[BATCH] Staged ${userIds.length} ${logName} users into ${Object.keys(updatesByShard).length} buckets (Modulo ${TOTAL_SHARDS}) for ${basePath}.`);
|
|
169
|
+
delete batchData[basePath];
|
|
187
170
|
}
|
|
171
|
+
return count;
|
|
172
|
+
}
|
|
188
173
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
174
|
+
async flushBatches() {
|
|
175
|
+
if (this.batchTimeout) { clearTimeout(this.batchTimeout); this.batchTimeout = null; }
|
|
176
|
+
const firestoreBatch = this.db.batch();
|
|
177
|
+
let batchOps = 0;
|
|
178
|
+
batchOps += this._flushDataBatch(this.portfolioBatch, firestoreBatch, 'Portfolio');
|
|
179
|
+
batchOps += this._flushDataBatch(this.tradingHistoryBatch, firestoreBatch, 'Trade History');
|
|
180
|
+
for (const docPath in this.timestampBatch) {
|
|
181
|
+
const timestamps = this.timestampBatch[docPath];
|
|
182
|
+
if (!Object.keys(timestamps).length) continue;
|
|
183
|
+
const docRef = this.db.collection(docPath.split('/')[0]).doc('timestamps').collection('users').doc('normal');
|
|
184
|
+
firestoreBatch.set(docRef, { users: timestamps }, { merge: true });
|
|
185
|
+
batchOps++;
|
|
186
|
+
delete this.timestampBatch[docPath];
|
|
197
187
|
}
|
|
198
188
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
async flushBatches() {
|
|
207
|
-
if (this.batchTimeout) { clearTimeout(this.batchTimeout); this.batchTimeout = null; }
|
|
208
|
-
const firestoreBatch = this.db.batch();
|
|
209
|
-
let batchOps = 0;
|
|
210
|
-
batchOps += this._flushDataBatch(this.portfolioBatch, firestoreBatch, 'Portfolio');
|
|
211
|
-
batchOps += this._flushDataBatch(this.tradingHistoryBatch, firestoreBatch, 'Trade History');
|
|
212
|
-
for (const docPath in this.timestampBatch) {
|
|
213
|
-
const timestamps = this.timestampBatch[docPath];
|
|
214
|
-
if (!Object.keys(timestamps).length) continue;
|
|
215
|
-
const docRef = this.db.collection(docPath.split('/')[0]).doc('timestamps').collection('users').doc('normal');
|
|
216
|
-
firestoreBatch.set(docRef, { users: timestamps }, { merge: true });
|
|
217
|
-
batchOps++;
|
|
218
|
-
delete this.timestampBatch[docPath];
|
|
219
|
-
}
|
|
189
|
+
for (const docPath in this.speculatorTimestampFixBatch) {
|
|
190
|
+
const updates = this.speculatorTimestampFixBatch[docPath];
|
|
191
|
+
if (!Object.keys(updates).length) continue;
|
|
192
|
+
firestoreBatch.set(this.db.doc(docPath), updates, { merge: true });
|
|
193
|
+
batchOps++;
|
|
194
|
+
delete this.speculatorTimestampFixBatch[docPath];
|
|
195
|
+
}
|
|
220
196
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
197
|
+
for (const shardId in this.usernameMapUpdates) {
|
|
198
|
+
const updates = this.usernameMapUpdates[shardId];
|
|
199
|
+
if (updates && Object.keys(updates).length > 0) { firestoreBatch.set( this.db.collection(this.usernameMapCollectionName).doc(shardId), updates, { merge: true } ); batchOps++; this.logger.log('INFO', `[BATCH] Flushing ${Object.keys(updates).length} username updates to ${shardId}.`); } }
|
|
200
|
+
this.usernameMapUpdates = {};
|
|
201
|
+
|
|
202
|
+
if (this.processedSpeculatorCids.size) {
|
|
203
|
+
const cids = Array.from(this.processedSpeculatorCids);
|
|
204
|
+
this.processedSpeculatorCids.clear();
|
|
205
|
+
const snapshot = await this.db.collection(this.config.PENDING_SPECULATORS_COLLECTION).get();
|
|
206
|
+
snapshot.forEach(doc => { const docData = doc.data().users || {}; const cidsInDoc = cids.filter(cid => docData[cid]); if (!cidsInDoc.length) return;
|
|
207
|
+
const delBatch = this.db.batch();
|
|
208
|
+
const updates = Object.fromEntries(cidsInDoc.map(cid => [`users.${cid}`, FieldValue.delete()]));
|
|
209
|
+
delBatch.update(doc.ref, updates);
|
|
210
|
+
delBatch.commit();
|
|
211
|
+
this.logger.log('INFO', `[BATCH] Deleted ${cidsInDoc.length} CIDs from ${doc.id}`); }); }
|
|
212
|
+
|
|
213
|
+
if (batchOps) await firestoreBatch.commit();
|
|
214
|
+
await this.headerManager.flushPerformanceUpdates();
|
|
215
|
+
this.logger.log('INFO', '[BATCH] All batches flushed successfully.');
|
|
227
216
|
}
|
|
228
|
-
|
|
229
|
-
for (const shardId in this.usernameMapUpdates) {
|
|
230
|
-
const updates = this.usernameMapUpdates[shardId];
|
|
231
|
-
if (updates && Object.keys(updates).length > 0) { firestoreBatch.set( this.db.collection(this.usernameMapCollectionName).doc(shardId), updates, { merge: true } ); batchOps++; this.logger.log('INFO', `[BATCH] Flushing ${Object.keys(updates).length} username updates to ${shardId}.`); } }
|
|
232
|
-
this.usernameMapUpdates = {};
|
|
233
|
-
|
|
234
|
-
if (this.processedSpeculatorCids.size) {
|
|
235
|
-
const cids = Array.from(this.processedSpeculatorCids);
|
|
236
|
-
this.processedSpeculatorCids.clear();
|
|
237
|
-
const snapshot = await this.db.collection(this.config.PENDING_SPECULATORS_COLLECTION).get();
|
|
238
|
-
snapshot.forEach(doc => { const docData = doc.data().users || {}; const cidsInDoc = cids.filter(cid => docData[cid]); if (!cidsInDoc.length) return;
|
|
239
|
-
const delBatch = this.db.batch();
|
|
240
|
-
const updates = Object.fromEntries(cidsInDoc.map(cid => [`users.${cid}`, FieldValue.delete()]));
|
|
241
|
-
delBatch.update(doc.ref, updates);
|
|
242
|
-
delBatch.commit();
|
|
243
|
-
this.logger.log('INFO', `[BATCH] Deleted ${cidsInDoc.length} CIDs from ${doc.id}`); }); }
|
|
244
|
-
|
|
245
|
-
if (batchOps) await firestoreBatch.commit();
|
|
246
|
-
await this.headerManager.flushPerformanceUpdates();
|
|
247
|
-
this.logger.log('INFO', '[BATCH] All batches flushed successfully.');
|
|
248
|
-
}
|
|
249
217
|
}
|
|
250
218
|
|
|
251
|
-
module.exports = { FirestoreBatchManager };
|
|
219
|
+
module.exports = { FirestoreBatchManager };
|