bulltrackers-module 1.0.731 → 1.0.733

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.
Files changed (52) hide show
  1. package/functions/orchestrator/index.js +19 -17
  2. package/index.js +8 -29
  3. package/package.json +6 -5
  4. package/functions/computation-system/WorkflowOrchestrator.js +0 -213
  5. package/functions/computation-system/config/monitoring_config.js +0 -31
  6. package/functions/computation-system/config/validation_overrides.js +0 -10
  7. package/functions/computation-system/context/ContextFactory.js +0 -132
  8. package/functions/computation-system/context/ManifestBuilder.js +0 -379
  9. package/functions/computation-system/data/AvailabilityChecker.js +0 -236
  10. package/functions/computation-system/data/CachedDataLoader.js +0 -325
  11. package/functions/computation-system/data/DependencyFetcher.js +0 -455
  12. package/functions/computation-system/executors/MetaExecutor.js +0 -279
  13. package/functions/computation-system/executors/PriceBatchExecutor.js +0 -108
  14. package/functions/computation-system/executors/StandardExecutor.js +0 -465
  15. package/functions/computation-system/helpers/computation_dispatcher.js +0 -750
  16. package/functions/computation-system/helpers/computation_worker.js +0 -375
  17. package/functions/computation-system/helpers/monitor.js +0 -64
  18. package/functions/computation-system/helpers/on_demand_helpers.js +0 -154
  19. package/functions/computation-system/layers/extractors.js +0 -1097
  20. package/functions/computation-system/layers/index.js +0 -40
  21. package/functions/computation-system/layers/mathematics.js +0 -522
  22. package/functions/computation-system/layers/profiling.js +0 -537
  23. package/functions/computation-system/layers/validators.js +0 -170
  24. package/functions/computation-system/legacy/AvailabilityCheckerOld.js +0 -388
  25. package/functions/computation-system/legacy/CachedDataLoaderOld.js +0 -357
  26. package/functions/computation-system/legacy/DependencyFetcherOld.js +0 -478
  27. package/functions/computation-system/legacy/MetaExecutorold.js +0 -364
  28. package/functions/computation-system/legacy/StandardExecutorold.js +0 -476
  29. package/functions/computation-system/legacy/computation_dispatcherold.js +0 -944
  30. package/functions/computation-system/logger/logger.js +0 -297
  31. package/functions/computation-system/persistence/ContractValidator.js +0 -81
  32. package/functions/computation-system/persistence/FirestoreUtils.js +0 -56
  33. package/functions/computation-system/persistence/ResultCommitter.js +0 -283
  34. package/functions/computation-system/persistence/ResultsValidator.js +0 -130
  35. package/functions/computation-system/persistence/RunRecorder.js +0 -142
  36. package/functions/computation-system/persistence/StatusRepository.js +0 -52
  37. package/functions/computation-system/reporter_epoch.js +0 -6
  38. package/functions/computation-system/scripts/UpdateContracts.js +0 -128
  39. package/functions/computation-system/services/SnapshotService.js +0 -148
  40. package/functions/computation-system/simulation/Fabricator.js +0 -285
  41. package/functions/computation-system/simulation/SeededRandom.js +0 -41
  42. package/functions/computation-system/simulation/SimRunner.js +0 -51
  43. package/functions/computation-system/system_epoch.js +0 -2
  44. package/functions/computation-system/tools/BuildReporter.js +0 -531
  45. package/functions/computation-system/tools/ContractDiscoverer.js +0 -144
  46. package/functions/computation-system/tools/DeploymentValidator.js +0 -536
  47. package/functions/computation-system/tools/FinalSweepReporter.js +0 -322
  48. package/functions/computation-system/topology/HashManager.js +0 -55
  49. package/functions/computation-system/topology/ManifestLoader.js +0 -47
  50. package/functions/computation-system/utils/data_loader.js +0 -597
  51. package/functions/computation-system/utils/schema_capture.js +0 -121
  52. package/functions/computation-system/utils/utils.js +0 -188
