import { logger } from '../observability/logger.js'; import { producer } from '../../config/kafka.js'; import { outboxRepository } from './outbox.repository.js'; import type { OutboxMessage } from './outbox.schema.js'; const TOPIC_MAP: Record = { 'exam.created': 'edu.exam.events', 'exam.updated': 'edu.exam.events', 'exam.deleted': 'edu.exam.events', 'homework.assigned': 'edu.homework.events', 'homework.submitted': 'edu.homework.events', 'homework.graded': 'edu.homework.events', 'grade.recorded': 'edu.grade.events', 'grade.updated': 'edu.grade.events', 'class.transferred': 'edu.class.events', }; const POLL_INTERVAL_MS = 5000; const BATCH_SIZE = 100; const MAX_RETRY = 5; export class OutboxPublisher { private intervalId: NodeJS.Timeout | null = null; private isPolling = false; async start(): Promise { logger.info('OutboxPublisher started'); this.intervalId = setInterval(() => { void this.poll(); }, POLL_INTERVAL_MS); } async stop(): Promise { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } logger.info('OutboxPublisher stopped'); } private async poll(): Promise { if (this.isPolling) return; this.isPolling = true; try { const messages = await outboxRepository.findPending(BATCH_SIZE); for (const message of messages) { await this.publish(message); } } catch (error) { logger.error({ error }, 'Outbox poll failed'); } finally { this.isPolling = false; } } private async publish(message: OutboxMessage): Promise { const topic = TOPIC_MAP[message.eventType] ?? 'edu.fallback.events'; try { await producer.send({ topic, messages: [ { key: message.aggregateId, value: message.payload, headers: { eventType: message.eventType, aggregateType: message.aggregateType, }, }, ], }); await outboxRepository.markProcessed(message.id); logger.info( { id: message.id, eventType: message.eventType, topic }, 'Outbox message published', ); } catch (error) { logger.error({ error, id: message.id }, 'Outbox publish failed'); if (message.retryCount + 1 >= MAX_RETRY) { await outboxRepository.markFailed(message.id); } else { await outboxRepository.incrementRetry(message.id); } } } } export const outboxPublisher = new OutboxPublisher();