bulltrackers-module 1.0.519 → 1.0.520
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/alert-system/helpers/alert_helpers.js +5 -0
- package/functions/computation-system/context/ContextFactory.js +18 -4
- package/functions/computation-system/data/AvailabilityChecker.js +136 -43
- package/functions/computation-system/data/CachedDataLoader.js +58 -11
- package/functions/computation-system/executors/MetaExecutor.js +164 -3
- package/functions/computation-system/executors/StandardExecutor.js +72 -1
- package/functions/computation-system/layers/extractors.js +402 -1
- package/functions/computation-system/utils/data_loader.js +113 -1
- package/functions/core/utils/firestore_utils.js +28 -23
- package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +6 -0
- package/functions/generic-api/user-api/helpers/reviews/review_helpers.js +5 -0
- package/functions/generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +292 -0
- package/functions/generic-api/user-api/helpers/sync/user_sync_helpers.js +17 -9
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +38 -0
- package/functions/root-data-indexer/index.js +42 -3
- package/functions/task-engine/helpers/popular_investor_helpers.js +50 -16
- package/package.json +1 -1
|
@@ -127,6 +127,11 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
127
127
|
// 6. Commit batch
|
|
128
128
|
await batch.commit();
|
|
129
129
|
|
|
130
|
+
// 7. Update global rootdata collection for computation system
|
|
131
|
+
const { updateAlertHistoryRootData } = require('../../generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers');
|
|
132
|
+
const triggeredUserCids = subscriptions.map(s => s.userCid);
|
|
133
|
+
await updateAlertHistoryRootData(db, logger, piCid, alertType, computationMetadata, computationDate, triggeredUserCids);
|
|
134
|
+
|
|
130
135
|
logger.log('SUCCESS', `[processAlertForPI] Created ${notificationRefs.length} notifications for PI ${piCid}, alert type ${alertType.id}`);
|
|
131
136
|
|
|
132
137
|
} catch (error) {
|
|
@@ -25,7 +25,9 @@ class ContextFactory {
|
|
|
25
25
|
verification,
|
|
26
26
|
rankings, yesterdayRankings, // User-specific rank entries
|
|
27
27
|
allRankings, allRankingsYesterday, // Global rank lists
|
|
28
|
-
allVerifications
|
|
28
|
+
allVerifications,
|
|
29
|
+
// [NEW] New Root Data Types for Profile Metrics
|
|
30
|
+
ratings, pageViews, watchlistMembership, alertHistory
|
|
29
31
|
} = options;
|
|
30
32
|
|
|
31
33
|
return {
|
|
@@ -49,7 +51,12 @@ class ContextFactory {
|
|
|
49
51
|
globalData: {
|
|
50
52
|
rankings: allRankings || [],
|
|
51
53
|
rankingsYesterday: allRankingsYesterday || [],
|
|
52
|
-
verifications: allVerifications || {}
|
|
54
|
+
verifications: allVerifications || {},
|
|
55
|
+
// [NEW] New Root Data Types for Profile Metrics
|
|
56
|
+
ratings: ratings || {},
|
|
57
|
+
pageViews: pageViews || {},
|
|
58
|
+
watchlistMembership: watchlistMembership || {},
|
|
59
|
+
alertHistory: alertHistory || {}
|
|
53
60
|
}
|
|
54
61
|
};
|
|
55
62
|
}
|
|
@@ -59,7 +66,9 @@ class ContextFactory {
|
|
|
59
66
|
dateStr, metadata, mappings, insights, socialData, prices,
|
|
60
67
|
computedDependencies, previousComputedDependencies, config, deps,
|
|
61
68
|
allRankings, allRankingsYesterday, // [UPDATED] Accepted here
|
|
62
|
-
allVerifications
|
|
69
|
+
allVerifications,
|
|
70
|
+
// [NEW] New Root Data Types for Profile Metrics
|
|
71
|
+
ratings, pageViews, watchlistMembership, alertHistory
|
|
63
72
|
} = options;
|
|
64
73
|
|
|
65
74
|
return {
|
|
@@ -75,7 +84,12 @@ class ContextFactory {
|
|
|
75
84
|
globalData: {
|
|
76
85
|
rankings: allRankings || [],
|
|
77
86
|
rankingsYesterday: allRankingsYesterday || [], // [UPDATED] Injected here
|
|
78
|
-
verifications: allVerifications || {}
|
|
87
|
+
verifications: allVerifications || {},
|
|
88
|
+
// [NEW] New Root Data Types for Profile Metrics
|
|
89
|
+
ratings: ratings || {},
|
|
90
|
+
pageViews: pageViews || {},
|
|
91
|
+
watchlistMembership: watchlistMembership || {},
|
|
92
|
+
alertHistory: alertHistory || {}
|
|
79
93
|
}
|
|
80
94
|
};
|
|
81
95
|
}
|
|
@@ -10,65 +10,150 @@ const INDEX_COLLECTION = process.env.ROOT_DATA_AVAILABILITY_COLLECTION || 'syste
|
|
|
10
10
|
|
|
11
11
|
function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
12
12
|
const missing = [];
|
|
13
|
-
|
|
13
|
+
const available = [];
|
|
14
|
+
|
|
15
|
+
if (!calcManifest.rootDataDependencies) return { canRun: true, missing, available };
|
|
16
|
+
|
|
17
|
+
// Check if computation can run with missing rootdata
|
|
18
|
+
const canHaveMissingRoots = calcManifest.canHaveMissingRoots === true;
|
|
14
19
|
|
|
15
20
|
// Normalize userType to lowercase for comparison (computations use uppercase)
|
|
16
21
|
const userType = (calcManifest.userType || 'all').toLowerCase();
|
|
17
22
|
|
|
18
23
|
for (const dep of calcManifest.rootDataDependencies) {
|
|
24
|
+
let isAvailable = false;
|
|
25
|
+
|
|
19
26
|
if (dep === 'portfolio') {
|
|
20
|
-
if (userType === 'speculator' &&
|
|
21
|
-
else if (userType === 'normal' &&
|
|
22
|
-
else if (userType === 'popular_investor')
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (!rootDataStatus.hasPortfolio) missing.push('portfolio (fallback)');
|
|
27
|
+
if (userType === 'speculator' && rootDataStatus.speculatorPortfolio) isAvailable = true;
|
|
28
|
+
else if (userType === 'normal' && rootDataStatus.normalPortfolio) isAvailable = true;
|
|
29
|
+
else if (userType === 'popular_investor' && rootDataStatus.piPortfolios) isAvailable = true;
|
|
30
|
+
else if (userType === 'signed_in_user' && rootDataStatus.signedInUserPortfolio) isAvailable = true;
|
|
31
|
+
else if (userType === 'all' && rootDataStatus.hasPortfolio) isAvailable = true;
|
|
32
|
+
else if (rootDataStatus.hasPortfolio) isAvailable = true; // Fallback
|
|
33
|
+
|
|
34
|
+
if (!isAvailable) {
|
|
35
|
+
if (userType === 'speculator') missing.push('speculatorPortfolio');
|
|
36
|
+
else if (userType === 'normal') missing.push('normalPortfolio');
|
|
37
|
+
else if (userType === 'popular_investor') missing.push('piPortfolios');
|
|
38
|
+
else if (userType === 'signed_in_user') missing.push('signedInUserPortfolio');
|
|
39
|
+
else missing.push('portfolio');
|
|
40
|
+
} else {
|
|
41
|
+
available.push('portfolio');
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
else if (dep === 'history') {
|
|
39
|
-
if (userType === 'speculator' &&
|
|
40
|
-
else if (userType === 'normal' &&
|
|
41
|
-
else if (userType === 'popular_investor' &&
|
|
42
|
-
else if (userType === 'signed_in_user' &&
|
|
43
|
-
else if (userType === 'all' &&
|
|
44
|
-
else
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
if (userType === 'speculator' && rootDataStatus.speculatorHistory) isAvailable = true;
|
|
46
|
+
else if (userType === 'normal' && rootDataStatus.normalHistory) isAvailable = true;
|
|
47
|
+
else if (userType === 'popular_investor' && rootDataStatus.piHistory) isAvailable = true;
|
|
48
|
+
else if (userType === 'signed_in_user' && rootDataStatus.signedInUserHistory) isAvailable = true;
|
|
49
|
+
else if (userType === 'all' && rootDataStatus.hasHistory) isAvailable = true;
|
|
50
|
+
else if (rootDataStatus.hasHistory) isAvailable = true; // Fallback
|
|
51
|
+
|
|
52
|
+
if (!isAvailable) {
|
|
53
|
+
if (userType === 'speculator') missing.push('speculatorHistory');
|
|
54
|
+
else if (userType === 'normal') missing.push('normalHistory');
|
|
55
|
+
else if (userType === 'popular_investor') missing.push('piHistory');
|
|
56
|
+
else if (userType === 'signed_in_user') missing.push('signedInUserHistory');
|
|
57
|
+
else missing.push('history');
|
|
58
|
+
} else {
|
|
59
|
+
available.push('history');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (dep === 'rankings') {
|
|
63
|
+
if (rootDataStatus.piRankings) {
|
|
64
|
+
isAvailable = true;
|
|
65
|
+
available.push('rankings');
|
|
66
|
+
} else {
|
|
67
|
+
missing.push('piRankings');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (dep === 'verification') {
|
|
71
|
+
if (rootDataStatus.signedInUserVerification) {
|
|
72
|
+
isAvailable = true;
|
|
73
|
+
available.push('verification');
|
|
74
|
+
} else {
|
|
75
|
+
missing.push('signedInUserVerification');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (dep === 'insights') {
|
|
79
|
+
if (rootDataStatus.hasInsights) {
|
|
80
|
+
isAvailable = true;
|
|
81
|
+
available.push('insights');
|
|
82
|
+
} else {
|
|
83
|
+
missing.push('insights');
|
|
47
84
|
}
|
|
48
85
|
}
|
|
49
|
-
else if (dep === 'rankings' && !rootDataStatus.piRankings) missing.push('piRankings');
|
|
50
|
-
else if (dep === 'verification' && !rootDataStatus.signedInUserVerification) missing.push('signedInUserVerification');
|
|
51
|
-
else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
|
|
52
|
-
|
|
53
86
|
// [UPDATED] Strict Social Data Checking
|
|
54
87
|
else if (dep === 'social') {
|
|
55
|
-
if (userType === 'popular_investor') {
|
|
56
|
-
|
|
57
|
-
|
|
88
|
+
if (userType === 'popular_investor' && rootDataStatus.hasPISocial) {
|
|
89
|
+
isAvailable = true;
|
|
90
|
+
available.push('social');
|
|
91
|
+
} else if (userType === 'signed_in_user' && rootDataStatus.hasSignedInSocial) {
|
|
92
|
+
isAvailable = true;
|
|
93
|
+
available.push('social');
|
|
94
|
+
} else if (rootDataStatus.hasSocial) {
|
|
95
|
+
isAvailable = true;
|
|
96
|
+
available.push('social');
|
|
97
|
+
} else {
|
|
98
|
+
if (userType === 'popular_investor') missing.push('piSocial');
|
|
99
|
+
else if (userType === 'signed_in_user') missing.push('signedInSocial');
|
|
100
|
+
else missing.push('social');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (dep === 'price') {
|
|
104
|
+
if (rootDataStatus.hasPrices) {
|
|
105
|
+
isAvailable = true;
|
|
106
|
+
available.push('price');
|
|
107
|
+
} else {
|
|
108
|
+
missing.push('price');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// [NEW] New Root Data Types for Profile Metrics
|
|
112
|
+
else if (dep === 'ratings') {
|
|
113
|
+
if (rootDataStatus.piRatings) {
|
|
114
|
+
isAvailable = true;
|
|
115
|
+
available.push('ratings');
|
|
116
|
+
} else {
|
|
117
|
+
missing.push('piRatings');
|
|
58
118
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
119
|
+
}
|
|
120
|
+
else if (dep === 'pageViews') {
|
|
121
|
+
if (rootDataStatus.piPageViews) {
|
|
122
|
+
isAvailable = true;
|
|
123
|
+
available.push('pageViews');
|
|
124
|
+
} else {
|
|
125
|
+
missing.push('piPageViews');
|
|
62
126
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
127
|
+
}
|
|
128
|
+
else if (dep === 'watchlist') {
|
|
129
|
+
if (rootDataStatus.watchlistMembership) {
|
|
130
|
+
isAvailable = true;
|
|
131
|
+
available.push('watchlist');
|
|
132
|
+
} else {
|
|
133
|
+
missing.push('watchlistMembership');
|
|
66
134
|
}
|
|
67
135
|
}
|
|
68
|
-
|
|
69
|
-
|
|
136
|
+
else if (dep === 'alerts') {
|
|
137
|
+
if (rootDataStatus.piAlertHistory) {
|
|
138
|
+
isAvailable = true;
|
|
139
|
+
available.push('alerts');
|
|
140
|
+
} else {
|
|
141
|
+
missing.push('piAlertHistory');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// [FIX] Enforce canHaveMissingRoots logic:
|
|
147
|
+
// - If canHaveMissingRoots: false, ALL required rootdata must be available
|
|
148
|
+
// - If canHaveMissingRoots: true, AT LEAST ONE required rootdata must be available (not zero)
|
|
149
|
+
if (canHaveMissingRoots) {
|
|
150
|
+
// Must have at least one available rootdata type
|
|
151
|
+
const canRun = available.length > 0;
|
|
152
|
+
return { canRun, missing, available };
|
|
70
153
|
}
|
|
71
|
-
|
|
154
|
+
|
|
155
|
+
// Strict mode: all required rootdata must be available
|
|
156
|
+
return { canRun: missing.length === 0, missing, available };
|
|
72
157
|
}
|
|
73
158
|
|
|
74
159
|
function getViableCalculations(candidates, fullManifest, rootDataStatus, dailyStatus) {
|
|
@@ -138,7 +223,13 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
|
|
|
138
223
|
|
|
139
224
|
// [UPDATED] Granular Social Flags
|
|
140
225
|
hasPISocial: !!details.hasPISocial || !!data.hasPISocial,
|
|
141
|
-
hasSignedInSocial: !!details.hasSignedInSocial || !!data.hasSignedInSocial
|
|
226
|
+
hasSignedInSocial: !!details.hasSignedInSocial || !!data.hasSignedInSocial,
|
|
227
|
+
|
|
228
|
+
// [NEW] New Root Data Types for Profile Metrics
|
|
229
|
+
piRatings: !!details.piRatings,
|
|
230
|
+
piPageViews: !!details.piPageViews,
|
|
231
|
+
watchlistMembership: !!details.watchlistMembership,
|
|
232
|
+
piAlertHistory: !!details.piAlertHistory
|
|
142
233
|
},
|
|
143
234
|
portfolioRefs: null,
|
|
144
235
|
historyRefs: null,
|
|
@@ -154,7 +245,9 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
|
|
|
154
245
|
speculatorPortfolio: false, normalPortfolio: false, speculatorHistory: false, normalHistory: false,
|
|
155
246
|
piRankings: false, piPortfolios: false, piDeepPortfolios: false, piHistory: false,
|
|
156
247
|
signedInUserPortfolio: false, signedInUserHistory: false, signedInUserVerification: false,
|
|
157
|
-
hasPISocial: false, hasSignedInSocial: false
|
|
248
|
+
hasPISocial: false, hasSignedInSocial: false,
|
|
249
|
+
// New Root Data Types
|
|
250
|
+
piRatings: false, piPageViews: false, watchlistMembership: false, piAlertHistory: false
|
|
158
251
|
}
|
|
159
252
|
};
|
|
160
253
|
}
|
|
@@ -10,7 +10,11 @@ const {
|
|
|
10
10
|
getRelevantShardRefs,
|
|
11
11
|
getPriceShardRefs,
|
|
12
12
|
loadVerificationProfiles, // [NEW]
|
|
13
|
-
loadPopularInvestorRankings
|
|
13
|
+
loadPopularInvestorRankings, // [NEW]
|
|
14
|
+
loadPIRatings, // [NEW]
|
|
15
|
+
loadPIPageViews, // [NEW]
|
|
16
|
+
loadWatchlistMembership: loadWatchlistMembershipData, // [NEW] Renamed to avoid conflict
|
|
17
|
+
loadPIAlertHistory // [NEW]
|
|
14
18
|
} = require('../utils/data_loader');
|
|
15
19
|
const zlib = require('zlib');
|
|
16
20
|
|
|
@@ -23,7 +27,11 @@ class CachedDataLoader {
|
|
|
23
27
|
insights: new Map(),
|
|
24
28
|
social: new Map(),
|
|
25
29
|
verifications: null, // [NEW]
|
|
26
|
-
rankings: new Map() // [NEW]
|
|
30
|
+
rankings: new Map(), // [NEW]
|
|
31
|
+
ratings: new Map(), // [NEW]
|
|
32
|
+
pageViews: new Map(), // [NEW]
|
|
33
|
+
watchlistMembership: new Map(), // [NEW]
|
|
34
|
+
alertHistory: new Map() // [NEW]
|
|
27
35
|
};
|
|
28
36
|
}
|
|
29
37
|
|
|
@@ -46,18 +54,20 @@ class CachedDataLoader {
|
|
|
46
54
|
return this.cache.mappings;
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
// [FIX] Cache promises to prevent race conditions when multiple users request same data
|
|
49
58
|
async loadInsights(dateStr) {
|
|
50
59
|
if (this.cache.insights.has(dateStr)) return this.cache.insights.get(dateStr);
|
|
51
|
-
const
|
|
52
|
-
this.cache.insights.set(dateStr,
|
|
53
|
-
return
|
|
60
|
+
const promise = loadDailyInsights(this.config, this.deps, dateStr);
|
|
61
|
+
this.cache.insights.set(dateStr, promise);
|
|
62
|
+
return promise;
|
|
54
63
|
}
|
|
55
64
|
|
|
65
|
+
// [FIX] Cache promises to prevent race conditions when multiple users request same data
|
|
56
66
|
async loadSocial(dateStr) {
|
|
57
67
|
if (this.cache.social.has(dateStr)) return this.cache.social.get(dateStr);
|
|
58
|
-
const
|
|
59
|
-
this.cache.social.set(dateStr,
|
|
60
|
-
return
|
|
68
|
+
const promise = loadDailySocialPostInsights(this.config, this.deps, dateStr);
|
|
69
|
+
this.cache.social.set(dateStr, promise);
|
|
70
|
+
return promise;
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
// [NEW]
|
|
@@ -69,11 +79,48 @@ class CachedDataLoader {
|
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
// [NEW]
|
|
82
|
+
// [FIX] Cache promises to prevent race conditions when multiple users request same data
|
|
72
83
|
async loadRankings(dateStr) {
|
|
73
84
|
if (this.cache.rankings.has(dateStr)) return this.cache.rankings.get(dateStr);
|
|
74
|
-
const
|
|
75
|
-
this.cache.rankings.set(dateStr,
|
|
76
|
-
return
|
|
85
|
+
const promise = loadPopularInvestorRankings(this.config, this.deps, dateStr);
|
|
86
|
+
this.cache.rankings.set(dateStr, promise);
|
|
87
|
+
return promise;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// [NEW] Load PI Ratings Data
|
|
91
|
+
// [FIX] Cache promises to prevent race conditions when multiple users request same data
|
|
92
|
+
async loadRatings(dateStr) {
|
|
93
|
+
if (this.cache.ratings.has(dateStr)) return this.cache.ratings.get(dateStr);
|
|
94
|
+
const promise = loadPIRatings(this.config, this.deps, dateStr);
|
|
95
|
+
this.cache.ratings.set(dateStr, promise);
|
|
96
|
+
return promise;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// [NEW] Load PI Page Views Data
|
|
100
|
+
// [FIX] Cache promises to prevent race conditions when multiple users request same data
|
|
101
|
+
async loadPageViews(dateStr) {
|
|
102
|
+
if (this.cache.pageViews.has(dateStr)) return this.cache.pageViews.get(dateStr);
|
|
103
|
+
const promise = loadPIPageViews(this.config, this.deps, dateStr);
|
|
104
|
+
this.cache.pageViews.set(dateStr, promise);
|
|
105
|
+
return promise;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// [NEW] Load Watchlist Membership Data
|
|
109
|
+
// [FIX] Cache promises to prevent race conditions when multiple users request same data
|
|
110
|
+
async loadWatchlistMembership(dateStr) {
|
|
111
|
+
if (this.cache.watchlistMembership.has(dateStr)) return this.cache.watchlistMembership.get(dateStr);
|
|
112
|
+
const promise = loadWatchlistMembershipData(this.config, this.deps, dateStr);
|
|
113
|
+
this.cache.watchlistMembership.set(dateStr, promise);
|
|
114
|
+
return promise;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// [NEW] Load PI Alert History Data
|
|
118
|
+
// [FIX] Cache promises to prevent race conditions when multiple users request same data
|
|
119
|
+
async loadAlertHistory(dateStr) {
|
|
120
|
+
if (this.cache.alertHistory.has(dateStr)) return this.cache.alertHistory.get(dateStr);
|
|
121
|
+
const promise = loadPIAlertHistory(this.config, this.deps, dateStr);
|
|
122
|
+
this.cache.alertHistory.set(dateStr, promise);
|
|
123
|
+
return promise;
|
|
77
124
|
}
|
|
78
125
|
|
|
79
126
|
async getPriceShardReferences() {
|
|
@@ -34,6 +34,96 @@ class MetaExecutor {
|
|
|
34
34
|
loader.loadRankings(dStr),
|
|
35
35
|
loader.loadVerifications()
|
|
36
36
|
]);
|
|
37
|
+
|
|
38
|
+
// [NEW] Load New Root Data Types for Profile Metrics (if any calc needs them)
|
|
39
|
+
const needsNewRootData = calcs.some(c => {
|
|
40
|
+
const deps = c.rootDataDependencies || [];
|
|
41
|
+
return deps.includes('ratings') || deps.includes('pageViews') ||
|
|
42
|
+
deps.includes('watchlist') || deps.includes('alerts');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let ratings = null, pageViews = null, watchlistMembership = null, alertHistory = null;
|
|
46
|
+
if (needsNewRootData) {
|
|
47
|
+
const loadPromises = [];
|
|
48
|
+
if (calcs.some(c => c.rootDataDependencies?.includes('ratings'))) {
|
|
49
|
+
loadPromises.push(loader.loadRatings(dStr).then(r => { ratings = r; }).catch(e => {
|
|
50
|
+
// Only catch if ALL calcs allow missing roots
|
|
51
|
+
const allAllowMissing = calcs.every(c => {
|
|
52
|
+
const needsRatings = c.rootDataDependencies?.includes('ratings');
|
|
53
|
+
return !needsRatings || c.canHaveMissingRoots === true;
|
|
54
|
+
});
|
|
55
|
+
if (allAllowMissing) {
|
|
56
|
+
ratings = null;
|
|
57
|
+
} else {
|
|
58
|
+
throw e; // Re-throw if any calc requires this data
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
if (calcs.some(c => c.rootDataDependencies?.includes('pageViews'))) {
|
|
63
|
+
loadPromises.push(loader.loadPageViews(dStr).then(pv => { pageViews = pv; }).catch(e => {
|
|
64
|
+
const allAllowMissing = calcs.every(c => {
|
|
65
|
+
const needsPageViews = c.rootDataDependencies?.includes('pageViews');
|
|
66
|
+
return !needsPageViews || c.canHaveMissingRoots === true;
|
|
67
|
+
});
|
|
68
|
+
if (allAllowMissing) {
|
|
69
|
+
pageViews = null;
|
|
70
|
+
} else {
|
|
71
|
+
throw e;
|
|
72
|
+
}
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
if (calcs.some(c => c.rootDataDependencies?.includes('watchlist'))) {
|
|
76
|
+
loadPromises.push(loader.loadWatchlistMembership(dStr).then(w => { watchlistMembership = w; }).catch(e => {
|
|
77
|
+
const allAllowMissing = calcs.every(c => {
|
|
78
|
+
const needsWatchlist = c.rootDataDependencies?.includes('watchlist');
|
|
79
|
+
return !needsWatchlist || c.canHaveMissingRoots === true;
|
|
80
|
+
});
|
|
81
|
+
if (allAllowMissing) {
|
|
82
|
+
watchlistMembership = null;
|
|
83
|
+
} else {
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
if (calcs.some(c => c.rootDataDependencies?.includes('alerts'))) {
|
|
89
|
+
loadPromises.push(loader.loadAlertHistory(dStr).then(a => { alertHistory = a; }).catch(e => {
|
|
90
|
+
const allAllowMissing = calcs.every(c => {
|
|
91
|
+
const needsAlerts = c.rootDataDependencies?.includes('alerts');
|
|
92
|
+
return !needsAlerts || c.canHaveMissingRoots === true;
|
|
93
|
+
});
|
|
94
|
+
if (allAllowMissing) {
|
|
95
|
+
alertHistory = null;
|
|
96
|
+
} else {
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
await Promise.all(loadPromises);
|
|
102
|
+
|
|
103
|
+
// [FIX] Enforce canHaveMissingRoots - validate after loading
|
|
104
|
+
for (const c of calcs) {
|
|
105
|
+
const deps = c.rootDataDependencies || [];
|
|
106
|
+
const canSkip = c.canHaveMissingRoots === true;
|
|
107
|
+
|
|
108
|
+
// Helper to check if a specific root is missing
|
|
109
|
+
const isMissing = (key, val) => deps.includes(key) && (val === null || val === undefined);
|
|
110
|
+
|
|
111
|
+
if (!canSkip) {
|
|
112
|
+
if (isMissing('ratings', ratings)) {
|
|
113
|
+
throw new Error(`[MetaExecutor] Missing required root 'ratings' for ${c.name}`);
|
|
114
|
+
}
|
|
115
|
+
if (isMissing('pageViews', pageViews)) {
|
|
116
|
+
throw new Error(`[MetaExecutor] Missing required root 'pageViews' for ${c.name}`);
|
|
117
|
+
}
|
|
118
|
+
if (isMissing('watchlist', watchlistMembership)) {
|
|
119
|
+
throw new Error(`[MetaExecutor] Missing required root 'watchlist' for ${c.name}`);
|
|
120
|
+
}
|
|
121
|
+
if (isMissing('alerts', alertHistory)) {
|
|
122
|
+
throw new Error(`[MetaExecutor] Missing required root 'alerts' for ${c.name}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
37
127
|
|
|
38
128
|
const state = {};
|
|
39
129
|
for (const c of calcs) {
|
|
@@ -51,7 +141,12 @@ class MetaExecutor {
|
|
|
51
141
|
config, deps,
|
|
52
142
|
allRankings: rankings,
|
|
53
143
|
allRankingsYesterday: rankingsYesterday, // [FIX] Injected
|
|
54
|
-
allVerifications: verifications
|
|
144
|
+
allVerifications: verifications,
|
|
145
|
+
// [NEW] Pass New Root Data Types for Profile Metrics
|
|
146
|
+
ratings: ratings || {},
|
|
147
|
+
pageViews: pageViews || {},
|
|
148
|
+
watchlistMembership: watchlistMembership || {},
|
|
149
|
+
alertHistory: alertHistory || {}
|
|
55
150
|
});
|
|
56
151
|
|
|
57
152
|
try {
|
|
@@ -87,6 +182,62 @@ class MetaExecutor {
|
|
|
87
182
|
|
|
88
183
|
// Load current rankings (often needed for ContextFactory.buildMetaContext)
|
|
89
184
|
const rankings = await loader.loadRankings(dateStr);
|
|
185
|
+
|
|
186
|
+
// [NEW] Load New Root Data Types for Profile Metrics
|
|
187
|
+
// [FIX] Enforce canHaveMissingRoots
|
|
188
|
+
const allowMissing = metadata.canHaveMissingRoots === true;
|
|
189
|
+
|
|
190
|
+
let ratings = null;
|
|
191
|
+
if (metadata.rootDataDependencies?.includes('ratings')) {
|
|
192
|
+
try {
|
|
193
|
+
ratings = await loader.loadRatings(dateStr);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
if (!allowMissing) throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'ratings' failed to load for ${metadata.name}: ${e.message}`);
|
|
196
|
+
ratings = null;
|
|
197
|
+
}
|
|
198
|
+
if (!ratings && !allowMissing) {
|
|
199
|
+
throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'ratings' is missing for ${metadata.name}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let pageViews = null;
|
|
204
|
+
if (metadata.rootDataDependencies?.includes('pageViews')) {
|
|
205
|
+
try {
|
|
206
|
+
pageViews = await loader.loadPageViews(dateStr);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
if (!allowMissing) throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'pageViews' failed to load for ${metadata.name}: ${e.message}`);
|
|
209
|
+
pageViews = null;
|
|
210
|
+
}
|
|
211
|
+
if (!pageViews && !allowMissing) {
|
|
212
|
+
throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'pageViews' is missing for ${metadata.name}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let watchlistMembership = null;
|
|
217
|
+
if (metadata.rootDataDependencies?.includes('watchlist')) {
|
|
218
|
+
try {
|
|
219
|
+
watchlistMembership = await loader.loadWatchlistMembership(dateStr);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
if (!allowMissing) throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'watchlist' failed to load for ${metadata.name}: ${e.message}`);
|
|
222
|
+
watchlistMembership = null;
|
|
223
|
+
}
|
|
224
|
+
if (!watchlistMembership && !allowMissing) {
|
|
225
|
+
throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'watchlist' is missing for ${metadata.name}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let alertHistory = null;
|
|
230
|
+
if (metadata.rootDataDependencies?.includes('alerts')) {
|
|
231
|
+
try {
|
|
232
|
+
alertHistory = await loader.loadAlertHistory(dateStr);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
if (!allowMissing) throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'alerts' failed to load for ${metadata.name}: ${e.message}`);
|
|
235
|
+
alertHistory = null;
|
|
236
|
+
}
|
|
237
|
+
if (!alertHistory && !allowMissing) {
|
|
238
|
+
throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'alerts' is missing for ${metadata.name}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
90
241
|
|
|
91
242
|
if (metadata.rootDataDependencies?.includes('price')) {
|
|
92
243
|
logger.log('INFO', `[Executor] Running Batched/Sharded Execution for ${metadata.name}`);
|
|
@@ -101,7 +252,12 @@ class MetaExecutor {
|
|
|
101
252
|
prices: { history: shardData }, computedDependencies: computedDeps,
|
|
102
253
|
previousComputedDependencies: prevDeps, config, deps,
|
|
103
254
|
allRankings: rankings,
|
|
104
|
-
allRankingsYesterday: rankingsYesterday
|
|
255
|
+
allRankingsYesterday: rankingsYesterday,
|
|
256
|
+
// [NEW] Pass New Root Data Types for Profile Metrics
|
|
257
|
+
ratings: ratings || {},
|
|
258
|
+
pageViews: pageViews || {},
|
|
259
|
+
watchlistMembership: watchlistMembership || {},
|
|
260
|
+
alertHistory: alertHistory || {}
|
|
105
261
|
});
|
|
106
262
|
|
|
107
263
|
await calcInstance.process(partialContext);
|
|
@@ -121,7 +277,12 @@ class MetaExecutor {
|
|
|
121
277
|
prices: {}, computedDependencies: computedDeps,
|
|
122
278
|
previousComputedDependencies: prevDeps, config, deps,
|
|
123
279
|
allRankings: rankings,
|
|
124
|
-
allRankingsYesterday: rankingsYesterday
|
|
280
|
+
allRankingsYesterday: rankingsYesterday,
|
|
281
|
+
// [NEW] Pass New Root Data Types for Profile Metrics
|
|
282
|
+
ratings: ratings || {},
|
|
283
|
+
pageViews: pageViews || {},
|
|
284
|
+
watchlistMembership: watchlistMembership || {},
|
|
285
|
+
alertHistory: alertHistory || {}
|
|
125
286
|
});
|
|
126
287
|
const res = await calcInstance.process(context);
|
|
127
288
|
|