bulltrackers-module 1.0.68 → 1.0.70
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/helpers/orchestration_helpers.js +133 -51
- package/functions/computation-system/utils/data_loader.js +42 -2
- package/functions/social-orchestrator/helpers/orchestrator_helpers.js +62 -0
- package/functions/social-task-handler/helpers/handler_helpers.js +243 -0
- package/functions/user-activity-sampler/helpers/sampler_helpers.js +60 -44
- package/index.js +12 -0
- package/package.json +6 -3
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
const { FieldPath } = require('@google-cloud/firestore');
|
|
8
8
|
// Import sub-pipes/utils from their new locations
|
|
9
|
-
// --- MODIFIED: Added
|
|
10
|
-
const {
|
|
9
|
+
// --- MODIFIED: Added loadDailySocialPostInsights ---
|
|
10
|
+
const {
|
|
11
|
+
getPortfolioPartRefs,
|
|
12
|
+
loadFullDayMap,
|
|
13
|
+
loadDataByRefs,
|
|
14
|
+
loadDailyInsights,
|
|
15
|
+
loadDailySocialPostInsights // <-- NEW
|
|
16
|
+
} = require('../utils/data_loader.js');
|
|
11
17
|
const {
|
|
12
18
|
historicalCalculations, dailyCalculations, HISTORICAL_CALC_NAMES,
|
|
13
19
|
withRetry,
|
|
@@ -34,13 +40,22 @@ async function runComputationOrchestrator(config, dependencies) {
|
|
|
34
40
|
const masterDailyList = Object.entries(dailyCalculations).flatMap(([cat, calcs]) =>
|
|
35
41
|
Object.keys(calcs).map(name => ({ category: cat, calcName: name }))
|
|
36
42
|
);
|
|
37
|
-
// ---
|
|
43
|
+
// --- (EXISTING) insights calculations ---
|
|
38
44
|
const insightsCalculations = require('aiden-shared-calculations-unified').calculations.insights || {};
|
|
39
45
|
const masterInsightsList = Object.keys(insightsCalculations).map(name => ({ category: 'insights', calcName: name }));
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
// --- NEW: Add social post calculations to the master lists ---
|
|
48
|
+
const socialPostCalculations = require('aiden-shared-calculations-unified').calculations.socialPosts || {};
|
|
49
|
+
const masterSocialPostList = Object.keys(socialPostCalculations).map(name => ({ category: 'socialPosts', calcName: name }));
|
|
42
50
|
// --- END NEW ---
|
|
43
51
|
|
|
52
|
+
const masterFullList = [
|
|
53
|
+
...masterHistoricalList,
|
|
54
|
+
...masterDailyList,
|
|
55
|
+
...masterInsightsList,
|
|
56
|
+
...masterSocialPostList // <-- NEW
|
|
57
|
+
];
|
|
58
|
+
|
|
44
59
|
// Pass dependencies to sub-pipe
|
|
45
60
|
const firstDate = await getFirstDateFromSourceData(config, dependencies);
|
|
46
61
|
const startDateUTC = firstDate
|
|
@@ -54,18 +69,25 @@ async function runComputationOrchestrator(config, dependencies) {
|
|
|
54
69
|
const existingDateIds = new Set(insightDocs.docs.map(d => d.id).filter(id => /^\d{4}-\d{2}-\d{2}$/.test(id)));
|
|
55
70
|
|
|
56
71
|
// --- PASS 1 ---
|
|
57
|
-
// --- MODIFIED: Include insights calcs in missing list ---
|
|
72
|
+
// --- MODIFIED: Include insights & social calcs in missing list ---
|
|
58
73
|
const missingDates = allExpectedDates.filter(dateStr => !existingDateIds.has(dateStr));
|
|
59
|
-
const pass1Jobs = missingDates.map(date => ({
|
|
74
|
+
const pass1Jobs = missingDates.map(date => ({
|
|
75
|
+
date,
|
|
76
|
+
missing: [...masterDailyList, ...masterInsightsList, ...masterSocialPostList] // <-- NEW
|
|
77
|
+
}));
|
|
60
78
|
// --- END MODIFIED ---
|
|
61
79
|
|
|
62
|
-
logger.log('INFO', `[Orchestrator] Pass 1: Found ${pass1Jobs.length} missing dates to process (daily &
|
|
80
|
+
logger.log('INFO', `[Orchestrator] Pass 1: Found ${pass1Jobs.length} missing dates to process (daily, insights & social calcs).`);
|
|
63
81
|
// Pass dependencies to sub-pipe
|
|
64
|
-
// --- MODIFIED: Combine daily and
|
|
65
|
-
const pass1SourcePackage = {
|
|
82
|
+
// --- MODIFIED: Combine daily, insights, and social calcs for Pass 1 run ---
|
|
83
|
+
const pass1SourcePackage = {
|
|
84
|
+
...dailyCalculations,
|
|
85
|
+
insights: insightsCalculations,
|
|
86
|
+
socialPosts: socialPostCalculations // <-- NEW
|
|
87
|
+
};
|
|
66
88
|
const pass1Results = await processJobsInParallel(
|
|
67
89
|
pass1Jobs,
|
|
68
|
-
(date, missing) => runUnifiedComputation(date, missing, 'Pass 1 (Daily &
|
|
90
|
+
(date, missing) => runUnifiedComputation(date, missing, 'Pass 1 (Daily, Insights & Social)', pass1SourcePackage, config, dependencies),
|
|
69
91
|
'Pass 1',
|
|
70
92
|
config
|
|
71
93
|
);
|
|
@@ -94,9 +116,10 @@ async function runComputationOrchestrator(config, dependencies) {
|
|
|
94
116
|
pass2Jobs,
|
|
95
117
|
async (date, missing) => {
|
|
96
118
|
const historicalMissing = missing.filter(c => HISTORICAL_CALC_NAMES.has(c.calcName));
|
|
97
|
-
// --- MODIFIED: Include insights in daily missing ---
|
|
98
|
-
const dailyMissing = missing.filter(c => !HISTORICAL_CALC_NAMES.has(c.calcName) && c.category !== 'insights');
|
|
119
|
+
// --- MODIFIED: Include insights & social in daily missing ---
|
|
120
|
+
const dailyMissing = missing.filter(c => !HISTORICAL_CALC_NAMES.has(c.calcName) && c.category !== 'insights' && c.category !== 'socialPosts');
|
|
99
121
|
const insightsMissing = missing.filter(c => c.category === 'insights');
|
|
122
|
+
const socialPostsMissing = missing.filter(c => c.category === 'socialPosts'); // <-- NEW
|
|
100
123
|
// --- END MODIFIED ---
|
|
101
124
|
const results = [];
|
|
102
125
|
|
|
@@ -105,12 +128,16 @@ async function runComputationOrchestrator(config, dependencies) {
|
|
|
105
128
|
const histResult = await runUnifiedComputation(date, historicalMissing, 'Pass 2 (Historical)', historicalCalculations, config, dependencies);
|
|
106
129
|
results.push(histResult);
|
|
107
130
|
}
|
|
108
|
-
// --- MODIFIED: Run daily
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
111
|
-
logger.log('INFO', `[Pass 2] Running ${
|
|
112
|
-
const dailyAndInsightsSourcePackage = {
|
|
113
|
-
|
|
131
|
+
// --- MODIFIED: Run daily, insights, and social ---
|
|
132
|
+
const dailyAndInsightsAndSocialMissing = [...dailyMissing, ...insightsMissing, ...socialPostsMissing]; // <-- NEW
|
|
133
|
+
if (dailyAndInsightsAndSocialMissing.length > 0) {
|
|
134
|
+
logger.log('INFO', `[Pass 2] Running ${dailyAndInsightsAndSocialMissing.length} daily/insights/social calcs for ${date.toISOString().slice(0, 10)}.`);
|
|
135
|
+
const dailyAndInsightsSourcePackage = {
|
|
136
|
+
...dailyCalculations,
|
|
137
|
+
insights: insightsCalculations,
|
|
138
|
+
socialPosts: socialPostCalculations // <-- NEW
|
|
139
|
+
};
|
|
140
|
+
const dailyResult = await runUnifiedComputation(date, dailyAndInsightsAndSocialMissing, 'Pass 2 (Daily/Insights/Social)', dailyAndInsightsSourcePackage, config, dependencies);
|
|
114
141
|
results.push(dailyResult);
|
|
115
142
|
}
|
|
116
143
|
// --- END MODIFIED ---
|
|
@@ -149,16 +176,23 @@ function initializeCalculators(calculationsToRun, sourcePackage, logger) {
|
|
|
149
176
|
|
|
150
177
|
/**
|
|
151
178
|
* Internal sub-pipe: Streams data and calls process() on calculators.
|
|
152
|
-
* --- MODIFIED: Added
|
|
179
|
+
* --- MODIFIED: Added today/yesterday social post insights ---
|
|
153
180
|
*/
|
|
154
|
-
async function streamAndProcess(
|
|
181
|
+
async function streamAndProcess(
|
|
182
|
+
dateStr, todayRefs, state, passName, config, dependencies,
|
|
183
|
+
yesterdayPortfolios = {},
|
|
184
|
+
todayInsights = null,
|
|
185
|
+
yesterdayInsights = null,
|
|
186
|
+
todaySocialPostInsights = null, // <-- NEW
|
|
187
|
+
yesterdaySocialPostInsights = null // <-- NEW
|
|
188
|
+
) {
|
|
155
189
|
const { db, logger } = dependencies;
|
|
156
190
|
logger.log('INFO', `[${passName}] Streaming ${todayRefs.length} 'today' part docs for ${dateStr}...`);
|
|
157
191
|
|
|
158
192
|
const { instrumentToTicker, instrumentToSector } = await unifiedUtils.loadInstrumentMappings();
|
|
159
193
|
const context = { instrumentMappings: instrumentToTicker, sectorMapping: instrumentToSector };
|
|
160
194
|
const batchSize = config.partRefBatchSize || 10;
|
|
161
|
-
let isFirstUser = true; // Flag for insights calculations
|
|
195
|
+
let isFirstUser = true; // Flag for insights/social calculations
|
|
162
196
|
|
|
163
197
|
for (let i = 0; i < todayRefs.length; i += batchSize) {
|
|
164
198
|
const batchRefs = todayRefs.slice(i, i + batchSize);
|
|
@@ -178,14 +212,21 @@ async function streamAndProcess(dateStr, todayRefs, state, passName, config, dep
|
|
|
178
212
|
|
|
179
213
|
const [category, calcName] = key.split('/');
|
|
180
214
|
let processArgs;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
215
|
+
const allContextArgs = [
|
|
216
|
+
context,
|
|
217
|
+
todayInsights,
|
|
218
|
+
yesterdayInsights,
|
|
219
|
+
todaySocialPostInsights, // <-- NEW
|
|
220
|
+
yesterdaySocialPostInsights // <-- NEW
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
// --- MODIFIED: Handle insights & social calculations ---
|
|
224
|
+
if (category === 'insights' || category === 'socialPosts') {
|
|
225
|
+
// Only process these once per day, using the first user as a trigger
|
|
185
226
|
if (isFirstUser) {
|
|
186
|
-
processArgs = [null, null, null,
|
|
227
|
+
processArgs = [null, null, null, ...allContextArgs];
|
|
187
228
|
} else {
|
|
188
|
-
continue; // Skip
|
|
229
|
+
continue; // Skip processing for subsequent users
|
|
189
230
|
}
|
|
190
231
|
} else if (HISTORICAL_CALC_NAMES.has(calcName)) {
|
|
191
232
|
// --- MODIFIED: Handle missing yesterday's portfolio gracefully ---
|
|
@@ -195,14 +236,14 @@ async function streamAndProcess(dateStr, todayRefs, state, passName, config, dep
|
|
|
195
236
|
// logger.log('TRACE', `Skipping historical calc ${key} for user ${uid} due to missing yesterday portfolio.`);
|
|
196
237
|
continue;
|
|
197
238
|
}
|
|
198
|
-
processArgs = [p, pYesterday, uid,
|
|
239
|
+
processArgs = [p, pYesterday, uid, ...allContextArgs]; // Pass all context
|
|
199
240
|
} else {
|
|
200
241
|
// Standard daily calculation
|
|
201
242
|
if ((userType === 'normal' && category === 'speculators') ||
|
|
202
243
|
(userType === 'speculator' && !['speculators', 'sanity'].includes(category))) {
|
|
203
244
|
continue;
|
|
204
245
|
}
|
|
205
|
-
processArgs = [p, null, uid,
|
|
246
|
+
processArgs = [p, null, uid, ...allContextArgs]; // Pass null for yesterdayPortfolio, add all context
|
|
206
247
|
}
|
|
207
248
|
// --- END MODIFIED ---
|
|
208
249
|
|
|
@@ -219,7 +260,7 @@ async function streamAndProcess(dateStr, todayRefs, state, passName, config, dep
|
|
|
219
260
|
|
|
220
261
|
/**
|
|
221
262
|
* Internal sub-pipe: Runs computations for a single date.
|
|
222
|
-
* --- MODIFIED: Load and pass insights data ---
|
|
263
|
+
* --- MODIFIED: Load and pass social post insights data ---
|
|
223
264
|
*/
|
|
224
265
|
async function runUnifiedComputation(dateToProcess, calculationsToRun, passName, sourcePackage, config, dependencies) {
|
|
225
266
|
const { db, logger } = dependencies;
|
|
@@ -227,33 +268,35 @@ async function runUnifiedComputation(dateToProcess, calculationsToRun, passName,
|
|
|
227
268
|
logger.log('INFO', `[${passName}] Starting run for ${dateStr} with ${calculationsToRun.length} calcs.`);
|
|
228
269
|
|
|
229
270
|
try {
|
|
230
|
-
// ---
|
|
271
|
+
// --- (EXISTING) Load today's instrument insights ---
|
|
231
272
|
const todayInsightsData = await loadDailyInsights(config, dependencies, dateStr);
|
|
273
|
+
// --- NEW: Load today's social post insights ---
|
|
274
|
+
const todaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, dateStr);
|
|
232
275
|
// --- END NEW ---
|
|
233
276
|
|
|
234
277
|
// Pass dependencies to sub-pipe
|
|
235
278
|
const todayRefs = await getPortfolioPartRefs(config, dependencies, dateStr);
|
|
236
|
-
// --- MODIFIED: Check if *any* data exists (portfolio OR insights) ---
|
|
237
|
-
if (todayRefs.length === 0 && !todayInsightsData) {
|
|
238
|
-
logger.log('WARN', `[${passName}] No portfolio
|
|
279
|
+
// --- MODIFIED: Check if *any* data exists (portfolio OR insights OR social) ---
|
|
280
|
+
if (todayRefs.length === 0 && !todayInsightsData && !todaySocialPostInsightsData) {
|
|
281
|
+
logger.log('WARN', `[${passName}] No portfolio, instrument insights, OR social post data found for ${dateStr}. Skipping.`);
|
|
239
282
|
return { success: true, date: dateStr, message: "No source data for today." };
|
|
240
283
|
}
|
|
241
284
|
// --- END MODIFIED ---
|
|
242
285
|
|
|
243
286
|
|
|
244
287
|
let yesterdayPortfolios = {};
|
|
245
|
-
let yesterdayInsightsData = null;
|
|
246
|
-
|
|
288
|
+
let yesterdayInsightsData = null;
|
|
289
|
+
let yesterdaySocialPostInsightsData = null; // <-- NEW
|
|
290
|
+
|
|
291
|
+
// --- MODIFIED: Check if *any* calc requires yesterday data ---
|
|
247
292
|
const requiresYesterdayPortfolio = calculationsToRun.some(c => sourcePackage[c.category]?.[c.calcName]?.prototype?.process.length >= 3);
|
|
248
|
-
const requiresYesterdayInsights = calculationsToRun.some(c => c.category === 'insights');
|
|
293
|
+
const requiresYesterdayInsights = calculationsToRun.some(c => c.category === 'insights');
|
|
294
|
+
const requiresYesterdaySocialPosts = calculationsToRun.some(c => c.category === 'socialPosts'); // <-- NEW
|
|
249
295
|
// --- END MODIFIED ---
|
|
250
296
|
|
|
251
|
-
if (requiresYesterdayPortfolio || requiresYesterdayInsights) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const prevStr = prev.toISOString().slice(0, 10);
|
|
255
|
-
|
|
256
|
-
// --- MODIFIED: Load yesterday's insights if needed ---
|
|
297
|
+
if (requiresYesterdayPortfolio || requiresYesterdayInsights || requiresYesterdaySocialPosts) { // <-- NEW
|
|
298
|
+
|
|
299
|
+
// --- (EXISTING) Load yesterday's instrument insights ---
|
|
257
300
|
if(requiresYesterdayInsights) {
|
|
258
301
|
let daysAgo = 1;
|
|
259
302
|
const maxLookback = 30; // Or from config
|
|
@@ -263,38 +306,77 @@ async function runUnifiedComputation(dateToProcess, calculationsToRun, passName,
|
|
|
263
306
|
prev.setUTCDate(prev.getUTCDate() - daysAgo);
|
|
264
307
|
const prevStr = prev.toISOString().slice(0, 10);
|
|
265
308
|
|
|
266
|
-
// Keep trying to load data until we find some
|
|
267
309
|
yesterdayInsightsData = await loadDailyInsights(config, dependencies, prevStr);
|
|
268
310
|
|
|
269
311
|
if (yesterdayInsightsData) {
|
|
270
|
-
logger.log('INFO', `[${passName}] Found 'yesterday' insights data from ${daysAgo} day(s) ago (${prevStr}).`);
|
|
312
|
+
logger.log('INFO', `[${passName}] Found 'yesterday' instrument insights data from ${daysAgo} day(s) ago (${prevStr}).`);
|
|
271
313
|
} else {
|
|
272
|
-
daysAgo++;
|
|
314
|
+
daysAgo++;
|
|
273
315
|
}
|
|
274
316
|
}
|
|
275
|
-
|
|
276
317
|
if (!yesterdayInsightsData) {
|
|
277
|
-
logger.log('WARN', `[${passName}] Could not find any 'yesterday' insights data within a ${maxLookback} day lookback.`);
|
|
318
|
+
logger.log('WARN', `[${passName}] Could not find any 'yesterday' instrument insights data within a ${maxLookback} day lookback.`);
|
|
278
319
|
}
|
|
279
320
|
}
|
|
280
|
-
// --- END
|
|
321
|
+
// --- END (EXISTING) ---
|
|
322
|
+
|
|
323
|
+
// --- NEW: Load yesterday's social post insights ---
|
|
324
|
+
if(requiresYesterdaySocialPosts) {
|
|
325
|
+
let daysAgo = 1;
|
|
326
|
+
const maxLookback = 30;
|
|
327
|
+
|
|
328
|
+
while (!yesterdaySocialPostInsightsData && daysAgo <= maxLookback) {
|
|
329
|
+
const prev = new Date(dateToProcess);
|
|
330
|
+
prev.setUTCDate(prev.getUTCDate() - daysAgo);
|
|
331
|
+
const prevStr = prev.toISOString().slice(0, 10);
|
|
332
|
+
|
|
333
|
+
yesterdaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, prevStr);
|
|
334
|
+
|
|
335
|
+
if (yesterdaySocialPostInsightsData) {
|
|
336
|
+
logger.log('INFO', `[${passName}] Found 'yesterday' social post insights data from ${daysAgo} day(s) ago (${prevStr}).`);
|
|
337
|
+
} else {
|
|
338
|
+
daysAgo++;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (!yesterdaySocialPostInsightsData) {
|
|
342
|
+
logger.log('WARN', `[${passName}] Could not find any 'yesterday' social post insights data within a ${maxLookback} day lookback.`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// --- END NEW ---
|
|
281
346
|
|
|
347
|
+
// --- (EXISTING) Load yesterday's portfolio data ---
|
|
282
348
|
if (requiresYesterdayPortfolio) {
|
|
349
|
+
const prev = new Date(dateToProcess);
|
|
350
|
+
prev.setUTCDate(prev.getUTCDate() - 1);
|
|
351
|
+
const prevStr = prev.toISOString().slice(0, 10);
|
|
283
352
|
const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, prevStr);
|
|
353
|
+
|
|
284
354
|
if (yesterdayRefs.length > 0) {
|
|
285
355
|
yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
|
|
286
356
|
logger.log('INFO', `[${passName}] Loaded yesterday's (${prevStr}) portfolio map for historical calcs.`);
|
|
287
357
|
} else {
|
|
288
358
|
logger.log('WARN', `[${passName}] Yesterday's (${prevStr}) portfolio data not found. Historical calcs requiring it will be skipped.`);
|
|
289
|
-
// Ensure calculations that strictly need yesterday's portfolio don't run or handle the missing data
|
|
290
359
|
}
|
|
291
360
|
}
|
|
361
|
+
// --- END (EXISTING) ---
|
|
292
362
|
}
|
|
293
363
|
|
|
294
364
|
|
|
295
365
|
const state = initializeCalculators(calculationsToRun, sourcePackage, logger);
|
|
296
366
|
// Pass dependencies AND insights data to sub-pipe
|
|
297
|
-
await streamAndProcess(
|
|
367
|
+
await streamAndProcess(
|
|
368
|
+
dateStr,
|
|
369
|
+
todayRefs,
|
|
370
|
+
state,
|
|
371
|
+
passName,
|
|
372
|
+
config,
|
|
373
|
+
dependencies,
|
|
374
|
+
yesterdayPortfolios,
|
|
375
|
+
todayInsightsData,
|
|
376
|
+
yesterdayInsightsData,
|
|
377
|
+
todaySocialPostInsightsData, // <-- NEW
|
|
378
|
+
yesterdaySocialPostInsightsData // <-- NEW
|
|
379
|
+
);
|
|
298
380
|
|
|
299
381
|
let successCount = 0;
|
|
300
382
|
const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection); // Use db
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* REFACTORED: Now stateless and receive dependencies.
|
|
4
4
|
*/
|
|
5
5
|
const { withRetry } = require('aiden-shared-calculations-unified').utils;
|
|
6
|
+
const { FieldPath } = require('@google-cloud/firestore'); // <<< --- ADD FieldPath
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Sub-pipe: pipe.computationSystem.dataLoader.getPortfolioPartRefs
|
|
@@ -99,7 +100,7 @@ async function loadFullDayMap(config, dependencies, partRefs) {
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
/**
|
|
102
|
-
* ---
|
|
103
|
+
* --- (EXISTING) ---
|
|
103
104
|
* Sub-pipe: pipe.computationSystem.dataLoader.loadDailyInsights
|
|
104
105
|
* Fetches the daily instrument insights document for a specific date.
|
|
105
106
|
* @param {object} config - The computation system configuration object.
|
|
@@ -126,11 +127,50 @@ async function loadDailyInsights(config, dependencies, dateString) {
|
|
|
126
127
|
return null; // Return null on error to allow computations to proceed partially if possible
|
|
127
128
|
}
|
|
128
129
|
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* --- NEW ---
|
|
133
|
+
* Sub-pipe: pipe.computationSystem.dataLoader.loadDailySocialPostInsights
|
|
134
|
+
* Fetches all analyzed social post documents for a specific date.
|
|
135
|
+
* @param {object} config - The computation system configuration object.
|
|
136
|
+
* @param {object} dependencies - Contains db, logger.
|
|
137
|
+
* @param {string} dateString - The date in YYYY-MM-DD format.
|
|
138
|
+
* @returns {Promise<object|null>} An object map of { [postId]: postData } or null.
|
|
139
|
+
*/
|
|
140
|
+
async function loadDailySocialPostInsights(config, dependencies, dateString) {
|
|
141
|
+
const { db, logger } = dependencies;
|
|
142
|
+
const socialInsightsCollectionName = config.socialInsightsCollectionName || 'daily_social_insights';
|
|
143
|
+
logger.log('INFO', `Loading social post insights for date: ${dateString} from ${socialInsightsCollectionName}`);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const postsCollectionRef = db.collection(socialInsightsCollectionName).doc(dateString).collection('posts');
|
|
147
|
+
const querySnapshot = await withRetry(() => postsCollectionRef.get(), `getSocialPosts(${dateString})`);
|
|
148
|
+
|
|
149
|
+
if (querySnapshot.empty) {
|
|
150
|
+
logger.log('WARN', `No social post insights found for ${dateString}.`);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const postsMap = {};
|
|
155
|
+
querySnapshot.forEach(doc => {
|
|
156
|
+
postsMap[doc.id] = doc.data();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
logger.log('TRACE', `Successfully loaded ${Object.keys(postsMap).length} social post insights for ${dateString}.`);
|
|
160
|
+
return postsMap;
|
|
161
|
+
|
|
162
|
+
} catch (error) {
|
|
163
|
+
logger.log('ERROR', `Failed to load social post insights for ${dateString}`, { errorMessage: error.message });
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
129
167
|
// --- END NEW ---
|
|
130
168
|
|
|
169
|
+
|
|
131
170
|
module.exports = {
|
|
132
171
|
getPortfolioPartRefs,
|
|
133
172
|
loadDataByRefs,
|
|
134
173
|
loadFullDayMap,
|
|
135
|
-
loadDailyInsights,
|
|
174
|
+
loadDailyInsights,
|
|
175
|
+
loadDailySocialPostInsights, // Export the new function
|
|
136
176
|
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main pipe: pipe.maintenance.runSocialOrchestrator
|
|
3
|
+
* This function is triggered by a schedule. It fans out the work by
|
|
4
|
+
* publishing one Pub/Sub message for each target ticker.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Main pipe (Orchestrator): pipe.maintenance.runSocialOrchestrator
|
|
9
|
+
* @param {object} config - Configuration object.
|
|
10
|
+
* @param {object} dependencies - Contains db, logger, firestoreUtils, pubsubUtils.
|
|
11
|
+
* @returns {Promise<object>} Summary of the orchestration.
|
|
12
|
+
*/
|
|
13
|
+
exports.runSocialOrchestrator = async (config, dependencies) => {
|
|
14
|
+
const { logger, pubsubUtils } = dependencies;
|
|
15
|
+
logger.log('INFO', '[SocialOrchestrator] Starting social post fetching orchestration...');
|
|
16
|
+
|
|
17
|
+
// Validate configuration
|
|
18
|
+
if (!config.targetTickerIds || !Array.isArray(config.targetTickerIds) || !config.socialFetchTaskTopicName || !config.socialFetchTimeWindowHours) {
|
|
19
|
+
logger.log('ERROR', '[SocialOrchestrator] Missing required configuration: targetTickerIds (array), socialFetchTaskTopicName, or socialFetchTimeWindowHours.');
|
|
20
|
+
throw new Error('Missing required configuration for Social Orchestrator.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const tasks = [];
|
|
25
|
+
const { targetTickerIds, socialFetchTimeWindowHours } = config;
|
|
26
|
+
|
|
27
|
+
// Calculate the 'since' timestamp
|
|
28
|
+
const sinceTimestamp = new Date();
|
|
29
|
+
sinceTimestamp.setHours(sinceTimestamp.getHours() - socialFetchTimeWindowHours);
|
|
30
|
+
|
|
31
|
+
// Add a 15-minute buffer to ensure overlap
|
|
32
|
+
sinceTimestamp.setMinutes(sinceTimestamp.getMinutes() - 15);
|
|
33
|
+
|
|
34
|
+
const sinceISO = sinceTimestamp.toISOString();
|
|
35
|
+
|
|
36
|
+
for (const tickerId of targetTickerIds) {
|
|
37
|
+
tasks.push({
|
|
38
|
+
tickerId: String(tickerId), // Ensure it's a string
|
|
39
|
+
since: sinceISO
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (tasks.length === 0) {
|
|
44
|
+
logger.log('WARN', '[SocialOrchestrator] No target tickers found to process.');
|
|
45
|
+
return { success: true, message: "No target tickers configured." };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Use pubsubUtils to batch publish all ticker tasks
|
|
49
|
+
await pubsubUtils.batchPublishTasks(dependencies, {
|
|
50
|
+
topicName: config.socialFetchTaskTopicName,
|
|
51
|
+
tasks: tasks,
|
|
52
|
+
taskType: 'social-fetch-task'
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
logger.log('SUCCESS', `[SocialOrchestrator] Successfully published ${tasks.length} social fetch tasks for window >= ${sinceISO}.`);
|
|
56
|
+
return { success: true, tasksQueued: tasks.length };
|
|
57
|
+
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.log('ERROR', '[SocialOrchestrator] Fatal error during orchestration.', { errorMessage: error.message, errorStack: error.stack });
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main pipe: pipe.maintenance.handleSocialTask
|
|
3
|
+
* This function is triggered by Pub/Sub for a single ticker.
|
|
4
|
+
* It fetches posts, deduplicates, analyzes, and stores them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { FieldValue, FieldPath } = require('@google-cloud/firestore');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* --- NEW HELPER FUNCTION ---
|
|
11
|
+
* Calls Gemini to classify sentiment.
|
|
12
|
+
* @param {object} dependencies - Contains logger and geminiModel.
|
|
13
|
+
* @param {string} snippet - The text snippet to analyze.
|
|
14
|
+
* @returns {Promise<string>} 'Bullish', 'Bearish', or 'Neutral'.
|
|
15
|
+
*/
|
|
16
|
+
async function getSentimentFromGemini(dependencies, snippet) {
|
|
17
|
+
const { logger, geminiModel } = dependencies;
|
|
18
|
+
|
|
19
|
+
if (!geminiModel) {
|
|
20
|
+
logger.log('WARN', '[getSentimentFromGemini] Gemini model not found in dependencies.');
|
|
21
|
+
return 'Neutral';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const prompt = `You are a financial sentiment analyzer for social media posts.
|
|
25
|
+
Classify the following post as 'Bullish', 'Bearish', or 'Neutral'.
|
|
26
|
+
Respond with only one of those three words.
|
|
27
|
+
|
|
28
|
+
Post: "${snippet}"`;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const request = {
|
|
32
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
33
|
+
generationConfig: {
|
|
34
|
+
// Set low temperature for classification
|
|
35
|
+
temperature: 0.1,
|
|
36
|
+
topP: 0.1,
|
|
37
|
+
// Set max output tokens to be safe
|
|
38
|
+
maxOutputTokens: 10
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const result = await geminiModel.generateContent(request);
|
|
43
|
+
const response = result.response;
|
|
44
|
+
const text = response.candidates[0].content.parts[0].text.trim();
|
|
45
|
+
|
|
46
|
+
// Robust parsing: Find the keyword, case-insensitive.
|
|
47
|
+
const match = text.match(/(Bullish|Bearish|Neutral)/i);
|
|
48
|
+
if (match && match[0]) {
|
|
49
|
+
// Capitalize first letter, e.g., "bullish" -> "Bullish"
|
|
50
|
+
const sentiment = match[0].charAt(0).toUpperCase() + match[0].slice(1).toLowerCase();
|
|
51
|
+
return sentiment;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
logger.log('WARN', `[getSentimentFromGemini] Unexpected response from AI: "${text}". Defaulting to Neutral.`);
|
|
55
|
+
return 'Neutral';
|
|
56
|
+
|
|
57
|
+
} catch (error) {
|
|
58
|
+
logger.log('ERROR', '[getSentimentFromGemini] Error calling Gemini API.', {
|
|
59
|
+
errorMessage: error.message,
|
|
60
|
+
errorStack: error.stack
|
|
61
|
+
});
|
|
62
|
+
// Default to 'Neutral' on API error to avoid halting the pipeline
|
|
63
|
+
return 'Neutral';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Main pipe (Task Handler): pipe.maintenance.handleSocialTask
|
|
70
|
+
* @param {object} message - The Pub/Sub message.
|
|
71
|
+
* @param {object} context - The message context.
|
|
72
|
+
* @param {object} config - Configuration object.
|
|
73
|
+
* @param {object} dependencies - Contains db, logger, headerManager, proxyManager, geminiModel.
|
|
74
|
+
* @returns {Promise<void>}
|
|
75
|
+
*/
|
|
76
|
+
exports.handleSocialTask = async (message, context, config, dependencies) => {
|
|
77
|
+
// --- MODIFIED: Added geminiModel to dependencies ---
|
|
78
|
+
const { db, logger, headerManager, proxyManager } = dependencies;
|
|
79
|
+
|
|
80
|
+
let task;
|
|
81
|
+
try {
|
|
82
|
+
task = JSON.parse(Buffer.from(message.data, 'base64').toString('utf-8'));
|
|
83
|
+
} catch (e) {
|
|
84
|
+
logger.log('ERROR', '[SocialTask] Failed to parse Pub/Sub message data.', { error: e.message, data: message.data });
|
|
85
|
+
return; // Acknowledge the message
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { tickerId, since } = task;
|
|
89
|
+
const sinceDate = new Date(since);
|
|
90
|
+
const taskId = `social-${tickerId}-${context.eventId || Date.now()}`;
|
|
91
|
+
logger.log('INFO', `[SocialTask/${taskId}] Processing ticker ${tickerId} for posts since ${since}.`);
|
|
92
|
+
|
|
93
|
+
// --- Config validation ---
|
|
94
|
+
if (!config.socialApiBaseUrl || !config.socialInsightsCollectionName || !config.processedPostsCollectionName) {
|
|
95
|
+
logger.log('ERROR', `[SocialTask/${taskId}] Missing required configuration.`);
|
|
96
|
+
throw new Error('Missing required configuration for Social Task.');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const processedPostsRef = db.collection(config.processedPostsCollectionName);
|
|
100
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
101
|
+
const insightsCollectionRef = db.collection(config.socialInsightsCollectionName).doc(today).collection('posts');
|
|
102
|
+
|
|
103
|
+
let offset = 0;
|
|
104
|
+
const take = 10; // hardcode to 10
|
|
105
|
+
let keepFetching = true;
|
|
106
|
+
let postsProcessed = 0;
|
|
107
|
+
let postsSaved = 0;
|
|
108
|
+
const processedInThisRun = new Set(); // Local dedupe
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
while (keepFetching) {
|
|
112
|
+
const url = `${config.socialApiBaseUrl}${tickerId}?take=${take}&offset=${offset}&reactionsPageSize=20`;
|
|
113
|
+
logger.log('TRACE', `[SocialTask/${taskId}] Fetching: ${url}`);
|
|
114
|
+
|
|
115
|
+
const selectedHeader = await headerManager.selectHeader();
|
|
116
|
+
let wasSuccess = false;
|
|
117
|
+
let response;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
response = await proxyManager.fetch(url, { headers: selectedHeader.header });
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
throw new Error(`API error ${response.status}`);
|
|
123
|
+
}
|
|
124
|
+
wasSuccess = true;
|
|
125
|
+
} catch (fetchError) {
|
|
126
|
+
logger.log('WARN', `[SocialTask/${taskId}] Fetch failed for offset ${offset}.`, { err: fetchError.message });
|
|
127
|
+
keepFetching = false; // Stop on fetch error
|
|
128
|
+
if (selectedHeader) headerManager.updatePerformance(selectedHeader.id, false);
|
|
129
|
+
continue;
|
|
130
|
+
} finally {
|
|
131
|
+
if (selectedHeader) headerManager.updatePerformance(selectedHeader.id, wasSuccess);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const page = await response.json();
|
|
135
|
+
const discussions = page?.discussions;
|
|
136
|
+
|
|
137
|
+
if (!Array.isArray(discussions) || discussions.length === 0) {
|
|
138
|
+
logger.log('INFO', `[SocialTask/${taskId}] No more posts found at offset ${offset}. Stopping.`);
|
|
139
|
+
keepFetching = false;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const postIds = discussions.map(d => d.post.id).filter(Boolean);
|
|
144
|
+
if (postIds.length === 0) {
|
|
145
|
+
offset += take;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- Deduplication Check ---
|
|
150
|
+
const existingDocs = await processedPostsRef.where(FieldPath.documentId(), 'in', postIds).get();
|
|
151
|
+
const existingIds = new Set(existingDocs.docs.map(d => d.id));
|
|
152
|
+
|
|
153
|
+
const batch = db.batch();
|
|
154
|
+
let newPostsInBatch = 0;
|
|
155
|
+
|
|
156
|
+
for (const discussion of discussions) {
|
|
157
|
+
const post = discussion?.post;
|
|
158
|
+
if (!post || !post.id || !post.message?.text) continue;
|
|
159
|
+
|
|
160
|
+
// Stop pagination if we've reached posts older than our window
|
|
161
|
+
const postCreatedDate = new Date(post.created);
|
|
162
|
+
if (postCreatedDate < sinceDate) {
|
|
163
|
+
keepFetching = false;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Skip if already processed
|
|
168
|
+
if (existingIds.has(post.id) || processedInThisRun.has(post.id)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Filter language (accept 'en' and 'en-gb')
|
|
173
|
+
const lang = post.message.languageCode || 'unknown';
|
|
174
|
+
if (lang !== 'en' && lang !== 'en-gb') {
|
|
175
|
+
continue; // Skip non-English posts
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- Process the new post ---
|
|
179
|
+
postsProcessed++;
|
|
180
|
+
processedInThisRun.add(post.id);
|
|
181
|
+
const text = post.message.text;
|
|
182
|
+
|
|
183
|
+
// 1. Truncate for AI
|
|
184
|
+
const MAX_CHARS = 500; // ~125 tokens, very cheap
|
|
185
|
+
let snippet = text;
|
|
186
|
+
if (text.length > (MAX_CHARS * 2)) {
|
|
187
|
+
// Create a "summary" snippet
|
|
188
|
+
snippet = text.substring(0, MAX_CHARS) + " ... " + text.substring(text.length - MAX_CHARS);
|
|
189
|
+
} else if (text.length > MAX_CHARS) {
|
|
190
|
+
// Just truncate
|
|
191
|
+
snippet = text.substring(0, MAX_CHARS);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 2. AI Sentiment Analysis
|
|
195
|
+
// Pass full dependencies object
|
|
196
|
+
const sentiment = await getSentimentFromGemini(dependencies, snippet);
|
|
197
|
+
|
|
198
|
+
// 3. Prepare data for storage
|
|
199
|
+
const postData = {
|
|
200
|
+
sentiment: sentiment,
|
|
201
|
+
textSnippet: snippet,
|
|
202
|
+
language: lang,
|
|
203
|
+
tickers: post.tags.map(t => t.market?.symbolName).filter(Boolean),
|
|
204
|
+
postOwnerId: post.owner?.id,
|
|
205
|
+
createdAt: post.created,
|
|
206
|
+
fetchedAt: FieldValue.serverTimestamp()
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// 4. Add to batch for `daily_social_insights`
|
|
210
|
+
const insightDocRef = insightsCollectionRef.doc(post.id);
|
|
211
|
+
batch.set(insightDocRef, postData);
|
|
212
|
+
|
|
213
|
+
// 5. Add to batch for `processed_social_posts` (dedupe collection)
|
|
214
|
+
const dedupeDocRef = processedPostsRef.doc(post.id);
|
|
215
|
+
batch.set(dedupeDocRef, { processedAt: FieldValue.serverTimestamp() });
|
|
216
|
+
|
|
217
|
+
newPostsInBatch++;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (newPostsInBatch > 0) {
|
|
221
|
+
await batch.commit();
|
|
222
|
+
postsSaved += newPostsInBatch;
|
|
223
|
+
logger.log('INFO', `[SocialTask/${taskId}] Saved ${newPostsInBatch} new posts from offset ${offset}.`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Continue to next page
|
|
227
|
+
offset += take;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
logger.log('SUCCESS', `[SocialTask/${taskId}] Run complete. Processed ${postsProcessed} new posts, saved ${postsSaved}.`);
|
|
231
|
+
|
|
232
|
+
} catch (error) {
|
|
233
|
+
logger.log('ERROR', `[SocialTask/${taskId}] Fatal error during task execution.`, { errorMessage: error.message, errorStack: error.stack });
|
|
234
|
+
throw error;
|
|
235
|
+
} finally {
|
|
236
|
+
// Always flush header performance
|
|
237
|
+
try {
|
|
238
|
+
await headerManager.flushPerformanceUpdates();
|
|
239
|
+
} catch (flushError) {
|
|
240
|
+
logger.log('ERROR', `[SocialTask/${taskId}] Failed to flush header performance.`, { errorMessage: flushError.message });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
@@ -5,6 +5,36 @@
|
|
|
5
5
|
* - handleSampleBlockTask: Processes a single block with parallel fetching.
|
|
6
6
|
*/
|
|
7
7
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
8
|
+
const pLimit = require('p-limit'); // npm install p-limit
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper: delay
|
|
12
|
+
*/
|
|
13
|
+
function delay(ms) {
|
|
14
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Helper: fetch with retry and exponential backoff on 429
|
|
19
|
+
*/
|
|
20
|
+
async function fetchWithRetry(fetchFn, maxRetries = 5, logger, taskId) {
|
|
21
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetchFn();
|
|
24
|
+
if (response.status !== 429) return response;
|
|
25
|
+
|
|
26
|
+
const backoff = 1000 * Math.pow(2, attempt); // 1s, 2s, 4s, 8s...
|
|
27
|
+
logger.log('WARN', `[SamplerTask/${taskId}] Received 429. Retrying in ${backoff}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
28
|
+
await delay(backoff);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
logger.log('ERROR', `[SamplerTask/${taskId}] Fetch failed on attempt ${attempt + 1}.`, { errorMessage: err.message });
|
|
31
|
+
if (attempt === maxRetries) throw err;
|
|
32
|
+
const backoff = 500 * Math.pow(2, attempt); // slightly faster backoff for network errors
|
|
33
|
+
await delay(backoff);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
throw new Error('Max retries reached for fetchWithRetry');
|
|
37
|
+
}
|
|
8
38
|
|
|
9
39
|
/**
|
|
10
40
|
* Main pipe (Orchestrator): pipe.maintenance.runUserActivitySamplerOrchestrator
|
|
@@ -139,25 +169,17 @@ async function fetchSampleBatch(blockId, config, dependencies, processedInThisRu
|
|
|
139
169
|
}
|
|
140
170
|
|
|
141
171
|
/**
|
|
142
|
-
*
|
|
143
|
-
* This function is triggered by Pub/Sub for a single block.
|
|
144
|
-
* It runs the sampling in parallel to finish within timeout.
|
|
145
|
-
*
|
|
146
|
-
* @param {object} message - The Pub/Sub message.
|
|
147
|
-
* @param {object} context - The message context.
|
|
148
|
-
* @param {object} config - Configuration object.
|
|
149
|
-
* @param {object} dependencies - Contains db, logger, headerManager, proxyManager.
|
|
150
|
-
* @returns {Promise<void>}
|
|
172
|
+
* Updated Task Handler: handleSampleBlockTask
|
|
151
173
|
*/
|
|
152
174
|
exports.handleSampleBlockTask = async (message, context, config, dependencies) => {
|
|
153
|
-
const { db, logger, headerManager } = dependencies;
|
|
154
|
-
|
|
175
|
+
const { db, logger, headerManager, proxyManager } = dependencies;
|
|
176
|
+
|
|
155
177
|
let task;
|
|
156
178
|
try {
|
|
157
179
|
task = JSON.parse(Buffer.from(message.data, 'base64').toString('utf-8'));
|
|
158
180
|
} catch (e) {
|
|
159
181
|
logger.log('ERROR', '[SamplerTask] Failed to parse Pub/Sub message data.', { error: e.message, data: message.data });
|
|
160
|
-
return;
|
|
182
|
+
return;
|
|
161
183
|
}
|
|
162
184
|
|
|
163
185
|
const { blockId } = task;
|
|
@@ -165,7 +187,7 @@ exports.handleSampleBlockTask = async (message, context, config, dependencies) =
|
|
|
165
187
|
const today = new Date().toISOString().slice(0, 10);
|
|
166
188
|
logger.log('INFO', `[SamplerTask/${taskId}] Processing block ${blockId}...`);
|
|
167
189
|
|
|
168
|
-
//
|
|
190
|
+
// Config validation
|
|
169
191
|
if (!config.rankingsApiUrl || !config.targetPublicUsersPerBlock || !config.apiBatchSize || !config.outputCollectionName || !config.parallelRequests) {
|
|
170
192
|
logger.log('ERROR', `[SamplerTask/${taskId}] Missing required configuration for task execution.`);
|
|
171
193
|
throw new Error('Missing required configuration for Sampler Task.');
|
|
@@ -174,27 +196,37 @@ exports.handleSampleBlockTask = async (message, context, config, dependencies) =
|
|
|
174
196
|
const processedInThisRun = new Set();
|
|
175
197
|
let N_public_sampled_block = 0;
|
|
176
198
|
let N_sampled_total_block = 0;
|
|
177
|
-
const public_users_data = [];
|
|
178
|
-
|
|
199
|
+
const public_users_data = [];
|
|
179
200
|
const MAX_ATTEMPTS = config.maxSamplingAttemptsPerBlock || 1000;
|
|
180
201
|
let totalBatchesAttempted = 0;
|
|
181
|
-
|
|
202
|
+
|
|
203
|
+
const limit = pLimit(config.parallelRequests || 3);
|
|
182
204
|
|
|
183
205
|
try {
|
|
184
|
-
// --- Start Parallel Loop ---
|
|
185
206
|
while (N_public_sampled_block < config.targetPublicUsersPerBlock && totalBatchesAttempted < MAX_ATTEMPTS) {
|
|
207
|
+
const numRequests = Math.min(config.parallelRequests, MAX_ATTEMPTS - totalBatchesAttempted);
|
|
186
208
|
const promises = [];
|
|
187
|
-
const numRequests = Math.min(CONCURRENT_REQUESTS, MAX_ATTEMPTS - totalBatchesAttempted);
|
|
188
209
|
|
|
189
|
-
logger.log('TRACE', `[SamplerTask/${taskId}] Starting parallel batch of ${numRequests} requests...`);
|
|
190
210
|
for (let i = 0; i < numRequests; i++) {
|
|
191
|
-
promises.push(
|
|
211
|
+
promises.push(limit(async () => {
|
|
212
|
+
// Wrap fetchSampleBatch to include fetchWithRetry
|
|
213
|
+
async function fetchBatchWithRetry() {
|
|
214
|
+
return await fetchSampleBatch(blockId, config, {
|
|
215
|
+
...dependencies,
|
|
216
|
+
proxyManager: {
|
|
217
|
+
fetch: (url, options) =>
|
|
218
|
+
fetchWithRetry(() => proxyManager.fetch(url, options), 5, logger, taskId)
|
|
219
|
+
}
|
|
220
|
+
}, processedInThisRun);
|
|
221
|
+
}
|
|
222
|
+
return await fetchBatchWithRetry();
|
|
223
|
+
}));
|
|
192
224
|
}
|
|
193
225
|
|
|
194
226
|
const results = await Promise.allSettled(promises);
|
|
195
227
|
totalBatchesAttempted += numRequests;
|
|
196
228
|
|
|
197
|
-
// Process results
|
|
229
|
+
// Process results
|
|
198
230
|
for (const result of results) {
|
|
199
231
|
if (result.status === 'fulfilled' && result.value.success) {
|
|
200
232
|
const batchResult = result.value;
|
|
@@ -202,27 +234,21 @@ exports.handleSampleBlockTask = async (message, context, config, dependencies) =
|
|
|
202
234
|
N_public_sampled_block += batchResult.publicUsers.length;
|
|
203
235
|
public_users_data.push(...batchResult.publicUsers);
|
|
204
236
|
} else if (result.status === 'fulfilled' && !result.value.success) {
|
|
205
|
-
// Failed API call, but we still count the CIDs we tried to sample
|
|
206
237
|
N_sampled_total_block += result.value.cidsSent;
|
|
207
238
|
} else {
|
|
208
|
-
// Promise rejected (unexpected error)
|
|
209
239
|
logger.log('WARN', `[SamplerTask/${taskId}] A sample fetch promise was rejected.`, { reason: result.reason });
|
|
210
240
|
}
|
|
211
241
|
}
|
|
212
|
-
|
|
213
|
-
logger.log('INFO', `[SamplerTask/${taskId}] Batch complete. Total public sampled: ${N_public_sampled_block}/${config.targetPublicUsersPerBlock}`);
|
|
214
242
|
|
|
215
|
-
|
|
243
|
+
logger.log('INFO', `[SamplerTask/${taskId}] Batch complete. Total public sampled: ${N_public_sampled_block}/${config.targetPublicUsersPerBlock}`);
|
|
216
244
|
}
|
|
217
|
-
// --- End Parallel Loop ---
|
|
218
245
|
|
|
219
246
|
if (totalBatchesAttempted >= MAX_ATTEMPTS) {
|
|
220
247
|
logger.log('WARN', `[SamplerTask/${taskId}] Reached max sampling attempts (${MAX_ATTEMPTS}). Proceeding with ${N_public_sampled_block} users.`);
|
|
221
248
|
}
|
|
222
249
|
|
|
223
|
-
// --- Calculate and Store Results
|
|
250
|
+
// --- Calculate and Store Results ---
|
|
224
251
|
const f_private_block = N_sampled_total_block > 0 ? 1 - (N_public_sampled_block / N_sampled_total_block) : 0;
|
|
225
|
-
|
|
226
252
|
const counts = { A1: 0, A2: 0, A3: 0, A4: 0 };
|
|
227
253
|
const now = new Date();
|
|
228
254
|
const oneDayAgo = new Date(now.getTime() - (24 * 60 * 60 * 1000));
|
|
@@ -230,9 +256,7 @@ exports.handleSampleBlockTask = async (message, context, config, dependencies) =
|
|
|
230
256
|
const threeMonthsAgo = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000));
|
|
231
257
|
|
|
232
258
|
public_users_data.forEach(user => {
|
|
233
|
-
if (!user.LastActivity) {
|
|
234
|
-
counts.A4++; return;
|
|
235
|
-
}
|
|
259
|
+
if (!user.LastActivity) { counts.A4++; return; }
|
|
236
260
|
try {
|
|
237
261
|
const lastActivityDate = new Date(user.LastActivity);
|
|
238
262
|
if (isNaN(lastActivityDate)) { counts.A4++; return; }
|
|
@@ -240,25 +264,20 @@ exports.handleSampleBlockTask = async (message, context, config, dependencies) =
|
|
|
240
264
|
else if (lastActivityDate >= oneWeekAgo) counts.A2++;
|
|
241
265
|
else if (lastActivityDate >= threeMonthsAgo) counts.A3++;
|
|
242
266
|
else counts.A4++;
|
|
243
|
-
} catch
|
|
244
|
-
logger.log('WARN', `[SamplerTask/${taskId}] Error parsing LastActivity date '${user.LastActivity}' for user ${user.CID}. Counting as A4.`);
|
|
267
|
+
} catch {
|
|
245
268
|
counts.A4++;
|
|
246
269
|
}
|
|
247
270
|
});
|
|
248
271
|
|
|
249
272
|
const fractions = {};
|
|
250
|
-
for (const category in counts)
|
|
251
|
-
fractions[category] = N_public_sampled_block > 0 ? (counts[category] / N_public_sampled_block) : 0;
|
|
252
|
-
}
|
|
273
|
+
for (const category in counts) fractions[category] = N_public_sampled_block > 0 ? (counts[category] / N_public_sampled_block) : 0;
|
|
253
274
|
|
|
254
275
|
const N_block = 1000000;
|
|
255
276
|
const estimatedCounts = {};
|
|
256
|
-
for (const category in fractions)
|
|
257
|
-
estimatedCounts[category] = Math.round(fractions[category] * N_block);
|
|
258
|
-
}
|
|
277
|
+
for (const category in fractions) estimatedCounts[category] = Math.round(fractions[category] * N_block);
|
|
259
278
|
|
|
260
279
|
const result = {
|
|
261
|
-
blockId
|
|
280
|
+
blockId,
|
|
262
281
|
sampledDate: today,
|
|
263
282
|
f_private: f_private_block,
|
|
264
283
|
publicSampleSize: N_public_sampled_block,
|
|
@@ -272,15 +291,12 @@ exports.handleSampleBlockTask = async (message, context, config, dependencies) =
|
|
|
272
291
|
|
|
273
292
|
const docRef = db.collection(config.outputCollectionName).doc(`${blockId}_${today}`);
|
|
274
293
|
await docRef.set(result);
|
|
275
|
-
|
|
276
294
|
logger.log('SUCCESS', `[SamplerTask/${taskId}] Stored results for block ${blockId}. Sampled: ${N_public_sampled_block}. Private fraction: ${f_private_block.toFixed(3)}.`);
|
|
277
295
|
|
|
278
296
|
} catch (error) {
|
|
279
297
|
logger.log('ERROR', `[SamplerTask/${taskId}] Fatal error during task execution.`, { errorMessage: error.message, errorStack: error.stack });
|
|
280
|
-
// Re-throw the error to signal failure to Cloud Functions, which will trigger a retry.
|
|
281
298
|
throw error;
|
|
282
299
|
} finally {
|
|
283
|
-
// Always flush header performance at the end of the task, even on failure.
|
|
284
300
|
try {
|
|
285
301
|
await headerManager.flushPerformanceUpdates();
|
|
286
302
|
logger.log('INFO', `[SamplerTask/${taskId}] Header performance flushed.`);
|
package/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// --- Core Utilities (Classes and Stateless Helpers) ---
|
|
8
|
+
// ... (no changes here) ...
|
|
8
9
|
const core = {
|
|
9
10
|
IntelligentHeaderManager: require('./functions/core/utils/intelligent_header_manager').IntelligentHeaderManager,
|
|
10
11
|
IntelligentProxyManager: require('./functions/core/utils/intelligent_proxy_manager').IntelligentProxyManager,
|
|
@@ -14,6 +15,7 @@ const core = {
|
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
// --- Pipe 1: Orchestrator ---
|
|
18
|
+
// ... (no changes here) ...
|
|
17
19
|
const orchestrator = {
|
|
18
20
|
// Main Pipes (Entry points for Cloud Functions)
|
|
19
21
|
runDiscoveryOrchestrator: require('./functions/orchestrator/index').runDiscoveryOrchestrator,
|
|
@@ -30,6 +32,7 @@ const orchestrator = {
|
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
// --- Pipe 2: Dispatcher ---
|
|
35
|
+
// ... (no changes here) ...
|
|
33
36
|
const dispatcher = {
|
|
34
37
|
// Main Pipe
|
|
35
38
|
handleRequest: require('./functions/dispatcher/index').handleRequest,
|
|
@@ -39,6 +42,7 @@ const dispatcher = {
|
|
|
39
42
|
};
|
|
40
43
|
|
|
41
44
|
// --- Pipe 3: Task Engine ---
|
|
45
|
+
// ... (no changes here) ...
|
|
42
46
|
const taskEngine = {
|
|
43
47
|
// Main Pipe
|
|
44
48
|
handleRequest: require('./functions/task-engine/handler_creator').handleRequest,
|
|
@@ -50,6 +54,7 @@ const taskEngine = {
|
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
// --- Pipe 4: Computation System ---
|
|
57
|
+
// ... (no changes here) ...
|
|
53
58
|
const computationSystem = {
|
|
54
59
|
// Main Pipe
|
|
55
60
|
runOrchestration: require('./functions/computation-system/helpers/orchestration_helpers').runComputationOrchestrator,
|
|
@@ -60,6 +65,7 @@ const computationSystem = {
|
|
|
60
65
|
};
|
|
61
66
|
|
|
62
67
|
// --- Pipe 5: API ---
|
|
68
|
+
// ... (no changes here) ...
|
|
63
69
|
const api = {
|
|
64
70
|
// Main Pipe
|
|
65
71
|
createApiApp: require('./functions/generic-api/index').createApiApp,
|
|
@@ -80,9 +86,15 @@ const maintenance = {
|
|
|
80
86
|
runUserActivitySamplerOrchestrator: require('./functions/user-activity-sampler/helpers/sampler_helpers').runUserActivitySamplerOrchestrator,
|
|
81
87
|
handleSampleBlockTask: require('./functions/user-activity-sampler/helpers/sampler_helpers').handleSampleBlockTask,
|
|
82
88
|
// --- END UPDATE ---
|
|
89
|
+
|
|
90
|
+
// --- NEW SOCIAL SENTIMENT FUNCTIONS ---
|
|
91
|
+
runSocialOrchestrator: require('./functions/social-orchestrator/helpers/orchestrator_helpers').runSocialOrchestrator,
|
|
92
|
+
handleSocialTask: require('./functions/social-task-handler/helpers/handler_helpers').handleSocialTask,
|
|
93
|
+
// --- END NEW ---
|
|
83
94
|
};
|
|
84
95
|
|
|
85
96
|
// --- Pipe 7: Proxy ---
|
|
97
|
+
// ... (no changes here) ...
|
|
86
98
|
const proxy = {
|
|
87
99
|
handlePost: require('./functions/appscript-api/index').handlePost,
|
|
88
100
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bulltrackers-module",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.70",
|
|
4
4
|
"description": "Helper Functions for Bulltrackers.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"functions/fetch-insights/",
|
|
17
17
|
"functions/etoro-price-fetcher/",
|
|
18
18
|
"functions/appscript-api/",
|
|
19
|
-
"functions/user-activity-sampler/"
|
|
19
|
+
"functions/user-activity-sampler/",
|
|
20
|
+
"functions/social-orchestrator/",
|
|
21
|
+
"functions/social-task-handler/"
|
|
20
22
|
],
|
|
21
23
|
"keywords": [
|
|
22
24
|
"bulltrackers",
|
|
@@ -32,7 +34,8 @@
|
|
|
32
34
|
"aiden-shared-calculations-unified": "1.0.0",
|
|
33
35
|
"@google-cloud/pubsub": "latest",
|
|
34
36
|
"express": "^4.19.2",
|
|
35
|
-
"cors": "^2.8.5"
|
|
37
|
+
"cors": "^2.8.5",
|
|
38
|
+
"p-limit": "latest"
|
|
36
39
|
},
|
|
37
40
|
"engines": {
|
|
38
41
|
"node": ">=20"
|