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