import { Worker } from 'bullmq'; import { loadConfig } from './config.js'; import { createLogger } from './logger.js'; import { connect, ensureIndexes, ensureMatcherIndexes } from './db.js'; import { createMatcherRunner } from './matcher/runner.js'; import { createQueues, redisConnection, QUEUE_NAMES } from './queue.js'; import { createFcmClient, type FcmClient } from './fcm.js'; import { createMailer, type Mailer } from './mailer.js'; import { createPoller } from './poller.js'; import { processEmailJob, processFcmJob, type Deps } from './handlers.js'; import type { Channel, SendJob } from './types.js'; async function main(): Promise { const cfg = loadConfig(); const log = createLogger(cfg.logLevel, cfg.serviceName); log.info( { mode: cfg.mode, owned: cfg.ownedChannels, killSwitch: cfg.killSwitch, db: cfg.dbName }, 'tcef-notifications starting', ); if (cfg.killSwitch) log.warn('KILL_SWITCH is ON — no sends will happen'); const repos = await connect(cfg); await ensureIndexes(repos); const queues = createQueues(cfg); const connection = redisConnection(cfg); // Real senders are only constructed when the mode may actually send, and only // for owned channels — so dry-run/shadow never even needs the credentials. const willSend = cfg.mode === 'canary' || cfg.mode === 'full'; let fcm: FcmClient | undefined; let mailer: Mailer | undefined; if (willSend && cfg.ownedChannels.includes('fcm')) fcm = createFcmClient(cfg); if (willSend && cfg.ownedChannels.includes('email')) mailer = createMailer(cfg); const deps: Deps = { cfg, repos, log, fcm, mailer }; const workers: Worker[] = []; const handlerFor: Record Promise> = { fcm: (id) => processFcmJob(deps, id), email: (id) => processEmailJob(deps, id), }; for (const channel of cfg.ownedChannels) { const worker = new Worker( QUEUE_NAMES[channel], async (job) => handlerFor[channel](job.data.notificationId), { connection, concurrency: channel === 'email' ? 5 : 20 }, ); worker.on('failed', (job, err) => { log.error({ channel, jobId: job?.id, err: err.message }, 'worker: job failed (dead-letter if attempts exhausted)'); }); workers.push(worker); } const poller = createPoller(cfg, repos, queues, log); poller.start(); // Fase 1b — geospatial matcher (fire → subscriptions → notifications). if (cfg.matcher.mode !== 'off') await ensureMatcherIndexes(repos); const matcher = createMatcherRunner(cfg, repos, log); matcher.start(); const shutdown = async (signal: string): Promise => { log.info({ signal }, 'shutting down'); poller.stop(); matcher.stop(); await Promise.all(workers.map((w) => w.close())); await Promise.all(Object.values(queues).map((q) => q.close())); await repos.close(); process.exit(0); }; process.on('SIGINT', () => void shutdown('SIGINT')); process.on('SIGTERM', () => void shutdown('SIGTERM')); } main().catch((err) => { console.error('fatal:', err); process.exit(1); });