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
- fetchGlobalComputationStatus, // <--- New Import
10
- updateGlobalComputationStatus, // <--- New Import
9
+ fetchComputationStatus,
10
+ updateComputationStatus,
11
11
  runStandardComputationPass,
12
12
  runMetaComputationPass,
13
13
  checkRootDependencies
14
14
  } = require('./orchestration_helpers.js');
15
- const { getExpectedDateStrings } = require('../utils/utils.js');
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} with Global Status Check...`);
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
- // 1. Fetch Global Status ONCE (Memory Cache)
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
- const standardToRun = standardCalcs.filter(c => shouldRun(c, dateStr));
66
- const metaToRun = metaCalcs.filter(c => shouldRun(c, dateStr));
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); // skipStatusWrite=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); // skipStatusWrite=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
- // Mark failures
99
- [...finalStandardToRun, ...finalMetaToRun].forEach(c => dateUpdates[c.name] = false);
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
- --- MODIFIED: Added cross-invocation cache for history fetches. ---
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 { constructor(db, headerManager, logger, config) { this.db = db; this.headerManager = headerManager; this.logger = logger; this.config = config; this.portfolioBatch = {}; this.timestampBatch = {}; this.tradingHistoryBatch = {}; this.speculatorTimestampFixBatch = {};
17
-
18
- // Username map cache
19
- this.usernameMap = new Map();
20
- this.usernameMapUpdates = {};
21
- this.usernameMapLastLoaded = 0;
22
-
23
- // History fetch cache (NEW)
24
- this.historyFetchedUserIds = new Set();
25
- this.historyCacheTimestamp = Date.now();
26
- // Set a 10-minute TTL on this cache (600,000 ms)
27
- this.HISTORY_CACHE_TTL_MS = config.HISTORY_CACHE_TTL_MS || 600000;
28
-
29
- this.processedSpeculatorCids = new Set();
30
- this.usernameMapCollectionName = config.FIRESTORE_COLLECTION_USERNAME_MAP;
31
- this.normalHistoryCollectionName = config.FIRESTORE_COLLECTION_NORMAL_HISTORY;
32
- this.speculatorHistoryCollectionName = config.FIRESTORE_COLLECTION_SPECULATOR_HISTORY;
33
- this.batchTimeout = null;
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
- if (this.historyFetchedUserIds.has(userId)) {
51
- return true; // Yes, already fetched
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
- // Not fetched yet. Mark as fetched and return false.
55
- this.historyFetchedUserIds.add(userId);
56
- return false;
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
- // _scheduleFlush() { if (!this.batchTimeout) this.batchTimeout = setTimeout(() => this.flushBatches(), this.config.TASK_ENGINE_FLUSH_INTERVAL_MS); } Old version
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
- _scheduleFlush() {
64
- const totalOps = this._estimateBatchSize();
65
- if (totalOps >= 400) { this.flushBatches(); return; }
66
- if (!this.batchTimeout) { this.batchTimeout = setTimeout(() => this.flushBatches(), this.config.TASK_ENGINE_FLUSH_INTERVAL_MS); }
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
- _estimateBatchSize() {
70
- let ops = 0;
71
- ops += Object.keys(this.portfolioBatch).length;
72
- ops += Object.keys(this.tradingHistoryBatch).length;
73
- ops += Object.keys(this.timestampBatch).length;
74
- ops += Object.keys(this.speculatorTimestampFixBatch).length;
75
- return ops;
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
- async loadUsernameMap() {
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
- getUsername(cid) { return this.usernameMap.get(String(cid)); }
91
-
92
- addUsernameMapUpdate(cid, username) {
93
- if (!username) return;
94
- const cidStr = String(cid);
95
- this.usernameMap.set(cidStr, username);
96
- const shardId = this._getUsernameShardId(cidStr);
97
- if (!this.usernameMapUpdates[shardId]) { this.usernameMapUpdates[shardId] = {}; }
98
- this.usernameMapUpdates[shardId][cidStr] = { username };
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
- const collection = userType === 'speculator' ? this.speculatorHistoryCollectionName : this.normalHistoryCollectionName;
105
- const path = `${collection}/${blockId}/snapshots/${date}`;
106
- this.tradingHistoryBatch[path] ??= {};
107
- this.tradingHistoryBatch[path][userId] = historyData;
108
- this._scheduleFlush();
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
- const collection = userType === 'speculator' ? this.config.FIRESTORE_COLLECTION_SPECULATOR_PORTFOLIOS : this.config.FIRESTORE_COLLECTION_NORMAL_PORTFOLIOS;
113
- const path = `${collection}/${blockId}/snapshots/${date}`;
114
- this.portfolioBatch[path] ??= {};
115
- this.portfolioBatch[path][userId] = portfolioData;
116
- this._scheduleFlush();
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
- const collection = userType === 'speculator' ? this.config.FIRESTORE_COLLECTION_SPECULATOR_PORTFOLIOS : this.config.FIRESTORE_COLLECTION_NORMAL_PORTFOLIOS;
121
- const docPath = `${collection}/${userType === 'speculator' ? 'speculators' : 'normal'}`;
122
- this.timestampBatch[docPath] ??= {};
123
- const key = userType === 'speculator' ? `${userId}_${instrumentId}` : userId;
124
- this.timestampBatch[docPath][key] = new Date();
125
- this._scheduleFlush();
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
- const collection = userType === 'speculator' ? this.config.FIRESTORE_COLLECTION_SPECULATOR_PORTFOLIOS : this.config.FIRESTORE_COLLECTION_NORMAL_PORTFOLIOS;
130
- const docPath = `${collection}/${userType === 'speculator' ? 'speculators' : 'normal'}`;
131
- if (this.timestampBatch[docPath]) { const key = userType === 'speculator' ? `${userId}_${instrumentId}` : userId; delete this.timestampBatch[docPath][key]; }
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
- const docPath = `${this.config.FIRESTORE_COLLECTION_SPECULATOR_BLOCKS}/${orchestratorBlockId}`;
138
- this.speculatorTimestampFixBatch[docPath] ??= {};
139
- this.speculatorTimestampFixBatch[docPath][`users.${userId}.lastVerified`] = new Date();
140
- this.speculatorTimestampFixBatch[docPath][`users.${userId}.lastHeldSpeculatorAsset`] = new Date();
141
- this.logger.log('TRACE', `[BATCH] Queued speculator timestamp fix for ${userId} in block ${orchestratorBlockId}`);
142
- this._scheduleFlush();
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
- * --- REFACTORED: Deterministic Sharding ---
147
- * Groups users into 'part_N' shards based on their CID.
148
- * Uses { merge: true } to allow concurrent/sequential writes to fill documents
149
- * instead of creating fragmented random docs.
150
- */
151
- _flushDataBatch(batchData, firestoreBatch, logName) {
152
- let count = 0;
153
-
154
- // Use 200 as requested, or fall back to config if higher/defined.
155
- // This ensures we fit ~200 users per document to save space/reads.
156
- const SHARD_CAPACITY = this.config.TASK_ENGINE_MAX_USERS_PER_SHARD || 200;
157
-
158
- for (const basePath in batchData) {
159
- const users = batchData[basePath];
160
- const userIds = Object.keys(users);
161
- if (!userIds.length) continue;
162
-
163
- // 1. Group updates by Deterministic Shard ID
164
- const updatesByShard = {};
165
-
166
- for (const userId of userIds) {
167
- const cid = parseInt(userId, 10);
168
- let shardId;
169
-
170
- if (!isNaN(cid)) {
171
- // Block Logic:
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
- if (!updatesByShard[shardId]) {
184
- updatesByShard[shardId] = {};
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
- updatesByShard[shardId][userId] = users[userId];
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
- // 2. Queue Writes with Merge
190
- for (const shardId in updatesByShard) {
191
- const chunkData = updatesByShard[shardId];
192
- const docRef = this.db.collection(`${basePath}/parts`).doc(shardId);
193
-
194
- // CRITICAL: Use merge: true to append to existing shards
195
- firestoreBatch.set(docRef, chunkData, { merge: true });
196
- count++;
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
- this.logger.log('INFO', `[BATCH] Staged ${userIds.length} ${logName} users into ${Object.keys(updatesByShard).length} deterministic shards for ${basePath}.`);
200
-
201
- delete batchData[basePath];
202
- }
203
- return count;
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
- for (const docPath in this.speculatorTimestampFixBatch) {
222
- const updates = this.speculatorTimestampFixBatch[docPath];
223
- if (!Object.keys(updates).length) continue;
224
- firestoreBatch.set(this.db.doc(docPath), updates, { merge: true });
225
- batchOps++;
226
- delete this.speculatorTimestampFixBatch[docPath];
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.180",
3
+ "version": "1.0.182",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [