bulltrackers-module 1.0.659 → 1.0.661

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.
@@ -5,9 +5,12 @@
5
5
 
6
6
  const { FieldValue } = require('@google-cloud/firestore');
7
7
  const zlib = require('zlib');
8
+ const { Storage } = require('@google-cloud/storage');
8
9
  const { getAlertTypeByComputation, generateAlertMessage } = require('./alert_type_registry');
9
10
  // Migration helpers removed - write directly to new path
10
11
 
12
+ const storage = new Storage(); // Singleton GCS Client
13
+
11
14
  /**
12
15
  * Process alerts for a specific PI from computation results
13
16
  */
@@ -474,10 +477,48 @@ function readComputationResults(docData) {
474
477
  }
475
478
 
476
479
  /**
477
- * Read computation results, handling sharded data
480
+ * Read computation results, handling GCS pointers, sharded data, and compressed data
481
+ * UPDATED: Added GCS pointer support to read from GCS when data is offloaded
478
482
  */
479
- async function readComputationResultsWithShards(db, docData, docRef) {
483
+ async function readComputationResultsWithShards(db, docData, docRef, logger = null) {
480
484
  try {
485
+ // -------------------------------------------------------------------------
486
+ // 1. GCS POINTER HANDLER (Check first - highest priority)
487
+ // -------------------------------------------------------------------------
488
+ if (docData.gcsUri || (docData._gcs && docData.gcsBucket && docData.gcsPath)) {
489
+ try {
490
+ const bucketName = docData.gcsBucket || docData.gcsUri.split('/')[2];
491
+ const fileName = docData.gcsPath || docData.gcsUri.split('/').slice(3).join('/');
492
+
493
+ if (logger) {
494
+ logger.log('INFO', `[AlertSystem] Reading computation results from GCS: ${fileName}`);
495
+ }
496
+
497
+ // Stream download is memory efficient for large files
498
+ const [fileContent] = await storage.bucket(bucketName).file(fileName).download();
499
+
500
+ // Assume Gzip (as writer does it), if fails try plain
501
+ let decompressedData;
502
+ try {
503
+ decompressedData = JSON.parse(zlib.gunzipSync(fileContent).toString('utf8'));
504
+ } catch (gzipErr) {
505
+ // Fallback for uncompressed GCS files
506
+ decompressedData = JSON.parse(fileContent.toString('utf8'));
507
+ }
508
+
509
+ // Process the decompressed data through readComputationResults
510
+ return readComputationResults(decompressedData);
511
+ } catch (gcsErr) {
512
+ if (logger) {
513
+ logger.log('ERROR', `[AlertSystem] GCS fetch failed, falling back to Firestore: ${gcsErr.message}`);
514
+ }
515
+ // Fall through to Firestore logic below
516
+ }
517
+ }
518
+
519
+ // -------------------------------------------------------------------------
520
+ // 2. FIRESTORE SHARDED HANDLER
521
+ // -------------------------------------------------------------------------
481
522
  if (docData._sharded === true && docData._shardCount) {
482
523
  const shardsCol = docRef.collection('_shards');
483
524
  const shardsSnapshot = await shardsCol.get();
@@ -492,9 +533,17 @@ async function readComputationResultsWithShards(db, docData, docRef) {
492
533
  return readComputationResults(mergedData);
493
534
  }
494
535
  }
536
+
537
+ // -------------------------------------------------------------------------
538
+ // 3. FIRESTORE COMPRESSED OR DIRECT DATA HANDLER
539
+ // -------------------------------------------------------------------------
495
540
  return readComputationResults(docData);
496
541
  } catch (error) {
497
- console.error('[readComputationResultsWithShards] Error reading sharded results', error);
542
+ if (logger) {
543
+ logger.log('ERROR', `[AlertSystem] Error reading computation results: ${error.message}`);
544
+ } else {
545
+ console.error('[readComputationResultsWithShards] Error reading sharded results', error);
546
+ }
498
547
  return { cids: [], metadata: {}, perUserData: {} };
499
548
  }
500
549
  }
@@ -72,9 +72,9 @@ async function handleAlertTrigger(message, context, config, dependencies) {
72
72
  return;
73
73
  }
74
74
 
75
- // 3. Read and decompress computation results
75
+ // 3. Read and decompress computation results (handling GCS, shards, and compression)
76
76
  const docData = docSnapshot.data();
77
- const results = readComputationResults(docData);
77
+ const results = await readComputationResultsWithShards(db, docData, docRef, logger);
78
78
 
79
79
  if (!results.cids || results.cids.length === 0) {
80
80
  logger.log('INFO', `[AlertTrigger] No PIs found in computation results for ${computationName}`);
@@ -188,7 +188,7 @@ async function handleComputationResultWrite(change, context, config, dependencie
188
188
  // If it's PopularInvestorProfileMetrics, check for all-clear notifications only
189
189
  if (isProfileMetrics) {
190
190
  const docData = change.after.data();
191
- const results = await readComputationResultsWithShards(db, docData, change.after.ref);
191
+ const results = await readComputationResultsWithShards(db, docData, change.after.ref, logger);
192
192
  if (results.cids && results.cids.length > 0) {
193
193
  await checkAndSendAllClearNotifications(db, logger, results.cids, date, config, dependencies);
194
194
  }
@@ -203,9 +203,9 @@ async function handleComputationResultWrite(change, context, config, dependencie
203
203
 
204
204
  logger.log('INFO', `[AlertTrigger] Processing alert computation: ${computationName} for date ${date}`);
205
205
 
206
- // 2. Read and decompress computation results (handling shards)
206
+ // 2. Read and decompress computation results (handling GCS, shards, and compression)
207
207
  const docData = change.after.data();
208
- const results = await readComputationResultsWithShards(db, docData, change.after.ref);
208
+ const results = await readComputationResultsWithShards(db, docData, change.after.ref, logger);
209
209
 
210
210
  if (!results.cids || results.cids.length === 0) {
211
211
  logger.log('INFO', `[AlertTrigger] No PIs found in computation results for ${computationName}`);
@@ -1,10 +1,13 @@
1
1
  // Firestore helper functions for fetching data from collections
2
2
  const { FieldValue, Timestamp } = require('@google-cloud/firestore');
3
+ const { Storage } = require('@google-cloud/storage');
3
4
  const { dispatchSyncRequest } = require('../task_engine_helper.js');
4
5
  const { sanitizeCid, sanitizeDocId } = require('../security_utils.js');
5
6
  const crypto = require('crypto');
6
7
  const zlib = require('zlib');
7
8
 
9
+ const storage = new Storage(); // Singleton GCS Client
10
+
8
11
  // 1. Fetch latest stored snapshots of user data from a user-centric collection
9
12
 
10
13
  // Examples
@@ -1210,14 +1213,40 @@ const getComputationResults = async (db, computationName, dateStr, userId = null
1210
1213
 
1211
1214
  const pointerData = pointerSnap.data();
1212
1215
 
1213
- // 2. Strategy: Compressed Data
1216
+ // 2. Strategy: GCS Pointer (Check first - highest priority)
1217
+ // If _gcs is true or gcsUri exists, the data is stored in GCS
1218
+ // Note: Page mode is exempt from GCS logic (handled separately below)
1219
+ if (pointerData._isPageMode !== true && (pointerData.gcsUri || (pointerData._gcs && pointerData.gcsBucket && pointerData.gcsPath))) {
1220
+ try {
1221
+ const bucketName = pointerData.gcsBucket || pointerData.gcsUri.split('/')[2];
1222
+ const fileName = pointerData.gcsPath || pointerData.gcsUri.split('/').slice(3).join('/');
1223
+
1224
+ console.log(`[Computation] Reading from GCS: ${fileName} for ${computationName}`);
1225
+
1226
+ // Stream download is memory efficient for large files
1227
+ const [fileContent] = await storage.bucket(bucketName).file(fileName).download();
1228
+
1229
+ // Assume Gzip (as writer does it), if fails try plain
1230
+ try {
1231
+ return JSON.parse(zlib.gunzipSync(fileContent).toString('utf8'));
1232
+ } catch (gzipErr) {
1233
+ // Fallback for uncompressed GCS files
1234
+ return JSON.parse(fileContent.toString('utf8'));
1235
+ }
1236
+ } catch (gcsErr) {
1237
+ console.error(`[Computation] GCS fetch failed for ${computationName}, falling back to Firestore: ${gcsErr.message}`);
1238
+ // Fall through to Firestore strategies below
1239
+ }
1240
+ }
1241
+
1242
+ // 3. Strategy: Compressed Data
1214
1243
  // If _compressed is true, the data is inside the payload field, just zipped.
1215
1244
  if (pointerData._compressed === true) {
1216
1245
  console.log(`[Computation] Reading compressed data for ${computationName}`);
1217
1246
  return tryDecompress(pointerData);
1218
1247
  }
1219
1248
 
1220
- // 3. Strategy: Sharded Data
1249
+ // 4. Strategy: Sharded Data
1221
1250
  // If _sharded is true, we must fetch N documents from the _shards subcollection.
1222
1251
  if (pointerData._sharded === true) {
1223
1252
  const shardCount = pointerData._shardCount || 0;
@@ -1260,14 +1289,15 @@ const getComputationResults = async (db, computationName, dateStr, userId = null
1260
1289
  return reassembledData;
1261
1290
  }
1262
1291
 
1263
- // 4. Strategy: Page Mode (User Centric)
1292
+ // 5. Strategy: Page Mode (User Centric)
1264
1293
  // If _isPageMode is true, we delegate to the pageCollection helper.
1294
+ // Note: Page mode is exempt from GCS logic (uses individual user documents)
1265
1295
  if (pointerData._isPageMode === true) {
1266
1296
  console.log(`[Computation] Fetching page mode data for ${computationName} / User: ${userId}`);
1267
1297
  return await pageCollection(db, dateStr, computationName, userId);
1268
1298
  }
1269
1299
 
1270
- // 5. Strategy: Standard (Direct Read)
1300
+ // 6. Strategy: Standard (Direct Read)
1271
1301
  // If no flags are set, the data is in the pointer document itself.
1272
1302
  console.log(`[Computation] Returning direct pointer data for ${computationName}`);
1273
1303
  return pointerData;
@@ -190,15 +190,25 @@ async function handleForceRun(config, dependencies, computationManifest, reqBody
190
190
  const hasFailedDep = report.failedDependency.some(t => normalizeName(t.name) === targetComputationNormalized);
191
191
  const isImpossible = report.impossible.some(t => normalizeName(t.name) === targetComputationNormalized);
192
192
  const isBlocked = report.blocked.some(t => normalizeName(t.name) === targetComputationNormalized);
193
+ const isSkipped = report.skipped.some(t => normalizeName(t.name) === targetComputationNormalized);
193
194
 
194
- if (isRunnable || needsReRun || hasFailedDep) {
195
- runnableDates.push(date);
196
- } else if (isImpossible) {
195
+ // For force runs: treat skipped computations (already stored with valid hash) as runnable
196
+ // They will overwrite with the same result, which is fine for testing
197
+ // Only mark as impossible if root data or dependencies don't exist at all
198
+ if (isImpossible) {
197
199
  skippedDates.push({ date, reason: report.impossible.find(t => normalizeName(t.name) === targetComputationNormalized)?.reason || 'Impossible' });
200
+ } else if (isRunnable || needsReRun || hasFailedDep || isSkipped) {
201
+ // Runnable, needs re-run, has failed deps (but not impossible), or skipped (already stored)
202
+ // All of these are runnable for force runs - will overwrite existing results if needed
203
+ runnableDates.push(date);
198
204
  } else if (isBlocked) {
199
- skippedDates.push({ date, reason: report.blocked.find(t => normalizeName(t.name) === targetComputationNormalized)?.reason || 'Blocked' });
205
+ // Blocked usually means waiting for data - for force runs, if root data exists, still runnable
206
+ // Only skip if truly impossible (handled above)
207
+ runnableDates.push(date);
200
208
  } else {
201
- skippedDates.push({ date, reason: 'Not runnable (unknown reason)' });
209
+ // Unknown state - for force runs, if root data exists (which it does, since result is not null), treat as runnable
210
+ logger.log('WARN', `[ForceRun] Computation ${computationName} in unknown state for ${date}, treating as runnable`);
211
+ runnableDates.push(date);
202
212
  }
203
213
  }
204
214
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.659",
3
+ "version": "1.0.661",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [