bulltrackers-module 1.0.480 → 1.0.490
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/computation_worker.js +45 -0
- package/functions/generic-api/user-api/helpers/notification_helpers.js +131 -0
- package/functions/generic-api/user-api/helpers/user_sync_helpers.js +102 -5
- package/functions/task-engine/helpers/popular_investor_helpers.js +23 -1
- package/package.json +1 -1
|
@@ -231,6 +231,28 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
231
231
|
|
|
232
232
|
await db.doc(ledgerPath).update({ status: 'COMPLETED', completedAt: new Date() });
|
|
233
233
|
await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', null, metrics, triggerReason, resourceTier);
|
|
234
|
+
|
|
235
|
+
// Send notification if this was an on-demand computation
|
|
236
|
+
if (metadata?.onDemand && metadata?.requestId && metadata?.requestingUserCid) {
|
|
237
|
+
try {
|
|
238
|
+
const { notifyComputationComplete, getComputationDisplayName } = require('../../generic-api/user-api/helpers/notification_helpers');
|
|
239
|
+
await notifyComputationComplete(
|
|
240
|
+
dependencies.db,
|
|
241
|
+
dependencies.logger,
|
|
242
|
+
metadata.requestingUserCid,
|
|
243
|
+
metadata.requestId,
|
|
244
|
+
computation,
|
|
245
|
+
getComputationDisplayName(computation),
|
|
246
|
+
true,
|
|
247
|
+
null
|
|
248
|
+
);
|
|
249
|
+
} catch (notifError) {
|
|
250
|
+
// Non-critical, log and continue
|
|
251
|
+
if (dependencies.logger) {
|
|
252
|
+
dependencies.logger.log('WARN', `[Worker] Failed to send completion notification for ${computation}`, notifError);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
234
256
|
|
|
235
257
|
} catch (err) {
|
|
236
258
|
heartbeats.stop();
|
|
@@ -257,6 +279,29 @@ async function handleComputationTask(message, config, dependencies) {
|
|
|
257
279
|
}, { merge: true });
|
|
258
280
|
|
|
259
281
|
await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', { message: err.message, stage: err.stage || 'FATAL' }, { peakMemoryMB: heartbeats.getPeak() }, triggerReason, resourceTier);
|
|
282
|
+
|
|
283
|
+
// Send error notification if this was an on-demand computation
|
|
284
|
+
if (metadata?.onDemand && metadata?.requestId && metadata?.requestingUserCid) {
|
|
285
|
+
try {
|
|
286
|
+
const { notifyComputationComplete, getComputationDisplayName } = require('../../generic-api/user-api/helpers/notification_helpers');
|
|
287
|
+
await notifyComputationComplete(
|
|
288
|
+
db,
|
|
289
|
+
dependencies.logger,
|
|
290
|
+
metadata.requestingUserCid,
|
|
291
|
+
metadata.requestId,
|
|
292
|
+
computation,
|
|
293
|
+
getComputationDisplayName(computation),
|
|
294
|
+
false,
|
|
295
|
+
err.message
|
|
296
|
+
);
|
|
297
|
+
} catch (notifError) {
|
|
298
|
+
// Non-critical, log and continue
|
|
299
|
+
if (dependencies.logger) {
|
|
300
|
+
dependencies.logger.log('WARN', `[Worker] Failed to send error notification for ${computation}`, notifError);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
260
305
|
return;
|
|
261
306
|
}
|
|
262
307
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Notification Helpers for On-Demand Requests
|
|
3
|
+
* Sends notifications to users when their on-demand sync/computation requests complete
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { FieldValue } = require('@google-cloud/firestore');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Send a notification to a user about their on-demand request
|
|
10
|
+
* @param {Firestore} db - Firestore instance
|
|
11
|
+
* @param {Object} logger - Logger instance
|
|
12
|
+
* @param {number} userCid - User CID to notify
|
|
13
|
+
* @param {string} type - Notification type ('success', 'error', 'progress')
|
|
14
|
+
* @param {string} title - Notification title
|
|
15
|
+
* @param {string} message - Notification message
|
|
16
|
+
* @param {Object} metadata - Additional metadata (requestId, computationName, etc.)
|
|
17
|
+
*/
|
|
18
|
+
async function sendOnDemandNotification(db, logger, userCid, type, title, message, metadata = {}) {
|
|
19
|
+
try {
|
|
20
|
+
const notificationId = `ondemand_${Date.now()}_${userCid}_${Math.random().toString(36).substring(2, 9)}`;
|
|
21
|
+
const notificationRef = db.collection('user_notifications')
|
|
22
|
+
.doc(String(userCid))
|
|
23
|
+
.collection('notifications')
|
|
24
|
+
.doc(notificationId);
|
|
25
|
+
|
|
26
|
+
const notificationData = {
|
|
27
|
+
id: notificationId,
|
|
28
|
+
type: 'on_demand',
|
|
29
|
+
subType: type, // 'success', 'error', 'progress'
|
|
30
|
+
title,
|
|
31
|
+
message,
|
|
32
|
+
read: false,
|
|
33
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
34
|
+
metadata: {
|
|
35
|
+
...metadata,
|
|
36
|
+
userCid: Number(userCid)
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
await notificationRef.set(notificationData);
|
|
41
|
+
|
|
42
|
+
// Update notification counter
|
|
43
|
+
const today = new Date().toISOString().split('T')[0];
|
|
44
|
+
const counterRef = db.collection('user_notifications')
|
|
45
|
+
.doc(String(userCid))
|
|
46
|
+
.collection('counters')
|
|
47
|
+
.doc(today);
|
|
48
|
+
|
|
49
|
+
await counterRef.set({
|
|
50
|
+
date: today,
|
|
51
|
+
unreadCount: FieldValue.increment(1),
|
|
52
|
+
totalCount: FieldValue.increment(1),
|
|
53
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
54
|
+
}, { merge: true });
|
|
55
|
+
|
|
56
|
+
logger.log('INFO', `[sendOnDemandNotification] Sent ${type} notification to user ${userCid}: ${title}`);
|
|
57
|
+
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.log('ERROR', `[sendOnDemandNotification] Failed to send notification to user ${userCid}`, error);
|
|
60
|
+
// Don't throw - notifications are non-critical
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Send notification when task engine completes data fetch
|
|
66
|
+
*/
|
|
67
|
+
async function notifyTaskEngineComplete(db, logger, requestingUserCid, requestId, username, success, error = null) {
|
|
68
|
+
if (!requestingUserCid) return; // No user to notify
|
|
69
|
+
|
|
70
|
+
const type = success ? 'success' : 'error';
|
|
71
|
+
const title = success
|
|
72
|
+
? 'Data Sync Complete'
|
|
73
|
+
: 'Data Sync Failed';
|
|
74
|
+
const message = success
|
|
75
|
+
? `Your data sync for ${username} has completed. Portfolio, history, and social data have been stored.`
|
|
76
|
+
: (error || 'An error occurred while syncing your data. Please try again.');
|
|
77
|
+
|
|
78
|
+
await sendOnDemandNotification(db, logger, requestingUserCid, type, title, message, {
|
|
79
|
+
requestId,
|
|
80
|
+
username,
|
|
81
|
+
stage: 'task_engine',
|
|
82
|
+
success
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Send notification when computation completes
|
|
88
|
+
*/
|
|
89
|
+
async function notifyComputationComplete(db, logger, requestingUserCid, requestId, computationName, displayName, success, error = null) {
|
|
90
|
+
if (!requestingUserCid) return; // No user to notify
|
|
91
|
+
|
|
92
|
+
const type = success ? 'success' : 'error';
|
|
93
|
+
const title = success
|
|
94
|
+
? 'Computation Complete'
|
|
95
|
+
: 'Computation Failed';
|
|
96
|
+
const message = success
|
|
97
|
+
? `${displayName || computationName} has been computed and stored.`
|
|
98
|
+
: (error || `Failed to compute ${displayName || computationName}. Please try again.`);
|
|
99
|
+
|
|
100
|
+
await sendOnDemandNotification(db, logger, requestingUserCid, type, title, message, {
|
|
101
|
+
requestId,
|
|
102
|
+
computationName,
|
|
103
|
+
displayName,
|
|
104
|
+
stage: 'computation',
|
|
105
|
+
success
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get human-readable name for a computation
|
|
111
|
+
*/
|
|
112
|
+
function getComputationDisplayName(computationName) {
|
|
113
|
+
const displayNames = {
|
|
114
|
+
'SignedInUserProfileMetrics': 'Profile Metrics',
|
|
115
|
+
'SignedInUserCopiedList': 'Copied Investors List',
|
|
116
|
+
'SignedInUserCopiedPIs': 'Copied Popular Investors',
|
|
117
|
+
'SignedInUserPastCopies': 'Past Copy History',
|
|
118
|
+
'PopularInvestorProfileMetrics': 'Popular Investor Profile',
|
|
119
|
+
'SignedInUserPIPersonalizedMetrics': 'Personalized Metrics'
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return displayNames[computationName] || computationName;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
sendOnDemandNotification,
|
|
127
|
+
notifyTaskEngineComplete,
|
|
128
|
+
notifyComputationComplete,
|
|
129
|
+
getComputationDisplayName
|
|
130
|
+
};
|
|
131
|
+
|
|
@@ -163,11 +163,16 @@ async function requestUserSync(req, res, dependencies, config) {
|
|
|
163
163
|
requestId,
|
|
164
164
|
requestedBy: Number(requestingUserCid),
|
|
165
165
|
effectiveRequestedBy: effectiveCid,
|
|
166
|
+
data: {
|
|
167
|
+
includeSocial: true, // Always include social data for on-demand syncs
|
|
168
|
+
since: new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString() // Last 7 days
|
|
169
|
+
},
|
|
166
170
|
metadata: {
|
|
167
171
|
onDemand: true,
|
|
168
172
|
targetCid: targetCidNum, // Target specific user for optimization
|
|
169
173
|
requestedAt: now.toISOString(),
|
|
170
|
-
isImpersonating: isImpersonating || false
|
|
174
|
+
isImpersonating: isImpersonating || false,
|
|
175
|
+
requestingUserCid: Number(requestingUserCid) // Store for notifications
|
|
171
176
|
}
|
|
172
177
|
};
|
|
173
178
|
|
|
@@ -245,12 +250,15 @@ async function getUserSyncStatus(req, res, dependencies, config) {
|
|
|
245
250
|
const compsSub = config.computationsSubcollection || 'computations';
|
|
246
251
|
|
|
247
252
|
// Determine category and computation name based on user type
|
|
253
|
+
// NOTE: ResultCommitter ignores category metadata if computation is in a non-core folder
|
|
254
|
+
// SignedInUserProfileMetrics is in popular-investor folder, so it's stored in popular-investor category
|
|
248
255
|
let category, computationName;
|
|
249
256
|
if (userType === 'POPULAR_INVESTOR') {
|
|
250
257
|
category = 'popular-investor';
|
|
251
258
|
computationName = 'PopularInvestorProfileMetrics';
|
|
252
259
|
} else {
|
|
253
|
-
|
|
260
|
+
// SignedInUserProfileMetrics is in popular-investor folder, so stored in popular-investor category
|
|
261
|
+
category = 'popular-investor';
|
|
254
262
|
computationName = 'SignedInUserProfileMetrics';
|
|
255
263
|
}
|
|
256
264
|
|
|
@@ -318,7 +326,8 @@ async function getUserSyncStatus(req, res, dependencies, config) {
|
|
|
318
326
|
}
|
|
319
327
|
|
|
320
328
|
// Check if request is stale (stuck in processing state for too long)
|
|
321
|
-
|
|
329
|
+
// Increased timeout to 15 minutes to account for computation time
|
|
330
|
+
const STALE_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes
|
|
322
331
|
const processingStates = ['processing', 'dispatched', 'indexing', 'computing', 'queued'];
|
|
323
332
|
const isProcessingState = processingStates.includes(status);
|
|
324
333
|
|
|
@@ -336,8 +345,22 @@ async function getUserSyncStatus(req, res, dependencies, config) {
|
|
|
336
345
|
const referenceTime = dispatchedAt || createdAt || updatedAt;
|
|
337
346
|
|
|
338
347
|
if (referenceTime && (now - referenceTime) > STALE_THRESHOLD_MS) {
|
|
339
|
-
|
|
340
|
-
|
|
348
|
+
// Before marking as stale, do one final check for computation results
|
|
349
|
+
// Sometimes computation completes but status wasn't updated
|
|
350
|
+
const finalCheck = await checkComputationResults(db, targetCidNum, userType, insightsCollection, resultsSub, compsSub, logger);
|
|
351
|
+
if (!finalCheck) {
|
|
352
|
+
isStale = true;
|
|
353
|
+
logger.log('WARN', `[getUserSyncStatus] Detected stale request ${latestRequest.requestId} for user ${targetCidNum}. Status: ${status}, Age: ${Math.round((now - referenceTime) / 60000)} minutes`);
|
|
354
|
+
} else {
|
|
355
|
+
// Found results, update status to completed
|
|
356
|
+
status = 'completed';
|
|
357
|
+
await requestsSnapshot.docs[0].ref.update({
|
|
358
|
+
status: 'completed',
|
|
359
|
+
completedAt: FieldValue.serverTimestamp(),
|
|
360
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
361
|
+
});
|
|
362
|
+
logger.log('INFO', `[getUserSyncStatus] Found computation results on stale check, marked as completed`);
|
|
363
|
+
}
|
|
341
364
|
}
|
|
342
365
|
}
|
|
343
366
|
|
|
@@ -489,6 +512,80 @@ async function checkRateLimits(db, targetUserCid, logger) {
|
|
|
489
512
|
};
|
|
490
513
|
}
|
|
491
514
|
|
|
515
|
+
/**
|
|
516
|
+
* Helper function to check for computation results
|
|
517
|
+
*/
|
|
518
|
+
async function checkComputationResults(db, targetCidNum, userType, insightsCollection, resultsSub, compsSub, logger) {
|
|
519
|
+
try {
|
|
520
|
+
// Determine category and computation name based on user type
|
|
521
|
+
// NOTE: ResultCommitter ignores category metadata if computation is in a non-core folder
|
|
522
|
+
// SignedInUserProfileMetrics is in popular-investor folder, so it's stored in popular-investor category
|
|
523
|
+
let category, computationName;
|
|
524
|
+
if (userType === 'POPULAR_INVESTOR') {
|
|
525
|
+
category = 'popular-investor';
|
|
526
|
+
computationName = 'PopularInvestorProfileMetrics';
|
|
527
|
+
} else {
|
|
528
|
+
// SignedInUserProfileMetrics is in popular-investor folder, so stored in popular-investor category
|
|
529
|
+
category = 'popular-investor';
|
|
530
|
+
computationName = 'SignedInUserProfileMetrics';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Check today and yesterday for computation results
|
|
534
|
+
const checkDate = new Date();
|
|
535
|
+
for (let i = 0; i < 2; i++) {
|
|
536
|
+
const dateStr = new Date(checkDate);
|
|
537
|
+
dateStr.setDate(checkDate.getDate() - i);
|
|
538
|
+
const dateStrFormatted = dateStr.toISOString().split('T')[0];
|
|
539
|
+
|
|
540
|
+
const docRef = db.collection(insightsCollection)
|
|
541
|
+
.doc(dateStrFormatted)
|
|
542
|
+
.collection(resultsSub)
|
|
543
|
+
.doc(category)
|
|
544
|
+
.collection(compsSub)
|
|
545
|
+
.doc(computationName);
|
|
546
|
+
|
|
547
|
+
const doc = await docRef.get();
|
|
548
|
+
if (doc.exists) {
|
|
549
|
+
const docData = doc.data();
|
|
550
|
+
let mergedData = null;
|
|
551
|
+
|
|
552
|
+
// Check if data is sharded
|
|
553
|
+
if (docData._sharded === true && docData._shardCount) {
|
|
554
|
+
const shardsCol = docRef.collection('_shards');
|
|
555
|
+
const shardsSnapshot = await shardsCol.get();
|
|
556
|
+
|
|
557
|
+
if (!shardsSnapshot.empty) {
|
|
558
|
+
mergedData = {};
|
|
559
|
+
for (const shardDoc of shardsSnapshot.docs) {
|
|
560
|
+
const shardData = shardDoc.data();
|
|
561
|
+
Object.assign(mergedData, shardData);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
const { tryDecompress } = require('./data_helpers');
|
|
566
|
+
mergedData = tryDecompress(docData);
|
|
567
|
+
|
|
568
|
+
if (typeof mergedData === 'string') {
|
|
569
|
+
try {
|
|
570
|
+
mergedData = JSON.parse(mergedData);
|
|
571
|
+
} catch (e) {
|
|
572
|
+
mergedData = null;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (mergedData && typeof mergedData === 'object' && mergedData[String(targetCidNum)]) {
|
|
578
|
+
return true; // Found results
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return false; // No results found
|
|
583
|
+
} catch (error) {
|
|
584
|
+
logger.log('WARN', `[checkComputationResults] Error checking computation results`, error);
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
492
589
|
module.exports = {
|
|
493
590
|
requestUserSync,
|
|
494
591
|
getUserSyncStatus
|
|
@@ -761,6 +761,26 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
|
761
761
|
|
|
762
762
|
logger.log('SUCCESS', `[On-Demand Update] Complete for ${username} (Portfolio: ${portfolioFetched ? '✓' : '✗'}, History: ${historyFetched ? '✓' : '✗'}, Social: ${socialFetched ? '✓' : '✗'})`);
|
|
763
763
|
|
|
764
|
+
// Send notification to requesting user if this is an on-demand sync
|
|
765
|
+
if (requestId && source === 'on_demand_sync' && metadata?.requestingUserCid) {
|
|
766
|
+
try {
|
|
767
|
+
const { notifyTaskEngineComplete } = require('../../generic-api/user-api/helpers/notification_helpers');
|
|
768
|
+
const success = portfolioFetched || historyFetched; // At least one should succeed
|
|
769
|
+
await notifyTaskEngineComplete(
|
|
770
|
+
db,
|
|
771
|
+
logger,
|
|
772
|
+
metadata.requestingUserCid,
|
|
773
|
+
requestId,
|
|
774
|
+
username,
|
|
775
|
+
success,
|
|
776
|
+
success ? null : 'Failed to fetch portfolio or history data'
|
|
777
|
+
);
|
|
778
|
+
} catch (notifError) {
|
|
779
|
+
logger.log('WARN', `[On-Demand Update] Failed to send notification`, notifError);
|
|
780
|
+
// Non-critical, continue
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
764
784
|
// Update root data indexer after all data is fetched
|
|
765
785
|
// Then trigger computations for both sync requests AND user signups
|
|
766
786
|
const shouldTriggerComputations = (requestId && (source === 'on_demand_sync' || source === 'user_signup')) || isNewUser;
|
|
@@ -834,7 +854,9 @@ async function handleOnDemandUserUpdate(taskData, config, dependencies) {
|
|
|
834
854
|
requestId: requestId,
|
|
835
855
|
userCid: cid,
|
|
836
856
|
username: username,
|
|
837
|
-
targetCid: targetCid // Pass targetCid for optimization
|
|
857
|
+
targetCid: targetCid, // Pass targetCid for optimization
|
|
858
|
+
requestingUserCid: metadata?.requestingUserCid || null, // Pass for notifications
|
|
859
|
+
onDemand: true
|
|
838
860
|
}
|
|
839
861
|
);
|
|
840
862
|
triggeredMessages.push(...messages);
|