bulltrackers-module 1.0.658 → 1.0.659
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/data/AvailabilityChecker.js +163 -317
- package/functions/computation-system/data/CachedDataLoader.js +158 -222
- package/functions/computation-system/data/DependencyFetcher.js +201 -406
- package/functions/computation-system/executors/MetaExecutor.js +176 -280
- package/functions/computation-system/executors/StandardExecutor.js +325 -383
- package/functions/computation-system/helpers/computation_dispatcher.js +294 -699
- package/functions/computation-system/helpers/computation_worker.js +3 -2
- package/functions/computation-system/legacy/AvailabilityCheckerOld.js +382 -0
- package/functions/computation-system/legacy/CachedDataLoaderOld.js +357 -0
- package/functions/computation-system/legacy/DependencyFetcherOld.js +478 -0
- package/functions/computation-system/legacy/MetaExecutorold.js +364 -0
- package/functions/computation-system/legacy/StandardExecutorold.js +476 -0
- package/functions/computation-system/legacy/computation_dispatcherold.js +944 -0
- package/functions/computation-system/persistence/ResultCommitter.js +137 -188
- package/functions/computation-system/services/SnapshotService.js +129 -0
- package/functions/computation-system/tools/BuildReporter.js +12 -7
- package/functions/computation-system/utils/data_loader.js +213 -238
- package/package.json +3 -2
- package/functions/computation-system/workflows/bulltrackers_pipeline.yaml +0 -163
- package/functions/computation-system/workflows/data_feeder_pipeline.yaml +0 -115
- package/functions/computation-system/workflows/datafeederpipelineinstructions.md +0 -30
- package/functions/computation-system/workflows/morning_prep_pipeline.yaml +0 -55
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* type: uploaded file
|
|
4
4
|
* fileName: computation-system/data/CachedDataLoader.js
|
|
5
5
|
* }
|
|
6
|
+
* REFACTORED: Unified Loader Configuration for DRY principles.
|
|
6
7
|
*/
|
|
7
8
|
const {
|
|
8
9
|
loadDailyInsights,
|
|
@@ -21,153 +22,153 @@ const {
|
|
|
21
22
|
const { getAvailabilityWindow } = require('./AvailabilityChecker');
|
|
22
23
|
const zlib = require('zlib');
|
|
23
24
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// CONFIGURATION: Unified Loader Definitions
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Centralizes config keys, defaults, loader functions, and availability flags.
|
|
29
|
+
const LOADER_DEFINITIONS = {
|
|
30
|
+
loadRankings: {
|
|
31
|
+
cache: 'rankings',
|
|
32
|
+
configKey: 'popularInvestorRankingsCollection',
|
|
33
|
+
defaultCol: 'popular_investor_rankings',
|
|
34
|
+
fn: loadPopularInvestorRankings,
|
|
35
|
+
flag: 'piRankings'
|
|
36
|
+
},
|
|
37
|
+
loadRatings: {
|
|
38
|
+
cache: 'ratings',
|
|
39
|
+
configKey: 'piRatingsCollection',
|
|
40
|
+
defaultCol: 'PIRatingsData',
|
|
41
|
+
fn: loadPIRatings,
|
|
42
|
+
flag: 'piRatings'
|
|
43
|
+
},
|
|
44
|
+
loadPageViews: {
|
|
45
|
+
cache: 'pageViews',
|
|
46
|
+
configKey: 'piPageViewsCollection',
|
|
47
|
+
defaultCol: 'PIPageViewsData',
|
|
48
|
+
fn: loadPIPageViews,
|
|
49
|
+
flag: 'piPageViews'
|
|
50
|
+
},
|
|
51
|
+
loadWatchlistMembership: {
|
|
52
|
+
cache: 'watchlistMembership',
|
|
53
|
+
configKey: 'watchlistMembershipCollection',
|
|
54
|
+
defaultCol: 'WatchlistMembershipData',
|
|
55
|
+
fn: loadWatchlistMembershipData,
|
|
56
|
+
flag: 'watchlistMembership'
|
|
57
|
+
},
|
|
58
|
+
loadAlertHistory: {
|
|
59
|
+
cache: 'alertHistory',
|
|
60
|
+
configKey: 'piAlertHistoryCollection',
|
|
61
|
+
defaultCol: 'PIAlertHistoryData',
|
|
62
|
+
fn: loadPIAlertHistory,
|
|
63
|
+
flag: 'piAlertHistory'
|
|
64
|
+
},
|
|
65
|
+
loadInsights: {
|
|
66
|
+
cache: 'insights',
|
|
67
|
+
configKey: 'insightsCollectionName',
|
|
68
|
+
defaultCol: 'daily_instrument_insights',
|
|
69
|
+
fn: loadDailyInsights,
|
|
70
|
+
flag: 'hasInsights'
|
|
71
|
+
},
|
|
72
|
+
loadSocial: {
|
|
73
|
+
cache: 'social',
|
|
74
|
+
configKey: 'socialInsightsCollection',
|
|
75
|
+
defaultCol: 'daily_social_insights',
|
|
76
|
+
fn: loadDailySocialPostInsights,
|
|
77
|
+
flag: 'hasSocial'
|
|
78
|
+
},
|
|
79
|
+
loadPIWatchlistData: {
|
|
80
|
+
cache: 'piWatchlistData',
|
|
81
|
+
// No collection key needed for direct implementation, but handled by fn
|
|
82
|
+
fn: loadPIWatchlistData,
|
|
83
|
+
isIdBased: true // Uses ID instead of Date
|
|
84
|
+
}
|
|
44
85
|
};
|
|
45
86
|
|
|
46
87
|
class CachedDataLoader {
|
|
47
88
|
constructor(config, dependencies) {
|
|
48
89
|
this.config = config;
|
|
49
90
|
this.deps = dependencies;
|
|
91
|
+
|
|
92
|
+
// Initialize caches dynamically based on definitions + static extras
|
|
50
93
|
this.cache = {
|
|
51
94
|
mappings: null,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
watchlistMembership: new Map(),
|
|
59
|
-
alertHistory: new Map(),
|
|
60
|
-
piWatchlistData: new Map(),
|
|
61
|
-
piMasterList: null
|
|
95
|
+
verifications: null,
|
|
96
|
+
piMasterList: null,
|
|
97
|
+
...Object.values(LOADER_DEFINITIONS).reduce((acc, def) => {
|
|
98
|
+
acc[def.cache] = new Map();
|
|
99
|
+
return acc;
|
|
100
|
+
}, {})
|
|
62
101
|
};
|
|
63
102
|
}
|
|
64
103
|
|
|
65
104
|
_tryDecompress(data) {
|
|
66
|
-
if (data
|
|
67
|
-
try {
|
|
68
|
-
|
|
69
|
-
} catch (e) {
|
|
70
|
-
console.error('[CachedDataLoader] Decompression failed', e);
|
|
71
|
-
return {};
|
|
72
|
-
}
|
|
105
|
+
if (data?._compressed === true && data.payload) {
|
|
106
|
+
try { return JSON.parse(zlib.gunzipSync(data.payload).toString()); }
|
|
107
|
+
catch (e) { console.error('[CachedDataLoader] Decompression failed', e); return {}; }
|
|
73
108
|
}
|
|
74
109
|
return data;
|
|
75
110
|
}
|
|
76
111
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
112
|
+
// =========================================================================
|
|
113
|
+
// GENERIC LOADER HELPER
|
|
114
|
+
// =========================================================================
|
|
115
|
+
async _loadGeneric(methodName, key) {
|
|
116
|
+
const def = LOADER_DEFINITIONS[methodName];
|
|
117
|
+
if (!def) throw new Error(`Unknown loader method: ${methodName}`);
|
|
84
118
|
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
const collectionName = this.config.insightsCollectionName || 'daily_instrument_insights';
|
|
88
|
-
const path = `${collectionName}/${dateStr}`;
|
|
89
|
-
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'insights' from: ${path}`);
|
|
90
|
-
const promise = loadDailyInsights(this.config, this.deps, dateStr);
|
|
91
|
-
this.cache.insights.set(dateStr, promise);
|
|
92
|
-
return promise;
|
|
93
|
-
}
|
|
119
|
+
const cacheMap = this.cache[def.cache];
|
|
120
|
+
if (cacheMap.has(key)) return cacheMap.get(key);
|
|
94
121
|
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const promise = loadDailySocialPostInsights(this.config, this.deps, dateStr);
|
|
101
|
-
this.cache.social.set(dateStr, promise);
|
|
102
|
-
return promise;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async loadVerifications() {
|
|
106
|
-
if (this.cache.verifications) return this.cache.verifications;
|
|
107
|
-
const collectionName = this.config.verificationsCollection || 'verification_profiles';
|
|
108
|
-
const path = `${collectionName}`;
|
|
109
|
-
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'verifications' from: ${path}`);
|
|
110
|
-
const verifications = await loadVerificationProfiles(this.config, this.deps);
|
|
111
|
-
this.cache.verifications = verifications;
|
|
112
|
-
return verifications;
|
|
113
|
-
}
|
|
122
|
+
const collection = this.config[def.configKey] || def.defaultCol;
|
|
123
|
+
// Only log if we have a collection context (some ID-based loaders might differ)
|
|
124
|
+
if (def.configKey) {
|
|
125
|
+
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading '${def.cache}' from: ${collection}/${key}`);
|
|
126
|
+
}
|
|
114
127
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const collectionName = this.config.popularInvestorRankingsCollection || 'popular_investor_rankings';
|
|
118
|
-
const path = `${collectionName}/${dateStr}`;
|
|
119
|
-
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'rankings' from: ${path}`);
|
|
120
|
-
const promise = loadPopularInvestorRankings(this.config, this.deps, dateStr);
|
|
121
|
-
this.cache.rankings.set(dateStr, promise);
|
|
128
|
+
const promise = def.fn(this.config, this.deps, key);
|
|
129
|
+
cacheMap.set(key, promise);
|
|
122
130
|
return promise;
|
|
123
131
|
}
|
|
124
132
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
133
|
+
// =========================================================================
|
|
134
|
+
// PUBLIC ACCESSORS (Generated Wrappers)
|
|
135
|
+
// =========================================================================
|
|
136
|
+
|
|
137
|
+
// Explicitly defined for IDE autocompletion / static analysis,
|
|
138
|
+
// but internally they all delegate to _loadGeneric.
|
|
139
|
+
async loadInsights(dateStr) { return this._loadGeneric('loadInsights' , dateStr); }
|
|
140
|
+
async loadSocial(dateStr) { return this._loadGeneric('loadSocial' , dateStr); }
|
|
141
|
+
async loadRankings(dateStr) { return this._loadGeneric('loadRankings' , dateStr); }
|
|
142
|
+
async loadRatings(dateStr) { return this._loadGeneric('loadRatings' , dateStr); }
|
|
143
|
+
async loadPageViews(dateStr) { return this._loadGeneric('loadPageViews' , dateStr); }
|
|
144
|
+
async loadWatchlistMembership(dateStr) { return this._loadGeneric('loadWatchlistMembership', dateStr); }
|
|
145
|
+
async loadAlertHistory(dateStr) { return this._loadGeneric('loadAlertHistory' , dateStr); }
|
|
146
|
+
async loadPIWatchlistData(piCid) { return this._loadGeneric('loadPIWatchlistData' , String(piCid)); }
|
|
147
|
+
|
|
148
|
+
// =========================================================================
|
|
149
|
+
// SPECIALIZED LOADERS (Non-Standard Patterns)
|
|
150
|
+
// =========================================================================
|
|
134
151
|
|
|
135
|
-
async
|
|
136
|
-
if (this.cache.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'pageViews' from: ${path}`);
|
|
140
|
-
const promise = loadPIPageViews(this.config, this.deps, dateStr);
|
|
141
|
-
this.cache.pageViews.set(dateStr, promise);
|
|
142
|
-
return promise;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async loadWatchlistMembership(dateStr) {
|
|
146
|
-
if (this.cache.watchlistMembership.has(dateStr)) return this.cache.watchlistMembership.get(dateStr);
|
|
147
|
-
const collectionName = this.config.watchlistMembershipCollection || 'WatchlistMembershipData';
|
|
148
|
-
const path = `${collectionName}/${dateStr}`;
|
|
149
|
-
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading Root Data 'watchlistMembership' from: ${path}`);
|
|
150
|
-
const promise = loadWatchlistMembershipData(this.config, this.deps, dateStr);
|
|
151
|
-
this.cache.watchlistMembership.set(dateStr, promise);
|
|
152
|
-
return promise;
|
|
152
|
+
async loadMappings() {
|
|
153
|
+
if (this.cache.mappings) return this.cache.mappings;
|
|
154
|
+
this.cache.mappings = await this.deps.calculationUtils.loadInstrumentMappings();
|
|
155
|
+
return this.cache.mappings;
|
|
153
156
|
}
|
|
154
157
|
|
|
155
|
-
async
|
|
156
|
-
if (this.cache.
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
async loadVerifications(dateStr) { // <--- Added dateStr arg
|
|
159
|
+
if (this.cache.verifications) return this.cache.verifications;
|
|
160
|
+
|
|
161
|
+
const col = this.config.verificationsCollection || 'verification_profiles';
|
|
162
|
+
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Loading 'verifications' from: ${col} (Context: ${dateStr || 'Global'})`);
|
|
163
|
+
|
|
164
|
+
// Pass dateStr so data_loader can check GCS snapshots
|
|
165
|
+
this.cache.verifications = await loadVerificationProfiles(this.config, this.deps, dateStr);
|
|
166
|
+
return this.cache.verifications;
|
|
163
167
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const promise = loadPIWatchlistData(this.config, this.deps, piCidStr);
|
|
169
|
-
this.cache.piWatchlistData.set(piCidStr, promise);
|
|
170
|
-
return promise;
|
|
168
|
+
async loadPIMasterList() {
|
|
169
|
+
if (this.cache.piMasterList) return this.cache.piMasterList;
|
|
170
|
+
this.cache.piMasterList = await loadPopularInvestorMasterList(this.config, this.deps);
|
|
171
|
+
return this.cache.piMasterList;
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
async getPriceShardReferences() {
|
|
@@ -181,147 +182,92 @@ class CachedDataLoader {
|
|
|
181
182
|
async loadPriceShard(docRef) {
|
|
182
183
|
try {
|
|
183
184
|
const snap = await docRef.get();
|
|
184
|
-
|
|
185
|
-
return this._tryDecompress(snap.data());
|
|
185
|
+
return snap.exists ? this._tryDecompress(snap.data()) : {};
|
|
186
186
|
} catch (e) {
|
|
187
187
|
console.error(`Error loading shard ${docRef.path}:`, e);
|
|
188
188
|
return {};
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this.cache.piMasterList = data;
|
|
196
|
-
return data;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// --- [UPDATED] Batched Series Loading Logic ---
|
|
192
|
+
// =========================================================================
|
|
193
|
+
// BATCH SERIES LOADING
|
|
194
|
+
// =========================================================================
|
|
200
195
|
/**
|
|
201
|
-
* Optimistically loads
|
|
202
|
-
*
|
|
203
|
-
* 2. Constructs Refs for all existing dates.
|
|
204
|
-
* 3. Fetches all in ONE db.getAll() request.
|
|
196
|
+
* Optimistically loads data series using Batch Reads (db.getAll).
|
|
197
|
+
* Uses Availability Index to minimize costs.
|
|
205
198
|
*/
|
|
206
199
|
async loadSeries(loaderMethod, dateStr, lookbackDays) {
|
|
207
|
-
|
|
200
|
+
const def = LOADER_DEFINITIONS[loaderMethod];
|
|
201
|
+
if (!def) throw new Error(`[CachedDataLoader] Unknown series method ${loaderMethod}`);
|
|
208
202
|
|
|
203
|
+
// Fallback to legacy loop if method isn't configured for batching (missing config/flag)
|
|
204
|
+
if (!def.configKey || !def.flag) return this._loadSeriesLegacy(loaderMethod, dateStr, lookbackDays);
|
|
205
|
+
|
|
209
206
|
// 1. Calculate Date Range
|
|
210
207
|
const endDate = new Date(dateStr);
|
|
211
208
|
const startDate = new Date(endDate);
|
|
212
209
|
startDate.setUTCDate(startDate.getUTCDate() - (lookbackDays - 1));
|
|
213
210
|
|
|
214
|
-
|
|
215
|
-
const endStr = endDate.toISOString().slice(0, 10);
|
|
216
|
-
|
|
217
|
-
// 2. Pre-flight: Fetch Availability Window
|
|
211
|
+
// 2. Pre-flight: Check Availability
|
|
218
212
|
let availabilityMap = new Map();
|
|
219
213
|
try {
|
|
220
|
-
availabilityMap = await getAvailabilityWindow(this.deps,
|
|
214
|
+
availabilityMap = await getAvailabilityWindow(this.deps, startDate.toISOString().slice(0, 10), endDate.toISOString().slice(0, 10));
|
|
221
215
|
} catch (e) {
|
|
222
|
-
console.warn(`[CachedDataLoader] Availability check failed
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// 3. Identify Collection & Required Flag
|
|
226
|
-
const collectionInfo = LOADER_COLLECTION_MAP[loaderMethod];
|
|
227
|
-
const requiredFlag = LOADER_DEPENDENCY_MAP[loaderMethod];
|
|
228
|
-
|
|
229
|
-
if (!collectionInfo) {
|
|
230
|
-
// Fallback for methods not in the batch map (use legacy parallel loop)
|
|
231
|
-
return this._loadSeriesLegacy(loaderMethod, dateStr, lookbackDays);
|
|
216
|
+
console.warn(`[CachedDataLoader] Availability check failed. Optimistic batching enabled.`);
|
|
232
217
|
}
|
|
233
218
|
|
|
234
|
-
|
|
219
|
+
// 3. Construct Batch Refs
|
|
220
|
+
const collectionName = this.config[def.configKey] || def.defaultCol;
|
|
235
221
|
const batchRefs = [];
|
|
236
|
-
const dateKeyMap = [];
|
|
222
|
+
const dateKeyMap = [];
|
|
237
223
|
|
|
238
|
-
// 4. Build Batch References
|
|
239
224
|
for (let i = 0; i < lookbackDays; i++) {
|
|
240
225
|
const d = new Date(endDate);
|
|
241
226
|
d.setUTCDate(d.getUTCDate() - i);
|
|
242
227
|
const dString = d.toISOString().slice(0, 10);
|
|
243
228
|
|
|
244
|
-
// Check Availability
|
|
245
229
|
const dayStatus = availabilityMap.get(dString);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// If index exists, trust it
|
|
250
|
-
if (dayStatus && (!requiredFlag || dayStatus[requiredFlag])) {
|
|
251
|
-
shouldFetch = true;
|
|
252
|
-
}
|
|
253
|
-
} else {
|
|
254
|
-
// If index check failed/empty, try optimistically
|
|
255
|
-
shouldFetch = true;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (shouldFetch) {
|
|
259
|
-
const ref = this.deps.db.collection(collectionName).doc(dString);
|
|
260
|
-
batchRefs.push(ref);
|
|
230
|
+
// Fetch if index says data exists OR if index is missing (optimistic)
|
|
231
|
+
if (!dayStatus || dayStatus[def.flag]) {
|
|
232
|
+
batchRefs.push(this.deps.db.collection(collectionName).doc(dString));
|
|
261
233
|
dateKeyMap.push(dString);
|
|
262
234
|
}
|
|
263
235
|
}
|
|
264
236
|
|
|
265
|
-
//
|
|
237
|
+
// 4. Execute Batch Read
|
|
266
238
|
const results = {};
|
|
267
|
-
let foundCount = 0;
|
|
268
|
-
|
|
269
239
|
if (batchRefs.length > 0) {
|
|
270
|
-
|
|
271
|
-
const paths = batchRefs.map(ref => ref.path).join(', ');
|
|
272
|
-
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Batch loading ${batchRefs.length} documents for '${loaderMethod}': ${paths}`);
|
|
273
|
-
|
|
240
|
+
this.deps.logger?.log('INFO', `[CachedDataLoader] 📂 Batch loading ${batchRefs.length} docs for '${loaderMethod}'`);
|
|
274
241
|
try {
|
|
275
242
|
const snapshots = await this.deps.db.getAll(...batchRefs);
|
|
276
|
-
|
|
277
|
-
snapshots.forEach((snap, index) => {
|
|
243
|
+
snapshots.forEach((snap, idx) => {
|
|
278
244
|
if (snap.exists) {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
// Handle standard data shapes (removing metadata fields if necessary)
|
|
286
|
-
// Most root data loaders return the full object, so we do too.
|
|
287
|
-
// Specific logic from data_loader.js (like stripping 'date' key) is handled here generically
|
|
288
|
-
// or by the consumer. For series data, returning the whole object is usually safer.
|
|
289
|
-
|
|
290
|
-
// Special handling for cleaner output (mimicking data_loader.js logic)
|
|
291
|
-
if (loaderMethod === 'loadRatings' || loaderMethod === 'loadPageViews' ||
|
|
292
|
-
loaderMethod === 'loadWatchlistMembership' || loaderMethod === 'loadAlertHistory') {
|
|
293
|
-
const { date, lastUpdated, ...cleanData } = decompressed;
|
|
294
|
-
results[dString] = cleanData;
|
|
245
|
+
const raw = this._tryDecompress(snap.data());
|
|
246
|
+
// Clean metadata if necessary
|
|
247
|
+
if (['loadRatings', 'loadPageViews', 'loadWatchlistMembership', 'loadAlertHistory'].includes(loaderMethod)) {
|
|
248
|
+
const { date, lastUpdated, ...clean } = raw;
|
|
249
|
+
results[dateKeyMap[idx]] = clean;
|
|
295
250
|
} else if (loaderMethod === 'loadRankings') {
|
|
296
|
-
results[
|
|
251
|
+
results[dateKeyMap[idx]] = raw.Items || [];
|
|
297
252
|
} else {
|
|
298
|
-
results[
|
|
253
|
+
results[dateKeyMap[idx]] = raw;
|
|
299
254
|
}
|
|
300
|
-
|
|
301
|
-
foundCount++;
|
|
302
255
|
}
|
|
303
256
|
});
|
|
304
257
|
} catch (err) {
|
|
305
|
-
console.warn(`[CachedDataLoader] Batch
|
|
258
|
+
console.warn(`[CachedDataLoader] Batch failed: ${err.message}. Legacy fallback.`);
|
|
306
259
|
return this._loadSeriesLegacy(loaderMethod, dateStr, lookbackDays);
|
|
307
260
|
}
|
|
308
261
|
}
|
|
309
262
|
|
|
310
|
-
|
|
263
|
+
return {
|
|
311
264
|
dates: Object.keys(results).sort(),
|
|
312
265
|
data: results,
|
|
313
|
-
found:
|
|
266
|
+
found: Object.keys(results).length,
|
|
314
267
|
requested: lookbackDays
|
|
315
268
|
};
|
|
316
|
-
|
|
317
|
-
this.deps.logger?.log('INFO', `[CachedDataLoader] ✅ Loaded ${foundCount}/${lookbackDays} dates for '${loaderMethod}' (found: ${summary.dates.join(', ') || 'none'})`);
|
|
318
|
-
|
|
319
|
-
return summary;
|
|
320
269
|
}
|
|
321
270
|
|
|
322
|
-
/**
|
|
323
|
-
* Legacy Fallback: Loads series using parallel promises (for custom/unmapped loaders)
|
|
324
|
-
*/
|
|
325
271
|
async _loadSeriesLegacy(loaderMethod, dateStr, lookbackDays) {
|
|
326
272
|
const results = {};
|
|
327
273
|
const promises = [];
|
|
@@ -330,21 +276,11 @@ class CachedDataLoader {
|
|
|
330
276
|
for (let i = 0; i < lookbackDays; i++) {
|
|
331
277
|
const d = new Date(endDate);
|
|
332
278
|
d.setUTCDate(d.getUTCDate() - i);
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
// Log path for legacy loader (will be logged by individual load methods)
|
|
336
|
-
promises.push(
|
|
337
|
-
this[loaderMethod](dString)
|
|
338
|
-
.then(data => ({ date: dString, data }))
|
|
339
|
-
.catch(() => ({ date: dString, data: null }))
|
|
340
|
-
);
|
|
279
|
+
const dStr = d.toISOString().slice(0, 10);
|
|
280
|
+
promises.push(this[loaderMethod](dStr).then(data => data ? results[dStr] = data : null).catch(() => null));
|
|
341
281
|
}
|
|
342
282
|
|
|
343
|
-
|
|
344
|
-
loaded.forEach(({ date, data }) => {
|
|
345
|
-
if (data) results[date] = data;
|
|
346
|
-
});
|
|
347
|
-
|
|
283
|
+
await Promise.all(promises);
|
|
348
284
|
return {
|
|
349
285
|
dates: Object.keys(results).sort(),
|
|
350
286
|
data: results,
|