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
|
@@ -249,10 +249,11 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
249
249
|
|
|
250
250
|
// [FIX] Declare manifest here so it is accessible in both try and catch blocks
|
|
251
251
|
let manifest;
|
|
252
|
+
const taskStartTime = Date.now(); // Declare outside try/catch for access in error handler
|
|
252
253
|
|
|
253
254
|
try {
|
|
254
255
|
manifest = getManifest(config.activeProductLines || [], calculations, runDeps);
|
|
255
|
-
const startTime =
|
|
256
|
+
const startTime = taskStartTime;
|
|
256
257
|
|
|
257
258
|
const result = await executeDispatchTask(
|
|
258
259
|
date, pass, computation, config, runDeps,
|
|
@@ -336,7 +337,7 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
336
337
|
resourceTier: resourceTier
|
|
337
338
|
}, { merge: true });
|
|
338
339
|
|
|
339
|
-
await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', { message: err.message, stage: err.stage || 'FATAL' }, { peakMemoryMB: heartbeats.getPeak() }, triggerReason, resourceTier);
|
|
340
|
+
await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', { message: err.message, stage: err.stage || 'FATAL' }, { durationMs: Date.now() - taskStartTime, peakMemoryMB: heartbeats.getPeak() }, triggerReason, resourceTier);
|
|
340
341
|
|
|
341
342
|
// Send error notification if this was an on-demand computation
|
|
342
343
|
if (metadata?.onDemand && metadata?.requestId && metadata?.requestingUserCid) {
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Checks availability of root data via the Root Data Index.
|
|
3
|
+
* REFACTORED: Fully supports granular flags for PI, Signed-In Users, Rankings, and Verification.
|
|
4
|
+
* UPDATED: Enforces 'mandatoryRoots' metadata to override permissive flags.
|
|
5
|
+
* NEW: Added 'getAvailabilityWindow' for efficient batch availability lookups using range queries.
|
|
6
|
+
*/
|
|
7
|
+
const { normalizeName } = require('../utils/utils');
|
|
8
|
+
const { FieldPath } = require('@google-cloud/firestore');
|
|
9
|
+
|
|
10
|
+
const INDEX_COLLECTION = process.env.ROOT_DATA_AVAILABILITY_COLLECTION || 'system_root_data_index';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checks if a specific calculation can run based on its dependencies and the current data status.
|
|
14
|
+
* @param {Object} calcManifest - The calculation manifest.
|
|
15
|
+
* @param {Object} rootDataStatus - The availability status object.
|
|
16
|
+
* @returns {Object} { canRun: boolean, missing: Array, available: Array }
|
|
17
|
+
*/
|
|
18
|
+
function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
19
|
+
const missing = [];
|
|
20
|
+
const available = [];
|
|
21
|
+
|
|
22
|
+
if (!calcManifest.rootDataDependencies) return { canRun: true, missing, available };
|
|
23
|
+
|
|
24
|
+
// Check if computation can run with missing rootdata
|
|
25
|
+
const canHaveMissingRoots = calcManifest.canHaveMissingRoots === true;
|
|
26
|
+
|
|
27
|
+
// Normalize userType to lowercase for comparison (computations use uppercase)
|
|
28
|
+
const userType = (calcManifest.userType || 'all').toLowerCase();
|
|
29
|
+
|
|
30
|
+
for (const dep of calcManifest.rootDataDependencies) {
|
|
31
|
+
let isAvailable = false;
|
|
32
|
+
|
|
33
|
+
if (dep === 'portfolio') {
|
|
34
|
+
if (userType === 'speculator' && rootDataStatus.speculatorPortfolio) isAvailable = true;
|
|
35
|
+
else if (userType === 'normal' && rootDataStatus.normalPortfolio) isAvailable = true;
|
|
36
|
+
else if (userType === 'popular_investor' && rootDataStatus.piPortfolios) isAvailable = true;
|
|
37
|
+
else if (userType === 'signed_in_user' && rootDataStatus.signedInUserPortfolio) isAvailable = true;
|
|
38
|
+
else if (userType === 'all' && rootDataStatus.hasPortfolio) isAvailable = true;
|
|
39
|
+
|
|
40
|
+
if (!isAvailable) {
|
|
41
|
+
// [OPTIMIZATION] Optimistic Series Check
|
|
42
|
+
if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
|
|
43
|
+
available.push('portfolio');
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (userType === 'speculator') missing.push('speculatorPortfolio');
|
|
48
|
+
else if (userType === 'normal') missing.push('normalPortfolio');
|
|
49
|
+
else if (userType === 'popular_investor') missing.push('piPortfolios');
|
|
50
|
+
else if (userType === 'signed_in_user') missing.push('signedInUserPortfolio');
|
|
51
|
+
else missing.push('portfolio');
|
|
52
|
+
} else {
|
|
53
|
+
available.push('portfolio');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (dep === 'history') {
|
|
57
|
+
if (userType === 'speculator' && rootDataStatus.speculatorHistory) isAvailable = true;
|
|
58
|
+
else if (userType === 'normal' && rootDataStatus.normalHistory) isAvailable = true;
|
|
59
|
+
else if (userType === 'popular_investor' && rootDataStatus.piHistory) isAvailable = true;
|
|
60
|
+
else if (userType === 'signed_in_user' && rootDataStatus.signedInUserHistory) isAvailable = true;
|
|
61
|
+
else if (userType === 'all' && rootDataStatus.hasHistory) isAvailable = true;
|
|
62
|
+
|
|
63
|
+
if (!isAvailable) {
|
|
64
|
+
if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
|
|
65
|
+
available.push('history');
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (userType === 'speculator') missing.push('speculatorHistory');
|
|
70
|
+
else if (userType === 'normal') missing.push('normalHistory');
|
|
71
|
+
else if (userType === 'popular_investor') missing.push('piHistory');
|
|
72
|
+
else if (userType === 'signed_in_user') missing.push('signedInUserHistory');
|
|
73
|
+
else missing.push('history');
|
|
74
|
+
} else {
|
|
75
|
+
available.push('history');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (dep === 'rankings') {
|
|
79
|
+
if (rootDataStatus.piRankings) {
|
|
80
|
+
isAvailable = true;
|
|
81
|
+
available.push('rankings');
|
|
82
|
+
} else {
|
|
83
|
+
if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
|
|
84
|
+
available.push('rankings');
|
|
85
|
+
} else {
|
|
86
|
+
missing.push('piRankings');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (dep === 'verification') {
|
|
91
|
+
if (rootDataStatus.signedInUserVerification) {
|
|
92
|
+
isAvailable = true;
|
|
93
|
+
available.push('verification');
|
|
94
|
+
} else {
|
|
95
|
+
missing.push('signedInUserVerification');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (dep === 'insights') {
|
|
99
|
+
if (rootDataStatus.hasInsights) {
|
|
100
|
+
isAvailable = true;
|
|
101
|
+
available.push('insights');
|
|
102
|
+
} else {
|
|
103
|
+
if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
|
|
104
|
+
available.push('insights');
|
|
105
|
+
} else {
|
|
106
|
+
missing.push('insights');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (dep === 'social') {
|
|
111
|
+
if (userType === 'popular_investor') {
|
|
112
|
+
if (rootDataStatus.hasPISocial) isAvailable = true;
|
|
113
|
+
} else if (userType === 'signed_in_user') {
|
|
114
|
+
if (rootDataStatus.hasSignedInSocial) isAvailable = true;
|
|
115
|
+
} else {
|
|
116
|
+
if (rootDataStatus.hasSocial) isAvailable = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (isAvailable) {
|
|
120
|
+
available.push('social');
|
|
121
|
+
} else {
|
|
122
|
+
if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
|
|
123
|
+
available.push('social');
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (userType === 'popular_investor') missing.push('piSocial');
|
|
128
|
+
else if (userType === 'signed_in_user') missing.push('signedInSocial');
|
|
129
|
+
else missing.push('social');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (dep === 'price') {
|
|
133
|
+
if (rootDataStatus.hasPrices) {
|
|
134
|
+
isAvailable = true;
|
|
135
|
+
available.push('price');
|
|
136
|
+
} else {
|
|
137
|
+
if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
|
|
138
|
+
available.push('price');
|
|
139
|
+
} else {
|
|
140
|
+
missing.push('price');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if (dep === 'ratings') {
|
|
145
|
+
if (rootDataStatus.piRatings) {
|
|
146
|
+
isAvailable = true;
|
|
147
|
+
available.push('ratings');
|
|
148
|
+
} else {
|
|
149
|
+
if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
|
|
150
|
+
available.push('ratings');
|
|
151
|
+
} else {
|
|
152
|
+
missing.push('piRatings');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (dep === 'pageViews') {
|
|
157
|
+
if (rootDataStatus.piPageViews) {
|
|
158
|
+
isAvailable = true;
|
|
159
|
+
available.push('pageViews');
|
|
160
|
+
} else {
|
|
161
|
+
if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
|
|
162
|
+
available.push('pageViews');
|
|
163
|
+
} else {
|
|
164
|
+
missing.push('piPageViews');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (dep === 'watchlist') {
|
|
169
|
+
if (rootDataStatus.watchlistMembership) {
|
|
170
|
+
isAvailable = true;
|
|
171
|
+
available.push('watchlist');
|
|
172
|
+
} else {
|
|
173
|
+
if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
|
|
174
|
+
available.push('watchlist');
|
|
175
|
+
} else {
|
|
176
|
+
missing.push('watchlistMembership');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (dep === 'alerts') {
|
|
181
|
+
if (rootDataStatus.piAlertHistory) {
|
|
182
|
+
isAvailable = true;
|
|
183
|
+
available.push('alerts');
|
|
184
|
+
} else {
|
|
185
|
+
if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
|
|
186
|
+
available.push('alerts');
|
|
187
|
+
} else {
|
|
188
|
+
missing.push('piAlertHistory');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// [NEW] Enforce Mandatory Roots (defined by computation)
|
|
195
|
+
// This allows granular control: "I need portfolio, but ratings are optional"
|
|
196
|
+
if (calcManifest.mandatoryRoots && Array.isArray(calcManifest.mandatoryRoots) && calcManifest.mandatoryRoots.length > 0) {
|
|
197
|
+
const missingMandatory = calcManifest.mandatoryRoots.filter(r => !available.includes(r));
|
|
198
|
+
if (missingMandatory.length > 0) {
|
|
199
|
+
return { canRun: false, missing, available };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (canHaveMissingRoots) {
|
|
204
|
+
const canRun = available.length > 0;
|
|
205
|
+
return { canRun, missing, available };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Strict mode: all required rootdata must be available
|
|
209
|
+
return { canRun: missing.length === 0, missing, available };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getViableCalculations(candidates, fullManifest, rootDataStatus, dailyStatus) {
|
|
213
|
+
const viable = [];
|
|
214
|
+
const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
|
|
215
|
+
|
|
216
|
+
for (const calc of candidates) {
|
|
217
|
+
const rootCheck = checkRootDependencies(calc, rootDataStatus);
|
|
218
|
+
if (!rootCheck.canRun) continue;
|
|
219
|
+
|
|
220
|
+
let dependenciesMet = true;
|
|
221
|
+
if (calc.dependencies && calc.dependencies.length > 0) {
|
|
222
|
+
for (const depName of calc.dependencies) {
|
|
223
|
+
const normDep = normalizeName(depName);
|
|
224
|
+
const storedHash = dailyStatus[normDep];
|
|
225
|
+
const depManifest = manifestMap.get(normDep);
|
|
226
|
+
|
|
227
|
+
if (!depManifest) { dependenciesMet = false; break; }
|
|
228
|
+
if (!storedHash) { dependenciesMet = false; break; }
|
|
229
|
+
if (storedHash.hash !== depManifest.hash) { dependenciesMet = false; break; }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (dependenciesMet) { viable.push(calc); }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return viable;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Checks root data availability for a single date.
|
|
241
|
+
*/
|
|
242
|
+
async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
|
|
243
|
+
const { logger, db } = dependencies;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const indexDoc = await db.collection(INDEX_COLLECTION).doc(dateStr).get();
|
|
247
|
+
|
|
248
|
+
if (indexDoc.exists) {
|
|
249
|
+
const data = indexDoc.data();
|
|
250
|
+
const details = data.details || {};
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
status: {
|
|
254
|
+
hasPortfolio: !!data.hasPortfolio,
|
|
255
|
+
hasHistory: !!data.hasHistory,
|
|
256
|
+
hasSocial: !!data.hasSocial,
|
|
257
|
+
hasInsights: !!data.hasInsights,
|
|
258
|
+
hasPrices: !!data.hasPrices,
|
|
259
|
+
speculatorPortfolio: !!details.speculatorPortfolio,
|
|
260
|
+
normalPortfolio: !!details.normalPortfolio,
|
|
261
|
+
speculatorHistory: !!details.speculatorHistory,
|
|
262
|
+
normalHistory: !!details.normalHistory,
|
|
263
|
+
piRankings: !!details.piRankings,
|
|
264
|
+
piPortfolios: !!details.piPortfolios,
|
|
265
|
+
piDeepPortfolios: !!details.piDeepPortfolios,
|
|
266
|
+
piHistory: !!details.piHistory,
|
|
267
|
+
signedInUserPortfolio: !!details.signedInUserPortfolio,
|
|
268
|
+
signedInUserHistory: !!details.signedInUserHistory,
|
|
269
|
+
signedInUserVerification: !!details.signedInUserVerification,
|
|
270
|
+
hasPISocial: !!details.hasPISocial || !!data.hasPISocial,
|
|
271
|
+
hasSignedInSocial: !!details.hasSignedInSocial || !!data.hasSignedInSocial,
|
|
272
|
+
piRatings: !!details.piRatings,
|
|
273
|
+
piPageViews: !!details.piPageViews,
|
|
274
|
+
watchlistMembership: !!details.watchlistMembership,
|
|
275
|
+
piAlertHistory: !!details.piAlertHistory,
|
|
276
|
+
piMasterList: !!details.piMasterList
|
|
277
|
+
},
|
|
278
|
+
portfolioRefs: null,
|
|
279
|
+
historyRefs: null,
|
|
280
|
+
todayInsights: null,
|
|
281
|
+
todaySocialPostInsights: null,
|
|
282
|
+
yesterdayPortfolioRefs: null
|
|
283
|
+
};
|
|
284
|
+
} else {
|
|
285
|
+
logger.log('WARN', `[Availability] Index not found for ${dateStr}. Assuming NO data.`);
|
|
286
|
+
return {
|
|
287
|
+
status: {
|
|
288
|
+
hasPortfolio: false,
|
|
289
|
+
hasHistory: false,
|
|
290
|
+
hasSocial: false,
|
|
291
|
+
hasInsights: false,
|
|
292
|
+
hasPrices: false,
|
|
293
|
+
speculatorPortfolio: false,
|
|
294
|
+
normalPortfolio: false,
|
|
295
|
+
speculatorHistory: false,
|
|
296
|
+
normalHistory: false,
|
|
297
|
+
piRankings: false,
|
|
298
|
+
piPortfolios: false,
|
|
299
|
+
piDeepPortfolios: false,
|
|
300
|
+
piHistory: false,
|
|
301
|
+
signedInUserPortfolio: false,
|
|
302
|
+
signedInUserHistory: false,
|
|
303
|
+
signedInUserVerification: false,
|
|
304
|
+
hasPISocial: false,
|
|
305
|
+
hasSignedInSocial: false,
|
|
306
|
+
piRatings: false,
|
|
307
|
+
piPageViews: false,
|
|
308
|
+
watchlistMembership: false,
|
|
309
|
+
piAlertHistory: false
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
} catch (err) {
|
|
315
|
+
logger.log('ERROR', `Error checking availability index: ${err.message}`);
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* [NEW] Fetches availability status for a range of dates.
|
|
322
|
+
* Uses a range query to only retrieve indices that actually exist, preventing wasted reads on empty days.
|
|
323
|
+
* @param {Object} deps - Dependencies (must include db)
|
|
324
|
+
* @param {string} startDateStr - ISO Date string (YYYY-MM-DD) inclusive start
|
|
325
|
+
* @param {string} endDateStr - ISO Date string (YYYY-MM-DD) inclusive end
|
|
326
|
+
* @returns {Promise<Map<string, Object>>} Map of dateStr -> status object
|
|
327
|
+
*/
|
|
328
|
+
async function getAvailabilityWindow(deps, startDateStr, endDateStr) {
|
|
329
|
+
const { db } = deps;
|
|
330
|
+
|
|
331
|
+
// Perform Range Query on Document ID (Date String)
|
|
332
|
+
const snapshot = await db.collection(INDEX_COLLECTION)
|
|
333
|
+
.where(FieldPath.documentId(), '>=', startDateStr)
|
|
334
|
+
.where(FieldPath.documentId(), '<=', endDateStr)
|
|
335
|
+
.get();
|
|
336
|
+
|
|
337
|
+
const availabilityMap = new Map();
|
|
338
|
+
|
|
339
|
+
snapshot.forEach(doc => {
|
|
340
|
+
const data = doc.data();
|
|
341
|
+
const details = data.details || {};
|
|
342
|
+
const dateStr = doc.id;
|
|
343
|
+
|
|
344
|
+
// Construct status object matching checkRootDataAvailability structure
|
|
345
|
+
const status = {
|
|
346
|
+
hasPortfolio: !!data.hasPortfolio,
|
|
347
|
+
hasHistory: !!data.hasHistory,
|
|
348
|
+
hasSocial: !!data.hasSocial,
|
|
349
|
+
hasInsights: !!data.hasInsights,
|
|
350
|
+
hasPrices: !!data.hasPrices,
|
|
351
|
+
speculatorPortfolio: !!details.speculatorPortfolio,
|
|
352
|
+
normalPortfolio: !!details.normalPortfolio,
|
|
353
|
+
speculatorHistory: !!details.speculatorHistory,
|
|
354
|
+
normalHistory: !!details.normalHistory,
|
|
355
|
+
piRankings: !!details.piRankings,
|
|
356
|
+
piPortfolios: !!details.piPortfolios,
|
|
357
|
+
piDeepPortfolios: !!details.piDeepPortfolios,
|
|
358
|
+
piHistory: !!details.piHistory,
|
|
359
|
+
signedInUserPortfolio: !!details.signedInUserPortfolio,
|
|
360
|
+
signedInUserHistory: !!details.signedInUserHistory,
|
|
361
|
+
signedInUserVerification: !!details.signedInUserVerification,
|
|
362
|
+
hasPISocial: !!details.hasPISocial || !!data.hasPISocial,
|
|
363
|
+
hasSignedInSocial: !!details.hasSignedInSocial || !!data.hasSignedInSocial,
|
|
364
|
+
piRatings: !!details.piRatings,
|
|
365
|
+
piPageViews: !!details.piPageViews,
|
|
366
|
+
watchlistMembership: !!details.watchlistMembership,
|
|
367
|
+
piAlertHistory: !!details.piAlertHistory,
|
|
368
|
+
piMasterList: !!details.piMasterList
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
availabilityMap.set(dateStr, status);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
return availabilityMap;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
module.exports = {
|
|
378
|
+
checkRootDependencies,
|
|
379
|
+
checkRootDataAvailability,
|
|
380
|
+
getViableCalculations,
|
|
381
|
+
getAvailabilityWindow
|
|
382
|
+
};
|