import { Module, type DynamicModule } from "@nestjs/common"; import pino from "pino"; import { OutboxService } from "./outbox.service.js"; import { OutboxPublisher } from "./publisher.js"; import { createOutboxTable } from "./schema.js"; import { OUTBOX_CONFIG, OUTBOX_DB, OUTBOX_KAFKA_PRODUCER, OUTBOX_LOGGER, OUTBOX_TABLE, type OutboxConfig, type OutboxDbClient, type OutboxKafkaProducer, type OutboxLogger, } from "./types.js"; /** * forRoot 选项。 * * `config` 即 OutboxConfig(表名 / topic / 轮询参数)。 * `db` / `kafkaProducer` / `logger` 为基础设施实例:NestJS DynamicModule * 无法跨模块边界注入宿主已存在的具体单例,故统一通过 forRoot 注入。 * * `kafkaProducer` 应由调用方以 idempotent 方式创建: * `kafka.producer({ idempotent: true, transactionalId: '-tx' })`。 */ export interface OutboxRootOptions { config: OutboxConfig; db: OutboxDbClient; kafkaProducer: OutboxKafkaProducer; /** 可选;省略时使用默认 pino logger(name=outbox) */ logger?: OutboxLogger; } function resolveLogger(logger: OutboxLogger | undefined): OutboxLogger { return logger ?? pino({ name: "outbox", level: "info" }); } /** * OutboxModule —— 事务性 Outbox 模式的 NestJS DynamicModule。 * * 注册并启动: * - OutboxService:业务代码注入后调用 `publish` 写入 outbox 记录 * - OutboxPublisher:onModuleInit 启动轮询,onModuleDestroy 停止轮询 * * 用法: * ```ts * @Module({ * imports: [OutboxModule.forRoot({ config, db, kafkaProducer })], * }) * export class AppModule {} * ``` */ @Module({}) export class OutboxModule { static forRoot(options: OutboxRootOptions): DynamicModule { const table = createOutboxTable(options.config.tableName); const logger = resolveLogger(options.logger); return { module: OutboxModule, global: true, providers: [ { provide: OUTBOX_CONFIG, useValue: options.config }, { provide: OUTBOX_DB, useValue: options.db }, { provide: OUTBOX_TABLE, useValue: table }, { provide: OUTBOX_KAFKA_PRODUCER, useValue: options.kafkaProducer }, { provide: OUTBOX_LOGGER, useValue: logger }, OutboxService, OutboxPublisher, ], exports: [OutboxService], }; } }