@shirudo/ddd-kit 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,716 @@
1
+ import { Result } from '@shirudo/result';
2
+ import { BaseError } from '@shirudo/base-error';
3
+
4
+ /**
5
+ * Branded string ID. `Tag` carries the aggregate / entity name so two ids
6
+ * with different tags are not assignable to each other even though both
7
+ * are strings at runtime.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * type UserId = Id<"UserId">;
12
+ * type OrderId = Id<"OrderId">;
13
+ *
14
+ * const u = "user-1" as UserId;
15
+ * const o: OrderId = u; // ❌ compile error
16
+ * ```
17
+ */
18
+ type Id<Tag extends string> = string & {
19
+ readonly __brand: Tag;
20
+ };
21
+ /**
22
+ * Produces fresh ids of a single, fixed tag. The tag is bound at the
23
+ * generator type: `IdGenerator<"UserId">.next()` returns `Id<"UserId">`
24
+ * with no caller-side generic to abuse.
25
+ *
26
+ * **Your factory must produce unique ids under concurrent calls.**
27
+ * The kit makes no attempt to dedupe or detect collisions: a collision
28
+ * silently overwrites earlier rows (under unique-key constraints) or
29
+ * silently aliases two different entities (without them). Safe choices:
30
+ * `crypto.randomUUID()` (UUIDv4, the default for events), ULID, UUIDv7,
31
+ * KSUID: all collision-resistant by design. Unsafe choices: `Date.now()`
32
+ * alone (duplicates within the same millisecond), a process-local
33
+ * counter without persistence (resets to 1 on restart, collides with
34
+ * prior runs), a sequential id derived from non-atomic state.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { ulid } from "ulid";
39
+ *
40
+ * const userIds: IdGenerator<"UserId"> = { next: () => ulid() as Id<"UserId"> };
41
+ * const id = userIds.next(); // Id<"UserId">
42
+ * ```
43
+ *
44
+ * The previous shape (`IdGenerator { next<T extends string>(): Id<T> }`)
45
+ * let callers pick `T` themselves: `gen.next<"AnyTag">()` typechecked
46
+ * even when the generator produced different-tag ids, silently defeating
47
+ * the brand.
48
+ */
49
+ interface IdGenerator<Tag extends string> {
50
+ next: () => Id<Tag>;
51
+ }
52
+
53
+ /**
54
+ * Abstract base for **domain-invariant violations**. Domain methods
55
+ * (aggregates, entity validation hooks, value-object constructors)
56
+ * throw `DomainError`-derived exceptions when a business rule is
57
+ * violated. Consumers derive their own concrete errors (e.g.
58
+ * `class OrderAlreadyShippedError extends DomainError<"OrderAlreadyShippedError"> {}`)
59
+ * for `instanceof`-style catching at the App-Service boundary, where
60
+ * they typically map to HTTP 400 / business-rule responses.
61
+ *
62
+ * The library itself does **not** ship any concrete `DomainError`
63
+ * subclass: the kit can't know your invariants.
64
+ *
65
+ * Extends `BaseError<Name>`; see `@shirudo/base-error` for the inherited
66
+ * surface (timestamps, cause chains, `toJSON()`, `getUserMessage()`,
67
+ * `isRetryable`, …).
68
+ */
69
+ declare abstract class DomainError<Name extends string = string> extends BaseError<Name> {
70
+ }
71
+ /**
72
+ * Abstract base for **infrastructure / persistence failures** that the
73
+ * App-Service can recover from: typically by retrying, by returning
74
+ * HTTP 404 / 409, or by surfacing a "please try again" UX. These are
75
+ * not domain-invariant violations (the business rules were not
76
+ * broken); they describe race conditions and missing rows at the
77
+ * storage boundary.
78
+ *
79
+ * Library-internal concrete subclasses: {@link AggregateNotFoundError},
80
+ * {@link ConcurrencyConflictError}, {@link DuplicateAggregateError},
81
+ * plus the unit-of-work lifecycle wrappers `CommitError` and
82
+ * `RollbackError` (in `src/app/unit-of-work.ts`).
83
+ */
84
+ declare abstract class InfrastructureError<Name extends string = string> extends BaseError<Name> {
85
+ }
86
+ /**
87
+ * Thrown by `EventSourcedAggregate.apply()` when no handler is
88
+ * registered for the event's type. This means the aggregate's subclass
89
+ * forgot to add an entry to its `handlers` map: a programming /
90
+ * configuration bug, not a domain or infrastructure failure.
91
+ *
92
+ * Deliberately **not** on `DomainError` or `InfrastructureError`:
93
+ * a generic `catch (e instanceof DomainError)` handler at the App
94
+ * layer must not mask a forgotten handler; this should crash loud and
95
+ * fail the calling Use Case so the bug surfaces in development. The
96
+ * replay methods (`loadFromHistory`, `restoreFromSnapshotWithEvents`)
97
+ * also let it propagate uncaught instead of wrapping it in `Result.Err`.
98
+ *
99
+ * Use `isBaseError(e)` from `@shirudo/base-error` to detect
100
+ * "any structured error from the kit or any other BaseError-using
101
+ * library" at the App boundary.
102
+ */
103
+ declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
104
+ readonly eventType: string;
105
+ constructor(eventType: string, cause?: unknown);
106
+ }
107
+ /**
108
+ * Thrown by `withCommit` when an event harvested from an aggregate cannot
109
+ * be safely committed: it is missing `aggregateId` / `aggregateType`
110
+ * (downstream routing would break), or it carries a pre-set
111
+ * `aggregateVersion` AHEAD of the aggregate's commit version (a leaked or
112
+ * copied fixture that would advance consumer idempotency watermarks past
113
+ * real history). Both are programming bugs in how the aggregate recorded
114
+ * the event, deterministic, and fail identically on every retry.
115
+ *
116
+ * Deliberately **not** an {@link InfrastructureError} (same reasoning as
117
+ * {@link MissingHandlerError}): the failure happens after the work
118
+ * callback completed, but it is NOT transient. A `catch (e instanceof
119
+ * InfrastructureError)` retry handler, or a retrying `TransactionScope`,
120
+ * must NOT mask it or loop on it forever; it should crash loud so the
121
+ * recordEvent / createDomainEvent misuse surfaces in development. This is
122
+ * why `withCommit` throws it directly and `UnitOfWork.run` passes it
123
+ * through unchanged instead of wrapping it in `CommitError`.
124
+ */
125
+ declare class EventHarvestError extends BaseError<"EventHarvestError"> {
126
+ /** The `type` of the offending event, for programmatic routing. */
127
+ readonly eventType?: string | undefined;
128
+ constructor(message: string,
129
+ /** The `type` of the offending event, for programmatic routing. */
130
+ eventType?: string | undefined);
131
+ }
132
+ /**
133
+ * Thrown at the end of a `UnitOfWork.run` when an aggregate that was
134
+ * loaded into the identity map during the operation carries unflushed
135
+ * `pendingEvents` but was never enrolled (no `session.enrollSaved`, and
136
+ * not deleted). The almost-certain cause is a repository `save()` that
137
+ * forgot to call `enrollSaved`, or a use case that recorded events on a
138
+ * loaded aggregate and never saved it. Without this guard those events
139
+ * would be silently dropped: never harvested into the outbox, never
140
+ * published.
141
+ *
142
+ * Deliberately **not** an `InfrastructureError` (same posture as
143
+ * {@link MissingHandlerError}): a programming bug that must crash loud,
144
+ * not be absorbed by a generic infrastructure-error handler. The throw
145
+ * happens inside the transaction, so the unit of work rolls back and
146
+ * leaves no partial state.
147
+ *
148
+ * **Scope of the guard.** A best-effort runtime safety net, not a proof.
149
+ * It only sees aggregates the identity map knows about (those loaded via
150
+ * `getById`), and detects new events by comparing the pending-event COUNT
151
+ * at load against commit, which assumes the kit's append-only event model
152
+ * (so it cannot see events that were recorded and then cleared within the
153
+ * same run). A freshly *created* aggregate that was never enrolled is
154
+ * invisible to the kit. The repository contract test suite remains the
155
+ * full mitigation. See the Unit of Work guide.
156
+ */
157
+ declare class UnenrolledChangesError extends BaseError<"UnenrolledChangesError"> {
158
+ readonly aggregateId: string;
159
+ constructor(aggregateId: string);
160
+ }
161
+ /**
162
+ * Thrown when an aggregate that was deleted within the current unit of
163
+ * work is saved or re-registered again in the same operation: by
164
+ * `UnitOfWorkSession.enrollSaved` after `enrollDeleted` of the same
165
+ * instance, and by `IdentityMap.set` for a type+id that was deleted.
166
+ * Deletion is final within an operation; saving afterwards would write
167
+ * a row the delete just removed (or resurrect it), which is always a
168
+ * use-case bug.
169
+ *
170
+ * Extends `BaseError` directly (same reasoning as
171
+ * {@link MissingHandlerError}): a programming bug that should crash
172
+ * loud, not be absorbed by a generic infrastructure-error handler.
173
+ */
174
+ declare class AggregateDeletedError extends BaseError<"AggregateDeletedError"> {
175
+ readonly aggregateId: string;
176
+ constructor(aggregateId: string);
177
+ }
178
+ /**
179
+ * Thrown by `IRepository.getByIdOrFail()` when an aggregate with the
180
+ * given id does not exist. `InfrastructureError` because the storage
181
+ * boundary, not a business rule, decided the row is absent. Use the
182
+ * nullable variant `getById()` if "not found" is a valid outcome.
183
+ *
184
+ * Accepts an optional `cause` so a `Repository.save()` implementation
185
+ * can wrap a lower-level "row not found" / driver-level error without
186
+ * losing context. Cause-chain helpers (`getRootCause`,
187
+ * `findInCauseChain`) from `@shirudo/base-error` traverse the chain.
188
+ *
189
+ * Not retryable: retrying won't make the row appear.
190
+ */
191
+ declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFoundError"> {
192
+ readonly aggregateType: string;
193
+ readonly id: string;
194
+ constructor(aggregateType: string, id: string, cause?: unknown);
195
+ }
196
+ /**
197
+ * Thrown by a repository's `save()` INSERT path when a row with the
198
+ * aggregate's id already exists (unique-constraint violation): two
199
+ * concurrent creators raced on the same business-derived id, or the
200
+ * id generator collided. Same delegation model as
201
+ * {@link ConcurrencyConflictError}: the kit ships the class, the
202
+ * consumer repository maps its driver's unique-violation signal to it
203
+ * instead of letting a raw driver error escape -
204
+ *
205
+ * - Postgres: SQLSTATE `23505` (`unique_violation`)
206
+ * - MySQL/MariaDB: errno `1062` (`ER_DUP_ENTRY`)
207
+ * - SQLite: `SQLITE_CONSTRAINT_UNIQUE` (extended code 2067)
208
+ *
209
+ * `InfrastructureError` because the storage boundary detects the
210
+ * collision. NOT retryable: re-running the same INSERT cannot succeed.
211
+ * The right reactions are domain decisions - map to HTTP 409, or for
212
+ * idempotency-key flows load the existing aggregate and treat the
213
+ * request as already-applied.
214
+ */
215
+ declare class DuplicateAggregateError extends InfrastructureError<"DuplicateAggregateError"> {
216
+ readonly aggregateType: string;
217
+ readonly aggregateId: string;
218
+ constructor(aggregateType: string, aggregateId: string, cause?: unknown);
219
+ }
220
+ /**
221
+ * Thrown by `IRepository.save()` when the aggregate's expected version
222
+ * does not match the version currently persisted: i.e. another writer
223
+ * updated the aggregate concurrently. The canonical optimistic-
224
+ * concurrency signal; the App-Service typically reloads, re-applies
225
+ * the use case, and retries, or surfaces HTTP 409 to the caller.
226
+ *
227
+ * **Retry means a FRESH unit of work** (a new `UnitOfWork.run()` /
228
+ * `withCommit` invocation): reload, re-apply, save. Do NOT catch this
229
+ * inside the same `run()` callback and continue: the failed aggregate
230
+ * is already enrolled (its events would be committed for a write that
231
+ * never happened) and the identity map still serves the same stale
232
+ * instance to any in-place "reload".
233
+ *
234
+ * `InfrastructureError` because the persistence layer (not a domain
235
+ * rule) detects the race. Marks itself as `retryable: true` so the
236
+ * `isRetryable` predicate from `@shirudo/base-error` picks it up.
237
+ */
238
+ declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyConflictError"> {
239
+ readonly aggregateType: string;
240
+ readonly aggregateId: string;
241
+ readonly expectedVersion: number;
242
+ readonly actualVersion: number;
243
+ /**
244
+ * Marks this error as retryable so `isRetryable(err)` returns
245
+ * true. The canonical OCC pattern is to reload the aggregate, re-apply
246
+ * the use case, and retry on this exception.
247
+ */
248
+ readonly retryable: true;
249
+ constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number, cause?: unknown);
250
+ }
251
+
252
+ /**
253
+ * Factory function producing a fresh, unique event identifier for each call.
254
+ *
255
+ * The library ships a default that uses Web Crypto `crypto.randomUUID()`
256
+ * (works on Node 19+, modern browsers in secure contexts, Deno, Bun,
257
+ * Cloudflare Workers, Vercel Edge, and any runtime that implements Web
258
+ * Crypto). Note that `crypto.randomUUID()` returns **UUID v4** (purely
259
+ * random); for production event stores prefer a **time-ordered** id
260
+ * format (UUID v7 / ULID / KSUID) so B-tree indexes on the eventId
261
+ * column stay clustered and `ORDER BY eventId` matches creation order.
262
+ * Swap one in via `setEventIdFactory(() => uuidv7())` or `() => ulid()`.
263
+ */
264
+ type EventIdFactory = () => string;
265
+ /**
266
+ * Replaces the global event-id factory used by `createDomainEvent` and
267
+ * `createDomainEventWithMetadata`. Call once during application bootstrap,
268
+ * for example:
269
+ *
270
+ * ```ts
271
+ * import { ulid } from "ulid";
272
+ * import { setEventIdFactory } from "@shirudo/ddd-kit";
273
+ *
274
+ * setEventIdFactory(() => ulid());
275
+ * ```
276
+ *
277
+ * The per-call `options.eventId` override always wins over this factory.
278
+ *
279
+ * **Module-scoped: last setter wins.** The factory lives as a single
280
+ * module variable; importing two libraries that both call this races on
281
+ * load order, and parallel test workers will see each other's factory.
282
+ * For test isolation and short-lived contexts prefer
283
+ * {@link withEventIdFactory}; for multi-tenant request isolation
284
+ * (e.g. one factory per tenant in a single Worker invocation) **prefer
285
+ * the per-call `options.eventId`** instead of mutating the global. Same
286
+ * caveat applies to `setClockFactory`.
287
+ */
288
+ declare function setEventIdFactory(factory: EventIdFactory): void;
289
+ /**
290
+ * Scoped variant of {@link setEventIdFactory}: installs `factory`,
291
+ * runs `fn`, then restores the previous factory in a `finally` block,
292
+ * so the restoration happens even if `fn` throws. Safe for parallel
293
+ * tests and for synchronous request handlers that need a tenant-
294
+ * specific factory without polluting the global.
295
+ *
296
+ * **Synchronous-only, enforced at runtime.** If `fn` returns a
297
+ * thenable (a `Promise` or any object with a `then` method), the
298
+ * helper throws *before* returning the value to the caller. This
299
+ * catches the async-misuse footgun where the factory would be
300
+ * restored before the awaited body of `fn` runs, leaving the awaited
301
+ * code reading the previous factory. For async scoping across `await`
302
+ * boundaries, use `AsyncLocalStorage`, which is out of scope for this
303
+ * helper; build it on top if you need it.
304
+ *
305
+ * Composes by nesting: an inner `withEventIdFactory` restores back to
306
+ * the outer's factory; the outer restores to the original.
307
+ *
308
+ * **When to prefer the per-call `options.eventId` instead.** If you're
309
+ * constructing a single event and want full control over its id,
310
+ * passing `{ eventId: "..." }` to `createDomainEvent` is the strongest
311
+ * isolation: it bypasses the factory mechanism entirely, no global
312
+ * mutation, no scope to manage. Reach for `withEventIdFactory` when
313
+ * the events are constructed deep inside domain methods you can't
314
+ * thread an explicit id through (typical test scenario), or when many
315
+ * events in a scope should share the same factory.
316
+ *
317
+ * @example
318
+ * ```ts
319
+ * // In a vitest test:
320
+ * it("emits deterministic ids", () => {
321
+ * withEventIdFactory(() => "evt-fixed", () => {
322
+ * const e = createDomainEvent("X", { v: 1 });
323
+ * expect(e.eventId).toBe("evt-fixed");
324
+ * });
325
+ * // Outside the callback the default crypto.randomUUID is restored,
326
+ * // even if the body had thrown.
327
+ * });
328
+ * ```
329
+ */
330
+ declare function withEventIdFactory<T>(factory: EventIdFactory, fn: () => T): T;
331
+ /**
332
+ * Restores the default event-id factory (`crypto.randomUUID()`).
333
+ * Intended for use in test `afterEach` hooks.
334
+ */
335
+ declare function resetEventIdFactory(): void;
336
+ /**
337
+ * Clock function producing a fresh `Date` for each call. The library
338
+ * defaults to `() => new Date()`; override globally via `setClockFactory`
339
+ * for deterministic event-sourcing tests, time-travel debugging, or any
340
+ * scenario where `occurredAt` must be reproducible.
341
+ */
342
+ type ClockFactory = () => Date;
343
+ /**
344
+ * Replaces the global clock factory used by `createDomainEvent` and
345
+ * `createDomainEventWithMetadata`. Call once during application bootstrap
346
+ * (or per-test in deterministic test suites):
347
+ *
348
+ * ```ts
349
+ * import { setClockFactory } from "@shirudo/ddd-kit";
350
+ *
351
+ * setClockFactory(() => new Date("2026-01-01T00:00:00Z"));
352
+ * ```
353
+ *
354
+ * The per-call `options.occurredAt` override always wins over this
355
+ * factory. Symmetric to `setEventIdFactory`.
356
+ *
357
+ * Module-scoped: see {@link setEventIdFactory} for the global-state
358
+ * caveats. For test isolation prefer {@link withClockFactory}; for
359
+ * multi-tenant request isolation prefer the per-call
360
+ * `options.occurredAt`.
361
+ */
362
+ declare function setClockFactory(factory: ClockFactory): void;
363
+ /**
364
+ * Scoped variant of {@link setClockFactory}: installs `factory`, runs
365
+ * `fn`, then restores the previous factory in a `finally` block.
366
+ * Synchronous-only, with the same constraints (and same runtime thenable
367
+ * guard) as {@link withEventIdFactory}.
368
+ *
369
+ * **When to prefer the per-call `options.occurredAt` instead.** Same
370
+ * trade-off as {@link withEventIdFactory}: passing `{ occurredAt }`
371
+ * to `createDomainEvent` is the strongest isolation for single-event
372
+ * cases. The scoped helper is for events constructed deep inside
373
+ * domain methods where threading an explicit timestamp is awkward.
374
+ *
375
+ * @example
376
+ * ```ts
377
+ * it("stamps events with a fixed clock", () => {
378
+ * const fixed = new Date("2026-01-01T00:00:00Z");
379
+ * withClockFactory(() => fixed, () => {
380
+ * const e = createDomainEvent("X", { v: 1 });
381
+ * expect(e.occurredAt).toEqual(fixed);
382
+ * });
383
+ * });
384
+ * ```
385
+ */
386
+ declare function withClockFactory<T>(factory: ClockFactory, fn: () => T): T;
387
+ /**
388
+ * Restores the default clock factory (`() => new Date()`).
389
+ * Intended for use in test `afterEach` hooks.
390
+ */
391
+ declare function resetClockFactory(): void;
392
+ /**
393
+ * Metadata associated with a domain event for traceability and correlation.
394
+ * Used in event-driven architectures to track event flow across services.
395
+ */
396
+ interface EventMetadata {
397
+ /**
398
+ * Correlation ID for tracing events across multiple services/components.
399
+ * Typically used to group related events in a distributed system.
400
+ */
401
+ correlationId?: string;
402
+ /**
403
+ * Causation ID referencing the event or command that caused this event.
404
+ * Used to build event chains and understand causality.
405
+ */
406
+ causationId?: string;
407
+ /**
408
+ * User ID of the person or system that triggered the event.
409
+ */
410
+ userId?: string;
411
+ /**
412
+ * Source service or component that produced the event.
413
+ */
414
+ source?: string;
415
+ /**
416
+ * Additional custom metadata fields.
417
+ * Allows extensibility for domain-specific metadata.
418
+ */
419
+ [key: string]: unknown;
420
+ }
421
+ /**
422
+ * Domain Event represents something meaningful that happened in the domain.
423
+ * Events are immutable and carry information about what occurred.
424
+ *
425
+ * **Events are PLAIN DATA objects**, constructed via `createDomainEvent`
426
+ * (or the aggregate's `recordEvent` helper) and deeply frozen. Class-based
427
+ * event objects that satisfy this shape structurally via prototype
428
+ * members are unsupported: the `withCommit` harvest copies events with a
429
+ * shallow spread (to stamp `aggregateVersion`), which only carries own
430
+ * enumerable properties.
431
+ *
432
+ * **Field-accretion boundary.** This type already carries the write-side
433
+ * transport concerns the outbox needs (`aggregateId`, `aggregateType`,
434
+ * `aggregateVersion`, `metadata`). That is the line: further transport
435
+ * fields (partition keys, tenancy, schema URNs, …) belong in an outbox
436
+ * envelope / `metadata`, not on the domain event: the next first-class
437
+ * transport field forces an `OutboxMessage` envelope port instead.
438
+ *
439
+ * @template T - The event type name (e.g., "OrderCreated")
440
+ * @template P - The event payload type
441
+ */
442
+ interface DomainEvent<T extends string, P = void> {
443
+ /**
444
+ * Unique identifier for this specific event instance. Used by idempotent
445
+ * consumers, outbox dispatch tracking, and as the target of
446
+ * `metadata.causationId`. Defaults to `crypto.randomUUID()` if not
447
+ * supplied.
448
+ */
449
+ eventId: string;
450
+ /**
451
+ * The type of the event, used for routing and handling.
452
+ */
453
+ type: T;
454
+ /**
455
+ * Identifier of the aggregate that produced the event. Optional at the
456
+ * library level; set it whenever the producing aggregate is known so
457
+ * downstream subscribers, outboxes, and projections can scope by entity.
458
+ */
459
+ aggregateId?: string;
460
+ /**
461
+ * Name of the aggregate type that produced the event (e.g. "Order").
462
+ * Pairs with `aggregateId` to fully qualify the source aggregate.
463
+ */
464
+ aggregateType?: string;
465
+ /**
466
+ * The event payload containing the domain data. The field is always
467
+ * present; its value is `undefined` when `P` is `void`.
468
+ */
469
+ payload: P;
470
+ /**
471
+ * Timestamp when the event occurred.
472
+ */
473
+ occurredAt: Date;
474
+ /**
475
+ * Event schema version for handling schema evolution.
476
+ * Required for safe schema migration in event-sourced systems.
477
+ * Use 1 for the initial schema version.
478
+ *
479
+ * **NOT the aggregate's version**: that is
480
+ * {@link aggregateVersion}. The two are deliberately distinct
481
+ * fields: this one says "which shape does the payload have"
482
+ * (upcasting), the other says "which state revision of the
483
+ * aggregate emitted this".
484
+ */
485
+ version: number;
486
+ /**
487
+ * The version of the producing aggregate at COMMIT time: the same
488
+ * value the OCC row write carries. Stamped automatically by
489
+ * `withCommit` at the harvest boundary (all events of one aggregate
490
+ * in one commit share it; their relative order within the commit is
491
+ * the harvest order), or set manually via
492
+ * `CreateDomainEventOptions.aggregateVersion`; a pre-set value is
493
+ * never overwritten.
494
+ *
495
+ * Consumers use it for ordering ("apply projections up to aggregate
496
+ * version N"), idempotency watermarks, debugging, and integration
497
+ * logs. Optional at the type level: events created outside an
498
+ * aggregate (system/integration events) and events from older kit
499
+ * versions don't carry it.
500
+ */
501
+ aggregateVersion?: number;
502
+ /**
503
+ * Optional metadata for traceability, correlation, and auditing.
504
+ * Includes correlationId, causationId, userId, source, and custom fields.
505
+ */
506
+ metadata?: EventMetadata;
507
+ }
508
+ /**
509
+ * Upper-bound alias for "any `DomainEvent` shape". Use as a generic
510
+ * constraint when a type parameter should accept any concrete event
511
+ * union. The `unknown` payload is the upper bound; concrete unions
512
+ * still narrow via `Extract<Evt, { type: K }>` at the use-site.
513
+ */
514
+ type AnyDomainEvent = DomainEvent<string, unknown>;
515
+ /**
516
+ * Shared option bag for the `createDomainEvent*` factories.
517
+ */
518
+ interface CreateDomainEventOptions {
519
+ /**
520
+ * Override for the auto-generated `eventId`. Pass an existing id (for
521
+ * replay, tests, or deterministic event sourcing) instead of letting the
522
+ * factory call `crypto.randomUUID()`.
523
+ */
524
+ eventId?: string;
525
+ /**
526
+ * Identifier of the aggregate that produced the event.
527
+ */
528
+ aggregateId?: string;
529
+ /**
530
+ * Name of the aggregate type that produced the event.
531
+ */
532
+ aggregateType?: string;
533
+ /**
534
+ * Override for the auto-generated `occurredAt` timestamp.
535
+ */
536
+ occurredAt?: Date;
537
+ /**
538
+ * Override for the default schema version (1).
539
+ */
540
+ version?: number;
541
+ /**
542
+ * Pre-set the producing aggregate's version (see
543
+ * `DomainEvent.aggregateVersion`). Normally left unset (`withCommit`
544
+ * stamps it at the harvest boundary with the commit version), but
545
+ * useful for replay fixtures and events constructed outside an
546
+ * aggregate. A pre-set value is never overwritten by the harvest.
547
+ */
548
+ aggregateVersion?: number;
549
+ /**
550
+ * Event metadata: correlation, causation, user, source, custom fields.
551
+ */
552
+ metadata?: EventMetadata;
553
+ }
554
+ /**
555
+ * Creates a domain event with default values.
556
+ * Sets occurredAt to current date and version to 1 if not provided.
557
+ *
558
+ * **For aggregate-internal events, prefer `this.recordEvent(...)` on
559
+ * `AggregateRoot` / `EventSourcedAggregate`.** That helper auto-injects
560
+ * `aggregateId` (from `this.id`) and `aggregateType` (from the
561
+ * aggregate's declared `aggregateType` property), which downstream
562
+ * consumers (outbox dispatchers, projection handlers, audit logs)
563
+ * route by. The `withCommit` harvest boundary now validates both fields
564
+ * are present and throws if they're missing, so a direct
565
+ * `createDomainEvent(...)` call inside an aggregate that forgets the
566
+ * options is caught at runtime.
567
+ *
568
+ * Use `createDomainEvent(...)` directly for events that don't belong to
569
+ * an aggregate: system events, integration events, configuration events,
570
+ * test fixtures. For those, set `aggregateId` / `aggregateType` in
571
+ * `options` if downstream consumers expect routing metadata.
572
+ *
573
+ * @param type - The event type
574
+ * @param payload - The event payload
575
+ * @param options - Optional event configuration (including `aggregateId`
576
+ * and `aggregateType` for routing)
577
+ * @returns A domain event
578
+ *
579
+ * @example
580
+ * ```typescript
581
+ * const event = createDomainEvent("OrderCreated", { orderId: "123" });
582
+ * ```
583
+ */
584
+ declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: CreateDomainEventOptions): DomainEvent<T, void>;
585
+ declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: CreateDomainEventOptions): DomainEvent<T, P>;
586
+ /**
587
+ * Creates a domain event with metadata for traceability.
588
+ * Convenience function for creating events with correlation and causation IDs.
589
+ *
590
+ * @example
591
+ * ```typescript
592
+ * const event = createDomainEventWithMetadata(
593
+ * "OrderCreated",
594
+ * { orderId: "123" },
595
+ * { correlationId: "corr-123", causationId: "cmd-456", userId: "user-789" }
596
+ * );
597
+ * ```
598
+ */
599
+ declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?: Omit<CreateDomainEventOptions, "metadata">): DomainEvent<T, P>;
600
+ /**
601
+ * Copies metadata from a source event to a new event.
602
+ * Useful for maintaining correlation chains in event-driven architectures.
603
+ *
604
+ * @example
605
+ * ```typescript
606
+ * const newEvent = createDomainEvent(
607
+ * "OrderShipped",
608
+ * { orderId: "123" },
609
+ * { metadata: copyMetadata(previousEvent, { causationId: previousEvent.type }) }
610
+ * );
611
+ * ```
612
+ */
613
+ declare function copyMetadata(sourceEvent: AnyDomainEvent, additionalMetadata?: Partial<EventMetadata>): EventMetadata;
614
+ /**
615
+ * Merges multiple metadata objects into one.
616
+ * Later metadata objects override earlier ones for the same keys.
617
+ *
618
+ * @example
619
+ * ```typescript
620
+ * const metadata = mergeMetadata(
621
+ * { correlationId: "corr-123" },
622
+ * { userId: "user-456" },
623
+ * { source: "order-service" }
624
+ * );
625
+ * ```
626
+ */
627
+ declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
628
+
629
+ type Version = number & {
630
+ readonly __v: true;
631
+ };
632
+ /**
633
+ * Snapshot of an aggregate state at a specific point in time.
634
+ * Used for optimizing event replay by starting from a snapshot
635
+ * instead of replaying all events from the beginning.
636
+ *
637
+ * @template TState - The type of the aggregate state
638
+ */
639
+ interface AggregateSnapshot<TState> {
640
+ /**
641
+ * The state of the aggregate at the time of the snapshot.
642
+ */
643
+ state: TState;
644
+ /**
645
+ * The version of the aggregate when the snapshot was taken.
646
+ */
647
+ version: Version;
648
+ /**
649
+ * Timestamp when the snapshot was created.
650
+ */
651
+ snapshotAt: Date;
652
+ }
653
+ /**
654
+ * Public contract every Aggregate Root satisfies. Implemented by
655
+ * `BaseAggregate` and inherited by both `AggregateRoot` and
656
+ * `EventSourcedAggregate`. Repository implementations type their
657
+ * `save(aggregate)` parameter against this interface rather than the
658
+ * concrete classes, so the repo layer does not take a compile-time
659
+ * dependency on the aggregate hierarchy.
660
+ *
661
+ * Full per-member documentation lives on the concrete `BaseAggregate`
662
+ * class; the interface is intentionally terse to avoid drift.
663
+ *
664
+ * @template TId - The aggregate root identifier (branded via `Id<Tag>`)
665
+ * @template TEvent - The domain-event union, defaults to `never`
666
+ */
667
+ interface IAggregateRoot<TId extends Id<string>, TEvent = never> {
668
+ readonly id: TId;
669
+ readonly version: Version;
670
+ readonly persistedVersion: Version | undefined;
671
+ readonly pendingEvents: ReadonlyArray<TEvent>;
672
+ clearPendingEvents(): void;
673
+ markPersisted(version: Version): void;
674
+ }
675
+ /**
676
+ * Public contract for Event-Sourced Aggregate Roots. Extends
677
+ * `IAggregateRoot` with the replay-from-history boundary.
678
+ *
679
+ * @template TId - The aggregate root identifier
680
+ * @template TEvent - The union type of all domain events
681
+ */
682
+ interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends AnyDomainEvent> extends IAggregateRoot<TId, TEvent> {
683
+ /**
684
+ * Reconstitutes the aggregate from an event history. Returns
685
+ * `Result` because event-stream corruption is an expected
686
+ * recoverable failure at the infrastructure boundary.
687
+ */
688
+ loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
689
+ }
690
+ /**
691
+ * Checks if two aggregates are at the same version (same ID and version).
692
+ * Useful for optimistic concurrency control checks.
693
+ *
694
+ * Note: Two aggregates with the same ID ARE the same aggregate (identity).
695
+ * This function checks if they are at the same version: i.e., no concurrent modification.
696
+ *
697
+ * @example
698
+ * ```typescript
699
+ * const before = await repository.getById(id);
700
+ * // ... some operations ...
701
+ * const after = await repository.getById(id);
702
+ *
703
+ * if (!sameVersion(before, after)) {
704
+ * throw new Error("Aggregate was modified by another process");
705
+ * }
706
+ * ```
707
+ */
708
+ declare function sameVersion<TId extends Id<string>>(a: {
709
+ id: TId;
710
+ version: Version;
711
+ }, b: {
712
+ id: TId;
713
+ version: Version;
714
+ }): boolean;
715
+
716
+ export { type AnyDomainEvent as A, type CreateDomainEventOptions as C, DomainError as D, type EventIdFactory as E, type IAggregateRoot as I, MissingHandlerError as M, UnenrolledChangesError as U, type Version as V, type Id as a, type AggregateSnapshot as b, type IEventSourcedAggregate as c, InfrastructureError as d, setEventIdFactory as e, type ClockFactory as f, setClockFactory as g, withClockFactory as h, resetClockFactory as i, type EventMetadata as j, type DomainEvent as k, createDomainEvent as l, createDomainEventWithMetadata as m, copyMetadata as n, mergeMetadata as o, EventHarvestError as p, AggregateDeletedError as q, resetEventIdFactory as r, sameVersion as s, AggregateNotFoundError as t, DuplicateAggregateError as u, ConcurrencyConflictError as v, withEventIdFactory as w, type IdGenerator as x };