bulltrackers-module 1.0.667 → 1.0.669
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.
|
@@ -14,12 +14,69 @@ const { runFinalSweepCheck } = require('../tools/FinalSweepReporter');
|
|
|
14
14
|
// 1. IMPORT SNAPSHOT SERVICE
|
|
15
15
|
const { generateDailySnapshots } = require('../services/SnapshotService');
|
|
16
16
|
const crypto = require('crypto');
|
|
17
|
+
// Import Google Auth Library for OAuth 2.0 authentication
|
|
18
|
+
let GoogleAuth = null;
|
|
19
|
+
try {
|
|
20
|
+
GoogleAuth = require('google-auth-library').GoogleAuth;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
// google-auth-library might not be installed, will handle gracefully
|
|
23
|
+
}
|
|
17
24
|
|
|
18
25
|
const BASE_SECONDS_PER_WEIGHT_UNIT = 3;
|
|
19
26
|
const SESSION_CACHE_DURATION_MS = 1000 * 60 * 30; // 30 Minutes
|
|
20
27
|
const STALE_LOCK_THRESHOLD_MS = 1000 * 60 * 15;
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Helper function to send an authenticated callback to Cloud Workflows
|
|
31
|
+
* Uses OAuth 2.0 access token from the default service account
|
|
32
|
+
*/
|
|
33
|
+
async function sendAuthenticatedCallback(callbackUrl, payload, logger, directFetch) {
|
|
34
|
+
if (!GoogleAuth) {
|
|
35
|
+
throw new Error('google-auth-library is required for authenticated callbacks. Please install: npm install google-auth-library');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Get OAuth 2.0 access token using default service account credentials
|
|
40
|
+
const auth = new GoogleAuth({
|
|
41
|
+
scopes: ['https://www.googleapis.com/auth/cloud-platform']
|
|
42
|
+
});
|
|
43
|
+
const accessToken = await auth.getAccessToken();
|
|
44
|
+
|
|
45
|
+
if (!accessToken) {
|
|
46
|
+
throw new Error('Failed to obtain OAuth 2.0 access token');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
logger.log('INFO', '[Dispatcher] ✅ Obtained OAuth 2.0 access token for callback');
|
|
50
|
+
|
|
51
|
+
// Add timeout to prevent hanging (30 seconds should be plenty for a callback)
|
|
52
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
53
|
+
setTimeout(() => reject(new Error('Callback timeout after 30s')), 30000)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const fetchPromise = directFetch(callbackUrl, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
'Authorization': `Bearer ${accessToken}`
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify(payload)
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const response = await Promise.race([fetchPromise, timeoutPromise]);
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const errorText = await response.text().catch(() => 'Unable to read response');
|
|
69
|
+
throw new Error(`Callback returned non-OK status: ${response.status} ${response.statusText}. Response: ${errorText.substring(0, 200)}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logger.log('INFO', `[Dispatcher] ✅ Callback sent successfully (status: ${response.status})`);
|
|
73
|
+
return response;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logger.log('ERROR', `[Dispatcher] Authenticated callback failed: ${err.message}. Stack: ${err.stack?.substring(0, 300)}`);
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
23
80
|
|
|
24
81
|
function getMillis(field) {
|
|
25
82
|
if (!field) return 0;
|
|
@@ -168,27 +225,9 @@ async function handleSnapshot(config, dependencies, reqBody) {
|
|
|
168
225
|
if (callbackUrl) {
|
|
169
226
|
logger.log('INFO', `[Dispatcher] 📞 Calling back Workflow at: ${callbackUrl}`);
|
|
170
227
|
try {
|
|
171
|
-
|
|
172
|
-
const timeoutPromise = new Promise((_, reject) =>
|
|
173
|
-
setTimeout(() => reject(new Error('Callback timeout after 30s')), 30000)
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
const fetchPromise = directFetch(callbackUrl, {
|
|
177
|
-
method: 'POST',
|
|
178
|
-
headers: { 'Content-Type': 'application/json' },
|
|
179
|
-
body: JSON.stringify(finalResult)
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
const response = await Promise.race([fetchPromise, timeoutPromise]);
|
|
183
|
-
|
|
184
|
-
if (!response.ok) {
|
|
185
|
-
const errorText = await response.text().catch(() => 'Unable to read response');
|
|
186
|
-
logger.log('ERROR', `[Dispatcher] Callback returned non-OK status: ${response.status} ${response.statusText}. Response: ${errorText.substring(0, 200)}`);
|
|
187
|
-
} else {
|
|
188
|
-
logger.log('INFO', `[Dispatcher] ✅ Callback sent successfully (status: ${response.status})`);
|
|
189
|
-
}
|
|
228
|
+
await sendAuthenticatedCallback(callbackUrl, finalResult, logger, directFetch);
|
|
190
229
|
} catch (err) {
|
|
191
|
-
logger.log('ERROR', `[Dispatcher] Callback failed: ${err.message}
|
|
230
|
+
logger.log('ERROR', `[Dispatcher] Callback failed: ${err.message}`);
|
|
192
231
|
}
|
|
193
232
|
} else {
|
|
194
233
|
logger.log('WARN', '[Dispatcher] No callback URL provided, workflow will not be notified');
|
|
@@ -240,27 +279,9 @@ async function handleSnapshot(config, dependencies, reqBody) {
|
|
|
240
279
|
if (callbackUrl) {
|
|
241
280
|
logger.log('INFO', `[Dispatcher] 📞 Calling back Workflow at: ${callbackUrl}`);
|
|
242
281
|
try {
|
|
243
|
-
|
|
244
|
-
const timeoutPromise = new Promise((_, reject) =>
|
|
245
|
-
setTimeout(() => reject(new Error('Callback timeout after 30s')), 30000)
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
const fetchPromise = directFetch(callbackUrl, {
|
|
249
|
-
method: 'POST',
|
|
250
|
-
headers: { 'Content-Type': 'application/json' },
|
|
251
|
-
body: JSON.stringify(finalResult)
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
const response = await Promise.race([fetchPromise, timeoutPromise]);
|
|
255
|
-
|
|
256
|
-
if (!response.ok) {
|
|
257
|
-
const errorText = await response.text().catch(() => 'Unable to read response');
|
|
258
|
-
logger.log('ERROR', `[Dispatcher] Callback returned non-OK status: ${response.status} ${response.statusText}. Response: ${errorText.substring(0, 200)}`);
|
|
259
|
-
} else {
|
|
260
|
-
logger.log('INFO', `[Dispatcher] ✅ Callback sent successfully (status: ${response.status})`);
|
|
261
|
-
}
|
|
282
|
+
await sendAuthenticatedCallback(callbackUrl, finalResult, logger, directFetch);
|
|
262
283
|
} catch (err) {
|
|
263
|
-
logger.log('ERROR', `[Dispatcher] Callback failed: ${err.message}
|
|
284
|
+
logger.log('ERROR', `[Dispatcher] Callback failed: ${err.message}`);
|
|
264
285
|
}
|
|
265
286
|
} else {
|
|
266
287
|
logger.log('WARN', '[Dispatcher] No callback URL provided, workflow will not be notified');
|
|
@@ -275,27 +296,9 @@ async function handleSnapshot(config, dependencies, reqBody) {
|
|
|
275
296
|
if (callbackUrl) {
|
|
276
297
|
logger.log('INFO', `[Dispatcher] 📞 Calling back Workflow with error at: ${callbackUrl}`);
|
|
277
298
|
try {
|
|
278
|
-
|
|
279
|
-
const timeoutPromise = new Promise((_, reject) =>
|
|
280
|
-
setTimeout(() => reject(new Error('Callback timeout after 30s')), 30000)
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
const fetchPromise = directFetch(callbackUrl, {
|
|
284
|
-
method: 'POST',
|
|
285
|
-
headers: { 'Content-Type': 'application/json' },
|
|
286
|
-
body: JSON.stringify(errorResult)
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
const response = await Promise.race([fetchPromise, timeoutPromise]);
|
|
290
|
-
|
|
291
|
-
if (!response.ok) {
|
|
292
|
-
const errorText = await response.text().catch(() => 'Unable to read response');
|
|
293
|
-
logger.log('ERROR', `[Dispatcher] Error callback returned non-OK status: ${response.status} ${response.statusText}. Response: ${errorText.substring(0, 200)}`);
|
|
294
|
-
} else {
|
|
295
|
-
logger.log('INFO', `[Dispatcher] ✅ Error callback sent successfully (status: ${response.status})`);
|
|
296
|
-
}
|
|
299
|
+
await sendAuthenticatedCallback(callbackUrl, errorResult, logger, directFetch);
|
|
297
300
|
} catch (err) {
|
|
298
|
-
logger.log('ERROR', `[Dispatcher] Error callback failed: ${err.message}
|
|
301
|
+
logger.log('ERROR', `[Dispatcher] Error callback failed: ${err.message}`);
|
|
299
302
|
}
|
|
300
303
|
} else {
|
|
301
304
|
logger.log('WARN', '[Dispatcher] No callback URL provided, workflow will not be notified of error');
|
|
@@ -38,7 +38,18 @@ async function tryLoadFromGCS(config, dateString, snapshotName, logger) {
|
|
|
38
38
|
if (exists) {
|
|
39
39
|
logger.log('INFO', `[DataLoader] ⚡️ GCS HIT: ${snapshotName} for ${dateString}`);
|
|
40
40
|
const [content] = await file.download();
|
|
41
|
-
|
|
41
|
+
|
|
42
|
+
// FIX: GCS client auto-decompresses if Content-Encoding is gzip.
|
|
43
|
+
// We try gunzip first; if it fails with header check, it's likely already JSON.
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(zlib.gunzipSync(content).toString());
|
|
46
|
+
} catch (zipError) {
|
|
47
|
+
if (zipError.message && zipError.message.includes('incorrect header check')) {
|
|
48
|
+
// Content was already decompressed by the client
|
|
49
|
+
return JSON.parse(content.toString());
|
|
50
|
+
}
|
|
51
|
+
throw zipError;
|
|
52
|
+
}
|
|
42
53
|
}
|
|
43
54
|
} catch (e) {
|
|
44
55
|
logger.log('WARN', `[DataLoader] GCS Check Failed (${snapshotName}): ${e.message}`);
|
|
@@ -434,7 +445,19 @@ async function* streamPortfolioData(config, deps, dateString, providedRefs = nul
|
|
|
434
445
|
if (exists) {
|
|
435
446
|
logger.log('INFO', `[DataLoader] ⚡️ STREAMING: Hydrating Portfolios from GCS Snapshot`);
|
|
436
447
|
const [content] = await file.download();
|
|
437
|
-
|
|
448
|
+
|
|
449
|
+
// FIX: Handle Double Decompression
|
|
450
|
+
let fullData;
|
|
451
|
+
try {
|
|
452
|
+
fullData = JSON.parse(zlib.gunzipSync(content).toString());
|
|
453
|
+
} catch (zipError) {
|
|
454
|
+
if (zipError.message && zipError.message.includes('incorrect header check')) {
|
|
455
|
+
fullData = JSON.parse(content.toString());
|
|
456
|
+
} else {
|
|
457
|
+
throw zipError;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
438
461
|
yield fullData; // Yield all in one chunk as it fits in memory
|
|
439
462
|
return;
|
|
440
463
|
}
|
|
@@ -473,7 +496,7 @@ async function* streamHistoryData(config, deps, dateString, providedRefs = null,
|
|
|
473
496
|
if (exists) {
|
|
474
497
|
logger.log('INFO', `[DataLoader] ⚡️ STREAMING: Hydrating History from GCS (JSONL)`);
|
|
475
498
|
|
|
476
|
-
const fileStream = file.createReadStream()
|
|
499
|
+
const fileStream = file.createReadStream();
|
|
477
500
|
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
478
501
|
|
|
479
502
|
let currentBatch = {};
|