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.
@@ -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 loadDailyInsights ---
10
- const { getPortfolioPartRefs, loadFullDayMap, loadDataByRefs, loadDailyInsights } = require('../utils/data_loader.js');
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
- // --- NEW: Add insights calculations to the master lists ---
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
- const masterFullList = [...masterHistoricalList, ...masterDailyList, ...masterInsightsList];
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 => ({ date, missing: [...masterDailyList, ...masterInsightsList] }));
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 & insights calcs).`);
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 insights calcs for Pass 1 run ---
65
- const pass1SourcePackage = { ...dailyCalculations, insights: insightsCalculations };
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 & Insights)', pass1SourcePackage, config, dependencies),
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 and insights separately or combined ---
109
- const dailyAndInsightsMissing = [...dailyMissing, ...insightsMissing];
110
- if (dailyAndInsightsMissing.length > 0) {
111
- logger.log('INFO', `[Pass 2] Running ${dailyAndInsightsMissing.length} daily/insights calcs for ${date.toISOString().slice(0, 10)}.`);
112
- const dailyAndInsightsSourcePackage = { ...dailyCalculations, insights: insightsCalculations };
113
- const dailyResult = await runUnifiedComputation(date, dailyAndInsightsMissing, 'Pass 2 (Daily/Insights)', dailyAndInsightsSourcePackage, config, dependencies);
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 todayInsights and yesterdayInsights ---
179
+ * --- MODIFIED: Added today/yesterday social post insights ---
153
180
  */
154
- async function streamAndProcess(dateStr, todayRefs, state, passName, config, dependencies, yesterdayPortfolios = {}, todayInsights = null, yesterdayInsights = null) {
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
- // --- MODIFIED: Handle insights calculations ---
183
- if (category === 'insights') {
184
- // Only process insights once per day, using the first user as a trigger
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, context, todayInsights, yesterdayInsights];
227
+ processArgs = [null, null, null, ...allContextArgs];
187
228
  } else {
188
- continue; // Skip insights processing for subsequent users
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, context, todayInsights, yesterdayInsights]; // Pass insights too
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, context, todayInsights, yesterdayInsights]; // Pass null for yesterdayPortfolio, add insights
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
- // --- NEW: Load today's insights ---
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 data OR insights data found for ${dateStr}. Skipping.`);
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; // NEW: Variable for yesterday's insights
246
- // --- MODIFIED: Check if *any* calc requires yesterday data (portfolio OR insights) ---
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'); // Insights calcs always need yesterday
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
- const prev = new Date(dateToProcess);
253
- prev.setUTCDate(prev.getUTCDate() - 1);
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++; // Try the previous day
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 MODIFIED ---
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(dateStr, todayRefs, state, passName, config, dependencies, yesterdayPortfolios, todayInsightsData, yesterdayInsightsData);
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
- * --- NEW ---
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, // Export the new function
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
- * Main pipe (Task Handler): pipe.maintenance.handleSampleBlockTask
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; // Acknowledge the message to prevent retries
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
- // --- Config validation for this task ---
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 = []; // Store { CID, LastActivity }
178
-
199
+ const public_users_data = [];
179
200
  const MAX_ATTEMPTS = config.maxSamplingAttemptsPerBlock || 1000;
180
201
  let totalBatchesAttempted = 0;
181
- const CONCURRENT_REQUESTS = config.parallelRequests;
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(fetchSampleBatch(blockId, config, dependencies, processedInThisRun));
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 from the parallel batch
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
- // --- REMOVED artificial delay ---
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 (same as before) ---
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 (e) {
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: 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.68",
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"