bulltrackers-module 1.0.60 → 1.0.62

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.
@@ -388,11 +388,17 @@ async function getSpeculatorsToUpdate(dependencies, config) {
388
388
 
389
389
  snapshot.forEach(doc => {
390
390
  const blockData = doc.data();
391
- const users = blockData.users || {};
391
+ // const users = blockData.users || {}; // <<< REMOVED
392
+
393
+ // Iterate over the document's top-level keys
394
+ for (const key in blockData) {
395
+ // Filter for keys that match the 'users.CID' format
396
+ if (!key.startsWith('users.')) continue;
392
397
 
393
- for (const key in users) {
394
398
  const userId = key.split('.')[1];
395
- const userData = users[key];
399
+ if (!userId) continue; // Safety check
400
+
401
+ const userData = blockData[key]; // Get the user's map
396
402
 
397
403
  const lastVerified = userData.lastVerified?.toDate ? userData.lastVerified.toDate() : new Date(0);
398
404
  const lastHeld = userData.lastHeldSpeculatorAsset?.toDate ? userData.lastHeldSpeculatorAsset.toDate() : new Date(0);
@@ -1,217 +1,290 @@
1
1
  /**
2
2
  * @fileoverview Main pipe: pipe.maintenance.runUserActivitySampler
3
- * Samples user activity levels based on the Rankings API.
4
- * REVISED: Removed exclusion list logic for simplification.
3
+ * REFACTORED: Now split into an orchestrator and a task handler.
4
+ * - runUserActivitySamplerOrchestrator: Publishes one task per block.
5
+ * - handleSampleBlockTask: Processes a single block with parallel fetching.
5
6
  */
6
7
  const { FieldValue } = require('@google-cloud/firestore');
7
8
 
8
9
  /**
9
- * Main pipe: pipe.maintenance.runUserActivitySampler
10
+ * Main pipe (Orchestrator): pipe.maintenance.runUserActivitySamplerOrchestrator
11
+ * This function is triggered by a schedule. It fans out the work by
12
+ * publishing one Pub/Sub message for each block to be sampled.
13
+ *
10
14
  * @param {object} config - Configuration object.
11
- * @param {object} dependencies - Contains db, logger, headerManager, proxyManager, firestoreUtils.
12
- * @returns {Promise<object>} Summary of the sampling process.
15
+ * @param {object} dependencies - Contains db, logger, firestoreUtils, pubsubUtils.
16
+ * @returns {Promise<object>} Summary of the orchestration.
13
17
  */
14
- exports.runUserActivitySampler = async (config, dependencies) => {
15
- // NOTE: firestoreUtils is still in dependencies but getExclusionIds is no longer called from here.
16
- const { db, logger, headerManager, proxyManager, firestoreUtils } = dependencies;
17
- const today = new Date().toISOString().slice(0, 10);
18
- logger.log('INFO', '[UserActivitySampler] Starting user activity sampling (simplified)...');
19
-
20
- // Validate configuration (Removed exclusionConfig validation)
21
- if (!config.rankingsApiUrl || !config.targetPublicUsersPerBlock || !config.apiBatchSize || !config.outputCollectionName || !config.allHighValueBlocks ) {
22
- logger.log('ERROR', '[UserActivitySampler] Missing required configuration.');
23
- throw new Error('Missing required configuration for User Activity Sampler.');
18
+ exports.runUserActivitySamplerOrchestrator = async (config, dependencies) => {
19
+ const { logger, firestoreUtils, pubsubUtils } = dependencies;
20
+ logger.log('INFO', '[SamplerOrchestrator] Starting user activity sampling orchestration...');
21
+
22
+ // Validate configuration
23
+ if (!config.allHighValueBlocks || !Array.isArray(config.allHighValueBlocks) || !config.samplerTaskTopicName) {
24
+ logger.log('ERROR', '[SamplerOrchestrator] Missing required configuration: allHighValueBlocks (array) or samplerTaskTopicName.');
25
+ throw new Error('Missing required configuration for Sampler Orchestrator.');
24
26
  }
25
27
 
26
28
  try {
27
- await firestoreUtils.resetProxyLocks(dependencies, config); // Use shared config for proxy path
29
+ // Reset locks once for all tasks
30
+ await firestoreUtils.resetProxyLocks(dependencies, config);
28
31
 
29
- // --- REMOVED Exclusion ID fetching ---
30
- // const exclusionIds = await firestoreUtils.getExclusionIds(dependencies, config.exclusionConfig, 'sampler');
31
- // logger.log('INFO', `[UserActivitySampler] Fetched ${exclusionIds.size} exclusion IDs.`);
32
- const processedInThisRun = new Set(); // Keep track of IDs processed in this run to avoid duplicates within batches
32
+ const tasks = [];
33
+ for (const block of config.allHighValueBlocks) {
34
+ // Ensure block and block.startId exist
35
+ if (!block || typeof block.startId === 'undefined') {
36
+ logger.log('WARN', '[SamplerOrchestrator] Skipping invalid block configuration:', block);
37
+ continue;
38
+ }
39
+ tasks.push({
40
+ type: 'sample-block',
41
+ blockId: block.startId
42
+ });
43
+ }
33
44
 
34
- const blockResults = [];
35
- let totalPublicSampled = 0;
36
- let totalPrivateEstimate = 0;
45
+ if (tasks.length === 0) {
46
+ logger.log('WARN', '[SamplerOrchestrator] No valid blocks found to sample.');
47
+ return { success: true, message: "No valid blocks configured." };
48
+ }
37
49
 
38
- // Ensure allHighValueBlocks is defined and is an array
39
- if (!config.allHighValueBlocks || !Array.isArray(config.allHighValueBlocks)) {
40
- logger.log('ERROR', '[UserActivitySampler] Configuration error: allHighValueBlocks is missing or not an array.');
41
- throw new Error('Configuration error: allHighValueBlocks must be an array.');
50
+ // Use pubsubUtils to batch publish all block tasks
51
+ await pubsubUtils.batchPublishTasks(dependencies, {
52
+ topicName: config.samplerTaskTopicName,
53
+ tasks: tasks,
54
+ taskType: 'sampler-block-task'
55
+ });
56
+
57
+ logger.log('SUCCESS', `[SamplerOrchestrator] Successfully published ${tasks.length} block sampling tasks.`);
58
+ return { success: true, blocksQueued: tasks.length };
59
+
60
+ } catch (error) {
61
+ logger.log('ERROR', '[SamplerOrchestrator] Fatal error during orchestration.', { errorMessage: error.message, errorStack: error.stack });
62
+ throw error;
63
+ }
64
+ };
65
+
66
+ /**
67
+ * Internal Helper: Fetches a single batch of CIDs for the sampler.
68
+ * This is designed to be called in parallel.
69
+ */
70
+ async function fetchSampleBatch(blockId, config, dependencies, processedInThisRun) {
71
+ const { logger, headerManager, proxyManager } = dependencies;
72
+ const cidsToSample = [];
73
+
74
+ // 1. Generate CIDs for this batch
75
+ while (cidsToSample.length < config.apiBatchSize) {
76
+ const randomId = String(Math.floor(Math.random() * 1000000) + blockId);
77
+ // Use a Set to prevent processing the same ID twice *within this run*
78
+ if (!processedInThisRun.has(randomId)) {
79
+ cidsToSample.push(parseInt(randomId, 10));
80
+ processedInThisRun.add(randomId);
42
81
  }
82
+ // Note: If this loops too long, it could be inefficient, but it's
83
+ // unlikely with a large ID space and a reasonable sample size.
84
+ }
43
85
 
86
+ let selectedHeader = null;
87
+ let wasSuccess = false;
88
+ try {
89
+ selectedHeader = await headerManager.selectHeader();
90
+ if (!selectedHeader) throw new Error("Could not select header.");
91
+
92
+ const urlWithParam = `${config.rankingsApiUrl}?Period=LastTwoYears`;
93
+ const response = await proxyManager.fetch(urlWithParam, {
94
+ method: 'POST',
95
+ headers: { ...selectedHeader.header, 'Content-Type': 'application/json' },
96
+ body: JSON.stringify(cidsToSample),
97
+ });
98
+
99
+ if (!response || typeof response.json !== 'function') {
100
+ logger.log('WARN', `[SamplerTask] Invalid response structure from proxy for block ${blockId}.`);
101
+ return { success: false, cidsSent: cidsToSample.length, publicUsers: [] };
102
+ }
44
103
 
45
- for (const block of config.allHighValueBlocks) {
46
- // Ensure block and block.startId exist
47
- if (!block || typeof block.startId === 'undefined') {
48
- logger.log('WARN', '[UserActivitySampler] Skipping invalid block configuration:', block);
49
- continue;
50
- }
51
- const blockId = block.startId;
52
- logger.log('INFO', `[UserActivitySampler] Processing block ${blockId}...`);
53
-
54
- let N_public_sampled_block = 0;
55
- let N_sampled_total_block = 0;
56
- const public_users_data = []; // Store { CID, LastActivity }
57
-
58
- const MAX_ATTEMPTS = config.maxSamplingAttemptsPerBlock || 1000; // Prevent infinite loops
59
- let attempts = 0;
60
-
61
- while (N_public_sampled_block < config.targetPublicUsersPerBlock && attempts < MAX_ATTEMPTS) {
62
- attempts++;
63
- const cidsToSample = [];
64
- // Generate CIDs, ensuring they are not already processed in this run
65
- while (cidsToSample.length < config.apiBatchSize) {
66
- const randomId = String(Math.floor(Math.random() * 1000000) + blockId);
67
- // --- REMOVED exclusionIds check, added check for processedInThisRun ---
68
- if (!processedInThisRun.has(randomId)) {
69
- cidsToSample.push(parseInt(randomId, 10));
70
- // Add temporarily to processed set for this run to avoid duplicates within batches
71
- processedInThisRun.add(randomId);
72
- }
73
- }
74
- N_sampled_total_block += cidsToSample.length;
75
-
76
- let selectedHeader = null;
77
- let wasSuccess = false;
78
- try {
79
- selectedHeader = await headerManager.selectHeader();
80
- if (!selectedHeader) throw new Error("Could not select header.");
81
- const urlWithParam = `${config.rankingsApiUrl}?Period=LastTwoYears`;
82
- const response = await proxyManager.fetch(urlWithParam, {
83
- method: 'POST',
84
- headers: { ...selectedHeader.header, 'Content-Type': 'application/json' },
85
- body: JSON.stringify(cidsToSample),
86
- });
87
-
88
- if (!response || typeof response.json !== 'function') {
89
- logger.log('WARN', `[UserActivitySampler] Invalid response structure from proxy for block ${blockId}, batch attempt ${attempts}. Skipping batch.`);
90
- continue; // Skip to next attempt
91
- }
92
-
93
-
94
- if (!response.ok) {
95
- const errorText = await response.text();
96
- logger.log('WARN', `[UserActivitySampler] API error ${response.status} for block ${blockId}, batch attempt ${attempts}. Skipping batch. Error: ${errorText}`);
97
- wasSuccess = false;
98
- continue; // Skip to next attempt
99
- }
100
-
101
- wasSuccess = true;
102
- const publicUsersBatch = await response.json();
103
-
104
- if (Array.isArray(publicUsersBatch)) {
105
- const N_public_returned_batch = publicUsersBatch.length;
106
- N_public_sampled_block += N_public_returned_batch;
107
- public_users_data.push(...publicUsersBatch.map(u => ({
108
- CID: u.CID,
109
- LastActivity: u.Value?.LastActivity // Handle potential missing Value
110
- })));
111
- } else {
112
- logger.log('WARN', `[UserActivitySampler] API response was not an array for block ${blockId}, batch attempt ${attempts}. Skipping batch.`);
113
- continue;
114
- }
115
-
116
-
117
- } catch (fetchError) {
118
- logger.log('ERROR', `[UserActivitySampler] Fetch failed for block ${blockId}, batch attempt ${attempts}. Skipping batch.`, { errorMessage: fetchError.message });
119
- wasSuccess = false; // Mark as failure for header performance
120
- } finally {
121
- if (selectedHeader) {
122
- headerManager.updatePerformance(selectedHeader.id, wasSuccess);
123
- }
124
- }
125
- await new Promise(resolve => setTimeout(resolve, config.delayBetweenBatchesMs || 200));
104
+ if (!response.ok) {
105
+ const errorText = await response.text();
106
+ logger.log('WARN', `[SamplerTask] API error ${response.status} for block ${blockId}. Error: ${errorText}`);
107
+ wasSuccess = false;
108
+ return { success: false, cidsSent: cidsToSample.length, publicUsers: [] };
109
+ }
126
110
 
127
- } // End while loop for block sampling
111
+ wasSuccess = true;
112
+ const publicUsersBatch = await response.json();
128
113
 
129
- if (attempts >= MAX_ATTEMPTS) {
130
- logger.log('WARN', `[UserActivitySampler] Reached max sampling attempts (${MAX_ATTEMPTS}) for block ${blockId}. Proceeding with ${N_public_sampled_block} users.`);
131
- }
114
+ if (!Array.isArray(publicUsersBatch)) {
115
+ logger.log('WARN', `[SamplerTask] API response was not an array for block ${blockId}.`);
116
+ return { success: false, cidsSent: cidsToSample.length, publicUsers: [] };
117
+ }
118
+
119
+ // Return a successful result
120
+ return {
121
+ success: true,
122
+ cidsSent: cidsToSample.length,
123
+ publicUsers: publicUsersBatch.map(u => ({
124
+ CID: u.CID,
125
+ LastActivity: u.Value?.LastActivity
126
+ }))
127
+ };
128
+
129
+ } catch (fetchError) {
130
+ logger.log('ERROR', `[SamplerTask] Fetch failed for block ${blockId}.`, { errorMessage: fetchError.message });
131
+ wasSuccess = false;
132
+ return { success: false, cidsSent: cidsToSample.length, publicUsers: [] };
133
+ } finally {
134
+ if (selectedHeader) {
135
+ headerManager.updatePerformance(selectedHeader.id, wasSuccess);
136
+ }
137
+ }
138
+ }
132
139
 
140
+ /**
141
+ * Main pipe (Task Handler): pipe.maintenance.handleSampleBlockTask
142
+ * This function is triggered by Pub/Sub for a single block.
143
+ * It runs the sampling in parallel to finish within timeout.
144
+ *
145
+ * @param {object} message - The Pub/Sub message.
146
+ * @param {object} context - The message context.
147
+ * @param {object} config - Configuration object.
148
+ * @param {object} dependencies - Contains db, logger, headerManager, proxyManager.
149
+ * @returns {Promise<void>}
150
+ */
151
+ exports.handleSampleBlockTask = async (message, context, config, dependencies) => {
152
+ const { db, logger, headerManager } = dependencies;
153
+
154
+ let task;
155
+ try {
156
+ task = JSON.parse(Buffer.from(message.data, 'base64').toString('utf-8'));
157
+ } catch (e) {
158
+ logger.log('ERROR', '[SamplerTask] Failed to parse Pub/Sub message data.', { error: e.message, data: message.data });
159
+ return; // Acknowledge the message to prevent retries
160
+ }
133
161
 
134
- const f_private_block = N_sampled_total_block > 0 ? 1 - (N_public_sampled_block / N_sampled_total_block) : 0;
162
+ const { blockId } = task;
163
+ const taskId = `block-${blockId}-${context.eventId || Date.now()}`;
164
+ const today = new Date().toISOString().slice(0, 10);
165
+ logger.log('INFO', `[SamplerTask/${taskId}] Processing block ${blockId}...`);
135
166
 
136
- // Categorize users
137
- const counts = { A1: 0, A2: 0, A3: 0, A4: 0 };
138
- const now = new Date();
139
- const oneDayAgo = new Date(now.getTime() - (24 * 60 * 60 * 1000));
140
- const oneWeekAgo = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
141
- const threeMonthsAgo = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000)); // Approx
167
+ // --- Config validation for this task ---
168
+ if (!config.rankingsApiUrl || !config.targetPublicUsersPerBlock || !config.apiBatchSize || !config.outputCollectionName || !config.parallelRequests) {
169
+ logger.log('ERROR', `[SamplerTask/${taskId}] Missing required configuration for task execution.`);
170
+ throw new Error('Missing required configuration for Sampler Task.');
171
+ }
142
172
 
173
+ const processedInThisRun = new Set();
174
+ let N_public_sampled_block = 0;
175
+ let N_sampled_total_block = 0;
176
+ const public_users_data = []; // Store { CID, LastActivity }
143
177
 
144
- public_users_data.forEach(user => {
145
- if (!user.LastActivity) {
146
- counts.A4++; // Assume inactive if LastActivity is missing
147
- return;
148
- }
149
- try {
150
- const lastActivityDate = new Date(user.LastActivity);
151
- if (isNaN(lastActivityDate)) {
152
- counts.A4++; // Treat invalid dates as inactive
153
- return;
154
- }
155
-
156
- if (lastActivityDate >= oneDayAgo) counts.A1++;
157
- else if (lastActivityDate >= oneWeekAgo) counts.A2++;
158
- else if (lastActivityDate >= threeMonthsAgo) counts.A3++;
159
- else counts.A4++;
160
- } catch(e) {
161
- logger.log('WARN', `[UserActivitySampler] Error parsing LastActivity date '${user.LastActivity}' for user ${user.CID}. Counting as A4.`);
162
- counts.A4++;
163
- }
178
+ const MAX_ATTEMPTS = config.maxSamplingAttemptsPerBlock || 1000;
179
+ let totalBatchesAttempted = 0;
180
+ const CONCURRENT_REQUESTS = config.parallelRequests;
164
181
 
165
- });
182
+ try {
183
+ // --- Start Parallel Loop ---
184
+ while (N_public_sampled_block < config.targetPublicUsersPerBlock && totalBatchesAttempted < MAX_ATTEMPTS) {
185
+ const promises = [];
186
+ const numRequests = Math.min(CONCURRENT_REQUESTS, MAX_ATTEMPTS - totalBatchesAttempted);
187
+
188
+ logger.log('TRACE', `[SamplerTask/${taskId}] Starting parallel batch of ${numRequests} requests...`);
189
+ for (let i = 0; i < numRequests; i++) {
190
+ promises.push(fetchSampleBatch(blockId, config, dependencies, processedInThisRun));
191
+ }
166
192
 
167
- const fractions = {};
168
- for (const category in counts) {
169
- fractions[category] = N_public_sampled_block > 0 ? (counts[category] / N_public_sampled_block) : 0;
193
+ const results = await Promise.allSettled(promises);
194
+ totalBatchesAttempted += numRequests;
195
+
196
+ // Process results from the parallel batch
197
+ for (const result of results) {
198
+ if (result.status === 'fulfilled' && result.value.success) {
199
+ const batchResult = result.value;
200
+ N_sampled_total_block += batchResult.cidsSent;
201
+ N_public_sampled_block += batchResult.publicUsers.length;
202
+ public_users_data.push(...batchResult.publicUsers);
203
+ } else if (result.status === 'fulfilled' && !result.value.success) {
204
+ // Failed API call, but we still count the CIDs we tried to sample
205
+ N_sampled_total_block += result.value.cidsSent;
206
+ } else {
207
+ // Promise rejected (unexpected error)
208
+ logger.log('WARN', `[SamplerTask/${taskId}] A sample fetch promise was rejected.`, { reason: result.reason });
209
+ }
170
210
  }
211
+
212
+ logger.log('INFO', `[SamplerTask/${taskId}] Batch complete. Total public sampled: ${N_public_sampled_block}/${config.targetPublicUsersPerBlock}`);
171
213
 
172
- const N_block = 1000000; // Total users assumed per block
214
+ // --- REMOVED artificial delay ---
215
+ }
216
+ // --- End Parallel Loop ---
173
217
 
174
- const estimatedCounts = {};
175
- for (const category in fractions) {
176
- estimatedCounts[category] = Math.round(fractions[category] * N_block);
177
- }
218
+ if (totalBatchesAttempted >= MAX_ATTEMPTS) {
219
+ logger.log('WARN', `[SamplerTask/${taskId}] Reached max sampling attempts (${MAX_ATTEMPTS}). Proceeding with ${N_public_sampled_block} users.`);
220
+ }
178
221
 
179
- const result = {
180
- blockId: blockId,
181
- sampledDate: today,
182
- f_private: f_private_block,
183
- publicSampleSize: N_public_sampled_block,
184
- totalUsersInBlock: N_block,
185
- activityCounts_Sample: counts,
186
- activityFractions_Sample: fractions,
187
- estimatedCounts_TotalBlock: estimatedCounts,
188
- lastUpdated: FieldValue.serverTimestamp()
189
- };
222
+ // --- Calculate and Store Results (same as before) ---
223
+ const f_private_block = N_sampled_total_block > 0 ? 1 - (N_public_sampled_block / N_sampled_total_block) : 0;
190
224
 
191
- blockResults.push(result);
192
- totalPublicSampled += N_public_sampled_block;
193
- totalPrivateEstimate += f_private_block * N_block;
225
+ const counts = { A1: 0, A2: 0, A3: 0, A4: 0 };
226
+ const now = new Date();
227
+ const oneDayAgo = new Date(now.getTime() - (24 * 60 * 60 * 1000));
228
+ const oneWeekAgo = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
229
+ const threeMonthsAgo = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000));
194
230
 
195
- // Store result for this block
196
- const docRef = db.collection(config.outputCollectionName).doc(`${blockId}_${today}`);
197
- await docRef.set(result);
198
- logger.log('INFO', `[UserActivitySampler] Stored results for block ${blockId}. Sampled: ${N_public_sampled_block}. Private fraction: ${f_private_block.toFixed(3)}.`);
231
+ public_users_data.forEach(user => {
232
+ if (!user.LastActivity) {
233
+ counts.A4++; return;
234
+ }
235
+ try {
236
+ const lastActivityDate = new Date(user.LastActivity);
237
+ if (isNaN(lastActivityDate)) { counts.A4++; return; }
238
+ if (lastActivityDate >= oneDayAgo) counts.A1++;
239
+ else if (lastActivityDate >= oneWeekAgo) counts.A2++;
240
+ else if (lastActivityDate >= threeMonthsAgo) counts.A3++;
241
+ else counts.A4++;
242
+ } catch (e) {
243
+ logger.log('WARN', `[SamplerTask/${taskId}] Error parsing LastActivity date '${user.LastActivity}' for user ${user.CID}. Counting as A4.`);
244
+ counts.A4++;
245
+ }
246
+ });
199
247
 
200
- } // End for loop through blocks
248
+ const fractions = {};
249
+ for (const category in counts) {
250
+ fractions[category] = N_public_sampled_block > 0 ? (counts[category] / N_public_sampled_block) : 0;
251
+ }
201
252
 
202
- await headerManager.flushPerformanceUpdates();
203
- logger.log('SUCCESS', `[UserActivitySampler] Sampling complete. Processed ${blockResults.length} blocks. Total public sampled: ${totalPublicSampled}.`);
253
+ const N_block = 1000000;
254
+ const estimatedCounts = {};
255
+ for (const category in fractions) {
256
+ estimatedCounts[category] = Math.round(fractions[category] * N_block);
257
+ }
204
258
 
205
- return {
206
- success: true,
207
- processedBlocks: blockResults.length,
208
- totalPublicSampled: totalPublicSampled,
209
- estimatedTotalPrivate: Math.round(totalPrivateEstimate)
259
+ const result = {
260
+ blockId: blockId,
261
+ sampledDate: today,
262
+ f_private: f_private_block,
263
+ publicSampleSize: N_public_sampled_block,
264
+ totalSampleSize: N_sampled_total_block,
265
+ totalUsersInBlock: N_block,
266
+ activityCounts_Sample: counts,
267
+ activityFractions_Sample: fractions,
268
+ estimatedCounts_TotalBlock: estimatedCounts,
269
+ lastUpdated: FieldValue.serverTimestamp()
210
270
  };
211
271
 
272
+ const docRef = db.collection(config.outputCollectionName).doc(`${blockId}_${today}`);
273
+ await docRef.set(result);
274
+
275
+ logger.log('SUCCESS', `[SamplerTask/${taskId}] Stored results for block ${blockId}. Sampled: ${N_public_sampled_block}. Private fraction: ${f_private_block.toFixed(3)}.`);
276
+
212
277
  } catch (error) {
213
- logger.log('ERROR', '[UserActivitySampler] Fatal error during sampling process.', { errorMessage: error.message, errorStack: error.stack });
214
- try { await headerManager.flushPerformanceUpdates(); } catch (e) { /* ignore flush error */ }
278
+ logger.log('ERROR', `[SamplerTask/${taskId}] Fatal error during task execution.`, { errorMessage: error.message, errorStack: error.stack });
279
+ // Re-throw the error to signal failure to Cloud Functions, which will trigger a retry.
215
280
  throw error;
281
+ } finally {
282
+ // Always flush header performance at the end of the task, even on failure.
283
+ try {
284
+ await headerManager.flushPerformanceUpdates();
285
+ logger.log('INFO', `[SamplerTask/${taskId}] Header performance flushed.`);
286
+ } catch (flushError) {
287
+ logger.log('ERROR', `[SamplerTask/${taskId}] Failed to flush header performance.`, { errorMessage: flushError.message });
288
+ }
216
289
  }
217
290
  };
package/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  // --- Core Utilities (Classes and Stateless Helpers) ---
8
- // These are imported directly as they form the 'core' dependency set
8
+ // ... (no changes here) ...
9
9
  const core = {
10
10
  IntelligentHeaderManager: require('./functions/core/utils/intelligent_header_manager').IntelligentHeaderManager,
11
11
  IntelligentProxyManager: require('./functions/core/utils/intelligent_proxy_manager').IntelligentProxyManager,
@@ -15,7 +15,7 @@ const core = {
15
15
  };
16
16
 
17
17
  // --- Pipe 1: Orchestrator ---
18
- // The 'brain' that decides what work to do
18
+ // ... (no changes here) ...
19
19
  const orchestrator = {
20
20
  // Main Pipes (Entry points for Cloud Functions)
21
21
  runDiscoveryOrchestrator: require('./functions/orchestrator/index').runDiscoveryOrchestrator,
@@ -32,7 +32,7 @@ const orchestrator = {
32
32
  };
33
33
 
34
34
  // --- Pipe 2: Dispatcher ---
35
- // The 'throttle' that rate-limits tasks
35
+ // ... (no changes here) ...
36
36
  const dispatcher = {
37
37
  // Main Pipe
38
38
  handleRequest: require('./functions/dispatcher/index').handleRequest,
@@ -42,7 +42,7 @@ const dispatcher = {
42
42
  };
43
43
 
44
44
  // --- Pipe 3: Task Engine ---
45
- // The 'factory' that executes individual tasks
45
+ // ... (no changes here) ...
46
46
  const taskEngine = {
47
47
  // Main Pipe
48
48
  handleRequest: require('./functions/task-engine/handler_creator').handleRequest,
@@ -54,7 +54,7 @@ const taskEngine = {
54
54
  };
55
55
 
56
56
  // --- Pipe 4: Computation System ---
57
- // The 'refinery' that processes raw data
57
+ // ... (no changes here) ...
58
58
  const computationSystem = {
59
59
  // Main Pipe
60
60
  runOrchestration: require('./functions/computation-system/helpers/orchestration_helpers').runComputationOrchestrator,
@@ -65,7 +65,7 @@ const computationSystem = {
65
65
  };
66
66
 
67
67
  // --- Pipe 5: API ---
68
- // The 'tap' that serves processed data
68
+ // ... (no changes here) ...
69
69
  const api = {
70
70
  // Main Pipe
71
71
  createApiApp: require('./functions/generic-api/index').createApiApp,
@@ -81,11 +81,15 @@ const maintenance = {
81
81
  handleInvalidSpeculator: require('./functions/invalid-speculator-handler/helpers/handler_helpers').handleInvalidSpeculator,
82
82
  runFetchInsights: require('./functions/fetch-insights/helpers/handler_helpers').fetchAndStoreInsights,
83
83
  runFetchPrices: require('./functions/etoro-price-fetcher/helpers/handler_helpers').fetchAndStorePrices,
84
- runUserActivitySampler: require('./functions/user-activity-sampler/helpers/sampler_helpers').runUserActivitySampler,
84
+
85
+ // --- UPDATED ---
86
+ runUserActivitySamplerOrchestrator: require('./functions/user-activity-sampler/helpers/sampler_helpers').runUserActivitySamplerOrchestrator,
87
+ handleSampleBlockTask: require('./functions/user-activity-sampler/helpers/sampler_helpers').handleSampleBlockTask,
88
+ // --- END UPDATE ---
85
89
  };
86
90
 
87
91
  // --- Pipe 7: Proxy ---
88
- // The Google Apps Script proxy endpoint logic
92
+ // ... (no changes here) ...
89
93
  const proxy = {
90
94
  handlePost: require('./functions/appscript-api/index').handlePost,
91
95
  };
@@ -102,4 +106,4 @@ module.exports = {
102
106
  maintenance,
103
107
  proxy,
104
108
  }
105
- };
109
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.60",
3
+ "version": "1.0.62",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [