bulltrackers-module 1.0.431 → 1.0.433
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.
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Handles saving computation results with observability and Smart Cleanup.
|
|
3
|
-
* UPDATED:
|
|
4
|
-
* UPDATED: Added Dynamic Circuit Breaker Bypass for Price-Only calculations.
|
|
5
|
-
* UPDATED: Integrated Alert System Pub/Sub triggers for marked computations.
|
|
3
|
+
* UPDATED: Fixed bug where Alert Computations failed to trigger Pub/Sub on empty FINAL flush.
|
|
6
4
|
*/
|
|
7
5
|
const { commitBatchInChunks, generateDataHash } = require('../utils/utils');
|
|
8
6
|
const { updateComputationStatus } = require('./StatusRepository');
|
|
@@ -24,7 +22,7 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
24
22
|
const failureReport = [];
|
|
25
23
|
const schemas = [];
|
|
26
24
|
const cleanupTasks = [];
|
|
27
|
-
const alertTriggers = [];
|
|
25
|
+
const alertTriggers = [];
|
|
28
26
|
const { logger, db } = deps;
|
|
29
27
|
const pid = generateProcessId(PROCESS_TYPES.STORAGE, passName, dStr);
|
|
30
28
|
|
|
@@ -35,7 +33,6 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
35
33
|
const fanOutLimit = pLimit(10);
|
|
36
34
|
const pubSubUtils = new PubSubUtils(deps);
|
|
37
35
|
|
|
38
|
-
// 1. [BATCH OPTIMIZATION] Fetch all SimHashes and Contracts upfront
|
|
39
36
|
const calcNames = Object.keys(stateObj);
|
|
40
37
|
const hashKeys = calcNames.map(n => stateObj[n].manifest?.hash).filter(Boolean);
|
|
41
38
|
|
|
@@ -56,11 +53,13 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
56
53
|
io: { writes: 0, deletes: 0 }
|
|
57
54
|
};
|
|
58
55
|
|
|
56
|
+
// [NEW] Check metadata for alert flag (defaults to false)
|
|
57
|
+
const isAlertComputation = calc.manifest.isAlertComputation === true;
|
|
58
|
+
|
|
59
59
|
try {
|
|
60
60
|
const result = await calc.getResult();
|
|
61
61
|
const configOverrides = validationOverrides[calc.manifest.name] || {};
|
|
62
62
|
|
|
63
|
-
// --- [DYNAMIC OVERRIDE LOGIC START] ---
|
|
64
63
|
const dataDeps = calc.manifest.rootDataDependencies || [];
|
|
65
64
|
const isPriceOnly = (dataDeps.length === 1 && dataDeps[0] === 'price');
|
|
66
65
|
let effectiveOverrides = { ...configOverrides };
|
|
@@ -72,7 +71,6 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
72
71
|
effectiveOverrides.maxNanPct = 100;
|
|
73
72
|
delete effectiveOverrides.weekend;
|
|
74
73
|
}
|
|
75
|
-
// --- [DYNAMIC OVERRIDE LOGIC END] ---
|
|
76
74
|
|
|
77
75
|
const contract = contractMap[name];
|
|
78
76
|
if (contract) {
|
|
@@ -99,11 +97,27 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
99
97
|
|
|
100
98
|
const isEmpty = !result || (typeof result === 'object' && Object.keys(result).length === 0);
|
|
101
99
|
const resultHash = isEmpty ? 'empty' : generateDataHash(result);
|
|
102
|
-
|
|
103
100
|
const simHash = (flushMode !== 'INTERMEDIATE') ? (simHashMap[calc.manifest.hash] || null) : null;
|
|
104
101
|
|
|
105
102
|
if (isEmpty) {
|
|
106
|
-
if (flushMode === 'INTERMEDIATE') {
|
|
103
|
+
if (flushMode === 'INTERMEDIATE') {
|
|
104
|
+
nextShardIndexes[name] = currentShardIndex;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// [FIX] Force alert trigger on FINAL flush even if result is empty
|
|
109
|
+
// This handles cases where all data was written in previous INTERMEDIATE flushes
|
|
110
|
+
if (isAlertComputation && flushMode === 'FINAL') {
|
|
111
|
+
// Reconstruct path to ensure downstream system checks for data
|
|
112
|
+
const docPath = `${config.resultsCollection}/${dStr}/${config.resultsSubcollection}/${calc.manifest.category}/${config.computationsSubcollection}/${name}`;
|
|
113
|
+
|
|
114
|
+
alertTriggers.push({
|
|
115
|
+
date: dStr,
|
|
116
|
+
computationName: name,
|
|
117
|
+
documentPath: docPath
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
107
121
|
if (calc.manifest.hash) {
|
|
108
122
|
successUpdates[name] = {
|
|
109
123
|
hash: calc.manifest.hash, simHash: simHash, resultHash: resultHash,
|
|
@@ -119,9 +133,6 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
119
133
|
const resultKeys = Object.keys(result || {});
|
|
120
134
|
const isMultiDate = resultKeys.length > 0 && resultKeys.every(k => /^\d{4}-\d{2}-\d{2}$/.test(k));
|
|
121
135
|
|
|
122
|
-
// [NEW] Check metadata for alert flag (defaults to false)
|
|
123
|
-
const isAlertComputation = calc.manifest.isAlertComputation === true;
|
|
124
|
-
|
|
125
136
|
if (isMultiDate) {
|
|
126
137
|
const datePromises = resultKeys.map((historicalDate) => fanOutLimit(async () => {
|
|
127
138
|
const dailyData = result[historicalDate];
|
|
@@ -131,7 +142,6 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
131
142
|
runMetrics.io.writes += stats.opCounts.writes;
|
|
132
143
|
runMetrics.io.deletes += stats.opCounts.deletes;
|
|
133
144
|
|
|
134
|
-
// [NEW] Queue alert for historical date if applicable
|
|
135
145
|
if (isAlertComputation && flushMode !== 'INTERMEDIATE') {
|
|
136
146
|
alertTriggers.push({
|
|
137
147
|
date: historicalDate,
|
|
@@ -156,7 +166,6 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
156
166
|
nextShardIndexes[name] = writeStats.nextShardIndex;
|
|
157
167
|
if (calc.manifest.hash) { successUpdates[name] = { hash: calc.manifest.hash, simHash, resultHash, dependencyResultHashes: calc.manifest.dependencyResultHashes || {}, category: calc.manifest.category, composition: calc.manifest.composition, metrics: runMetrics }; }
|
|
158
168
|
|
|
159
|
-
// [NEW] Queue alert for standard write
|
|
160
169
|
if (isAlertComputation && flushMode !== 'INTERMEDIATE') {
|
|
161
170
|
alertTriggers.push({
|
|
162
171
|
date: dStr,
|
|
@@ -193,7 +202,7 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
193
202
|
await updateComputationStatus(dStr, successUpdates, config, deps);
|
|
194
203
|
}
|
|
195
204
|
|
|
196
|
-
//
|
|
205
|
+
// Flush Alert Triggers to Pub/Sub
|
|
197
206
|
if (alertTriggers.length > 0) {
|
|
198
207
|
const topicName = config.alertTopicName || 'alert-trigger';
|
|
199
208
|
try {
|
|
@@ -201,7 +210,6 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
201
210
|
logger.log('INFO', `[Alert System] Triggered ${alertTriggers.length} alerts to ${topicName}`);
|
|
202
211
|
} catch (alertErr) {
|
|
203
212
|
logger.log('ERROR', `[Alert System] Failed to publish alerts: ${alertErr.message}`);
|
|
204
|
-
// Non-blocking: We don't fail the computation if alerts fail to publish, but we log strictly.
|
|
205
213
|
}
|
|
206
214
|
}
|
|
207
215
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Data Feeder Pipeline
|
|
2
|
+
# Orchestrates data fetching relative to Market Close (22:00 UTC) and Midnight (00:00 UTC).
|
|
3
|
+
# Triggers at 22:00 UTC.
|
|
4
|
+
|
|
5
|
+
main:
|
|
6
|
+
params: [input]
|
|
7
|
+
steps:
|
|
8
|
+
- init:
|
|
9
|
+
assign:
|
|
10
|
+
- project: '${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}'
|
|
11
|
+
- location: "europe-west1"
|
|
12
|
+
- market_date: '${text.split(time.format(sys.now()), "T")[0]}'
|
|
13
|
+
|
|
14
|
+
# --- PHASE 1: MARKET CLOSE (22:00 UTC) ---
|
|
15
|
+
- run_market_close_tasks:
|
|
16
|
+
parallel:
|
|
17
|
+
branches:
|
|
18
|
+
- price_fetch:
|
|
19
|
+
steps:
|
|
20
|
+
- call_price_fetcher:
|
|
21
|
+
call: http.post
|
|
22
|
+
args:
|
|
23
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/price-fetcher"}'
|
|
24
|
+
auth: { type: OIDC }
|
|
25
|
+
- insights_fetch:
|
|
26
|
+
steps:
|
|
27
|
+
- call_insights_fetcher:
|
|
28
|
+
call: http.post
|
|
29
|
+
args:
|
|
30
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/insights-fetcher"}'
|
|
31
|
+
auth: { type: OIDC }
|
|
32
|
+
|
|
33
|
+
# [IMMEDIATE INDEX: MARKET DATA]
|
|
34
|
+
# Update index for the market date we just fetched
|
|
35
|
+
- index_market_data:
|
|
36
|
+
call: http.post
|
|
37
|
+
args:
|
|
38
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
|
|
39
|
+
body:
|
|
40
|
+
targetDate: '${market_date}'
|
|
41
|
+
auth: { type: OIDC }
|
|
42
|
+
|
|
43
|
+
# --- PHASE 2: WAIT FOR MIDNIGHT ---
|
|
44
|
+
# Trigger is at 22:00. We wait ~2 hours to align with 00:00 UTC.
|
|
45
|
+
- wait_for_midnight:
|
|
46
|
+
call: sys.sleep
|
|
47
|
+
args:
|
|
48
|
+
seconds: 7200 # 2 Hours
|
|
49
|
+
|
|
50
|
+
# --- PHASE 3: RANKINGS & INITIAL SOCIAL (00:00 UTC) ---
|
|
51
|
+
# Rankings must run first.
|
|
52
|
+
- run_rankings_fetch:
|
|
53
|
+
try:
|
|
54
|
+
call: http.post
|
|
55
|
+
args:
|
|
56
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/fetch-popular-investors"}'
|
|
57
|
+
auth: { type: OIDC }
|
|
58
|
+
except:
|
|
59
|
+
as: e
|
|
60
|
+
steps:
|
|
61
|
+
- log_rankings_error:
|
|
62
|
+
call: sys.log
|
|
63
|
+
args:
|
|
64
|
+
severity: "ERROR"
|
|
65
|
+
text: '${"Rankings Fetch Failed. Proceeding to Social Fetch. Error: " + json.encode(e)}'
|
|
66
|
+
|
|
67
|
+
- run_social_midnight:
|
|
68
|
+
call: http.post
|
|
69
|
+
args:
|
|
70
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
|
|
71
|
+
auth: { type: OIDC }
|
|
72
|
+
|
|
73
|
+
# [IMMEDIATE INDEX: MIDNIGHT DATA]
|
|
74
|
+
# We recalculate the date because we are now past midnight
|
|
75
|
+
- index_midnight_data:
|
|
76
|
+
assign:
|
|
77
|
+
- current_date: '${text.split(time.format(sys.now()), "T")[0]}'
|
|
78
|
+
next: call_index_midnight
|
|
79
|
+
|
|
80
|
+
- call_index_midnight:
|
|
81
|
+
call: http.post
|
|
82
|
+
args:
|
|
83
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
|
|
84
|
+
body:
|
|
85
|
+
targetDate: '${current_date}'
|
|
86
|
+
auth: { type: OIDC }
|
|
87
|
+
|
|
88
|
+
# --- PHASE 4: GLOBAL SYNC (Before 01:00 UTC) ---
|
|
89
|
+
# Critical: Indexes EVERYTHING (no targetDate) to ensure the Computation System
|
|
90
|
+
# (which starts at 01:00 UTC) has a fully consistent view of history.
|
|
91
|
+
- run_global_indexer:
|
|
92
|
+
call: http.post
|
|
93
|
+
args:
|
|
94
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
|
|
95
|
+
# No body = Full Scan
|
|
96
|
+
auth: { type: OIDC }
|
|
97
|
+
result: global_index_res
|
|
98
|
+
|
|
99
|
+
# --- PHASE 5: RECURRING SOCIAL FETCH (Every 3 Hours) ---
|
|
100
|
+
# We loop to cover the rest of the 24h cycle.
|
|
101
|
+
- init_social_loop:
|
|
102
|
+
assign:
|
|
103
|
+
- i: 0
|
|
104
|
+
|
|
105
|
+
- social_loop:
|
|
106
|
+
switch:
|
|
107
|
+
- condition: ${i < 7}
|
|
108
|
+
steps:
|
|
109
|
+
- wait_3_hours:
|
|
110
|
+
call: sys.sleep
|
|
111
|
+
args:
|
|
112
|
+
seconds: 10800 # 3 hours
|
|
113
|
+
|
|
114
|
+
- run_social_recurring:
|
|
115
|
+
call: http.post
|
|
116
|
+
args:
|
|
117
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
|
|
118
|
+
auth: { type: OIDC }
|
|
119
|
+
|
|
120
|
+
# [IMMEDIATE INDEX: RECURRING]
|
|
121
|
+
- calculate_recurring_date:
|
|
122
|
+
assign:
|
|
123
|
+
- current_date_rec: '${text.split(time.format(sys.now()), "T")[0]}'
|
|
124
|
+
|
|
125
|
+
- index_recurring:
|
|
126
|
+
call: http.post
|
|
127
|
+
args:
|
|
128
|
+
url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
|
|
129
|
+
body:
|
|
130
|
+
targetDate: '${current_date_rec}'
|
|
131
|
+
auth: { type: OIDC }
|
|
132
|
+
|
|
133
|
+
- increment:
|
|
134
|
+
assign:
|
|
135
|
+
- i: ${i + 1}
|
|
136
|
+
- next_iteration:
|
|
137
|
+
next: social_loop
|
|
138
|
+
|
|
139
|
+
- finish:
|
|
140
|
+
return: "Daily Data Pipeline Completed"
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Root Data Indexer
|
|
3
3
|
* Runs daily to index exactly what data is available for every date.
|
|
4
|
+
* UPDATED: Includes 'targetDate' optimization for fast single-day updates.
|
|
4
5
|
* UPDATED: Includes explicit checks for Signed-In User Portfolios, History, AND Social Data.
|
|
5
|
-
* FIXED: 'hasSocial' is now strictly for Generic Asset Data. PI/User data is separated.
|
|
6
|
-
* FIXED: PI Collection paths now match the '19M/snapshots' structure.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
@@ -63,7 +62,8 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
63
62
|
const {
|
|
64
63
|
availabilityCollection,
|
|
65
64
|
earliestDate,
|
|
66
|
-
collections
|
|
65
|
+
collections,
|
|
66
|
+
targetDate // [NEW] Optional parameter to scan a single specific date
|
|
67
67
|
} = config;
|
|
68
68
|
|
|
69
69
|
const PRICE_COLLECTION_NAME = 'asset_prices';
|
|
@@ -72,45 +72,56 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
72
72
|
const PI_SOCIAL_COLL_NAME = collections.piSocial || 'pi_social_posts';
|
|
73
73
|
const SIGNED_IN_SOCIAL_COLL_NAME = collections.signedInUserSocialCollection || 'signed_in_users';
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
const scanMode = targetDate ? 'SINGLE_DATE' : 'FULL_SCAN';
|
|
76
|
+
logger.log('INFO', `[RootDataIndexer] Starting Root Data Availability Scan... Mode: ${scanMode}`, { targetDate });
|
|
76
77
|
|
|
77
|
-
// 1. Price Availability
|
|
78
|
-
//
|
|
78
|
+
// 1. Price Availability (Global Scan Mode Only)
|
|
79
|
+
// If running for a single targetDate, we skip the global shard sampling to save time.
|
|
79
80
|
const priceAvailabilitySet = new Set();
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
81
|
+
|
|
82
|
+
if (!targetDate) {
|
|
83
|
+
try {
|
|
84
|
+
// Path: asset_prices/shard_*
|
|
85
|
+
const priceCollectionRef = db.collection(PRICE_COLLECTION_NAME);
|
|
86
|
+
const priceShardsSnapshot = await priceCollectionRef.limit(10).get();
|
|
87
|
+
|
|
88
|
+
if (!priceShardsSnapshot.empty) {
|
|
89
|
+
// Sample up to 10 shards and extract date keys from them
|
|
90
|
+
for (const shardDoc of priceShardsSnapshot.docs) {
|
|
91
|
+
if (shardDoc.id.startsWith('shard_')) {
|
|
92
|
+
const data = shardDoc.data();
|
|
93
|
+
Object.values(data).forEach(instrument => {
|
|
94
|
+
if (instrument && instrument.prices) {
|
|
95
|
+
Object.keys(instrument.prices).forEach(dateKey => {
|
|
96
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) {
|
|
97
|
+
priceAvailabilitySet.add(dateKey);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
100
103
|
}
|
|
101
104
|
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
logger.log('ERROR', '[RootDataIndexer] Failed to sample price shards.', { error: e.message });
|
|
102
107
|
}
|
|
103
|
-
} catch (e) {
|
|
104
|
-
logger.log('ERROR', '[RootDataIndexer] Failed to sample price shards.', { error: e.message });
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
// 2. Determine Date Range
|
|
108
|
-
const start = new Date(earliestDate || '2023-01-01');
|
|
109
|
-
const end = new Date();
|
|
110
|
-
end.setDate(end.getDate() + 1);
|
|
111
|
-
|
|
112
111
|
const datesToScan = [];
|
|
113
|
-
|
|
112
|
+
if (targetDate) {
|
|
113
|
+
// [NEW] Single Date Optimization
|
|
114
|
+
datesToScan.push(targetDate);
|
|
115
|
+
} else {
|
|
116
|
+
// [OLD] Full History
|
|
117
|
+
const start = new Date(earliestDate || '2023-01-01');
|
|
118
|
+
const end = new Date();
|
|
119
|
+
end.setDate(end.getDate() + 1);
|
|
120
|
+
|
|
121
|
+
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
|
122
|
+
datesToScan.push(d.toISOString().slice(0, 10));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
114
125
|
|
|
115
126
|
// 3. Scan in Parallel
|
|
116
127
|
const limit = pLimit(20);
|
|
@@ -211,9 +222,9 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
211
222
|
normHistExists, specHistExists,
|
|
212
223
|
insightsSnap, socialQuerySnap,
|
|
213
224
|
piRankingsSnap,
|
|
214
|
-
piPortExists,
|
|
215
|
-
piDeepExists,
|
|
216
|
-
piHistExists,
|
|
225
|
+
piPortExists,
|
|
226
|
+
piDeepExists,
|
|
227
|
+
piHistExists,
|
|
217
228
|
signedInPortExists, signedInHistExists,
|
|
218
229
|
verificationsQuery,
|
|
219
230
|
universalSocialSnap
|
|
@@ -253,7 +264,6 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
253
264
|
availability.details.speculatorHistory = specHistExists;
|
|
254
265
|
availability.details.piRankings = piRankingsSnap.exists;
|
|
255
266
|
|
|
256
|
-
// UPDATED: Now checking for any part_* document existence
|
|
257
267
|
availability.details.piPortfolios = piPortExists;
|
|
258
268
|
availability.details.piDeepPortfolios = piDeepExists;
|
|
259
269
|
availability.details.piHistory = piHistExists;
|
|
@@ -270,17 +280,24 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
270
280
|
availability.details.signedInUserVerification = !verificationsQuery.empty;
|
|
271
281
|
|
|
272
282
|
// Aggregates
|
|
273
|
-
// UPDATED: Using part existence checks
|
|
274
283
|
availability.hasPortfolio = normPortExists || specPortExists || piPortExists || signedInPortExists;
|
|
275
284
|
availability.hasHistory = normHistExists || specHistExists || piHistExists || signedInHistExists;
|
|
276
285
|
availability.hasInsights = insightsSnap.exists;
|
|
277
|
-
|
|
278
|
-
// [CRITICAL FIX]
|
|
279
|
-
// hasSocial strictly refers to GENERIC/ASSET social data.
|
|
280
|
-
// It does NOT include PI or SignedIn data (they have their own flags).
|
|
281
286
|
availability.hasSocial = !socialQuerySnap.empty;
|
|
282
287
|
|
|
283
|
-
|
|
288
|
+
// Price Check
|
|
289
|
+
if (targetDate) {
|
|
290
|
+
// In single-date mode, we do a quick check on the shard if we don't have the full set loaded
|
|
291
|
+
// For performance, we default hasPrices to false unless we implement a specific single-date shard check.
|
|
292
|
+
// However, since this is usually called right after fetching, we can leave it as a secondary concern
|
|
293
|
+
// or assume the Global Sync will catch it later.
|
|
294
|
+
// OPTIONAL: Implementation of specific shard check for single date:
|
|
295
|
+
// For now, we reuse the set if available (it won't be) or default false.
|
|
296
|
+
// NOTE: The Global Scan at 01:00 UTC will strictly verify prices.
|
|
297
|
+
availability.hasPrices = priceAvailabilitySet.has(dateStr);
|
|
298
|
+
} else {
|
|
299
|
+
availability.hasPrices = priceAvailabilitySet.has(dateStr);
|
|
300
|
+
}
|
|
284
301
|
|
|
285
302
|
await db.collection(availabilityCollection).doc(dateStr).set(availability);
|
|
286
303
|
updatesCount++;
|
|
@@ -288,14 +305,12 @@ exports.runRootDataIndexer = async (config, dependencies) => {
|
|
|
288
305
|
} catch (e) {
|
|
289
306
|
logger.log('ERROR', `[RootDataIndexer] Failed to index ${dateStr}`, {
|
|
290
307
|
message: e.message,
|
|
291
|
-
code: e.code
|
|
292
|
-
stack: e.stack,
|
|
293
|
-
full: JSON.stringify(e, Object.getOwnPropertyNames(e)),
|
|
308
|
+
code: e.code
|
|
294
309
|
});
|
|
295
310
|
}
|
|
296
311
|
}));
|
|
297
312
|
|
|
298
313
|
await Promise.all(promises);
|
|
299
|
-
logger.log('SUCCESS', `[RootDataIndexer] Indexing complete. Updated ${updatesCount} dates
|
|
300
|
-
return { success: true, count: updatesCount };
|
|
314
|
+
logger.log('SUCCESS', `[RootDataIndexer] Indexing complete. Updated ${updatesCount} dates. Mode: ${scanMode}`);
|
|
315
|
+
return { success: true, count: updatesCount, mode: scanMode };
|
|
301
316
|
};
|