Fase 1b: cableado del matcher (ingesta polling, runner shadow/full, comparador)

- 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.
This commit is contained in:
vjrj 2026-07-13 18:07:06 +02:00
parent b3aeb6108f
commit a1ed206985
13 changed files with 531 additions and 19 deletions

View file

@ -1,7 +1,8 @@
import { Worker } from 'bullmq';
import { loadConfig } from './config.js';
import { createLogger } from './logger.js';
import { connect, ensureIndexes } from './db.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';
@ -55,9 +56,15 @@ async function main(): Promise<void> {
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();