@shirudo/ddd-kit 2.2.0 → 3.0.0-rc.4

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