- Added @shieldai/shared-notifications, bullmq, ioredis deps to API - POST /api/waitlist/signup now sends waitlist_confirmation email via EmailService - Schedules welcome sequence (day1 intro, day3 features, day7 launch teaser) via BullMQ delayed jobs - Added waitlist email worker in @shieldai/jobs to process delayed welcome sequence emails - Templates already in place: waitlist_confirmation, waitlist_intro, waitlist_features, waitlist_launch_teaser with dark-themed HTML layouts Co-Authored-By: Paperclip <noreply@paperclip.ing>
192 lines
5.9 KiB
TypeScript
192 lines
5.9 KiB
TypeScript
import { Queue, Worker } from "bullmq";
|
|
import { Redis } from "ioredis";
|
|
import { ScanService, ScanScheduler, WebhookHandler } from "@shieldai/darkwatch";
|
|
import { AlertPipeline } from "@shieldai/darkwatch";
|
|
|
|
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
|
const connection = new Redis(redisUrl);
|
|
|
|
const scanQueue = new Queue("darkwatch-scans", { connection });
|
|
const alertQueue = new Queue("darkwatch-alerts", { connection });
|
|
const scheduleQueue = new Queue("darkwatch-scheduler", { connection });
|
|
|
|
const scanWorker = new Worker(
|
|
"darkwatch-scans",
|
|
async (job) => {
|
|
const { userId, source } = job.data;
|
|
const scanService = new ScanService();
|
|
const resultCount = await scanService.runScan(userId, source);
|
|
return { resultCount, completedAt: new Date().toISOString() };
|
|
},
|
|
{ connection, concurrency: 3 }
|
|
);
|
|
|
|
const alertWorker = new Worker(
|
|
"darkwatch-alerts",
|
|
async () => {
|
|
const pipeline = new AlertPipeline();
|
|
const sent = await pipeline.sendPendingAlerts();
|
|
return { sent, processedAt: new Date().toISOString() };
|
|
},
|
|
{ connection, concurrency: 1 }
|
|
);
|
|
|
|
const scheduleWorker = new Worker(
|
|
"darkwatch-scheduler",
|
|
async () => {
|
|
const scheduler = new ScanScheduler();
|
|
const dueSchedules = await scheduler.getDueSchedules();
|
|
const results: Array<{ userId: string; queued: boolean }> = [];
|
|
|
|
for (const schedule of dueSchedules) {
|
|
try {
|
|
await scanQueue.add("scheduled-scan", {
|
|
userId: schedule.userId,
|
|
source: undefined,
|
|
}, {
|
|
attempts: 3,
|
|
backoff: { type: "exponential", delay: 5000 },
|
|
jobId: `scheduled-scan-${schedule.userId}-${Date.now()}`,
|
|
});
|
|
|
|
await scheduler.markScanned(schedule.userId);
|
|
results.push({ userId: schedule.userId, queued: true });
|
|
} catch (err) {
|
|
console.error(`[Scheduler] Failed to queue scan for ${schedule.userId}:`, err);
|
|
results.push({ userId: schedule.userId, queued: false });
|
|
}
|
|
}
|
|
|
|
return { processed: results.length, completedAt: new Date().toISOString() };
|
|
},
|
|
{ connection, concurrency: 1 }
|
|
);
|
|
|
|
const webhookWorker = new Worker(
|
|
"darkwatch-webhooks",
|
|
async () => {
|
|
const handler = new WebhookHandler();
|
|
const processed = await handler.processPendingEvents();
|
|
return { processed, completedAt: new Date().toISOString() };
|
|
},
|
|
{ connection, concurrency: 1 }
|
|
);
|
|
|
|
scanWorker.on("completed", (job) => {
|
|
console.log(`[Scan] Job ${job?.id} completed: ${JSON.stringify(job?.returnvalue)}`);
|
|
});
|
|
|
|
scanWorker.on("failed", (job, err) => {
|
|
console.error(`[Scan] Job ${job?.id} failed: ${err.message}`);
|
|
});
|
|
|
|
alertWorker.on("completed", (job) => {
|
|
console.log(`[Alert] Job ${job?.id} completed: ${JSON.stringify(job?.returnvalue)}`);
|
|
});
|
|
|
|
alertWorker.on("failed", (job, err) => {
|
|
console.error(`[Alert] Job ${job?.id} failed: ${err.message}`);
|
|
});
|
|
|
|
scheduleWorker.on("completed", (job) => {
|
|
console.log(`[Scheduler] Job ${job?.id} completed: ${JSON.stringify(job?.returnvalue)}`);
|
|
});
|
|
|
|
scheduleWorker.on("failed", (job, err) => {
|
|
console.error(`[Scheduler] Job ${job?.id} failed: ${err.message}`);
|
|
});
|
|
|
|
webhookWorker.on("completed", (job) => {
|
|
console.log(`[Webhook] Job ${job?.id} completed: ${JSON.stringify(job?.returnvalue)}`);
|
|
});
|
|
|
|
webhookWorker.on("failed", (job, err) => {
|
|
console.error(`[Webhook] Job ${job?.id} failed: ${err.message}`);
|
|
});
|
|
|
|
export async function addScanJob(userId: string, source?: string) {
|
|
return scanQueue.add("scan", { userId, source }, {
|
|
attempts: 3,
|
|
backoff: { type: "exponential", delay: 5000 },
|
|
jobId: `scan-${userId}-${Date.now()}`,
|
|
});
|
|
}
|
|
|
|
export async function scheduleAlertProcessing() {
|
|
return alertQueue.add("process-alerts", {}, {
|
|
repeat: { pattern: "*/5 * * * *" },
|
|
jobId: "alert-processor-recurring",
|
|
});
|
|
}
|
|
|
|
export async function schedulePeriodicScanCheck() {
|
|
return scheduleQueue.add("check-due-scans", {}, {
|
|
repeat: { pattern: "*/10 * * * *" },
|
|
jobId: "scheduler-recurring",
|
|
});
|
|
}
|
|
|
|
export async function scheduleWebhookProcessor() {
|
|
const webhookQueue = new Queue("darkwatch-webhooks", { connection });
|
|
return webhookQueue.add("process-pending-webhooks", {}, {
|
|
repeat: { pattern: "*/2 * * * *" },
|
|
jobId: "webhook-processor-recurring",
|
|
});
|
|
}
|
|
|
|
// Waitlist email worker
|
|
import { EmailService } from '@shieldai/shared-notifications';
|
|
|
|
const waitlistEmailWorker = new Worker(
|
|
"waitlist-emails",
|
|
async (job) => {
|
|
const { email, name, entryId } = job.data;
|
|
const templateIdMap: Record<string, string> = {
|
|
'send-waitlist-intro': 'waitlist_intro',
|
|
'send-waitlist-features': 'waitlist_features',
|
|
'send-waitlist-launch-teaser': 'waitlist_launch_teaser',
|
|
};
|
|
|
|
const templateId = templateIdMap[job.name];
|
|
if (!templateId) {
|
|
throw new Error(`Unknown waitlist email job: ${job.name}`);
|
|
}
|
|
|
|
const emailService = EmailService.getInstance();
|
|
const result = await emailService.sendWithTemplate(email, {
|
|
templateId,
|
|
variables: { name, entryId },
|
|
});
|
|
|
|
if (result.status === 'failed') {
|
|
throw new Error(`Failed to send ${templateId} to ${email}: ${result.error}`);
|
|
}
|
|
|
|
return { templateId, email, deliveredAt: result.deliveredAt };
|
|
},
|
|
{ connection, concurrency: 5 }
|
|
);
|
|
|
|
waitlistEmailWorker.on("completed", (job) => {
|
|
console.log(`[WaitlistEmail] Job ${job?.id} (${job?.name}) completed for ${job?.data?.email}`);
|
|
});
|
|
|
|
waitlistEmailWorker.on("failed", (job, err) => {
|
|
console.error(`[WaitlistEmail] Job ${job?.id} (${job?.name}) failed: ${err.message}`);
|
|
});
|
|
|
|
console.log("Job workers started");
|
|
|
|
// Report generation workers
|
|
import {
|
|
reportGenerationWorker,
|
|
reportSchedulerWorker,
|
|
scheduleReportProcessor,
|
|
scheduleMonthlyReportTrigger,
|
|
scheduleAnnualReportTrigger,
|
|
} from './report.jobs';
|
|
|
|
scheduleReportProcessor().catch(console.error);
|
|
scheduleMonthlyReportTrigger().catch(console.error);
|
|
scheduleAnnualReportTrigger().catch(console.error);
|