- config: bloque matcher (MATCHER_MODE off/shadow/full, IRON_PASSWORD, audiencia, poll, batch) + validacion - db: colecciones subscriptions/activefires/notifications_shadow/matcher_state + ensureMatcherIndexes (2dsphere en notifications_shadow para el dedupe 500m) - matcher/context: MatchContext sobre Mongo real (candidatas geo $near 1000km + audiencia; dedupe geo $near 500m; lang del owner) - matcher/ingest: polling de activefires por createdAt>checkpoint (Mongo 3.2 sin change streams), checkpoint persistido en matcher_state - matcher/runner: loop shadow(->notifications_shadow)/full(->notifications), idempotente por dedupe+checkpoint - matcher/compare: comparador de shadow (shadowOnly=spam, realOnly=faltantes, contentMismatches) — el gate antes del cutover - index: arranca el matcher; fake-repo ampliado (upsert, sort, $gt/$lt) - docs/env/README actualizados 73 tests en verde, typecheck y build OK.
80 lines
3 KiB
TypeScript
80 lines
3 KiB
TypeScript
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<void> {
|
|
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<SendJob>[] = [];
|
|
const handlerFor: Record<Channel, (id: string) => Promise<void>> = {
|
|
fcm: (id) => processFcmJob(deps, id),
|
|
email: (id) => processEmailJob(deps, id),
|
|
};
|
|
|
|
for (const channel of cfg.ownedChannels) {
|
|
const worker = new Worker<SendJob>(
|
|
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<void> => {
|
|
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);
|
|
});
|