bulltrackers-module 1.0.473 → 1.0.475

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.
@@ -168,12 +168,17 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
168
168
  return report;
169
169
  }
170
170
 
171
- async function executeDispatchTask(dateStr, pass, targetComputation, config, dependencies, computationManifest, previousCategory = null, dependencyResultHashes = {}) {
171
+ async function executeDispatchTask(dateStr, pass, targetComputation, config, dependencies, computationManifest, previousCategory = null, dependencyResultHashes = {}, metadata = {}) {
172
172
  const { logger } = dependencies;
173
173
  const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
174
174
  const calcManifest = manifestMap.get(normalizeName(targetComputation));
175
175
 
176
176
  if (!calcManifest) throw new Error(`Calc '${targetComputation}' not found.`);
177
+
178
+ // Merge runtime metadata (like targetCid) into the manifest
179
+ // This allows the Executor to access it via 'calcInstance.manifest'
180
+ Object.assign(calcManifest, metadata);
181
+
177
182
  calcManifest.dependencyResultHashes = dependencyResultHashes;
178
183
 
179
184
  const rootData = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
@@ -235,6 +235,14 @@ class StandardExecutor {
235
235
  let chunkFailures = 0;
236
236
 
237
237
  for (const [userId, todayPortfolio] of Object.entries(portfolioData)) {
238
+ // --- OPTIMIZATION: TARGET SPECIFIC USER ---
239
+ // If the request contains a targetCid, skip all other users immediately
240
+ if (metadata.targetCid && String(userId) !== String(metadata.targetCid)) {
241
+ if (stats) stats.skippedUsers++;
242
+ continue;
243
+ }
244
+ // ------------------------------------------
245
+
238
246
  const yesterdayPortfolio = yesterdayPortfolioData ? yesterdayPortfolioData[userId] : null;
239
247
  const todayHistory = historyData ? historyData[userId] : null;
240
248
 
@@ -93,7 +93,7 @@ function startMemoryHeartbeat(db, ledgerPath, workerId, computationName, traceId
93
93
  /**
94
94
  * STRICT IDEMPOTENCY GATE
95
95
  */
96
- async function checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId) {
96
+ async function checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId, onDemand = false) {
97
97
  const docRef = db.doc(ledgerPath);
98
98
 
99
99
  try {
@@ -103,25 +103,27 @@ async function checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerI
103
103
  if (doc.exists) {
104
104
  const data = doc.data();
105
105
 
106
- // [BUG] This blocks legitimate re-runs initiated by the Dispatcher
107
- /* if (['COMPLETED', 'FAILED', 'CRASH'].includes(data.status)) {
108
- return { shouldRun: false, reason: `Task already in terminal state: ${data.status}` };
109
- }
110
- */
111
-
112
- // [FIX] Only block if it's the EXACT SAME Dispatch ID (Duplicate Delivery)
113
- if (['COMPLETED', 'FAILED', 'CRASH'].includes(data.status)) {
114
- if (data.dispatchId === dispatchId) {
115
- return { shouldRun: false, reason: `Task already in terminal state: ${data.status}` };
106
+ // [ON-DEMAND OVERRIDE] If this is an on-demand request, force re-run even if completed
107
+ if (onDemand && ['COMPLETED', 'FAILED', 'CRASH'].includes(data.status)) {
108
+ // Force overwrite - on-demand requests should always run
109
+ // Log will be handled by caller
110
+ // Continue to claim lease below
111
+ } else {
112
+ // [FIX] Only block if it's the EXACT SAME Dispatch ID (Duplicate Delivery)
113
+ if (['COMPLETED', 'FAILED', 'CRASH'].includes(data.status)) {
114
+ if (data.dispatchId === dispatchId) {
115
+ return { shouldRun: false, reason: `Task already in terminal state: ${data.status}` };
116
+ }
117
+ // If dispatchId differs, we allow the overwrite (Re-Run).
118
+ // The Dispatcher is the authority; if it sent a message, we run it.
116
119
  }
117
- // If dispatchId differs, we allow the overwrite (Re-Run).
118
- // The Dispatcher is the authority; if it sent a message, we run it.
119
120
  }
120
121
 
121
122
  if (data.status === 'IN_PROGRESS' && data.dispatchId === dispatchId) {
122
123
  return { shouldRun: false, reason: 'Duplicate delivery: Task already IN_PROGRESS with same ID.' };
123
124
  }
124
- if (data.status === 'IN_PROGRESS') {
125
+ if (data.status === 'IN_PROGRESS' && !onDemand) {
126
+ // On-demand can break locks if needed, but regular requests should wait
125
127
  return { shouldRun: false, reason: 'Collision: Task currently IN_PROGRESS by another worker.' };
126
128
  }
127
129
  }
@@ -130,7 +132,8 @@ async function checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerI
130
132
  status: 'IN_PROGRESS',
131
133
  workerId: workerId,
132
134
  dispatchId: dispatchId || 'unknown',
133
- startedAt: new Date()
135
+ startedAt: new Date(),
136
+ onDemand: onDemand || false
134
137
  };
135
138
 
136
139
  t.set(docRef, lease, { merge: true });
@@ -151,8 +154,9 @@ async function handleComputationTask(message, config, dependencies) {
151
154
 
152
155
  if (!data || data.action !== 'RUN_COMPUTATION_DATE') return;
153
156
 
154
- const { date, pass, computation, previousCategory, triggerReason, dispatchId, dependencyResultHashes, resources, traceContext } = data;
157
+ const { date, pass, computation, previousCategory, triggerReason, dispatchId, dependencyResultHashes, resources, traceContext, metadata } = data;
155
158
  const resourceTier = resources || 'standard';
159
+ const onDemand = metadata?.onDemand === true || false; // Extract on-demand flag
156
160
  const ledgerPath = `computation_audit_ledger/${date}/passes/${pass}/tasks/${computation}`;
157
161
  const workerId = process.env.K_REVISION || os.hostname();
158
162
 
@@ -174,12 +178,16 @@ async function handleComputationTask(message, config, dependencies) {
174
178
  const runDeps = { ...dependencies, logger };
175
179
  const db = dependencies.db;
176
180
 
177
- // --- STEP 1: IDEMPOTENCY CHECK ---
178
- const gate = await checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId);
181
+ // --- STEP 1: IDEMPOTENCY CHECK (with on-demand override) ---
182
+ const gate = await checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId, onDemand);
179
183
  if (!gate.shouldRun) {
180
184
  logger.log('WARN', `[Worker] 🛑 Idempotency Gate: Skipping ${computation}. Reason: ${gate.reason}`);
181
185
  return;
182
186
  }
187
+
188
+ if (onDemand) {
189
+ logger.log('INFO', `[Worker] 🔄 On-demand request: Forcing re-run of ${computation} for ${date}`);
190
+ }
183
191
 
184
192
  logger.log('INFO', `[Worker] 📥 Task: ${computation} (${date}) [Tier: ${resourceTier}] [ID: ${dispatchId}]`);
185
193
 
@@ -195,7 +203,8 @@ async function handleComputationTask(message, config, dependencies) {
195
203
 
196
204
  const result = await executeDispatchTask(
197
205
  date, pass, computation, config, runDeps,
198
- manifest, previousCategory, dependencyResultHashes
206
+ manifest, previousCategory, dependencyResultHashes,
207
+ metadata // Pass metadata (including targetCid) to orchestrator
199
208
  );
200
209
 
201
210
  heartbeats.stop();
@@ -0,0 +1,151 @@
1
+ /**
2
+ * @fileoverview Helpers for on-demand computation requests
3
+ * Handles dependency chain resolution and ordered triggering
4
+ */
5
+
6
+ const { getManifest } = require('../topology/ManifestLoader');
7
+ const { normalizeName } = require('../utils/utils');
8
+
9
+ // Import calculations package (matching computation_worker.js pattern)
10
+ let calculationPackage;
11
+ try {
12
+ calculationPackage = require('aiden-shared-calculations-unified');
13
+ } catch (e) {
14
+ throw new Error(`Failed to load calculations package: ${e.message}`);
15
+ }
16
+ const calculations = calculationPackage.calculations;
17
+
18
+ /**
19
+ * Resolves all dependencies for a given computation and returns them grouped by pass
20
+ * @param {string} computationName - The target computation name
21
+ * @param {Array} manifest - The computation manifest
22
+ * @returns {Array<{pass: number, computations: Array<string>}>} Dependencies grouped by pass
23
+ */
24
+ function resolveDependencyChain(computationName, manifest) {
25
+ const manifestMap = new Map(manifest.map(c => [normalizeName(c.name), c]));
26
+ const normalizedTarget = normalizeName(computationName);
27
+
28
+ const targetCalc = manifestMap.get(normalizedTarget);
29
+ if (!targetCalc) {
30
+ throw new Error(`Computation ${computationName} not found in manifest`);
31
+ }
32
+
33
+ // Build adjacency list (dependencies map)
34
+ const adjacency = new Map();
35
+ for (const calc of manifest) {
36
+ const normName = normalizeName(calc.name);
37
+ adjacency.set(normName, (calc.dependencies || []).map(d => normalizeName(d)));
38
+ }
39
+
40
+ // Get all dependencies recursively
41
+ const required = new Set([normalizedTarget]);
42
+ const queue = [normalizedTarget];
43
+
44
+ while (queue.length > 0) {
45
+ const calcName = queue.shift();
46
+ const dependencies = adjacency.get(calcName) || [];
47
+ for (const dep of dependencies) {
48
+ if (!required.has(dep)) {
49
+ required.add(dep);
50
+ queue.push(dep);
51
+ }
52
+ }
53
+ }
54
+
55
+ // Group by pass
56
+ const byPass = new Map();
57
+ for (const calcName of required) {
58
+ const calc = manifestMap.get(calcName);
59
+ if (calc) {
60
+ const pass = calc.pass || 1;
61
+ if (!byPass.has(pass)) {
62
+ byPass.set(pass, []);
63
+ }
64
+ byPass.get(pass).push(calc.name); // Use original name, not normalized
65
+ }
66
+ }
67
+
68
+ // Convert to sorted array
69
+ const passes = Array.from(byPass.entries())
70
+ .sort((a, b) => a[0] - b[0])
71
+ .map(([pass, computations]) => ({ pass, computations }));
72
+
73
+ return passes;
74
+ }
75
+
76
+ /**
77
+ * Triggers computations for a given date, handling dependencies in order
78
+ * @param {string} targetComputation - The target computation to run
79
+ * @param {string} date - The date string (YYYY-MM-DD)
80
+ * @param {object} dependencies - Contains pubsub, logger, etc.
81
+ * @param {object} config - Computation system config
82
+ * @param {object} metadata - Additional metadata for the computation request
83
+ * @returns {Promise<Array>} Array of triggered computation messages
84
+ */
85
+ async function triggerComputationWithDependencies(targetComputation, date, dependencies, config, metadata = {}) {
86
+ const { pubsub, logger } = dependencies;
87
+ const computationTopic = config.computationTopicStandard || 'computation-tasks';
88
+ const topic = pubsub.topic(computationTopic);
89
+ const crypto = require('crypto');
90
+
91
+ // Get manifest to resolve dependencies
92
+ const manifest = getManifest(config.activeProductLines || [], calculations);
93
+
94
+ // Resolve dependency chain
95
+ const dependencyPasses = resolveDependencyChain(targetComputation, manifest);
96
+
97
+ logger.log('INFO', `[On-Demand] Resolved dependency chain for ${targetComputation}:`, {
98
+ totalPasses: dependencyPasses.length,
99
+ passes: dependencyPasses.map(p => `Pass ${p.pass}: ${p.computations.length} computations`)
100
+ });
101
+
102
+ const triggeredMessages = [];
103
+
104
+ // Trigger each pass in order
105
+ for (const passGroup of dependencyPasses) {
106
+ const { pass, computations } = passGroup;
107
+
108
+ // Trigger all computations in this pass
109
+ for (const computation of computations) {
110
+ const dispatchId = crypto.randomUUID();
111
+ const isTarget = normalizeName(computation) === normalizeName(targetComputation);
112
+
113
+ const computationMessage = {
114
+ action: 'RUN_COMPUTATION_DATE',
115
+ computation: computation,
116
+ date: date,
117
+ pass: String(pass),
118
+ dispatchId: dispatchId,
119
+ triggerReason: metadata.triggerReason || 'on_demand',
120
+ resources: metadata.resources || 'standard',
121
+ metadata: {
122
+ ...metadata,
123
+ onDemand: true,
124
+ isTargetComputation: isTarget,
125
+ targetCid: metadata.targetCid || null // Pass through targetCid for optimization
126
+ },
127
+ traceContext: {
128
+ traceId: crypto.randomBytes(16).toString('hex'),
129
+ spanId: crypto.randomBytes(8).toString('hex'),
130
+ sampled: true
131
+ }
132
+ };
133
+
134
+ await topic.publishMessage({
135
+ data: Buffer.from(JSON.stringify(computationMessage))
136
+ });
137
+
138
+ triggeredMessages.push(computationMessage);
139
+
140
+ logger.log('INFO', `[On-Demand] Triggered ${computation} (Pass ${pass})${isTarget ? ' [TARGET]' : ' [DEPENDENCY]'}`);
141
+ }
142
+ }
143
+
144
+ return triggeredMessages;
145
+ }
146
+
147
+ module.exports = {
148
+ resolveDependencyChain,
149
+ triggerComputationWithDependencies
150
+ };
151
+
@@ -1,5 +1,6 @@
1
- # Data Feeder Pipeline (V2.3 - Try/Retry Syntax Fixed)
2
- # Orchestrates data fetching with UTC-alignment, Test Mode, and Reliability.
1
+ # Data Feeder Pipeline (V3.1 - Syntax Fixed)
2
+ # Starts at 22:00 UTC via Cloud Scheduler.
3
+ # Fixes: Split assign/call steps and corrected assign syntax.
3
4
 
4
5
  main:
5
6
  params: [input]
@@ -8,17 +9,8 @@ main:
8
9
  assign:
9
10
  - project: '${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}'
10
11
  - location: "europe-west1"
11
- - market_date: '${text.split(time.format(sys.now()), "T")[0]}'
12
- # Define a central retry policy to reuse across all HTTP calls
13
- - default_retry:
14
- predicate: ${http.default_retry_predicate}
15
- max_retries: 5
16
- backoff:
17
- initial_delay: 2
18
- max_delay: 60
19
- multiplier: 2
20
-
21
- # --- TEST MODE / SELECTIVE EXECUTION ---
12
+
13
+ # --- TEST MODE ---
22
14
  - check_test_mode:
23
15
  switch:
24
16
  - condition: '${input != null and "target_step" in input}'
@@ -26,130 +18,164 @@ main:
26
18
  - route_test:
27
19
  switch:
28
20
  - condition: '${input.target_step == "market"}'
29
- next: run_market_close_tasks
30
- - condition: '${input.target_step == "rankings"}'
31
- next: run_rankings_fetch
21
+ next: phase_2200_price
22
+ - condition: '${input.target_step == "midnight"}'
23
+ next: phase_0000_rankings
32
24
  - condition: '${input.target_step == "social"}'
33
- next: run_social_midnight
34
- - condition: '${input.target_step == "global"}'
35
- next: run_global_indexer
36
-
37
- # --- PHASE 1: MARKET CLOSE (22:00 UTC) ---
38
- - run_market_close_tasks:
39
- parallel:
40
- branches:
41
- - price_fetch:
42
- steps:
43
- - call_price_fetcher:
44
- try:
45
- call: http.post
46
- args:
47
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/price-fetcher"}'
48
- auth: { type: OIDC }
49
- timeout: 300
50
- retry: ${default_retry} # Fixed: Moved retry to a Try Step
51
- - insights_fetch:
52
- steps:
53
- - call_insights_fetcher:
54
- try:
55
- call: http.post
56
- args:
57
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/insights-fetcher"}'
58
- auth: { type: OIDC }
59
- timeout: 300
60
- retry: ${default_retry} # Fixed: Moved retry to a Try Step
61
-
62
- - index_market_data:
25
+ next: social_loop_start
26
+
27
+ # ==========================================
28
+ # PHASE 1: MARKET CLOSE (Starts 22:00 UTC)
29
+ # ==========================================
30
+
31
+ - phase_2200_price:
63
32
  try:
64
33
  call: http.post
65
34
  args:
66
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
67
- body:
68
- targetDate: '${market_date}'
35
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/price-fetcher"}'
69
36
  auth: { type: OIDC }
70
37
  timeout: 300
71
- retry: ${default_retry}
38
+ except:
39
+ as: e
40
+ steps:
41
+ - log_price_error:
42
+ call: sys.log
43
+ args: { severity: "WARNING", text: "Price fetch timed out/failed. Proceeding." }
44
+
45
+ - wait_10_after_price:
46
+ call: sys.sleep
47
+ args: { seconds: 600 } # 10 Minutes
72
48
 
73
- # --- PHASE 2: ALIGN TO MIDNIGHT ---
74
- - wait_for_midnight:
49
+ # FIX 1: Split assign and call
50
+ - prepare_index_price:
75
51
  assign:
76
- - now_sec: '${int(sys.now())}'
77
- - day_sec: 86400
78
- - sleep_midnight: '${day_sec - (now_sec % day_sec)}'
79
- - do_midnight_sleep:
80
- call: sys.sleep
52
+ - today: '${text.split(time.format(sys.now()), "T")[0]}'
53
+ - index_today_after_price:
54
+ call: http.post
81
55
  args:
82
- seconds: '${sleep_midnight}'
56
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
57
+ body: { targetDate: '${today}' }
58
+ auth: { type: OIDC }
83
59
 
84
- # --- PHASE 3: RANKINGS & INITIAL SOCIAL (00:00 UTC) ---
85
- - run_rankings_fetch:
60
+ - wait_10_before_insights:
61
+ call: sys.sleep
62
+ args: { seconds: 600 }
63
+
64
+ - phase_2200_insights:
86
65
  try:
87
66
  call: http.post
88
67
  args:
89
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/fetch-popular-investors"}'
68
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/insights-fetcher"}'
90
69
  auth: { type: OIDC }
91
70
  timeout: 300
92
- retry: ${default_retry}
93
71
  except:
94
72
  as: e
95
73
  steps:
96
- - log_rankings_error:
74
+ - log_insights_error:
97
75
  call: sys.log
98
- args:
99
- severity: "ERROR"
100
- text: '${"Rankings Fetch Failed: " + json.encode(e)}'
76
+ args: { severity: "WARNING", text: "Insights fetch timed out/failed. Proceeding." }
101
77
 
102
- - run_social_midnight:
103
- try:
104
- call: http.post
105
- args:
106
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
107
- auth: { type: OIDC }
108
- timeout: 300
109
- retry: ${default_retry}
78
+ - wait_10_after_insights:
79
+ call: sys.sleep
80
+ args: { seconds: 600 }
110
81
 
111
- - prepare_midnight_index:
82
+ # FIX 2: Split assign and call
83
+ - prepare_index_insights:
112
84
  assign:
113
- - current_date: '${text.split(time.format(sys.now()), "T")[0]}'
114
- - index_midnight_data:
85
+ - today: '${text.split(time.format(sys.now()), "T")[0]}'
86
+ - index_today_after_insights:
87
+ call: http.post
88
+ args:
89
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
90
+ body: { targetDate: '${today}' }
91
+ auth: { type: OIDC }
92
+
93
+ # ==========================================
94
+ # PHASE 2: WAIT FOR MIDNIGHT
95
+ # ==========================================
96
+
97
+ - align_to_midnight:
98
+ assign:
99
+ - now_sec: '${int(sys.now())}'
100
+ - day_sec: 86400
101
+ - sleep_midnight: '${day_sec - (now_sec % day_sec)}'
102
+ - wait_for_midnight:
103
+ call: sys.sleep
104
+ args: { seconds: '${sleep_midnight}' }
105
+
106
+ # ==========================================
107
+ # PHASE 3: MIDNIGHT TASKS (00:00 UTC)
108
+ # ==========================================
109
+
110
+ - phase_0000_rankings:
115
111
  try:
116
112
  call: http.post
117
113
  args:
118
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
119
- body:
120
- targetDate: '${current_date}'
114
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/fetch-popular-investors"}'
121
115
  auth: { type: OIDC }
122
116
  timeout: 300
123
- retry: ${default_retry}
117
+ except:
118
+ as: e
119
+ steps:
120
+ - log_ranking_error:
121
+ call: sys.log
122
+ args: { severity: "WARNING", text: "Rankings failed. Proceeding to Social (risky)." }
124
123
 
125
- - run_global_indexer:
124
+ - wait_10_after_rankings:
125
+ call: sys.sleep
126
+ args: { seconds: 600 }
127
+
128
+ # FIX 3: Split assign and call
129
+ - prepare_index_rankings:
130
+ assign:
131
+ - today: '${text.split(time.format(sys.now()), "T")[0]}'
132
+ - index_today_after_rankings:
133
+ call: http.post
134
+ args:
135
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
136
+ body: { targetDate: '${today}' }
137
+ auth: { type: OIDC }
138
+
139
+ - phase_0000_social:
126
140
  try:
127
141
  call: http.post
128
142
  args:
129
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
143
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
130
144
  auth: { type: OIDC }
131
145
  timeout: 300
132
- retry: ${default_retry}
146
+ except:
147
+ as: e
148
+ steps:
149
+ - log_social_error:
150
+ call: sys.log
151
+ args: { severity: "WARNING", text: "Social failed. Proceeding." }
152
+
153
+ - wait_10_after_social:
154
+ call: sys.sleep
155
+ args: { seconds: 600 }
156
+
157
+ - global_index_midnight:
158
+ call: http.post
159
+ args:
160
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
161
+ # No targetDate = Global Run
162
+ auth: { type: OIDC }
163
+
164
+ # ==========================================
165
+ # PHASE 4: SOCIAL LOOP (Every 3 Hours)
166
+ # ==========================================
133
167
 
134
- # --- PHASE 4: RECURRING SOCIAL FETCH (UTC Aligned 3hr) ---
135
168
  - init_social_loop:
136
169
  assign:
137
170
  - i: 0
138
171
 
139
- - social_loop:
172
+ - social_loop_start:
140
173
  switch:
141
- - condition: ${i < 7}
174
+ - condition: ${i < 7} # Covers the remainder of the 24h cycle
142
175
  steps:
143
- - calculate_next_window:
144
- assign:
145
- - now_sec_loop: '${int(sys.now())}'
146
- - window_size: 10800
147
- - sleep_loop: '${window_size - (now_sec_loop % window_size)}'
148
-
149
- - wait_for_3hr_window:
176
+ - wait_3_hours:
150
177
  call: sys.sleep
151
- args:
152
- seconds: '${sleep_loop}'
178
+ args: { seconds: 10800 }
153
179
 
154
180
  - run_social_recurring:
155
181
  try:
@@ -158,27 +184,34 @@ main:
158
184
  url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
159
185
  auth: { type: OIDC }
160
186
  timeout: 300
161
- retry: ${default_retry}
187
+ except:
188
+ as: e
189
+ steps:
190
+ - log_loop_social_warn:
191
+ call: sys.log
192
+ args: { severity: "WARNING", text: "Loop Social timed out. Proceeding." }
162
193
 
163
- - prepare_recurring_index:
194
+ - wait_10_in_loop:
195
+ call: sys.sleep
196
+ args: { seconds: 600 }
197
+
198
+ # FIX 4: Split assign and call
199
+ - prepare_index_loop:
164
200
  assign:
165
- - cur_date_rec: '${text.split(time.format(sys.now()), "T")[0]}'
166
- - index_recurring:
167
- try:
168
- call: http.post
169
- args:
170
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
171
- body:
172
- targetDate: '${cur_date_rec}'
173
- auth: { type: OIDC }
174
- timeout: 300
175
- retry: ${default_retry}
201
+ - today: '${text.split(time.format(sys.now()), "T")[0]}'
202
+ - index_today_in_loop:
203
+ call: http.post
204
+ args:
205
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
206
+ body: { targetDate: '${today}' }
207
+ auth: { type: OIDC }
176
208
 
177
- - increment:
178
- assign:
179
- - i: ${i + 1}
209
+ # FIX 5: Correct assign syntax (must be a list)
210
+ - increment_loop:
211
+ assign:
212
+ - i: '${i + 1}'
180
213
  - next_iteration:
181
- next: social_loop
214
+ next: social_loop_start
182
215
 
183
216
  - finish:
184
- return: "Daily Data Pipeline Completed"
217
+ return: "Complete 24h Cycle Finished"
@@ -94,6 +94,30 @@ exports.fetchAndStorePrices = async (config, dependencies) => {
94
94
 
95
95
  logger.log('INFO', `[PriceFetcherHelpers] Wrote price date tracking document for ${today} with ${priceDatesArray.length} dates (from August 2025 onwards)`);
96
96
 
97
+ // Update root data indexer for today's date after price data is stored
98
+ try {
99
+ const { runRootDataIndexer } = require('../../root-data-indexer/index');
100
+ const rootDataIndexerConfig = config.rootDataIndexer || {
101
+ availabilityCollection: 'system_root_data_index',
102
+ earliestDate: '2025-08-01',
103
+ collections: {
104
+ prices: priceCollectionName
105
+ }
106
+ };
107
+
108
+ const indexerConfig = {
109
+ ...rootDataIndexerConfig,
110
+ targetDate: today // Index only today's date for speed
111
+ };
112
+
113
+ logger.log('INFO', `[PriceFetcherHelpers] Triggering root data indexer for date ${today} after price data storage...`);
114
+ await runRootDataIndexer(indexerConfig, dependencies);
115
+ logger.log('INFO', `[PriceFetcherHelpers] Root data indexer completed for date ${today}`);
116
+ } catch (indexerError) {
117
+ logger.log('ERROR', `[PriceFetcherHelpers] Failed to run root data indexer for ${today}`, indexerError);
118
+ // Continue - price data is stored, indexer failure is non-critical
119
+ }
120
+
97
121
  const successMessage = `Successfully processed and saved daily prices for ${results.length} instruments to ${batchPromises.length} shards.`;
98
122
  logger.log('SUCCESS', `[PriceFetcherHelpers] ${successMessage}`);
99
123
  return { success: true, message: successMessage, instrumentsProcessed: results.length };