@shirudo/ddd-kit 2.0.0 → 3.0.0-rc.3

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