bulltrackers-module 1.0.721 → 1.0.722
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/data/CachedDataLoader.js +101 -102
- package/functions/computation-system/data/DependencyFetcher.js +48 -8
- package/functions/computation-system/persistence/ResultCommitter.js +158 -573
- package/functions/computation-system/utils/data_loader.js +253 -1088
- package/functions/core/utils/bigquery_utils.js +248 -112
- package/functions/etoro-price-fetcher/helpers/handler_helpers.js +4 -1
- package/functions/fetch-insights/helpers/handler_helpers.js +63 -65
- package/functions/fetch-popular-investors/helpers/fetch_helpers.js +143 -458
- package/functions/orchestrator/index.js +108 -141
- package/functions/root-data-indexer/index.js +130 -437
- package/package.json +3 -2
- package/functions/invalid-speculator-handler/helpers/handler_helpers.js +0 -38
- package/functions/speculator-cleanup-orchestrator/helpers/cleanup_helpers.js +0 -101
|
@@ -1,41 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Main orchestration logic.
|
|
3
|
-
* REFACTORED:
|
|
4
|
-
*
|
|
5
|
-
* It includes the new HTTP handlers for Workflow-driven "Slow-Trickle" updates.
|
|
6
|
-
* They receive all dependencies.
|
|
3
|
+
* REFACTORED: Implements 'Active Scheduling' via Cloud Tasks to enforce 1-hour gaps.
|
|
4
|
+
* This allows the Workflow to finish immediately while Cloud Tasks manages the pacing.
|
|
7
5
|
*/
|
|
8
6
|
const { checkDiscoveryNeed, getDiscoveryCandidates, dispatchDiscovery } = require('./helpers/discovery_helpers');
|
|
9
7
|
const { getUpdateTargets, dispatchUpdates } = require('./helpers/update_helpers');
|
|
10
8
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
9
|
+
const { CloudTasksClient } = require('@google-cloud/tasks');
|
|
10
|
+
|
|
11
|
+
// Initialize Cloud Tasks Client
|
|
12
|
+
const cloudTasksClient = new CloudTasksClient();
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* ENTRY POINT: HTTP Handler for Workflow Interaction
|
|
14
|
-
* Map this function to your HTTP Trigger in your index.js/exports.
|
|
15
|
-
* This handles the "PLAN" and "EXECUTE_WINDOW" phases of the slow-trickle update.
|
|
16
|
-
* * @param {object} req - Express request object.
|
|
17
|
-
* @param {object} res - Express response object.
|
|
18
|
-
* @param {object} dependencies - Contains logger, db, firestoreUtils, pubsubUtils.
|
|
19
|
-
* @param {object} config - Global configuration.
|
|
20
16
|
*/
|
|
21
17
|
async function handleOrchestratorHttp(req, res, dependencies, config) {
|
|
22
18
|
const { logger } = dependencies;
|
|
23
19
|
const body = req.body || {};
|
|
24
|
-
const { action, userType, date, windows, planId, windowId } = body;
|
|
20
|
+
const { action, userType, date, windows, planId, windowId, orchestratorUrlOverride } = body;
|
|
25
21
|
|
|
26
22
|
logger.log('INFO', `[Orchestrator HTTP] Received request: ${action}`, body);
|
|
27
23
|
|
|
28
24
|
try {
|
|
29
25
|
if (action === 'PLAN') {
|
|
30
|
-
// PHASE 1:
|
|
26
|
+
// PHASE 1: Plan AND Schedule
|
|
31
27
|
if (!userType || !date) {
|
|
32
28
|
throw new Error("Missing userType or date for PLAN action");
|
|
33
29
|
}
|
|
34
|
-
|
|
30
|
+
|
|
31
|
+
// Determine self-URL for callback
|
|
32
|
+
const project = process.env.GCP_PROJECT || process.env.GOOGLE_CLOUD_PROJECT;
|
|
33
|
+
const location = process.env.GCP_LOCATION || 'us-central1'; // Default to us-central1 if not set
|
|
34
|
+
// Fallback URL construction or use passed override
|
|
35
|
+
const orchestratorUrl = orchestratorUrlOverride || `https://${location}-${project}.cloudfunctions.net/orchestrator-http`;
|
|
36
|
+
|
|
37
|
+
const result = await planDailyUpdates(userType, date, windows || 10, config, dependencies, orchestratorUrl);
|
|
35
38
|
res.status(200).send(result);
|
|
36
39
|
|
|
37
40
|
} else if (action === 'EXECUTE_WINDOW') {
|
|
38
|
-
// PHASE 2:
|
|
41
|
+
// PHASE 2: Execute (Called by Cloud Tasks)
|
|
39
42
|
if (!planId || !windowId) {
|
|
40
43
|
throw new Error("Missing planId or windowId for EXECUTE_WINDOW action");
|
|
41
44
|
}
|
|
@@ -43,7 +46,6 @@ async function handleOrchestratorHttp(req, res, dependencies, config) {
|
|
|
43
46
|
res.status(200).send(result);
|
|
44
47
|
|
|
45
48
|
} else if (action === 'LEGACY_RUN') {
|
|
46
|
-
// Support for triggering the old brute-force method via HTTP if needed
|
|
47
49
|
await runUpdateOrchestrator(config, dependencies);
|
|
48
50
|
res.status(200).send({ status: 'Completed legacy run' });
|
|
49
51
|
|
|
@@ -57,96 +59,141 @@ async function handleOrchestratorHttp(req, res, dependencies, config) {
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
|
-
* LOGIC: Plan the updates (Split into windows)
|
|
62
|
+
* LOGIC: Plan the updates (Split into windows AND Schedule Execution)
|
|
61
63
|
* 1. Fetches all users needing updates.
|
|
62
64
|
* 2. Shuffles them.
|
|
63
65
|
* 3. Splits them into 'n' windows.
|
|
64
|
-
* 4.
|
|
66
|
+
* 4. Schedules Cloud Tasks for each window with a 1-hour delay between them.
|
|
65
67
|
*/
|
|
66
|
-
async function planDailyUpdates(userType, date, numberOfWindows, config, deps) {
|
|
68
|
+
async function planDailyUpdates(userType, date, numberOfWindows, config, deps, orchestratorUrl) {
|
|
67
69
|
const { logger, db } = deps;
|
|
68
70
|
|
|
71
|
+
// Cloud Tasks Config
|
|
72
|
+
const project = process.env.GCP_PROJECT || process.env.GOOGLE_CLOUD_PROJECT;
|
|
73
|
+
const location = process.env.GCP_LOCATION || 'us-central1';
|
|
74
|
+
const queue = process.env.GCP_QUEUE_NAME || 'orchestrator-queue'; // MUST EXIST in Cloud Console
|
|
75
|
+
|
|
76
|
+
// Construct the fully qualified queue name
|
|
77
|
+
const parentQueue = cloudTasksClient.queuePath(project, location, queue);
|
|
78
|
+
|
|
69
79
|
// 1. Get ALL targets
|
|
70
|
-
// We construct thresholds to capture everyone due for today
|
|
71
80
|
const now = new Date();
|
|
72
81
|
const startOfTodayUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
73
|
-
const DaysAgoUTC = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
74
|
-
|
|
75
82
|
const thresholds = {
|
|
76
83
|
dateThreshold: startOfTodayUTC,
|
|
77
|
-
gracePeriodThreshold:
|
|
84
|
+
gracePeriodThreshold: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
|
78
85
|
};
|
|
79
86
|
|
|
80
87
|
logger.log('INFO', `[Orchestrator Plan] Calculating targets for ${userType}...`);
|
|
81
|
-
|
|
82
|
-
// Reusing existing helper to get the raw list of users
|
|
83
88
|
const targets = await getUpdateTargets(userType, thresholds, config.updateConfig, deps);
|
|
84
|
-
logger.log('INFO', `[Orchestrator Plan] Found ${targets.length} candidates for ${userType}.`);
|
|
85
89
|
|
|
86
90
|
if (targets.length === 0) {
|
|
87
|
-
return { planId: null,
|
|
91
|
+
return { planId: null, message: `No targets found for ${userType}` };
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
// 2. Shuffle
|
|
91
|
-
// This ensures that we don't always update the same users at the same time of day
|
|
94
|
+
// 2. Shuffle (Fisher-Yates)
|
|
92
95
|
for (let i = targets.length - 1; i > 0; i--) {
|
|
93
96
|
const j = Math.floor(Math.random() * (i + 1));
|
|
94
97
|
[targets[i], targets[j]] = [targets[j], targets[i]];
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
// 3. Split and
|
|
100
|
+
// 3. Split, Save, and Schedule
|
|
98
101
|
const chunkSize = Math.ceil(targets.length / numberOfWindows);
|
|
99
102
|
const planId = `plan_${userType}_${date}`;
|
|
100
|
-
const windowIds = [];
|
|
101
|
-
|
|
102
103
|
const batchWriter = db.batch();
|
|
103
|
-
let writeCount = 0;
|
|
104
104
|
|
|
105
|
+
// We collect task creation promises to await them at the end
|
|
106
|
+
const tasksToCreate = [];
|
|
107
|
+
|
|
105
108
|
for (let i = 0; i < numberOfWindows; i++) {
|
|
106
109
|
const start = i * chunkSize;
|
|
107
110
|
const end = start + chunkSize;
|
|
108
111
|
const chunk = targets.slice(start, end);
|
|
109
112
|
|
|
110
113
|
if (chunk.length > 0) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
const windowId = i + 1;
|
|
115
|
+
|
|
116
|
+
// A. Prepare Firestore Write
|
|
117
|
+
const windowDocRef = db.collection('system_update_plans').doc(planId).collection('windows').doc(String(windowId));
|
|
114
118
|
|
|
115
119
|
batchWriter.set(windowDocRef, {
|
|
116
120
|
userType: userType,
|
|
117
|
-
users: chunk,
|
|
121
|
+
users: chunk,
|
|
118
122
|
status: 'pending',
|
|
119
|
-
windowId:
|
|
123
|
+
windowId: windowId,
|
|
120
124
|
userCount: chunk.length,
|
|
121
125
|
createdAt: FieldValue.serverTimestamp(),
|
|
122
126
|
scheduledForDate: date
|
|
123
127
|
});
|
|
124
128
|
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
// B. Prepare Cloud Task
|
|
130
|
+
// Delay Logic: Window 1 = 0s, Window 2 = 3600s (1hr), Window 3 = 7200s (2hr)...
|
|
131
|
+
const delaySeconds = i * 3600;
|
|
132
|
+
const scheduleTimeSeconds = (Date.now() / 1000) + delaySeconds;
|
|
133
|
+
|
|
134
|
+
const taskPayload = {
|
|
135
|
+
action: 'EXECUTE_WINDOW',
|
|
136
|
+
planId: planId,
|
|
137
|
+
windowId: windowId,
|
|
138
|
+
userType: userType,
|
|
139
|
+
date: date
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const taskRequest = {
|
|
143
|
+
parent: parentQueue,
|
|
144
|
+
task: {
|
|
145
|
+
httpRequest: {
|
|
146
|
+
httpMethod: 'POST',
|
|
147
|
+
url: orchestratorUrl,
|
|
148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
149
|
+
body: Buffer.from(JSON.stringify(taskPayload)).toString('base64'),
|
|
150
|
+
oidcToken: {
|
|
151
|
+
serviceAccountEmail: process.env.GCP_SERVICE_ACCOUNT_EMAIL
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
scheduleTime: {
|
|
155
|
+
seconds: scheduleTimeSeconds
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
tasksToCreate.push(taskRequest);
|
|
127
161
|
}
|
|
128
162
|
}
|
|
129
163
|
|
|
164
|
+
// Commit Firestore Writes First (Essential so tasks don't fail looking for data)
|
|
130
165
|
await batchWriter.commit();
|
|
131
|
-
logger.log('SUCCESS', `[Orchestrator Plan] Plan
|
|
166
|
+
logger.log('SUCCESS', `[Orchestrator Plan] Plan ${planId} saved to Firestore.`);
|
|
167
|
+
|
|
168
|
+
// Dispatch Cloud Tasks
|
|
169
|
+
logger.log('INFO', `[Orchestrator Plan] Scheduling ${tasksToCreate.length} tasks for ${userType}...`);
|
|
170
|
+
|
|
171
|
+
// Create tasks in parallel (with limit if needed, but usually fine for <50 tasks)
|
|
172
|
+
const scheduleResults = await Promise.allSettled(tasksToCreate.map(req => cloudTasksClient.createTask(req)));
|
|
173
|
+
|
|
174
|
+
const failedCount = scheduleResults.filter(r => r.status === 'rejected').length;
|
|
175
|
+
if (failedCount > 0) {
|
|
176
|
+
logger.log('ERROR', `[Orchestrator Plan] Failed to schedule ${failedCount} tasks.`);
|
|
177
|
+
// Note: We don't rollback Firestore here; manual retry or idempotency handles it.
|
|
178
|
+
}
|
|
132
179
|
|
|
133
180
|
return {
|
|
134
181
|
planId: planId,
|
|
135
182
|
totalUsers: targets.length,
|
|
136
|
-
|
|
137
|
-
|
|
183
|
+
windowsCreated: tasksToCreate.length,
|
|
184
|
+
scheduledCount: tasksToCreate.length - failedCount,
|
|
185
|
+
message: `Plan created. ${tasksToCreate.length} windows scheduled over ${tasksToCreate.length} hours.`
|
|
138
186
|
};
|
|
139
187
|
}
|
|
140
188
|
|
|
141
189
|
/**
|
|
142
190
|
* LOGIC: Execute a specific window
|
|
143
|
-
*
|
|
144
|
-
* 2. Calls dispatchUpdates to send them to the Task Engine.
|
|
191
|
+
* Triggered by Cloud Tasks when the schedule time arrives.
|
|
145
192
|
*/
|
|
146
193
|
async function executeUpdateWindow(planId, windowId, userType, config, deps) {
|
|
147
194
|
const { logger, db } = deps;
|
|
148
195
|
|
|
149
|
-
// 1. Fetch
|
|
196
|
+
// 1. Fetch window from Firestore
|
|
150
197
|
const windowRef = db.collection('system_update_plans').doc(planId).collection('windows').doc(String(windowId));
|
|
151
198
|
const windowDoc = await windowRef.get();
|
|
152
199
|
|
|
@@ -156,7 +203,7 @@ async function executeUpdateWindow(planId, windowId, userType, config, deps) {
|
|
|
156
203
|
|
|
157
204
|
const data = windowDoc.data();
|
|
158
205
|
|
|
159
|
-
// Idempotency
|
|
206
|
+
// Idempotency: Don't run if already done
|
|
160
207
|
if (data.status === 'completed') {
|
|
161
208
|
logger.log('WARN', `[Orchestrator Execute] Window ${windowId} already completed. Skipping.`);
|
|
162
209
|
return { dispatchedCount: 0, status: 'already_completed' };
|
|
@@ -165,13 +212,12 @@ async function executeUpdateWindow(planId, windowId, userType, config, deps) {
|
|
|
165
212
|
const targets = data.users;
|
|
166
213
|
logger.log('INFO', `[Orchestrator Execute] Window ${windowId}: Dispatching ${targets.length} users.`);
|
|
167
214
|
|
|
168
|
-
// 2. Dispatch
|
|
169
|
-
// The helper handles batching for Pub/Sub and logging.
|
|
215
|
+
// 2. Dispatch
|
|
170
216
|
if (targets && targets.length > 0) {
|
|
171
217
|
await dispatchUpdates(targets, userType, config.updateConfig, deps);
|
|
172
218
|
}
|
|
173
219
|
|
|
174
|
-
// 3. Mark
|
|
220
|
+
// 3. Mark Complete
|
|
175
221
|
await windowRef.update({
|
|
176
222
|
status: 'completed',
|
|
177
223
|
executedAt: FieldValue.serverTimestamp()
|
|
@@ -180,131 +226,52 @@ async function executeUpdateWindow(planId, windowId, userType, config, deps) {
|
|
|
180
226
|
return { dispatchedCount: targets.length, status: 'success' };
|
|
181
227
|
}
|
|
182
228
|
|
|
183
|
-
|
|
229
|
+
// --- Legacy / Helper Wrappers (Preserved for compatibility) ---
|
|
230
|
+
|
|
184
231
|
async function runDiscoveryOrchestrator(config, deps) {
|
|
185
232
|
const { logger, firestoreUtils } = deps;
|
|
186
233
|
logger.log('INFO', '🚀 Discovery Orchestrator triggered...');
|
|
187
234
|
await firestoreUtils.resetProxyLocks(deps, config);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (isUserTypeEnabled('normal', config.enabledUserTypes)) {
|
|
191
|
-
await runDiscovery('normal', config.discoveryConfig.normal, config, deps);
|
|
192
|
-
} else {
|
|
193
|
-
logger.log('INFO', '[Orchestrator] Normal user discovery is disabled. Skipping.');
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Check if speculator discovery is enabled
|
|
197
|
-
if (isUserTypeEnabled('speculator', config.enabledUserTypes)) {
|
|
198
|
-
await runDiscovery('speculator', config.discoveryConfig.speculator, config, deps);
|
|
199
|
-
} else {
|
|
200
|
-
logger.log('INFO', '[Orchestrator] Speculator discovery is disabled. Skipping.');
|
|
201
|
-
}
|
|
235
|
+
if (isUserTypeEnabled('normal', config.enabledUserTypes)) await runDiscovery('normal', config.discoveryConfig.normal, config, deps);
|
|
236
|
+
if (isUserTypeEnabled('speculator', config.enabledUserTypes)) await runDiscovery('speculator', config.discoveryConfig.speculator, config, deps);
|
|
202
237
|
}
|
|
203
238
|
|
|
204
|
-
/** Stage 2: Main update orchestrator pipe */
|
|
205
239
|
async function runUpdateOrchestrator(config, deps) {
|
|
206
240
|
const { logger, firestoreUtils } = deps;
|
|
207
241
|
logger.log('INFO', '🚀 Update Orchestrator triggered...');
|
|
208
242
|
await firestoreUtils.resetProxyLocks(deps, config);
|
|
209
|
-
|
|
210
243
|
const enabledTypes = config.enabledUserTypes || [];
|
|
211
|
-
const enabledTypesStr = Array.isArray(enabledTypes) ? enabledTypes.join(', ') : String(enabledTypes);
|
|
212
|
-
logger.log('INFO', `[Orchestrator] Configuration loaded. Enabled user types: [${enabledTypesStr || 'none'}]`);
|
|
213
|
-
logger.log('INFO', `[Orchestrator] Config object keys: ${Object.keys(config).join(', ')}`);
|
|
214
|
-
logger.log('INFO', `[Orchestrator] enabledUserTypes type: ${typeof config.enabledUserTypes}, value: ${JSON.stringify(config.enabledUserTypes)}`);
|
|
215
|
-
|
|
216
|
-
// 1. Normal Users
|
|
217
|
-
if (isUserTypeEnabled('normal', enabledTypes)) {
|
|
218
|
-
await runUpdates('normal', config.updateConfig, config, deps);
|
|
219
|
-
} else {
|
|
220
|
-
logger.log('INFO', '[Orchestrator] Normal user updates are disabled. Skipping.');
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// 2. Speculators
|
|
224
|
-
if (isUserTypeEnabled('speculator', enabledTypes)) {
|
|
225
|
-
await runUpdates('speculator', config.updateConfig, config, deps);
|
|
226
|
-
} else {
|
|
227
|
-
logger.log('INFO', '[Orchestrator] Speculator updates are disabled. Skipping.');
|
|
228
|
-
}
|
|
229
244
|
|
|
230
|
-
|
|
245
|
+
if (isUserTypeEnabled('normal', enabledTypes)) await runUpdates('normal', config.updateConfig, config, deps);
|
|
246
|
+
if (isUserTypeEnabled('speculator', enabledTypes)) await runUpdates('speculator', config.updateConfig, config, deps);
|
|
231
247
|
if (isUserTypeEnabled('popular_investor', enabledTypes)) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
...config.updateConfig,
|
|
235
|
-
popularInvestorRankingsCollection: config.updateConfig.popularInvestorRankingsCollection || process.env.FIRESTORE_COLLECTION_PI_RANKINGS || 'popular_investor_rankings'
|
|
236
|
-
};
|
|
237
|
-
await runUpdates('popular_investor', piConfig, config, deps);
|
|
238
|
-
} catch (e) {
|
|
239
|
-
logger.log('ERROR', '[Orchestrator] Failed to run Popular Investor updates.', e);
|
|
240
|
-
}
|
|
241
|
-
} else {
|
|
242
|
-
logger.log('INFO', '[Orchestrator] Popular Investor updates are disabled. Skipping.');
|
|
248
|
+
const piConfig = { ...config.updateConfig, popularInvestorRankingsCollection: config.updateConfig.popularInvestorRankingsCollection || 'popular_investor_rankings' };
|
|
249
|
+
await runUpdates('popular_investor', piConfig, config, deps);
|
|
243
250
|
}
|
|
244
|
-
|
|
245
|
-
// 4. Signed-In Users
|
|
246
251
|
if (isUserTypeEnabled('signed_in_user', enabledTypes)) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
...config.updateConfig,
|
|
250
|
-
signedInUsersCollection: config.updateConfig.signedInUsersCollection || process.env.FIRESTORE_COLLECTION_SIGNED_IN_USER_PORTFOLIOS || 'signed_in_users'
|
|
251
|
-
};
|
|
252
|
-
await runUpdates('signed_in_user', signedInConfig, config, deps);
|
|
253
|
-
} catch (e) {
|
|
254
|
-
logger.log('ERROR', '[Orchestrator] Failed to run Signed-In User updates.', e);
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
logger.log('INFO', '[Orchestrator] Signed-In User updates are disabled. Skipping.');
|
|
252
|
+
const signedInConfig = { ...config.updateConfig, signedInUsersCollection: config.updateConfig.signedInUsersCollection || 'signed_in_users' };
|
|
253
|
+
await runUpdates('signed_in_user', signedInConfig, config, deps);
|
|
258
254
|
}
|
|
259
255
|
}
|
|
260
256
|
|
|
261
|
-
/** Stage 3: Run discovery for a single user type */
|
|
262
257
|
async function runDiscovery(userType, userConfig, globalConfig, deps) {
|
|
263
258
|
const { logger } = deps;
|
|
264
|
-
logger.log('INFO', `[Orchestrator] Starting discovery for ${userType} users...`);
|
|
265
|
-
|
|
266
|
-
// Step 3.1: Check if discovery is needed
|
|
267
259
|
const { needsDiscovery, blocksToFill } = await checkDiscoveryNeed(userType, userConfig, deps);
|
|
268
|
-
if (!needsDiscovery)
|
|
269
|
-
|
|
270
|
-
// Step 3.2: Get discovery candidates
|
|
260
|
+
if (!needsDiscovery) return;
|
|
271
261
|
const candidates = await getDiscoveryCandidates(userType, blocksToFill, userConfig, deps);
|
|
272
|
-
|
|
273
|
-
// Step 3.3: Dispatch discovery tasks
|
|
274
262
|
await dispatchDiscovery(userType, candidates, userConfig, deps);
|
|
275
|
-
logger.log('SUCCESS', `[Orchestrator] Dispatched discovery tasks for ${userType}.`);
|
|
276
263
|
}
|
|
277
264
|
|
|
278
|
-
/** Stage 4: Run updates for a single user type */
|
|
279
265
|
async function runUpdates(userType, updateConfig, globalConfig, deps) {
|
|
280
|
-
const { logger } = deps;
|
|
281
|
-
logger.log('INFO', `[Orchestrator] Collecting users for daily update (${userType})...`);
|
|
282
|
-
|
|
283
|
-
// Step 4.1: Compute thresholds
|
|
284
266
|
const now = new Date();
|
|
285
267
|
const startOfTodayUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
286
|
-
const
|
|
287
|
-
const thresholds = { dateThreshold: startOfTodayUTC, gracePeriodThreshold: DaysAgoUTC };
|
|
288
|
-
|
|
289
|
-
// Step 4.2: Get update targets
|
|
268
|
+
const thresholds = { dateThreshold: startOfTodayUTC, gracePeriodThreshold: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) };
|
|
290
269
|
const targets = await getUpdateTargets(userType, thresholds, updateConfig, deps);
|
|
291
|
-
|
|
292
|
-
// Step 4.3: Dispatch update tasks
|
|
293
270
|
await dispatchUpdates(targets, userType, updateConfig, deps);
|
|
294
|
-
logger.log('SUCCESS', `[Orchestrator] Dispatched update tasks for ${userType}.`);
|
|
295
271
|
}
|
|
296
272
|
|
|
297
|
-
/**
|
|
298
|
-
* Helper function to check if a user type is enabled
|
|
299
|
-
* @param {string} userType - The user type to check ('normal', 'speculator', 'popular_investor', 'signed_in_user')
|
|
300
|
-
* @param {Array<string>} enabledTypes - Array of enabled user types from config
|
|
301
|
-
* @returns {boolean} True if the user type is enabled
|
|
302
|
-
*/
|
|
303
273
|
function isUserTypeEnabled(userType, enabledTypes) {
|
|
304
|
-
if (!enabledTypes || !Array.isArray(enabledTypes) || enabledTypes.length === 0)
|
|
305
|
-
// If no enabled types specified, default to all enabled (backward compatibility)
|
|
306
|
-
return true;
|
|
307
|
-
}
|
|
274
|
+
if (!enabledTypes || !Array.isArray(enabledTypes) || enabledTypes.length === 0) return true;
|
|
308
275
|
return enabledTypes.includes(userType);
|
|
309
276
|
}
|
|
310
277
|
|