bulltrackers-module 1.0.661 → 1.0.663
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.
- package/functions/computation-system/helpers/computation_dispatcher.js +62 -7
- package/functions/computation-system/persistence/ResultCommitter.js +2 -0
- package/functions/computation-system/services/SnapshotService.js +26 -7
- package/functions/computation-system/tools/BuildReporter.js +22 -10
- package/functions/computation-system/utils/schema_capture.js +36 -3
- package/package.json +1 -1
|
@@ -133,15 +133,70 @@ async function getStableDateSession(config, dependencies, pass, dateLimitStr, fo
|
|
|
133
133
|
// 2. NEW SNAPSHOT HANDLER
|
|
134
134
|
async function handleSnapshot(config, dependencies, reqBody) {
|
|
135
135
|
const { logger } = dependencies;
|
|
136
|
-
const
|
|
136
|
+
const targetDate = reqBody.date; // Optional: if provided, only process up to this date
|
|
137
137
|
|
|
138
|
-
if (!date) throw new Error('Snapshot action requires a "date"');
|
|
139
|
-
|
|
140
138
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
139
|
+
// Get earliest available root data date
|
|
140
|
+
const earliestDates = await getEarliestDataDates(config, dependencies);
|
|
141
|
+
const earliestDate = earliestDates.absoluteEarliest;
|
|
142
|
+
|
|
143
|
+
if (!earliestDate) {
|
|
144
|
+
throw new Error('Could not determine earliest available root data date');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Determine end date: use targetDate if provided, otherwise use today
|
|
148
|
+
const endDate = targetDate ? new Date(targetDate + 'T00:00:00Z') : new Date();
|
|
149
|
+
endDate.setUTCHours(0, 0, 0, 0);
|
|
150
|
+
|
|
151
|
+
// Generate all dates from earliest to end date
|
|
152
|
+
const startDate = new Date(earliestDate);
|
|
153
|
+
startDate.setUTCHours(0, 0, 0, 0);
|
|
154
|
+
|
|
155
|
+
const dateStrings = getExpectedDateStrings(startDate, endDate);
|
|
156
|
+
|
|
157
|
+
if (dateStrings.length === 0) {
|
|
158
|
+
logger.log('WARN', '[Dispatcher] No dates to process for snapshot');
|
|
159
|
+
return { status: 'OK', processed: 0, skipped: 0 };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
logger.log('INFO', `[Dispatcher] 📸 Processing snapshots for ${dateStrings.length} dates from ${dateStrings[0]} to ${dateStrings[dateStrings.length - 1]}`);
|
|
163
|
+
|
|
164
|
+
// Process each date (snapshot service will skip if already exists)
|
|
165
|
+
const results = [];
|
|
166
|
+
const BATCH_SIZE = 5; // Process 5 dates in parallel to avoid overwhelming the system
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < dateStrings.length; i += BATCH_SIZE) {
|
|
169
|
+
const batch = dateStrings.slice(i, i + BATCH_SIZE);
|
|
170
|
+
const batchResults = await Promise.allSettled(
|
|
171
|
+
batch.map(dateStr => generateDailySnapshots(dateStr, config, dependencies))
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
batchResults.forEach((result, idx) => {
|
|
175
|
+
const dateStr = batch[idx];
|
|
176
|
+
if (result.status === 'fulfilled') {
|
|
177
|
+
const value = result.value;
|
|
178
|
+
results.push({ date: dateStr, status: value.status || 'OK' });
|
|
179
|
+
} else {
|
|
180
|
+
logger.log('ERROR', `[Dispatcher] Snapshot failed for ${dateStr}: ${result.reason?.message || result.reason}`);
|
|
181
|
+
results.push({ date: dateStr, status: 'ERROR', error: result.reason?.message || String(result.reason) });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const successful = results.filter(r => r.status === 'OK').length;
|
|
187
|
+
const skipped = results.filter(r => r.status === 'SKIPPED').length;
|
|
188
|
+
const failed = results.filter(r => r.status === 'ERROR').length;
|
|
189
|
+
|
|
190
|
+
logger.log('INFO', `[Dispatcher] 📸 Snapshot batch complete: ${successful} processed, ${skipped} skipped, ${failed} failed out of ${results.length} total`);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
status: failed === 0 ? 'OK' : 'PARTIAL',
|
|
194
|
+
processed: successful,
|
|
195
|
+
skipped: skipped,
|
|
196
|
+
failed: failed,
|
|
197
|
+
total: results.length,
|
|
198
|
+
results: results
|
|
199
|
+
};
|
|
145
200
|
} catch (e) {
|
|
146
201
|
logger.log('ERROR', `[Dispatcher] Snapshot failed: ${e.message}`);
|
|
147
202
|
// Return error object so workflow can see failure
|
|
@@ -247,6 +247,8 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
247
247
|
|
|
248
248
|
if (calc.manifest.class.getSchema && flushMode !== 'INTERMEDIATE') {
|
|
249
249
|
const { class: _cls, ...safeMetadata } = calc.manifest;
|
|
250
|
+
// Ensure ttlDays is set to the resolved value (defaults to 90 if undefined)
|
|
251
|
+
safeMetadata.ttlDays = ttlDays;
|
|
250
252
|
schemas.push({ name, category: calc.manifest.category, schema: calc.manifest.class.getSchema(), metadata: safeMetadata });
|
|
251
253
|
}
|
|
252
254
|
if (calc.manifest.previousCategory && calc.manifest.previousCategory !== calc.manifest.category && flushMode !== 'INTERMEDIATE') {
|
|
@@ -10,11 +10,30 @@ const dataLoader = require('../utils/data_loader');
|
|
|
10
10
|
|
|
11
11
|
async function generateDailySnapshots(dateStr, config, deps) {
|
|
12
12
|
const { logger } = deps;
|
|
13
|
-
logger.log('INFO', `[SnapshotService] 📸 Starting Full System Snapshot for ${dateStr}`);
|
|
14
|
-
|
|
15
13
|
const bucketName = config.gcsBucketName || 'bulltrackers';
|
|
16
14
|
const bucket = storage.bucket(bucketName);
|
|
17
15
|
|
|
16
|
+
// Quick check: if all main snapshots exist, skip entirely
|
|
17
|
+
const mainFiles = [
|
|
18
|
+
`${dateStr}/snapshots/portfolios.json.gz`,
|
|
19
|
+
`${dateStr}/snapshots/social.json.gz`,
|
|
20
|
+
`${dateStr}/snapshots/history.jsonl.gz`,
|
|
21
|
+
`${dateStr}/snapshots/ratings.json.gz`,
|
|
22
|
+
`${dateStr}/snapshots/rankings.json.gz`
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
if (!config.forceSnapshot) {
|
|
26
|
+
const existenceChecks = await Promise.all(mainFiles.map(path => bucket.file(path).exists()));
|
|
27
|
+
const allExist = existenceChecks.every(([exists]) => exists);
|
|
28
|
+
|
|
29
|
+
if (allExist) {
|
|
30
|
+
logger.log('INFO', `[SnapshotService] ⏭️ All snapshots already exist for ${dateStr}, skipping`);
|
|
31
|
+
return { status: 'SKIPPED', date: dateStr, reason: 'all_exist' };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
logger.log('INFO', `[SnapshotService] 📸 Starting Full System Snapshot for ${dateStr}`);
|
|
36
|
+
|
|
18
37
|
// parallelize independent fetches
|
|
19
38
|
await Promise.all([
|
|
20
39
|
snapshotPortfolios(dateStr, bucket, config, deps), // Heavy
|
|
@@ -26,7 +45,7 @@ async function generateDailySnapshots(dateStr, config, deps) {
|
|
|
26
45
|
snapshotMetadata(dateStr, bucket, config, deps) // Small Docs (Insights, Alerts, Watchlist)
|
|
27
46
|
]);
|
|
28
47
|
|
|
29
|
-
logger.log('INFO', `[SnapshotService] ✅ Full System Snapshot Complete
|
|
48
|
+
logger.log('INFO', `[SnapshotService] ✅ Full System Snapshot Complete for ${dateStr}`);
|
|
30
49
|
return { status: 'OK', date: dateStr };
|
|
31
50
|
}
|
|
32
51
|
|
|
@@ -109,10 +128,10 @@ async function snapshotRankings(dateStr, bucket, config, deps) {
|
|
|
109
128
|
async function snapshotMetadata(dateStr, bucket, config, deps) {
|
|
110
129
|
// Bundle small files into one "metadata.json" or keep separate. Separate is safer for loaders.
|
|
111
130
|
const ops = [
|
|
112
|
-
{ name: 'insights',
|
|
113
|
-
{ name: 'page_views',
|
|
114
|
-
{ name: 'watchlist',
|
|
115
|
-
{ name: 'alerts',
|
|
131
|
+
{ name: 'insights', fn: () => dataLoader.loadDailyInsights(config, deps, dateStr) },
|
|
132
|
+
{ name: 'page_views', fn: () => dataLoader.loadPIPageViews(config, deps, dateStr) },
|
|
133
|
+
{ name: 'watchlist', fn: () => dataLoader.loadWatchlistMembership(config, deps, dateStr) },
|
|
134
|
+
{ name: 'alerts', fn: () => dataLoader.loadPIAlertHistory(config, deps, dateStr) },
|
|
116
135
|
{ name: 'master_list', fn: () => dataLoader.loadPopularInvestorMasterList(config, deps) } // Not date bound usually, but good to snapshot state
|
|
117
136
|
];
|
|
118
137
|
|
|
@@ -53,23 +53,35 @@ function getPackageVersions() {
|
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Publishes a message to trigger the dedicated Build Reporter Cloud Function.
|
|
56
|
+
* Fire-and-forget to avoid blocking initialization.
|
|
56
57
|
*/
|
|
57
58
|
async function requestBuildReport(config, dependencies) {
|
|
58
59
|
const { pubsubUtils, logger } = dependencies;
|
|
59
|
-
const { moduleVersion, calcVersion } = getPackageVersions();
|
|
60
60
|
|
|
61
|
+
// Get versions (synchronous but fast, wrapped in try-catch)
|
|
62
|
+
let moduleVersion = 'unknown';
|
|
63
|
+
let calcVersion = 'unknown';
|
|
61
64
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
calcVersion
|
|
66
|
-
});
|
|
67
|
-
logger.log('INFO', `[BuildReporter] 🛰️ Trigger message sent to ${config.buildReporterTopic}`);
|
|
68
|
-
return { success: true };
|
|
65
|
+
const versions = getPackageVersions();
|
|
66
|
+
moduleVersion = versions.moduleVersion;
|
|
67
|
+
calcVersion = versions.calcVersion;
|
|
69
68
|
} catch (e) {
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
// If version resolution fails, use defaults
|
|
70
|
+
logger.log('WARN', `[BuildReporter] Version resolution failed, using defaults: ${e.message}`);
|
|
72
71
|
}
|
|
72
|
+
|
|
73
|
+
// Fire-and-forget: don't await, just log errors
|
|
74
|
+
pubsubUtils.publish(config.buildReporterTopic, {
|
|
75
|
+
requestedAt: new Date().toISOString(),
|
|
76
|
+
moduleVersion,
|
|
77
|
+
calcVersion
|
|
78
|
+
}).then(() => {
|
|
79
|
+
logger.log('INFO', `[BuildReporter] 🛰️ Trigger message sent to ${config.buildReporterTopic}`);
|
|
80
|
+
}).catch(e => {
|
|
81
|
+
logger.log('ERROR', `[BuildReporter] Failed to publish trigger: ${e.message}`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return { success: true, status: 'PENDING' };
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
/**
|
|
@@ -4,6 +4,35 @@
|
|
|
4
4
|
* UPDATED: Added schema validation to prevent silent batch failures.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Recursively removes undefined values from an object.
|
|
9
|
+
* Firestore doesn't allow undefined values, so we filter them out entirely.
|
|
10
|
+
* @param {any} data - Data to sanitize
|
|
11
|
+
* @returns {any} Sanitized data with undefined values removed
|
|
12
|
+
*/
|
|
13
|
+
function removeUndefinedValues(data) {
|
|
14
|
+
if (data === undefined) return undefined; // Will be filtered out
|
|
15
|
+
if (data === null) return null;
|
|
16
|
+
if (data instanceof Date) return data;
|
|
17
|
+
|
|
18
|
+
if (Array.isArray(data)) {
|
|
19
|
+
return data.map(item => removeUndefinedValues(item)).filter(item => item !== undefined);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof data === 'object') {
|
|
23
|
+
const sanitized = {};
|
|
24
|
+
for (const [key, value] of Object.entries(data)) {
|
|
25
|
+
const sanitizedValue = removeUndefinedValues(value);
|
|
26
|
+
if (sanitizedValue !== undefined) {
|
|
27
|
+
sanitized[key] = sanitizedValue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return sanitized;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
|
|
7
36
|
/**
|
|
8
37
|
* Validates a schema object before storage.
|
|
9
38
|
* Checks for circular references and size limits.
|
|
@@ -58,13 +87,17 @@ async function batchStoreSchemas(dependencies, config, schemas) {
|
|
|
58
87
|
const docRef = db.collection(schemaCollection).doc(item.name);
|
|
59
88
|
|
|
60
89
|
// Critical: Always overwrite 'lastUpdated' to now
|
|
61
|
-
|
|
90
|
+
// Sanitize metadata to remove undefined values (Firestore doesn't allow undefined)
|
|
91
|
+
const sanitizedMetadata = item.metadata ? removeUndefinedValues(item.metadata) : {};
|
|
92
|
+
const docData = removeUndefinedValues({
|
|
62
93
|
computationName: item.name,
|
|
63
94
|
category: item.category,
|
|
64
95
|
schema: item.schema,
|
|
65
|
-
metadata:
|
|
96
|
+
metadata: sanitizedMetadata,
|
|
66
97
|
lastUpdated: new Date()
|
|
67
|
-
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
batch.set(docRef, docData, { merge: true });
|
|
68
101
|
|
|
69
102
|
validCount++;
|
|
70
103
|
|