diff --git a/packages/core/src/modules/events/domain-event.ts b/packages/core/src/modules/events/domain-event.ts index fac7d38..6fc4fd9 100644 --- a/packages/core/src/modules/events/domain-event.ts +++ b/packages/core/src/modules/events/domain-event.ts @@ -2,30 +2,38 @@ * Base class for all domain events. * Provides typed payloads and a consistent event name derived from the class name. * + * `occurredAt` is optional at construction time; if you don't pass one, + * `EventService.emit` stamps it from the injected `ClockService` before + * dispatching. Pass it explicitly when you need to control timing precisely + * (replays, deterministic tests). + * * @example * ```ts * class VacancyImported extends DomainEvent<{ vacancyId: string; source: string }> {} * - * // Emit: + * // Default emit (timestamp filled by EventService): * eventService.emit(new VacancyImported({ vacancyId: "123", source: "manual" })); * + * // Explicit timestamp (e.g. event replay): + * eventService.emit(new VacancyImported(payload, replayDate)); + * * // Listen: * @OnEvent(VacancyImported.eventName) * async handle(event: VacancyImported) { ... } * ``` */ -export abstract class DomainEvent { +export abstract class DomainEvent { static get eventName(): string { return DomainEvent.name; } readonly eventName: string; readonly payload: TPayload; - readonly occurredAt: Date; + occurredAt: Date | undefined; - constructor(...args: TPayload extends void ? [] : [payload: TPayload]) { + constructor(payload: TPayload, occurredAt?: Date) { this.eventName = (this.constructor as typeof DomainEvent).eventName; - this.payload = args[0] as TPayload; - this.occurredAt = new Date(); + this.payload = payload; + this.occurredAt = occurredAt; } } diff --git a/packages/core/src/modules/events/event.service.ts b/packages/core/src/modules/events/event.service.ts index ad9d522..68cc7e8 100644 --- a/packages/core/src/modules/events/event.service.ts +++ b/packages/core/src/modules/events/event.service.ts @@ -1,20 +1,36 @@ import { Injectable } from "@nestjs/common"; import { EventEmitter2 } from "@nestjs/event-emitter"; +import { ClockService } from "../../shared/clock.service"; import { DomainEvent } from "./domain-event"; /** * Typed wrapper around NestJS EventEmitter2. * Use this instead of injecting EventEmitter2 directly for type safety. + * + * Stamps `event.occurredAt` from `ClockService` if the caller didn't set + * one - so domain code can stay terse (`new SomeEvent(payload)`) while + * tests still get a mockable clock. */ @Injectable() export class EventService { - constructor(private readonly emitter: EventEmitter2) {} + constructor( + private readonly emitter: EventEmitter2, + private readonly clock: ClockService, + ) {} emit(event: DomainEvent): void { + this.stamp(event); this.emitter.emit(event.eventName, event); } async emitAsync(event: DomainEvent): Promise { + this.stamp(event); await this.emitter.emitAsync(event.eventName, event); } + + private stamp(event: DomainEvent): void { + if (event.occurredAt === undefined) { + event.occurredAt = this.clock.now(); + } + } } diff --git a/packages/core/src/modules/events/events.module.ts b/packages/core/src/modules/events/events.module.ts index 803a556..d0dc3ff 100644 --- a/packages/core/src/modules/events/events.module.ts +++ b/packages/core/src/modules/events/events.module.ts @@ -1,10 +1,11 @@ import { Global, Module } from "@nestjs/common"; import { EventEmitterModule } from "@nestjs/event-emitter"; +import { BaseModule } from "../../shared/base.module"; import { EventService } from "./event.service"; @Global() @Module({ - imports: [EventEmitterModule.forRoot()], + imports: [EventEmitterModule.forRoot(), BaseModule], providers: [EventService], exports: [EventService], })