bulltrackers-module 1.0.234 → 1.0.236
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/WorkflowOrchestrator.js +67 -17
- package/functions/computation-system/context/ManifestBuilder.js +8 -2
- package/functions/computation-system/executors/PriceBatchExecutor.js +11 -1
- package/functions/computation-system/helpers/computation_worker.js +5 -9
- package/functions/computation-system/persistence/ResultCommitter.js +92 -6
- package/functions/computation-system/persistence/StatusRepository.js +32 -6
- package/functions/computation-system/system_epoch.js +2 -0
- package/functions/computation-system/utils/data_loader.js +1 -1
- package/functions/computation-system/utils/utils.js +12 -12
- package/index.js +83 -56
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Main Orchestrator. Coordinates the topological execution.
|
|
3
|
-
* UPDATED:
|
|
3
|
+
* UPDATED: Detects Category changes to trigger migration/cleanup.
|
|
4
|
+
* FIX: Decouples migration detection from hash verification to handle simultaneous code & category changes.
|
|
4
5
|
*/
|
|
5
6
|
const { normalizeName } = require('./utils/utils');
|
|
6
7
|
const { checkRootDataAvailability } = require('./data/AvailabilityChecker');
|
|
@@ -33,22 +34,34 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
33
34
|
|
|
34
35
|
const isDepSatisfied = (depName, dailyStatus, manifestMap) => {
|
|
35
36
|
const norm = normalizeName(depName);
|
|
36
|
-
const
|
|
37
|
+
const stored = dailyStatus[norm]; // Now an object or null
|
|
37
38
|
const depManifest = manifestMap.get(norm);
|
|
38
39
|
|
|
39
|
-
if (
|
|
40
|
-
|
|
40
|
+
if (!stored) return false;
|
|
41
|
+
|
|
42
|
+
// Handle IMPOSSIBLE flag (stored as object property or legacy string check)
|
|
43
|
+
if (stored.hash === STATUS_IMPOSSIBLE) return false;
|
|
44
|
+
|
|
41
45
|
if (!depManifest) return false;
|
|
42
|
-
if (
|
|
46
|
+
if (stored.hash !== depManifest.hash) return false;
|
|
43
47
|
|
|
44
48
|
return true;
|
|
45
49
|
};
|
|
46
50
|
|
|
47
51
|
for (const calc of calcsInPass) {
|
|
48
52
|
const cName = normalizeName(calc.name);
|
|
49
|
-
const
|
|
53
|
+
const stored = dailyStatus[cName]; // Object { hash, category }
|
|
54
|
+
|
|
55
|
+
const storedHash = stored ? stored.hash : null;
|
|
56
|
+
const storedCategory = stored ? stored.category : null;
|
|
50
57
|
const currentHash = calc.hash;
|
|
51
58
|
|
|
59
|
+
// [SMART MIGRATION] Detect if category changed, independent of hash check
|
|
60
|
+
let migrationOldCategory = null;
|
|
61
|
+
if (storedCategory && storedCategory !== calc.category) {
|
|
62
|
+
migrationOldCategory = storedCategory;
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
// 1. Check Impossible
|
|
53
66
|
if (storedHash === STATUS_IMPOSSIBLE) {
|
|
54
67
|
report.skipped.push({ name: cName, reason: 'Permanently Impossible' });
|
|
@@ -83,8 +96,9 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
83
96
|
if (calc.dependencies) {
|
|
84
97
|
for (const dep of calc.dependencies) {
|
|
85
98
|
const normDep = normalizeName(dep);
|
|
99
|
+
const depStored = dailyStatus[normDep];
|
|
86
100
|
|
|
87
|
-
if (
|
|
101
|
+
if (depStored && depStored.hash === STATUS_IMPOSSIBLE) {
|
|
88
102
|
dependencyIsImpossible = true;
|
|
89
103
|
break;
|
|
90
104
|
}
|
|
@@ -105,13 +119,28 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
105
119
|
continue;
|
|
106
120
|
}
|
|
107
121
|
|
|
108
|
-
// 4. Hash
|
|
109
|
-
if (!storedHash
|
|
122
|
+
// 4. Hash & Category Check (Smart Migration Logic)
|
|
123
|
+
if (!storedHash) {
|
|
110
124
|
report.runnable.push(calc);
|
|
111
125
|
} else if (storedHash !== currentHash) {
|
|
112
|
-
|
|
126
|
+
// Hash Mismatch (Code Changed).
|
|
127
|
+
// Pass migration info here too, in case category ALSO changed.
|
|
128
|
+
report.reRuns.push({
|
|
129
|
+
name: cName,
|
|
130
|
+
oldHash: storedHash,
|
|
131
|
+
newHash: currentHash,
|
|
132
|
+
previousCategory: migrationOldCategory
|
|
133
|
+
});
|
|
134
|
+
} else if (migrationOldCategory) {
|
|
135
|
+
// Hash Matches, BUT category changed. Force Re-run.
|
|
136
|
+
report.reRuns.push({
|
|
137
|
+
name: cName,
|
|
138
|
+
reason: 'Category Migration',
|
|
139
|
+
previousCategory: migrationOldCategory,
|
|
140
|
+
newCategory: calc.category
|
|
141
|
+
});
|
|
113
142
|
} else {
|
|
114
|
-
// Stored Hash === Current Hash
|
|
143
|
+
// Stored Hash === Current Hash AND Category matches
|
|
115
144
|
report.skipped.push({ name: cName });
|
|
116
145
|
}
|
|
117
146
|
}
|
|
@@ -122,7 +151,7 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
|
|
|
122
151
|
async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, dependencies, computationManifest) {
|
|
123
152
|
const { logger } = dependencies;
|
|
124
153
|
const orchestratorPid = generateProcessId(PROCESS_TYPES.ORCHESTRATOR, passToRun, dateStr);
|
|
125
|
-
const dateToProcess
|
|
154
|
+
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
126
155
|
|
|
127
156
|
// 1. Fetch State
|
|
128
157
|
const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
|
|
@@ -154,21 +183,42 @@ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, d
|
|
|
154
183
|
|
|
155
184
|
// 5. UPDATE STATUS FOR NON-RUNNABLE ITEMS
|
|
156
185
|
const statusUpdates = {};
|
|
157
|
-
|
|
158
|
-
analysisReport.
|
|
159
|
-
analysisReport.
|
|
186
|
+
|
|
187
|
+
analysisReport.blocked.forEach(item => statusUpdates[item.name] = { hash: false, category: 'unknown' });
|
|
188
|
+
analysisReport.failedDependency.forEach(item => statusUpdates[item.name] = { hash: false, category: 'unknown' });
|
|
189
|
+
analysisReport.impossible.forEach(item => statusUpdates[item.name] = { hash: STATUS_IMPOSSIBLE, category: 'unknown' });
|
|
160
190
|
|
|
161
191
|
if (Object.keys(statusUpdates).length > 0) {
|
|
162
192
|
await updateComputationStatus(dateStr, statusUpdates, config, dependencies);
|
|
163
193
|
}
|
|
164
194
|
|
|
165
195
|
// 6. EXECUTE RUNNABLES
|
|
196
|
+
|
|
197
|
+
// [SMART MIGRATION] Build map of items needing cleanup
|
|
198
|
+
const migrationMap = {};
|
|
199
|
+
analysisReport.reRuns.forEach(item => {
|
|
200
|
+
if (item.previousCategory) {
|
|
201
|
+
migrationMap[normalizeName(item.name)] = item.previousCategory;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
166
205
|
const calcsToRunNames = new Set([
|
|
167
206
|
...analysisReport.runnable.map(c => c.name),
|
|
168
|
-
...analysisReport.reRuns.map(c => c.name)
|
|
207
|
+
...analysisReport.reRuns.map(c => c.name)
|
|
169
208
|
]);
|
|
170
209
|
|
|
171
|
-
|
|
210
|
+
// [SMART MIGRATION] Create Safe Copies with previousCategory attached
|
|
211
|
+
// We clone the manifest object so we don't pollute the global cache with run-specific flags
|
|
212
|
+
const finalRunList = calcsInThisPass
|
|
213
|
+
.filter(c => calcsToRunNames.has(normalizeName(c.name)))
|
|
214
|
+
.map(c => {
|
|
215
|
+
const clone = { ...c }; // Shallow copy
|
|
216
|
+
const prevCat = migrationMap[normalizeName(c.name)];
|
|
217
|
+
if (prevCat) {
|
|
218
|
+
clone.previousCategory = prevCat;
|
|
219
|
+
}
|
|
220
|
+
return clone;
|
|
221
|
+
});
|
|
172
222
|
|
|
173
223
|
if (!finalRunList.length) {
|
|
174
224
|
return {
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
const { generateCodeHash, LEGACY_MAPPING } = require('../topology/HashManager.js');
|
|
5
5
|
const { normalizeName } = require('../utils/utils');
|
|
6
6
|
|
|
7
|
+
const SYSTEM_EPOCH = require('../system_epoch');
|
|
8
|
+
|
|
7
9
|
// Import Layers
|
|
8
10
|
const MathematicsLayer = require('../layers/mathematics');
|
|
9
11
|
const ExtractorsLayer = require('../layers/extractors');
|
|
@@ -99,7 +101,7 @@ function buildManifest(productLinesToRun = [], calculations) {
|
|
|
99
101
|
const dependencies = Class.getDependencies().map(normalizeName);
|
|
100
102
|
const codeStr = Class.toString();
|
|
101
103
|
|
|
102
|
-
let compositeHashString = generateCodeHash(codeStr)
|
|
104
|
+
let compositeHashString = generateCodeHash(codeStr) + `|EPOCH:${SYSTEM_EPOCH}`; // Here we build the hash
|
|
103
105
|
const usedDeps = [];
|
|
104
106
|
|
|
105
107
|
for (const [layerName, exportsMap] of Object.entries(LAYER_TRIGGERS)) {
|
|
@@ -166,8 +168,12 @@ function buildManifest(productLinesToRun = [], calculations) {
|
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
const productLineEndpoints = [];
|
|
171
|
+
|
|
172
|
+
// [UPDATE] Check if we should run ALL product lines (if empty or wildcard)
|
|
173
|
+
const runAll = !productLinesToRun || productLinesToRun.length === 0 || productLinesToRun.includes('*');
|
|
174
|
+
|
|
169
175
|
for (const [name, entry] of manifestMap.entries()) {
|
|
170
|
-
if (productLinesToRun.includes(entry.category) || entry.sourcePackage === 'core') {
|
|
176
|
+
if (runAll || productLinesToRun.includes(entry.category) || entry.sourcePackage === 'core') {
|
|
171
177
|
productLineEndpoints.push(name);
|
|
172
178
|
}
|
|
173
179
|
}
|
|
@@ -69,7 +69,17 @@ async function runBatchPriceComputation(config, deps, dateStrings, calcs, target
|
|
|
69
69
|
.doc(calcManifest.category)
|
|
70
70
|
.collection(config.computationsSubcollection)
|
|
71
71
|
.doc(normalizeName(calcManifest.name));
|
|
72
|
-
|
|
72
|
+
|
|
73
|
+
// [UPDATE] Add _lastUpdated timestamp
|
|
74
|
+
writes.push({
|
|
75
|
+
ref: docRef,
|
|
76
|
+
data: {
|
|
77
|
+
...dataToWrite,
|
|
78
|
+
_completed: true,
|
|
79
|
+
_lastUpdated: new Date().toISOString()
|
|
80
|
+
},
|
|
81
|
+
options: { merge: true }
|
|
82
|
+
});
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
85
|
} catch (err) { logger.log('ERROR', `[BatchPrice] \u2716 Failed ${calcManifest.name} for ${dateStr}: ${err.message}`); }
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
const { runDateComputation, groupByPass } = require('../WorkflowOrchestrator.js');
|
|
8
8
|
const { getManifest } = require('../topology/ManifestLoader');
|
|
9
|
-
const { StructuredLogger } = require('../logger/logger');
|
|
9
|
+
const { StructuredLogger } = require('../logger/logger');
|
|
10
10
|
|
|
11
11
|
// 1. IMPORT CALCULATIONS
|
|
12
|
-
// We import the specific package containing
|
|
12
|
+
// We import the specific package containing the strategies (gem, pyro, core, etc.)
|
|
13
13
|
let calculationPackage;
|
|
14
14
|
try {
|
|
15
15
|
// Primary: Try to load from the installed npm package
|
|
@@ -33,17 +33,13 @@ const calculations = calculationPackage.calculations;
|
|
|
33
33
|
async function handleComputationTask(message, config, dependencies) {
|
|
34
34
|
|
|
35
35
|
// 2. INITIALIZE SYSTEM LOGGER
|
|
36
|
-
// We ignore the generic 'dependencies.logger' and instantiate our specialized one.
|
|
37
|
-
// This ensures methods like 'logDateAnalysis' and 'logStorage' exist.
|
|
38
36
|
const systemLogger = new StructuredLogger({
|
|
39
37
|
minLevel: config.minLevel || 'INFO',
|
|
40
38
|
enableStructured: true,
|
|
41
|
-
...config
|
|
39
|
+
...config
|
|
42
40
|
});
|
|
43
41
|
|
|
44
42
|
// 3. OVERRIDE DEPENDENCIES
|
|
45
|
-
// We create a new dependencies object to pass downstream.
|
|
46
|
-
// This fixes the "logger.logStorage is not a function" error in ResultCommitter.js
|
|
47
43
|
const runDependencies = {
|
|
48
44
|
...dependencies,
|
|
49
45
|
logger: systemLogger
|
|
@@ -57,7 +53,7 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
57
53
|
computationManifest = getManifest(
|
|
58
54
|
config.activeProductLines || [],
|
|
59
55
|
calculations,
|
|
60
|
-
runDependencies
|
|
56
|
+
runDependencies
|
|
61
57
|
);
|
|
62
58
|
} catch (manifestError) {
|
|
63
59
|
logger.log('FATAL', `[Worker] Failed to load Manifest: ${manifestError.message}`);
|
|
@@ -111,7 +107,7 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
111
107
|
pass,
|
|
112
108
|
calcsInThisPass,
|
|
113
109
|
config,
|
|
114
|
-
runDependencies,
|
|
110
|
+
runDependencies,
|
|
115
111
|
computationManifest
|
|
116
112
|
);
|
|
117
113
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Handles saving computation results with observability.
|
|
2
|
+
* @fileoverview Handles saving computation results with observability and Smart Cleanup.
|
|
3
3
|
*/
|
|
4
4
|
const { commitBatchInChunks } = require('./FirestoreUtils');
|
|
5
5
|
const { updateComputationStatus } = require('./StatusRepository');
|
|
@@ -9,6 +9,7 @@ const { generateProcessId, PROCESS_TYPES } = require('../logger/logger');
|
|
|
9
9
|
async function commitResults(stateObj, dStr, passName, config, deps, skipStatusWrite = false) {
|
|
10
10
|
const successUpdates = {};
|
|
11
11
|
const schemas = [];
|
|
12
|
+
const cleanupTasks = []; // Tasks to delete old data
|
|
12
13
|
const { logger } = deps;
|
|
13
14
|
const pid = generateProcessId(PROCESS_TYPES.STORAGE, passName, dStr);
|
|
14
15
|
|
|
@@ -16,7 +17,23 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
16
17
|
const calc = stateObj[name];
|
|
17
18
|
try {
|
|
18
19
|
const result = await calc.getResult();
|
|
19
|
-
|
|
20
|
+
|
|
21
|
+
// [UPDATE] Validate Result: Check for Null, Empty Object, or Zero
|
|
22
|
+
const isEmpty = !result ||
|
|
23
|
+
(typeof result === 'object' && Object.keys(result).length === 0) ||
|
|
24
|
+
(typeof result === 'number' && result === 0);
|
|
25
|
+
|
|
26
|
+
if (isEmpty) {
|
|
27
|
+
// [UPDATE] Mark status as FALSE (Failed/Empty) so it re-runs or is flagged
|
|
28
|
+
if (calc.manifest.hash) {
|
|
29
|
+
successUpdates[name] = {
|
|
30
|
+
hash: false,
|
|
31
|
+
category: calc.manifest.category
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Do not store empty results
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
20
37
|
|
|
21
38
|
const mainDocRef = deps.db.collection(config.resultsCollection)
|
|
22
39
|
.doc(dStr)
|
|
@@ -49,9 +66,18 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
49
66
|
logger.logStorage(pid, name, dStr, mainDocRef.path, totalSize, isSharded);
|
|
50
67
|
}
|
|
51
68
|
|
|
52
|
-
// Update success tracking
|
|
69
|
+
// Update success tracking (Include Category)
|
|
53
70
|
if (calc.manifest.hash) {
|
|
54
|
-
successUpdates[name] =
|
|
71
|
+
successUpdates[name] = {
|
|
72
|
+
hash: calc.manifest.hash,
|
|
73
|
+
category: calc.manifest.category
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// CHECK FOR MIGRATION CLEANUP
|
|
78
|
+
if (calc.manifest.previousCategory && calc.manifest.previousCategory !== calc.manifest.category) {
|
|
79
|
+
logger.log('INFO', `[Migration] Scheduled cleanup for ${name} from '${calc.manifest.previousCategory}'`);
|
|
80
|
+
cleanupTasks.push(deleteOldCalculationData(dStr, calc.manifest.previousCategory, name, config, deps));
|
|
55
81
|
}
|
|
56
82
|
}
|
|
57
83
|
} catch (e) {
|
|
@@ -63,12 +89,58 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
63
89
|
|
|
64
90
|
if (schemas.length) batchStoreSchemas(deps, config, schemas).catch(() => {});
|
|
65
91
|
|
|
92
|
+
// Execute Cleanup Tasks (orphaned data from category changes)
|
|
93
|
+
if (cleanupTasks.length > 0) {
|
|
94
|
+
await Promise.allSettled(cleanupTasks);
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
if (!skipStatusWrite && Object.keys(successUpdates).length > 0) {
|
|
67
98
|
await updateComputationStatus(dStr, successUpdates, config, deps);
|
|
68
99
|
}
|
|
69
100
|
return successUpdates;
|
|
70
101
|
}
|
|
71
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Deletes result documents from a previous category location.
|
|
105
|
+
* Must handle standard docs AND sharded docs (subcollections).
|
|
106
|
+
*/
|
|
107
|
+
async function deleteOldCalculationData(dateStr, oldCategory, calcName, config, deps) {
|
|
108
|
+
const { db, logger, calculationUtils } = deps;
|
|
109
|
+
const { withRetry } = calculationUtils || { withRetry: (fn) => fn() };
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const oldDocRef = db.collection(config.resultsCollection)
|
|
113
|
+
.doc(dateStr)
|
|
114
|
+
.collection(config.resultsSubcollection)
|
|
115
|
+
.doc(oldCategory)
|
|
116
|
+
.collection(config.computationsSubcollection)
|
|
117
|
+
.doc(calcName);
|
|
118
|
+
|
|
119
|
+
// 1. Check for Shards Subcollection
|
|
120
|
+
const shardsCol = oldDocRef.collection('_shards');
|
|
121
|
+
const shardsSnap = await withRetry(() => shardsCol.listDocuments(), 'ListOldShards');
|
|
122
|
+
|
|
123
|
+
const batch = db.batch();
|
|
124
|
+
let ops = 0;
|
|
125
|
+
|
|
126
|
+
// Delete shards
|
|
127
|
+
for (const shardDoc of shardsSnap) {
|
|
128
|
+
batch.delete(shardDoc);
|
|
129
|
+
ops++;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Delete main doc
|
|
133
|
+
batch.delete(oldDocRef);
|
|
134
|
+
ops++;
|
|
135
|
+
|
|
136
|
+
await withRetry(() => batch.commit(), 'CleanupOldCategory');
|
|
137
|
+
logger.log('INFO', `[Migration] Cleaned up ${ops} docs for ${calcName} in old category '${oldCategory}'`);
|
|
138
|
+
|
|
139
|
+
} catch (e) {
|
|
140
|
+
logger.log('WARN', `[Migration] Failed to clean up old data for ${calcName}: ${e.message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
72
144
|
function calculateFirestoreBytes(value) {
|
|
73
145
|
if (value === null) return 1;
|
|
74
146
|
if (value === undefined) return 0;
|
|
@@ -94,7 +166,16 @@ async function prepareAutoShardedWrites(result, docRef, logger) {
|
|
|
94
166
|
let currentChunkSize = 0;
|
|
95
167
|
let shardIndex = 0;
|
|
96
168
|
|
|
97
|
-
|
|
169
|
+
// [UPDATE] Add _lastUpdated to non-sharded writes
|
|
170
|
+
if ((totalSize + docPathSize) < CHUNK_LIMIT) {
|
|
171
|
+
const data = {
|
|
172
|
+
...result,
|
|
173
|
+
_completed: true,
|
|
174
|
+
_sharded: false,
|
|
175
|
+
_lastUpdated: new Date().toISOString()
|
|
176
|
+
};
|
|
177
|
+
return [{ ref: docRef, data, options: { merge: true } }];
|
|
178
|
+
}
|
|
98
179
|
|
|
99
180
|
for (const [key, value] of Object.entries(result)) {
|
|
100
181
|
if (key.startsWith('_')) continue;
|
|
@@ -114,7 +195,12 @@ async function prepareAutoShardedWrites(result, docRef, logger) {
|
|
|
114
195
|
|
|
115
196
|
if (Object.keys(currentChunk).length > 0) { writes.push({ ref: shardCollection.doc(`shard_${shardIndex}`), data: currentChunk, options: { merge: false } }); }
|
|
116
197
|
|
|
117
|
-
const pointerData = {
|
|
198
|
+
const pointerData = {
|
|
199
|
+
_completed: true,
|
|
200
|
+
_sharded: true,
|
|
201
|
+
_shardCount: shardIndex + 1,
|
|
202
|
+
_lastUpdated: new Date().toISOString()
|
|
203
|
+
};
|
|
118
204
|
writes.push({ ref: docRef, data: pointerData, options: { merge: false } });
|
|
119
205
|
return writes;
|
|
120
206
|
}
|
|
@@ -1,28 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Manages computation status tracking in Firestore.
|
|
3
|
+
* UPDATED: Supports Schema V2 (Object with Category) for smart migrations.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
async function fetchComputationStatus(dateStr, config, { db }) {
|
|
6
|
-
// FIX: Check dateStr directly, or define 'key' before checking it.
|
|
7
7
|
if (!dateStr) throw new Error('fetchStatus requires a key');
|
|
8
8
|
|
|
9
|
-
const key = dateStr;
|
|
10
9
|
const collection = config.computationStatusCollection || 'computation_status';
|
|
11
|
-
const docRef = db.collection(collection).doc(
|
|
10
|
+
const docRef = db.collection(collection).doc(dateStr);
|
|
12
11
|
|
|
13
12
|
const snap = await docRef.get();
|
|
14
|
-
|
|
13
|
+
if (!snap.exists) return {};
|
|
14
|
+
|
|
15
|
+
const rawData = snap.data();
|
|
16
|
+
const normalized = {};
|
|
17
|
+
|
|
18
|
+
// Normalize V1 (String) to V2 (Object)
|
|
19
|
+
for (const [name, value] of Object.entries(rawData)) {
|
|
20
|
+
if (typeof value === 'string') {
|
|
21
|
+
normalized[name] = { hash: value, category: null }; // Legacy entry
|
|
22
|
+
} else {
|
|
23
|
+
normalized[name] = value; // V2 entry { hash, category }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return normalized;
|
|
15
28
|
}
|
|
16
29
|
|
|
17
30
|
async function updateComputationStatus(dateStr, updates, config, { db }) {
|
|
18
31
|
if (!dateStr) throw new Error('updateStatus requires a key');
|
|
19
|
-
|
|
20
32
|
if (!updates || Object.keys(updates).length === 0) return;
|
|
21
33
|
|
|
22
34
|
const collection = config.computationStatusCollection || 'computation_status';
|
|
23
35
|
const docRef = db.collection(collection).doc(dateStr);
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
// We expect updates to be an object: { "CalcName": { hash: "...", category: "..." } }
|
|
38
|
+
// But result committer might still pass strings if we don't update it.
|
|
39
|
+
// We will enforce the structure here just in case.
|
|
40
|
+
|
|
41
|
+
const safeUpdates = {};
|
|
42
|
+
for (const [key, val] of Object.entries(updates)) {
|
|
43
|
+
if (typeof val === 'string') {
|
|
44
|
+
// Fallback if caller wasn't updated (shouldn't happen with full patch)
|
|
45
|
+
safeUpdates[key] = { hash: val, category: 'unknown', lastUpdated: new Date() };
|
|
46
|
+
} else {
|
|
47
|
+
safeUpdates[key] = { ...val, lastUpdated: new Date() };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await docRef.set(safeUpdates, { merge: true });
|
|
26
52
|
return true;
|
|
27
53
|
}
|
|
28
54
|
|
|
@@ -166,21 +166,21 @@ async function getEarliestDataDates(config, deps) {
|
|
|
166
166
|
};
|
|
167
167
|
|
|
168
168
|
const earliestPortfolioDate = getMinDate(investorDate, speculatorDate);
|
|
169
|
-
const earliestHistoryDate
|
|
170
|
-
const earliestInsightsDate
|
|
171
|
-
const earliestSocialDate
|
|
172
|
-
const earliestPriceDate
|
|
173
|
-
const absoluteEarliest
|
|
169
|
+
const earliestHistoryDate = getMinDate(investorHistoryDate, speculatorHistoryDate);
|
|
170
|
+
const earliestInsightsDate = getMinDate(insightsDate);
|
|
171
|
+
const earliestSocialDate = getMinDate(socialDate);
|
|
172
|
+
const earliestPriceDate = getMinDate(priceDate);
|
|
173
|
+
const absoluteEarliest = getMinDate(earliestPortfolioDate, earliestHistoryDate, earliestInsightsDate, earliestSocialDate, earliestPriceDate);
|
|
174
174
|
|
|
175
175
|
const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
|
|
176
176
|
|
|
177
177
|
return {
|
|
178
|
-
portfolio:
|
|
179
|
-
history:
|
|
180
|
-
insights:
|
|
181
|
-
social:
|
|
182
|
-
price:
|
|
183
|
-
absoluteEarliest: absoluteEarliest
|
|
178
|
+
portfolio: earliestPortfolioDate || new Date('2999-12-31T00:00:00Z'),
|
|
179
|
+
history: earliestHistoryDate || new Date('2999-12-31T00:00:00Z'),
|
|
180
|
+
insights: earliestInsightsDate || new Date('2999-12-31T00:00:00Z'),
|
|
181
|
+
social: earliestSocialDate || new Date('2999-12-31T00:00:00Z'),
|
|
182
|
+
price: earliestPriceDate || new Date('2999-12-31T00:00:00Z'),
|
|
183
|
+
absoluteEarliest: absoluteEarliest || fallbackDate
|
|
184
184
|
};
|
|
185
185
|
}
|
|
186
186
|
|
|
@@ -211,5 +211,5 @@ module.exports = {
|
|
|
211
211
|
getExpectedDateStrings,
|
|
212
212
|
getEarliestDataDates,
|
|
213
213
|
generateCodeHash,
|
|
214
|
-
withRetry
|
|
214
|
+
withRetry
|
|
215
215
|
};
|
package/index.js
CHANGED
|
@@ -3,82 +3,109 @@
|
|
|
3
3
|
* Export the pipes!
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
const
|
|
6
|
+
// Core utilities
|
|
7
|
+
const pubsubUtils = require('./functions/core/utils/pubsub_utils');
|
|
8
|
+
const { IntelligentHeaderManager } = require('./functions/core/utils/intelligent_header_manager');
|
|
9
|
+
const { IntelligentProxyManager } = require('./functions/core/utils/intelligent_proxy_manager');
|
|
10
|
+
const { FirestoreBatchManager } = require('./functions/task-engine/utils/firestore_batch_manager');
|
|
11
|
+
const firestoreUtils = require('./functions/core/utils/firestore_utils');
|
|
12
|
+
|
|
13
|
+
// Orchestrator
|
|
14
|
+
const { runDiscoveryOrchestrator, runUpdateOrchestrator } = require('./functions/orchestrator/index');
|
|
15
|
+
const { checkDiscoveryNeed, getDiscoveryCandidates, dispatchDiscovery } = require('./functions/orchestrator/helpers/discovery_helpers');
|
|
16
|
+
const { getUpdateTargets, dispatchUpdates } = require('./functions/orchestrator/helpers/update_helpers');
|
|
17
|
+
|
|
18
|
+
// Dispatcher
|
|
19
|
+
const { handleRequest: dispatchRequest } = require('./functions/dispatcher/index');
|
|
20
|
+
const { dispatchTasksInBatches } = require('./functions/dispatcher/helpers/dispatch_helpers');
|
|
21
|
+
|
|
22
|
+
// Task Engine
|
|
23
|
+
const { handleRequest: taskRequest } = require('./functions/task-engine/handler_creator');
|
|
24
|
+
const { handleDiscover } = require('./functions/task-engine/helpers/discover_helpers');
|
|
25
|
+
const { handleVerify } = require('./functions/task-engine/helpers/verify_helpers');
|
|
26
|
+
const { handleUpdate } = require('./functions/task-engine/helpers/update_helpers');
|
|
27
|
+
|
|
28
|
+
// Computation System
|
|
29
|
+
const { build: buildManifest } = require('./functions/computation-system/context/ManifestBuilder');
|
|
30
|
+
const { runDateComputation: runComputationPass } = require('./functions/computation-system/WorkflowOrchestrator');
|
|
31
|
+
const { dispatchComputationPass } = require('./functions/computation-system/helpers/computation_dispatcher');
|
|
32
|
+
const { handleComputationTask } = require('./functions/computation-system/helpers/computation_worker');
|
|
33
|
+
const dataLoader = require('./functions/computation-system/utils/data_loader');
|
|
34
|
+
const computationUtils = require('./functions/computation-system/utils/utils');
|
|
35
|
+
|
|
36
|
+
// API
|
|
37
|
+
const { createApiApp } = require('./functions/generic-api/index');
|
|
38
|
+
const apiHelpers = require('./functions/generic-api/helpers/api_helpers');
|
|
39
|
+
|
|
40
|
+
// Maintenance
|
|
41
|
+
const { runCleanup } = require('./functions/speculator-cleanup-orchestrator/helpers/cleanup_helpers');
|
|
42
|
+
const { handleInvalidSpeculator } = require('./functions/invalid-speculator-handler/helpers/handler_helpers');
|
|
43
|
+
const { fetchAndStoreInsights } = require('./functions/fetch-insights/helpers/handler_helpers');
|
|
44
|
+
const { fetchAndStorePrices } = require('./functions/etoro-price-fetcher/helpers/handler_helpers');
|
|
45
|
+
const { runSocialOrchestrator } = require('./functions/social-orchestrator/helpers/orchestrator_helpers');
|
|
46
|
+
const { handleSocialTask } = require('./functions/social-task-handler/helpers/handler_helpers');
|
|
47
|
+
const { runBackfillAssetPrices } = require('./functions/price-backfill/helpers/handler_helpers');
|
|
48
|
+
|
|
49
|
+
// Proxy
|
|
50
|
+
const { handlePost } = require('./functions/appscript-api/index');
|
|
8
51
|
|
|
9
|
-
// Core
|
|
10
52
|
const core = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
pubsubUtils: pubsubModule, // Keeps stateless function access
|
|
18
|
-
PubSubUtils: pubsubModule.PubSubUtils // Exposes the Class for 'new pipe.core.PubSubUtils()'
|
|
53
|
+
IntelligentHeaderManager,
|
|
54
|
+
IntelligentProxyManager,
|
|
55
|
+
FirestoreBatchManager,
|
|
56
|
+
firestoreUtils,
|
|
57
|
+
pubsubUtils,
|
|
58
|
+
PubSubUtils: pubsubUtils.PubSubUtils,
|
|
19
59
|
};
|
|
20
60
|
|
|
21
|
-
// Orchestrator
|
|
22
61
|
const orchestrator = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
62
|
+
runDiscoveryOrchestrator,
|
|
63
|
+
runUpdateOrchestrator,
|
|
64
|
+
checkDiscoveryNeed,
|
|
65
|
+
getDiscoveryCandidates,
|
|
66
|
+
dispatchDiscovery,
|
|
67
|
+
getUpdateTargets,
|
|
68
|
+
dispatchUpdates,
|
|
30
69
|
};
|
|
31
70
|
|
|
32
|
-
// Dispatcher
|
|
33
71
|
const dispatcher = {
|
|
34
|
-
|
|
35
|
-
|
|
72
|
+
handleRequest: dispatchRequest,
|
|
73
|
+
dispatchTasksInBatches,
|
|
36
74
|
};
|
|
37
75
|
|
|
38
|
-
// Task Engine
|
|
39
76
|
const taskEngine = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
77
|
+
handleRequest: taskRequest,
|
|
78
|
+
handleDiscover,
|
|
79
|
+
handleVerify,
|
|
80
|
+
handleUpdate,
|
|
44
81
|
};
|
|
45
82
|
|
|
46
|
-
// --- UPDATED IMPORT: Point to the new Context Domain ---
|
|
47
|
-
const { build: buildManifestFunc } = require('./functions/computation-system/context/ManifestBuilder');
|
|
48
|
-
|
|
49
|
-
// Computation System
|
|
50
83
|
const computationSystem = {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// Utils
|
|
59
|
-
dataLoader: require('./functions/computation-system/utils/data_loader'),
|
|
60
|
-
computationUtils: require('./functions/computation-system/utils/utils'),
|
|
61
|
-
buildManifest: buildManifestFunc
|
|
84
|
+
runComputationPass,
|
|
85
|
+
dispatchComputationPass,
|
|
86
|
+
handleComputationTask,
|
|
87
|
+
dataLoader,
|
|
88
|
+
computationUtils,
|
|
89
|
+
buildManifest,
|
|
62
90
|
};
|
|
63
91
|
|
|
64
|
-
// API
|
|
65
92
|
const api = {
|
|
66
|
-
|
|
67
|
-
|
|
93
|
+
createApiApp,
|
|
94
|
+
helpers: apiHelpers,
|
|
68
95
|
};
|
|
69
96
|
|
|
70
|
-
// Maintenance
|
|
71
97
|
const maintenance = {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
98
|
+
runSpeculatorCleanup: runCleanup,
|
|
99
|
+
handleInvalidSpeculator,
|
|
100
|
+
runFetchInsights: fetchAndStoreInsights,
|
|
101
|
+
runFetchPrices: fetchAndStorePrices,
|
|
102
|
+
runSocialOrchestrator,
|
|
103
|
+
handleSocialTask,
|
|
104
|
+
runBackfillAssetPrices,
|
|
79
105
|
};
|
|
80
106
|
|
|
81
|
-
|
|
82
|
-
const proxy = { handlePost: require('./functions/appscript-api/index').handlePost };
|
|
107
|
+
const proxy = { handlePost };
|
|
83
108
|
|
|
84
|
-
module.exports = {
|
|
109
|
+
module.exports = {
|
|
110
|
+
pipe: { core, orchestrator, dispatcher, taskEngine, computationSystem, api, maintenance, proxy },
|
|
111
|
+
};
|