@@ -1,375 +0,0 @@
1
- /**
2
- * FILENAME: computation-system/helpers/computation_worker.js
3
- * UPDATED: Implements Google Cloud Monitoring Heartbeats.
4
- * UPDATED: Implements Structured Logging with Trace Context.
5
- * FIXED: 'manifest' scope issue (declared outside try/catch).
6
- */
7
-
8
- const { executeDispatchTask } = require('../WorkflowOrchestrator.js');
9
- const { getManifest } = require('../topology/ManifestLoader');
10
- const { StructuredLogger } = require('../logger/logger');
11
- const { recordRunAttempt } = require('../persistence/RunRecorder');
12
- const { normalizeName } = require('../utils/utils');
13
- const os = require('os');
14
- const monitoring = require('@google-cloud/monitoring'); // [NEW]
15
- const monConfig = require('../config/monitoring_config'); // [NEW]
16
-
17
- // Initialize the Google Monitoring Client once
18
- const metricClient = new monitoring.MetricServiceClient();
19
-
20
- let calculationPackage;
21
- try { calculationPackage = require('aiden-shared-calculations-unified'); } catch (e) { throw e; }
22
- const calculations = calculationPackage.calculations;
23
-
24
- const MAX_RETRIES = 3;
25
- const STALE_LOCK_THRESHOLD_MS = 1000 * 60 * 15; // 15 minutes
26
-
27
- // =============================================================================
28
- // HELPER: Firestore Timestamp Conversion
29
- // =============================================================================
30
- /**
31
- * Converts a Firestore Timestamp or Date to milliseconds.
32
- * Firestore stores Date objects as Timestamp objects, which have a .toDate() method.
33
- * This function handles both cases correctly.
34
- * @param {any} field - Firestore Timestamp, Date object, or string
35
- * @returns {number} Milliseconds since epoch, or 0 if invalid
36
- */
37
- function getMillis(field) {
38
- if (!field) return 0;
39
- // Handle Firestore Timestamp (has .toDate() method)
40
- if (field.toDate && typeof field.toDate === 'function') {
41
- return field.toDate().getTime();
42
- }
43
- // Handle standard Date object or string
44
- const date = new Date(field);
45
- return isNaN(date.getTime()) ? 0 : date.getTime();
46
- }
47
-
48
- // [NEW] Helper to push metric to Google Cloud
49
- async function pushMetric(type, value, labels) {
50
- if (!monConfig.enabled) return;
51
- try {
52
- const dataPoint = {
53
- interval: { endTime: { seconds: Date.now() / 1000 } },
54
- value: { int64Value: value }, // Assumes memory in MB
55
- };
56
-
57
- const timeSeriesData = {
58
- name: metricClient.projectPath(monConfig.project),
59
- timeSeries: [{
60
- metric: {
61
- type: type,
62
- labels: labels,
63
- },
64
- resource: {
65
- type: 'global',
66
- labels: { project_id: monConfig.project },
67
- },
68
- points: [dataPoint],
69
- }],
70
- };
71
-
72
- // Fire and forget (don't await to avoid blocking compute)
73
- metricClient.createTimeSeries(timeSeriesData).catch(err => console.error('[Monitoring] Push failed', err.message));
74
- } catch (e) { /* Ignore setup errors */ }
75
- }
76
-
77
- function startMemoryHeartbeat(db, ledgerPath, workerId, computationName, traceId, intervalMs = 5000) {
78
- let peakRss = 0;
79
-
80
- // Firestore Heartbeat (Legacy/State)
81
- const firestoreTimer = setInterval(async () => {
82
- const rssMB = Math.round(process.memoryUsage().rss / 1024 / 1024);
83
- if (rssMB > peakRss) peakRss = rssMB;
84
- await db.doc(ledgerPath).update({
85
- 'telemetry.lastMemoryMB': rssMB,
86
- 'telemetry.lastHeartbeat': new Date()
87
- }).catch(() => {});
88
- }, 2000); // 2s for DB
89
-
90
- // Google Monitoring Heartbeat (New/Out-of-Band)
91
- const monitoringTimer = setInterval(async () => {
92
- const rssMB = Math.round(process.memoryUsage().rss / 1024 / 1024);
93
-
94
- // Push Memory Metric
95
- pushMetric(monConfig.metrics.memory, rssMB, {
96
- worker_id: workerId,
97
- computation: computationName,
98
- // Including trace_id in metric labels allows strict correlation
99
- // BUT can cause high cardinality. Use with caution or only for debugging.
100
- // Per user request, we include it to "attach eyeball".
101
- trace_id: traceId || 'unknown'
102
- });
103
-
104
- }, intervalMs); // 5s for API
105
-
106
- firestoreTimer.unref();
107
- monitoringTimer.unref();
108
-
109
- return {
110
- stop: () => { clearInterval(firestoreTimer); clearInterval(monitoringTimer); },
111
- getPeak: () => peakRss
112
- };
113
- }
114
-
115
- /**
116
- * STRICT IDEMPOTENCY GATE
117
- */
118
- async function checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId, onDemand = false) {
119
- const docRef = db.doc(ledgerPath);
120
-
121
- try {
122
- return await db.runTransaction(async (t) => {
123
- const doc = await t.get(docRef);
124
-
125
- if (doc.exists) {
126
- const data = doc.data();
127
-
128
- // [ON-DEMAND OVERRIDE] If this is an on-demand request, force re-run even if completed
129
- if (onDemand && ['COMPLETED', 'FAILED', 'CRASH'].includes(data.status)) {
130
- // Force overwrite - on-demand requests should always run
131
- // Log will be handled by caller
132
- // Continue to claim lease below
133
- } else {
134
- // [FIX] Only block if it's the EXACT SAME Dispatch ID (Duplicate Delivery)
135
- if (['COMPLETED', 'FAILED', 'CRASH'].includes(data.status)) {
136
- if (data.dispatchId === dispatchId) {
137
- return { shouldRun: false, reason: `Task already in terminal state: ${data.status}` };
138
- }
139
- // If dispatchId differs, we allow the overwrite (Re-Run).
140
- // The Dispatcher is the authority; if it sent a message, we run it.
141
- }
142
- }
143
-
144
- if (data.status === 'IN_PROGRESS' && data.dispatchId === dispatchId) {
145
- return { shouldRun: false, reason: 'Duplicate delivery: Task already IN_PROGRESS with same ID.' };
146
- }
147
- if (data.status === 'IN_PROGRESS' && !onDemand) {
148
- // CRITICAL FIX: Check if the lock is stale before blocking
149
- // If the task has been IN_PROGRESS for more than 15 minutes, break the lock
150
- const lastActivityTime = getMillis(data.telemetry?.lastHeartbeat) || getMillis(data.startedAt);
151
- const lockAge = Date.now() - lastActivityTime;
152
-
153
- if (lockAge > STALE_LOCK_THRESHOLD_MS) {
154
- // Stale lock detected - break it and allow this worker to claim it
155
- // The transaction will overwrite the stale lock below
156
- // Note: We can't log here because we're in a transaction, but the caller will log
157
- // Continue to claim lease below
158
- } else {
159
- // Valid active lock - wait for it to complete
160
- return { shouldRun: false, reason: 'Collision: Task currently IN_PROGRESS by another worker.' };
161
- }
162
- }
163
- }
164
-
165
- const lease = {
166
- status: 'IN_PROGRESS',
167
- workerId: workerId,
168
- dispatchId: dispatchId || 'unknown',
169
- startedAt: new Date(),
170
- onDemand: onDemand || false
171
- };
172
-
173
- t.set(docRef, lease, { merge: true });
174
- return { shouldRun: true, leaseData: lease };
175
- });
176
- } catch (e) {
177
- return { shouldRun: false, reason: `Transaction Error: ${e.message}` };
178
- }
179
- }
180
-
181
- async function handleComputationTask(message, config, dependencies) {
182
- // 1. Parse Message
183
- let data;
184
- try {
185
- const raw = message.data?.message?.data || message.data || message.json;
186
- data = (typeof raw === 'string') ? JSON.parse(Buffer.from(raw, 'base64').toString()) : raw;
187
- } catch (e) { return; }
188
-
189
- if (!data || data.action !== 'RUN_COMPUTATION_DATE') return;
190
-
191
- const { date, pass, computation, previousCategory, triggerReason, dispatchId, dependencyResultHashes, resources, traceContext, metadata } = data;
192
- const resourceTier = resources || 'standard';
193
- const onDemand = metadata?.onDemand === true || false; // Extract on-demand flag
194
- const ledgerPath = `computation_audit_ledger/${date}/passes/${pass}/tasks/${computation}`;
195
- const workerId = process.env.K_REVISION || os.hostname();
196
-
197
- // 2. Initialize Trace-Aware Logger (The "Eyeball")
198
- const globalMetadata = {};
199
- if (traceContext && monConfig.enabled) {
200
- globalMetadata['logging.googleapis.com/trace'] = `projects/${monConfig.project}/traces/${traceContext.traceId}`;
201
- globalMetadata['logging.googleapis.com/spanId'] = traceContext.spanId;
202
- globalMetadata['logging.googleapis.com/trace_sampled'] = traceContext.sampled;
203
- }
204
-
205
- const logger = new StructuredLogger({
206
- minLevel: config.minLevel || 'INFO',
207
- enableStructured: true,
208
- globalMetadata,
209
- ...config
210
- });
211
-
212
- const runDeps = { ...dependencies, logger };
213
- const db = dependencies.db;
214
-
215
- // --- STEP 1: IDEMPOTENCY CHECK (with on-demand override) ---
216
- // Check for stale locks before the transaction (for logging purposes)
217
- const preCheckSnap = await db.doc(ledgerPath).get();
218
- let wasStaleLock = false;
219
- if (preCheckSnap.exists) {
220
- const preCheckData = preCheckSnap.data();
221
- if (preCheckData.status === 'IN_PROGRESS' && preCheckData.workerId !== workerId) {
222
- const lastActivityTime = getMillis(preCheckData.telemetry?.lastHeartbeat) || getMillis(preCheckData.startedAt);
223
- const lockAge = Date.now() - lastActivityTime;
224
- if (lockAge > STALE_LOCK_THRESHOLD_MS) {
225
- wasStaleLock = true;
226
- logger.log('WARN', `[Worker] 🧟 Breaking stale lock for ${computation}. Previous worker ${preCheckData.workerId} was inactive for ${Math.round(lockAge / 1000 / 60)} minutes.`);
227
- }
228
- }
229
- }
230
-
231
- const gate = await checkIdempotencyAndClaimLease(db, ledgerPath, dispatchId, workerId, onDemand);
232
- if (!gate.shouldRun) {
233
- logger.log('WARN', `[Worker] 🛑 Idempotency Gate: Skipping ${computation}. Reason: ${gate.reason}`);
234
- return;
235
- }
236
-
237
- if (wasStaleLock) {
238
- logger.log('INFO', `[Worker] ✅ Successfully claimed lease for ${computation} after breaking stale lock.`);
239
- }
240
-
241
- if (onDemand) {
242
- logger.log('INFO', `[Worker] 🔄 On-demand request: Forcing re-run of ${computation} for ${date}`);
243
- }
244
-
245
- logger.log('INFO', `[Worker] 📥 Task: ${computation} (${date}) [Tier: ${resourceTier}] [ID: ${dispatchId}]`);
246
-
247
- // --- STEP 2: START DUAL HEARTBEATS ---
248
- const heartbeats = startMemoryHeartbeat(db, ledgerPath, workerId, computation, traceContext?.traceId);
249
-
250
- // [FIX] Declare manifest here so it is accessible in both try and catch blocks
251
- let manifest;
252
- const taskStartTime = Date.now(); // Declare outside try/catch for access in error handler
253
-
254
- try {
255
- manifest = getManifest(config.activeProductLines || [], calculations, runDeps);
256
- const startTime = taskStartTime;
257
-
258
- const result = await executeDispatchTask(
259
- date, pass, computation, config, runDeps,
260
- manifest, previousCategory, dependencyResultHashes,
261
- metadata // Pass metadata (including targetCid) to orchestrator
262
- );
263
-
264
- heartbeats.stop();
265
- const failureReport = result?.updates?.failureReport || [];
266
- const successUpdates = result?.updates?.successUpdates || {};
267
-
268
- if (failureReport.length > 0) {
269
- const reportedError = failureReport[0].error;
270
- const errorObj = new Error(reportedError.message);
271
- errorObj.stage = reportedError.stage;
272
- throw errorObj;
273
- }
274
-
275
- const calcUpdate = successUpdates[normalizeName(computation)] || {};
276
- const metrics = {
277
- durationMs: Date.now() - startTime,
278
- peakMemoryMB: heartbeats.getPeak(),
279
- io: calcUpdate.metrics?.io,
280
- storage: calcUpdate.metrics?.storage,
281
- execution: calcUpdate.metrics?.execution,
282
- validation: calcUpdate.metrics?.validation,
283
- composition: calcUpdate.composition
284
- };
285
-
286
- await db.doc(ledgerPath).update({ status: 'COMPLETED', completedAt: new Date() });
287
- await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', null, metrics, triggerReason, resourceTier);
288
-
289
- const { notifyComputationComplete, getComputationDisplayName } = require('../../api-v2/helpers/notification_helpers.js');
290
- // Send notification if this was an on-demand computation
291
- if (metadata?.onDemand && metadata?.requestId && metadata?.requestingUserCid) {
292
- try {
293
- await notifyComputationComplete(
294
- dependencies.db,
295
- dependencies.logger,
296
- metadata.requestingUserCid,
297
- metadata.requestId,
298
- computation,
299
- getComputationDisplayName(computation),
300
- true,
301
- null,
302
- {
303
- collectionRegistry: dependencies.collectionRegistry,
304
- config: config,
305
- notificationType: 'userActionCompletions'
306
- }
307
- );
308
- } catch (notifError) {
309
- // Non-critical, log and continue
310
- if (dependencies.logger) {
311
- dependencies.logger.log('WARN', `[Worker] Failed to send completion notification for ${computation}`, notifError);
312
- }
313
- }
314
- }
315
-
316
- } catch (err) {
317
- heartbeats.stop();
318
-
319
- const isDeterministic = ['SHARDING_LIMIT_EXCEEDED', 'QUALITY_CIRCUIT_BREAKER', 'SEMANTIC_GATE'].includes(err.stage);
320
-
321
- // [FIX] Now we can safely access 'manifest' to find the current hash
322
- let currentHash = 'unknown';
323
- if (typeof manifest !== 'undefined' && Array.isArray(manifest)) {
324
- const item = manifest.find(c => normalizeName(c.name) === normalizeName(computation));
325
- if (item) currentHash = item.hash;
326
- }
327
-
328
- if (isDeterministic || (message.deliveryAttempt || 1) >= MAX_RETRIES) {
329
- const errorPayload = { message: err.message, stage: err.stage || 'FATAL' };
330
-
331
- // [UPDATED] We now save 'hash' and 'resourceTier' to the ledger on failure
332
- await db.doc(ledgerPath).set({
333
- status: 'FAILED',
334
- error: errorPayload,
335
- failedAt: new Date(),
336
- hash: currentHash, // <--- CRITICAL FOR DISPATCHER DEAD-LETTER CHECK
337
- resourceTier: resourceTier
338
- }, { merge: true });
339
-
340
- await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', { message: err.message, stage: err.stage || 'FATAL' }, { durationMs: Date.now() - taskStartTime, peakMemoryMB: heartbeats.getPeak() }, triggerReason, resourceTier);
341
-
342
- // Send error notification if this was an on-demand computation
343
- if (metadata?.onDemand && metadata?.requestId && metadata?.requestingUserCid) {
344
- try {
345
- await notifyComputationComplete(
346
- db,
347
- dependencies.logger,
348
- metadata.requestingUserCid,
349
- metadata.requestId,
350
- computation,
351
- getComputationDisplayName(computation),
352
- false,
353
- err.message,
354
- {
355
- collectionRegistry: dependencies.collectionRegistry,
356
- config: config,
357
- notificationType: 'userActionCompletions'
358
- }
359
- );
360
- } catch (notifError) {
361
- // Non-critical, log and continue
362
- if (dependencies.logger) {
363
- dependencies.logger.log('WARN', `[Worker] Failed to send error notification for ${computation}`, notifError);
364
- }
365
- }
366
- }
367
-
368
- return;
369
- }
370
-
371
- throw err;
372
- }
373
- }
374
-
375
- module.exports = { handleComputationTask };
@@ -1,64 +0,0 @@
1
- /**
2
- * @fileoverview Monitor helper for Cloud Workflows.
3
- * Checks the state of the Audit Ledger to determine if a pass is complete.
4
- * This function is stateless and receives dependencies via injection.
5
- * THIS FILE IS NOW REDUNDANT, LOGIC HAS MOVED TO THE DISPATCHER AND WORKFLOW
6
- */
7
-
8
- /**
9
- * Checks the status of a specific computation pass.
10
- * @param {object} req - Express request object (query: date, pass).
11
- * @param {object} res - Express response object.
12
- * @param {object} dependencies - Contains db (Firestore), logger.
13
- */
14
- async function checkPassStatus(req, res, dependencies) {
15
- const { db, logger } = dependencies;
16
- const { date, pass } = req.query;
17
-
18
- if (!date || !pass) {
19
- return res.status(400).json({ error: "Missing 'date' or 'pass' query parameters." });
20
- }
21
-
22
- const ledgerPath = `computation_audit_ledger/${date}/passes/${pass}/tasks`;
23
- logger.log('INFO', `[Monitor] Checking status for ${date} Pass ${pass} at ${ledgerPath}`);
24
-
25
- try {
26
- const tasksRef = db.collection(ledgerPath);
27
-
28
- // 1. Check for Active Tasks (Blocking)
29
- // If anything is PENDING or IN_PROGRESS, the system is still working.
30
- const runningSnap = await tasksRef.where('status', 'in', ['PENDING', 'IN_PROGRESS']).get();
31
-
32
- if (!runningSnap.empty) {
33
- logger.log('INFO', `[Monitor] Pass ${pass} is RUNNING. Active tasks: ${runningSnap.size}`);
34
- return res.status(200).json({
35
- state: 'RUNNING',
36
- activeCount: runningSnap.size
37
- });
38
- }
39
-
40
- // 2. Check for Failures (Retry Condition)
41
- // If nothing is running, we check if anything ended in FAILED state.
42
- // We consider these "retryable" by re-triggering the dispatcher.
43
- const failedSnap = await tasksRef.where('status', '==', 'FAILED').get();
44
-
45
- if (!failedSnap.empty) {
46
- logger.log('WARN', `[Monitor] Pass ${pass} finished with FAILURES. Count: ${failedSnap.size}`);
47
- return res.status(200).json({
48
- state: 'HAS_FAILURES',
49
- failureCount: failedSnap.size
50
- });
51
- }
52
-
53
- // 3. Clean Success
54
- // No running tasks, no failed tasks.
55
- logger.log('INFO', `[Monitor] Pass ${pass} COMPLETED successfully.`);
56
- return res.status(200).json({ state: 'SUCCESS' });
57
-
58
- } catch (error) {
59
- logger.log('ERROR', `[Monitor] Failed to check status: ${error.message}`);
60
- return res.status(500).json({ error: error.message });
61
- }
62
- }
63
-
64
- module.exports = { checkPassStatus };
@@ -1,154 +0,0 @@
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
- // Use on-demand topic for on-demand computation requests
88
- const computationTopic = config.computationTopicOnDemand ||
89
- (config.getComputationTopic && config.getComputationTopic(true)) ||
90
- 'computation-tasks-ondemand';
91
- const topic = pubsub.topic(computationTopic);
92
- const crypto = require('crypto');
93
-
94
- // Get manifest to resolve dependencies
95
- const manifest = getManifest(config.activeProductLines || [], calculations);
96
-
97
- // Resolve dependency chain
98
- const dependencyPasses = resolveDependencyChain(targetComputation, manifest);
99
-
100
- logger.log('INFO', `[On-Demand] Resolved dependency chain for ${targetComputation}:`, {
101
- totalPasses: dependencyPasses.length,
102
- passes: dependencyPasses.map(p => `Pass ${p.pass}: ${p.computations.length} computations`)
103
- });
104
-
105
- const triggeredMessages = [];
106
-
107
- // Trigger each pass in order
108
- for (const passGroup of dependencyPasses) {
109
- const { pass, computations } = passGroup;
110
-
111
- // Trigger all computations in this pass
112
- for (const computation of computations) {
113
- const dispatchId = crypto.randomUUID();
114
- const isTarget = normalizeName(computation) === normalizeName(targetComputation);
115
-
116
- const computationMessage = {
117
- action: 'RUN_COMPUTATION_DATE',
118
- computation: computation,
119
- date: date,
120
- pass: String(pass),
121
- dispatchId: dispatchId,
122
- triggerReason: metadata.triggerReason || 'on_demand',
123
- resources: metadata.resources || 'standard',
124
- metadata: {
125
- ...metadata,
126
- onDemand: true,
127
- isTargetComputation: isTarget,
128
- targetCid: metadata.targetCid || null // Pass through targetCid for optimization
129
- },
130
- traceContext: {
131
- traceId: crypto.randomBytes(16).toString('hex'),
132
- spanId: crypto.randomBytes(8).toString('hex'),
133
- sampled: true
134
- }
135
- };
136
-
137
- await topic.publishMessage({
138
- data: Buffer.from(JSON.stringify(computationMessage))
139
- });
140
-
141
- triggeredMessages.push(computationMessage);
142
-
143
- logger.log('INFO', `[On-Demand] Triggered ${computation} (Pass ${pass})${isTarget ? ' [TARGET]' : ' [DEPENDENCY]'}`);
144
- }
145
- }
146
-
147
- return triggeredMessages;
148
- }
149
-
150
- module.exports = {
151
- resolveDependencyChain,
152
- triggerComputationWithDependencies
153
- };
154
-