@shirudo/ddd-kit 2.2.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.
@@ -0,0 +1,2808 @@
1
+ import { o as DomainError } from "./errors.js";
2
+ import { Result } from "@shirudo/result";
3
+
4
+ //#region src/core/id.d.ts
5
+ /**
6
+ * Branded string ID. `Tag` carries the aggregate / entity name so two ids
7
+ * with different tags are not assignable to each other even though both
8
+ * are strings at runtime.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * type UserId = Id<"UserId">;
13
+ * type OrderId = Id<"OrderId">;
14
+ *
15
+ * const u = "user-1" as UserId;
16
+ * const o: OrderId = u; // ❌ compile error
17
+ * ```
18
+ */
19
+ type Id<Tag extends string> = string & {
20
+ readonly __brand: Tag;
21
+ };
22
+ /**
23
+ * Produces fresh ids of a single, fixed tag. The tag is bound at the
24
+ * generator type: `IdGenerator<"UserId">.next()` returns `Id<"UserId">`
25
+ * with no caller-side generic to abuse.
26
+ *
27
+ * **Your factory must produce unique ids under concurrent calls.**
28
+ * The kit makes no attempt to dedupe or detect collisions: a collision
29
+ * silently overwrites earlier rows (under unique-key constraints) or
30
+ * silently aliases two different entities (without them). Safe choices:
31
+ * `crypto.randomUUID()` (UUIDv4, the default for events), ULID, UUIDv7,
32
+ * KSUID: all collision-resistant by design. Unsafe choices: `Date.now()`
33
+ * alone (duplicates within the same millisecond), a process-local
34
+ * counter without persistence (resets to 1 on restart, collides with
35
+ * prior runs), a sequential id derived from non-atomic state.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * import { ulid } from "ulid";
40
+ *
41
+ * const userIds: IdGenerator<"UserId"> = { next: () => ulid() as Id<"UserId"> };
42
+ * const id = userIds.next(); // Id<"UserId">
43
+ * ```
44
+ *
45
+ * The previous shape (`IdGenerator { next<T extends string>(): Id<T> }`)
46
+ * let callers pick `T` themselves: `gen.next<"AnyTag">()` typechecked
47
+ * even when the generator produced different-tag ids, silently defeating
48
+ * the brand.
49
+ */
50
+ interface IdGenerator<Tag extends string> {
51
+ next: () => Id<Tag>;
52
+ }
53
+ //#endregion
54
+ //#region src/aggregate/clock.d.ts
55
+ /**
56
+ * Clock function producing a valid `Date` for the current instant.
57
+ * Event-clock reads throw `TypeError` when the result is invalid.
58
+ */
59
+ type ClockFactory = () => Date;
60
+ //#endregion
61
+ //#region src/aggregate/domain-event.d.ts
62
+ /**
63
+ * Factory function producing a fresh, unique event identifier for each call.
64
+ *
65
+ * The library ships a default that uses Web Crypto `crypto.randomUUID()`
66
+ * (works on Node 19+, modern browsers in secure contexts, Deno, Bun,
67
+ * Cloudflare Workers, Vercel Edge, and any runtime that implements Web
68
+ * Crypto). Note that `crypto.randomUUID()` returns **UUID v4** (purely
69
+ * random); for production event stores prefer a **time-ordered** id
70
+ * format (UUID v7 / ULID / KSUID) so B-tree indexes on the eventId
71
+ * column stay clustered and `ORDER BY eventId` matches creation order.
72
+ * Supply one to {@link createDomainEventFactory} to use UUID v7, ULID,
73
+ * KSUID, or another collision-safe format without mutating module state.
74
+ */
75
+ type EventIdFactory = () => string;
76
+ /**
77
+ * Metadata associated with a domain event for traceability and correlation.
78
+ * Used in event-driven architectures to track event flow across services.
79
+ */
80
+ interface EventMetadata {
81
+ /**
82
+ * Correlation ID for tracing events across multiple services/components.
83
+ * Typically used to group related events in a distributed system.
84
+ */
85
+ readonly correlationId?: string;
86
+ /**
87
+ * Conversation ID shared by every message in one long-running business
88
+ * interaction, even when that interaction spans several correlations.
89
+ */
90
+ readonly conversationId?: string;
91
+ /**
92
+ * Causation ID referencing the event or command that caused this event.
93
+ * Used to build event chains and understand causality.
94
+ */
95
+ readonly causationId?: string;
96
+ /**
97
+ * W3C Trace Context parent for technical tracing across process boundaries.
98
+ * This is distinct from business correlation and conversation identifiers.
99
+ */
100
+ readonly traceparent?: string;
101
+ /** Optional W3C vendor trace state associated with `traceparent`. */
102
+ readonly tracestate?: string;
103
+ /**
104
+ * User ID of the person or system that triggered the event.
105
+ */
106
+ readonly userId?: string;
107
+ /**
108
+ * Source service or component that produced the event.
109
+ */
110
+ readonly source?: string;
111
+ /**
112
+ * Additional custom metadata fields.
113
+ * Allows extensibility for domain-specific metadata.
114
+ */
115
+ readonly [key: string]: unknown;
116
+ }
117
+ /**
118
+ * Domain Event represents something meaningful that happened in the domain.
119
+ * Events are immutable and carry information about what occurred.
120
+ *
121
+ * **Events are PLAIN DATA objects**, constructed via `createDomainEvent`
122
+ * (or the aggregate's `createEvent` plus application-shell recording path)
123
+ * and deeply frozen. Class-based
124
+ * event objects that satisfy this shape structurally via prototype
125
+ * members are unsupported.
126
+ *
127
+ * **Field-accretion boundary.** Persistence positions, commit boundaries,
128
+ * broker offsets, and other delivery concerns belong in an event envelope,
129
+ * not on the domain event itself.
130
+ *
131
+ * @template T - The event type name (e.g., "OrderCreated")
132
+ * @template P - The event payload type
133
+ */
134
+ interface DomainEvent<T extends string, P = void> {
135
+ /**
136
+ * Unique identifier for this specific event instance. Used by idempotent
137
+ * consumers, outbox dispatch tracking, and as the target of
138
+ * `metadata.causationId`. Convenience constructors default to
139
+ * `crypto.randomUUID()`; strict construction requires the caller to supply it.
140
+ */
141
+ readonly eventId: string;
142
+ /**
143
+ * The type of the event, used for routing and handling.
144
+ */
145
+ readonly type: T;
146
+ /**
147
+ * Identifier of the aggregate that produced the event. Optional at the
148
+ * library level; set it whenever the producing aggregate is known so
149
+ * downstream subscribers, outboxes, and projections can scope by entity.
150
+ */
151
+ readonly aggregateId?: string;
152
+ /**
153
+ * Name of the aggregate type that produced the event (e.g. "Order").
154
+ * Pairs with `aggregateId` to fully qualify the source aggregate.
155
+ */
156
+ readonly aggregateType?: string;
157
+ /**
158
+ * The event payload containing the domain data. The field is always
159
+ * present; its value is `undefined` when `P` is `void`.
160
+ */
161
+ readonly payload: P;
162
+ /**
163
+ * Timestamp when the accepted fact was recorded by the application shell.
164
+ * Put business-relevant time in the payload under a domain name.
165
+ */
166
+ readonly occurredAt: Date;
167
+ /**
168
+ * Event schema version for handling schema evolution.
169
+ * Required for safe schema migration in event-sourced systems.
170
+ * Use 1 for the initial schema version.
171
+ *
172
+ * This is the event PAYLOAD schema version, not a persisted aggregate
173
+ * position. Commit positions live on `CommittedDomainEvent`.
174
+ */
175
+ readonly version: number;
176
+ /**
177
+ * Optional metadata for traceability, correlation, and auditing.
178
+ * Includes correlationId, conversationId, causationId, userId, source, and
179
+ * custom fields.
180
+ */
181
+ readonly metadata?: EventMetadata;
182
+ }
183
+ /**
184
+ * Upper-bound alias for "any `DomainEvent` shape". Use as a generic
185
+ * constraint when a type parameter should accept any concrete event
186
+ * union. The `unknown` payload is the upper bound; concrete unions
187
+ * still narrow via `Extract<Evt, { type: K }>` at the use-site.
188
+ */
189
+ type AnyDomainEvent = DomainEvent<string, unknown>;
190
+ /**
191
+ * A domain event accepted by an aggregate but not yet given its recording
192
+ * identity, recording time, or delivery metadata.
193
+ *
194
+ * The aggregate owns the event type, payload, source address, and payload
195
+ * schema version because those values describe the business fact it produced.
196
+ * The application shell later turns this value into a {@link DomainEvent}.
197
+ */
198
+ interface UncommittedDomainEvent<T extends string, P = void> {
199
+ readonly type: T;
200
+ readonly aggregateId?: string;
201
+ readonly aggregateType?: string;
202
+ readonly payload: P;
203
+ readonly version: number;
204
+ }
205
+ /** Upper-bound alias for any uncommitted domain-event shape. */
206
+ type AnyUncommittedDomainEvent = UncommittedDomainEvent<string, unknown>;
207
+ /** Derives the uncommitted shape represented by a concrete event or event union. */
208
+ type UncommittedDomainEventOf<TEvent extends AnyDomainEvent> = TEvent extends DomainEvent<infer TType, infer TPayload> ? UncommittedDomainEvent<TType, TPayload> : never;
209
+ /** An aggregate may hold unstamped decisions and already recorded events together. */
210
+ type PendingDomainEvent<TEvent extends AnyDomainEvent> = TEvent | UncommittedDomainEventOf<TEvent>;
211
+ /** Producer-owned options for an uncommitted event. */
212
+ interface CreateUncommittedDomainEventOptions {
213
+ readonly aggregateId?: string;
214
+ readonly aggregateType?: string;
215
+ readonly version?: number;
216
+ }
217
+ /**
218
+ * Shared option bag for the `createDomainEvent*` factories.
219
+ */
220
+ interface CreateDomainEventOptions {
221
+ /**
222
+ * Override for the auto-generated `eventId`. Pass an existing id (for
223
+ * replay, tests, or deterministic event sourcing) instead of letting the
224
+ * factory call `crypto.randomUUID()`.
225
+ */
226
+ eventId?: string;
227
+ /**
228
+ * Identifier of the aggregate that produced the event.
229
+ */
230
+ aggregateId?: string;
231
+ /**
232
+ * Name of the aggregate type that produced the event.
233
+ */
234
+ aggregateType?: string;
235
+ /**
236
+ * Override for the auto-generated `occurredAt` timestamp.
237
+ */
238
+ occurredAt?: Date;
239
+ /**
240
+ * Override for the default schema version (1).
241
+ */
242
+ version?: number;
243
+ /**
244
+ * Event metadata: correlation, causation, user, source, custom fields.
245
+ */
246
+ metadata?: EventMetadata;
247
+ }
248
+ /** Technical recording data attached by the application shell. */
249
+ interface DomainEventStamp {
250
+ /** Stable identity for this event instance. */
251
+ readonly eventId: string;
252
+ /** Time at which the accepted domain fact was recorded. */
253
+ readonly occurredAt: Date;
254
+ /** Optional correlation, causation, actor, and source metadata. */
255
+ readonly metadata?: EventMetadata;
256
+ }
257
+ /** Full strict-construction options, including producer-owned event fields. */
258
+ interface CreateDomainEventFromFactsOptions extends DomainEventStamp {
259
+ readonly aggregateId?: string;
260
+ readonly aggregateType?: string;
261
+ readonly version?: number;
262
+ }
263
+ /** Overrides accepted when an application-shell factory creates a stamp. */
264
+ interface CreateDomainEventStampOptions {
265
+ readonly eventId?: string;
266
+ readonly occurredAt?: Date;
267
+ readonly metadata?: EventMetadata;
268
+ }
269
+ /** Dependencies captured by one immutable domain-event factory instance. */
270
+ interface DomainEventFactoryOptions {
271
+ /** Event-id generator. Defaults to Web Crypto `crypto.randomUUID()`. */
272
+ readonly eventIdFactory?: EventIdFactory;
273
+ /** Event-recording clock. Defaults to `() => new Date()`. */
274
+ readonly clock?: ClockFactory;
275
+ }
276
+ /**
277
+ * Instance-bound event constructor. Each factory permanently captures its
278
+ * own event-id and clock dependencies, so request and test instances cannot
279
+ * overwrite one another through module state.
280
+ */
281
+ interface DomainEventFactory {
282
+ /**
283
+ * Creates immutable technical recording data in the application shell.
284
+ */
285
+ readonly createStamp: (options?: CreateDomainEventStampOptions) => DomainEventStamp;
286
+ readonly create: {
287
+ <T extends string>(type: T, payload?: undefined, options?: CreateDomainEventOptions): DomainEvent<T, void>;
288
+ <T extends string, P>(type: T, payload: P, options?: CreateDomainEventOptions): DomainEvent<T, P>;
289
+ };
290
+ /**
291
+ * Reads the captured clock and returns a defensive `Date` copy.
292
+ * Throws `TypeError` when the clock does not return a valid date.
293
+ */
294
+ readonly now: () => Date;
295
+ }
296
+ /**
297
+ * Creates an immutable, instance-bound domain-event factory.
298
+ *
299
+ * The supplied functions are read once and captured by value. The returned
300
+ * object is frozen, so another request, test, or library cannot replace its
301
+ * policy. Its {@link DomainEventFactory.createStamp} method is the
302
+ * application-shell bridge that records an accepted aggregate decision.
303
+ * Passing the factory through `AggregateConfig`
304
+ * enables the explicitly named convenience methods, whose defaults read time
305
+ * and randomness.
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * const domainEvents = createDomainEventFactory({
310
+ * eventIdFactory: () => uuidv7(),
311
+ * clock: () => new Date(),
312
+ * });
313
+ * order.confirm();
314
+ * recordPendingEvents(order, domainEvents);
315
+ * ```
316
+ */
317
+ declare function createDomainEventFactory(options?: DomainEventFactoryOptions): DomainEventFactory;
318
+ /**
319
+ * Immutable UUID-v4/platform-clock factory used by the top-level
320
+ * {@link createDomainEvent}. It cannot be reconfigured; construct an instance
321
+ * with {@link createDomainEventFactory} for custom policy.
322
+ */
323
+ declare const defaultDomainEventFactory: DomainEventFactory;
324
+ declare function createUncommittedDomainEvent<T extends string>(type: T, payload?: undefined, options?: CreateUncommittedDomainEventOptions): UncommittedDomainEvent<T, void>;
325
+ declare function createUncommittedDomainEvent<T extends string, P>(type: T, payload: P, options?: CreateUncommittedDomainEventOptions): UncommittedDomainEvent<T, P>;
326
+ /**
327
+ * Attaches shell-owned recording data to an accepted aggregate decision.
328
+ *
329
+ * The decision supplies the domain type, payload, source address, and payload
330
+ * schema version. The stamp supplies only event identity, recording time, and
331
+ * trace metadata.
332
+ */
333
+ declare function recordDomainEvent<T extends string, P>(event: UncommittedDomainEvent<T, P>, stamp: DomainEventStamp): DomainEvent<T, P>;
334
+ declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: CreateDomainEventOptions): DomainEvent<T, void>;
335
+ declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: CreateDomainEventOptions): DomainEvent<T, P>;
336
+ /**
337
+ * Creates an already minted domain event exclusively from explicit envelope
338
+ * facts. Unlike {@link createDomainEvent}, it has no clock or event-id fallback
339
+ * and is useful when replay, migration, or a caller-owned boundary already has
340
+ * the final identity and occurrence time.
341
+ *
342
+ * Aggregate behavior normally creates an {@link UncommittedDomainEvent} through
343
+ * its protected `createEvent` helper. The application shell later records that
344
+ * pending fact with caller-owned time and identity.
345
+ */
346
+ declare function createDomainEventFromFacts<T extends string>(type: T, payload: undefined, options: CreateDomainEventFromFactsOptions): DomainEvent<T, void>;
347
+ declare function createDomainEventFromFacts<T extends string, P>(type: T, payload: P, options: CreateDomainEventFromFactsOptions): DomainEvent<T, P>;
348
+ /**
349
+ * Copies metadata from a source event to a new event.
350
+ * Useful for maintaining correlation chains in event-driven architectures.
351
+ *
352
+ * @example
353
+ * ```typescript
354
+ * const newEvent = createDomainEvent(
355
+ * "OrderShipped",
356
+ * { orderId: "123" },
357
+ * { metadata: copyMetadata(previousEvent, { causationId: previousEvent.type }) }
358
+ * );
359
+ * ```
360
+ */
361
+ declare function copyMetadata(sourceEvent: AnyDomainEvent, additionalMetadata?: Partial<EventMetadata>): EventMetadata;
362
+ /**
363
+ * Merges multiple metadata objects into one.
364
+ * Later metadata objects override earlier ones for the same keys.
365
+ *
366
+ * @example
367
+ * ```typescript
368
+ * const metadata = mergeMetadata(
369
+ * { correlationId: "corr-123" },
370
+ * { userId: "user-456" },
371
+ * { source: "order-service" }
372
+ * );
373
+ * ```
374
+ */
375
+ declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
376
+ //#endregion
377
+ //#region src/aggregate/aggregate.d.ts
378
+ type Version = number & {
379
+ readonly __v: true;
380
+ };
381
+ /**
382
+ * Snapshot of an aggregate state at a specific point in time.
383
+ * Used for optimizing event replay by starting from a snapshot
384
+ * instead of replaying all events from the beginning.
385
+ *
386
+ * @template TState - The type of the aggregate state
387
+ */
388
+ interface AggregateSnapshot<TState> {
389
+ /**
390
+ * The state of the aggregate at the time of the snapshot.
391
+ */
392
+ readonly state: TState;
393
+ /**
394
+ * The version of the aggregate when the snapshot was taken.
395
+ */
396
+ readonly version: Version;
397
+ /**
398
+ * Timestamp when the snapshot was created.
399
+ */
400
+ readonly snapshotAt: Date;
401
+ /**
402
+ * Schema version of the stored `state` shape, declared by its adapter-owned
403
+ * `SnapshotModel` and stamped by `captureAggregateSnapshot`. Distinct from
404
+ * {@link version}, which counts mutations: this field says "which
405
+ * shape does the stored state have", so a restore can detect a
406
+ * snapshot written against an older DTO shape and migrate or
407
+ * discard it instead of crashing later. Optional: absent on snapshots
408
+ * written by older kit versions, which restore treats as schema `1`.
409
+ */
410
+ readonly schemaVersion?: number;
411
+ }
412
+ /**
413
+ * Public contract every Aggregate Root satisfies. Implemented by
414
+ * `BaseAggregate` and inherited by both `AggregateRoot` and
415
+ * `EventSourcedAggregate`. Repository ports use this interface as their
416
+ * aggregate type rather than depending on concrete base classes, so persistence
417
+ * orchestration does not take a compile-time
418
+ * dependency on the aggregate hierarchy.
419
+ *
420
+ * Full per-member documentation lives on the concrete `BaseAggregate`
421
+ * class; the interface is intentionally terse to avoid drift. Persistence
422
+ * facts are readable, but acknowledgement and pending-event disposal are not
423
+ * part of this surface. `withCommit` and `UnitOfWork` hold that authority.
424
+ *
425
+ * @template TId - The aggregate root identifier (branded via `Id<Tag>`)
426
+ * @template TEvent - The domain-event union, defaults to `never`
427
+ */
428
+ interface IAggregateRoot<TId extends Id<string>, TEvent extends AnyDomainEvent = never> {
429
+ readonly id: TId;
430
+ readonly version: Version;
431
+ readonly pendingEvents: ReadonlyArray<PendingDomainEvent<TEvent>>;
432
+ }
433
+ /**
434
+ * Public contract for Event-Sourced Aggregate Roots. Extends
435
+ * `IAggregateRoot` with the replay-from-history boundary.
436
+ *
437
+ * @template TId - The aggregate root identifier
438
+ * @template TEvent - The union type of all domain events
439
+ */
440
+ interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends AnyDomainEvent> extends IAggregateRoot<TId, TEvent> {
441
+ /**
442
+ * Reconstitutes the aggregate from an event history. Returns
443
+ * `Result` because event-stream corruption is an expected
444
+ * recoverable failure at the infrastructure boundary.
445
+ */
446
+ loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
447
+ }
448
+ /**
449
+ * Checks if two aggregates are at the same version (same ID and version).
450
+ * Useful for optimistic concurrency control checks.
451
+ *
452
+ * Note: Two aggregates with the same ID ARE the same aggregate (identity).
453
+ * This function checks if they are at the same version: i.e., no concurrent modification.
454
+ *
455
+ * @example
456
+ * ```typescript
457
+ * const before = await repository.findById(id);
458
+ * // ... some operations ...
459
+ * const after = await repository.findById(id);
460
+ *
461
+ * if (!sameVersion(before, after)) {
462
+ * throw new Error("Aggregate was modified by another process");
463
+ * }
464
+ * ```
465
+ */
466
+ declare function sameVersion<TId extends Id<string>>(a: {
467
+ id: TId;
468
+ version: Version;
469
+ }, b: {
470
+ id: TId;
471
+ version: Version;
472
+ }): boolean;
473
+ //#endregion
474
+ //#region src/aggregate/aggregate-address.d.ts
475
+ /**
476
+ * Stable value address of one aggregate instance.
477
+ *
478
+ * Aggregate ids are type-scoped, so the raw id alone is not globally unique:
479
+ * `SalesOrder 1` and `FulfillmentOrder 1` are different aggregates. Event
480
+ * streams, snapshots, committed-event sources, and projection checkpoints
481
+ * therefore carry both fields instead of defining boundary-specific variants.
482
+ *
483
+ * `aggregateType` is a stable technical stream category. Renaming it changes
484
+ * persistence keys and orphans checkpoints unless the stored addresses are
485
+ * migrated. When bounded contexts share infrastructure and reuse a domain
486
+ * name, qualify it at the source (`sales.order`, `fulfillment.order`). The kit
487
+ * deliberately adds no separate `boundedContext` field: qualification remains
488
+ * the consumer's naming decision.
489
+ */
490
+ interface AggregateAddress<TAggregateId extends string = string> {
491
+ readonly aggregateType: string;
492
+ readonly aggregateId: TAggregateId;
493
+ }
494
+ //#endregion
495
+ //#region src/entity/entity.d.ts
496
+ /** A pure invariant check that throws when a candidate state is invalid. */
497
+ type StateValidator<TState> = (state: TState) => void;
498
+ /**
499
+ * Construction options shared by `Entity` and (via `AggregateConfig`) the
500
+ * aggregate base classes.
501
+ */
502
+ interface EntityConfig<TState = unknown> {
503
+ /**
504
+ * Pure state-invariant validator captured by the entity instance. It runs
505
+ * against the exact frozen state stored during construction and every
506
+ * {@link Entity.setState} call. Throw to reject the candidate state.
507
+ *
508
+ * Passing validation as data avoids virtual dispatch from the base
509
+ * constructor: the function cannot observe partly initialised subclass
510
+ * fields through `this`. Close over immutable policy supplied to the
511
+ * concrete constructor when validation needs instance-specific inputs.
512
+ */
513
+ readonly validateState?: StateValidator<TState>;
514
+ /**
515
+ * Opt-in: freeze the WHOLE state graph (via `deepFreeze`) instead of
516
+ * the default shallow freeze. This protects against nested aliases
517
+ * retained by constructor callers and against accidental in-place
518
+ * writes inside the entity; live state itself is never public.
519
+ *
520
+ * Defaults to `false` (the documented shallow contract): deep freezing
521
+ * costs a full state-graph walk on every state write, which is why it
522
+ * is not the default on hot paths.
523
+ *
524
+ * **Only for plain-data states.** The deep freeze walks the entire
525
+ * graph: a class-based child entity inside the state would be frozen
526
+ * too, and its own mutation methods would start throwing. States
527
+ * carrying class-based children must keep the default shallow freeze.
528
+ * Note that the ownership transfer widens accordingly: nested objects
529
+ * passed into the constructor or `setState` are frozen IN PLACE (the
530
+ * shallow copy protects only the top-level input object).
531
+ */
532
+ deepFreezeState?: boolean;
533
+ }
534
+ /**
535
+ * Functional definition of an Entity via its capability: an object is
536
+ * identifiable if it has an `id`.
537
+ *
538
+ * `TId` is constrained to `Id<string>` so the brand discipline that
539
+ * `Id<Tag>` enforces is preserved end-to-end: an `Identifiable<UserId>`
540
+ * cannot accidentally be paired with an `Identifiable<OrderId>` or with
541
+ * a plain `string`.
542
+ */
543
+ type Identifiable<TId extends Id<string>> = {
544
+ readonly id: TId;
545
+ };
546
+ /**
547
+ * Interface for Entities with state.
548
+ *
549
+ * In Domain-Driven Design, Entities have:
550
+ * - Identity (id): Distinguishes one entity from another
551
+ * - State: The attributes/properties of the entity
552
+ *
553
+ * Unlike Value Objects (which are immutable and compared by value),
554
+ * Entities are compared by identity and can have mutable state.
555
+ *
556
+ * @template TId - The type of the entity identifier
557
+ */
558
+ interface IEntity<TId extends Id<string>> extends Identifiable<TId> {
559
+ /**
560
+ * Unique identifier of the entity.
561
+ */
562
+ readonly id: TId;
563
+ }
564
+ /**
565
+ * Abstract base class for Entities with state.
566
+ *
567
+ * Provides:
568
+ * - Identity management (id)
569
+ * - State management
570
+ * - Instance-bound pure state validation
571
+ * - Protected state access for domain behavior
572
+ *
573
+ * This is the foundation for all Entities in DDD:
574
+ * - Child Entities within aggregates can extend this
575
+ * - Aggregate Roots extend this and add version + events
576
+ *
577
+ * @template TState - The type of the entity state
578
+ * @template TId - The type of the entity identifier
579
+ *
580
+ * @example
581
+ * ```typescript
582
+ * // Child Entity within an aggregate
583
+ * const validateOrderItemState = (state: OrderItemState): void => {
584
+ * if (state.quantity < 1) throw new Error("quantity must be positive");
585
+ * };
586
+ *
587
+ * class OrderItem extends Entity<OrderItemState, ItemId> {
588
+ * constructor(id: ItemId, initialState: OrderItemState) {
589
+ * super(id, initialState, { validateState: validateOrderItemState });
590
+ * }
591
+ *
592
+ * updateQuantity(quantity: number): void {
593
+ * // setState runs validateState and re-freezes; a direct
594
+ * // `this._state = ...` assignment would skip both.
595
+ * this.setState({ ...this.state, quantity });
596
+ * }
597
+ * }
598
+ * ```
599
+ */
600
+ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity<TId> {
601
+ readonly id: TId;
602
+ /**
603
+ * Returns the live state to subclass domain behavior.
604
+ *
605
+ * This accessor is deliberately protected: returning the generic
606
+ * `TState` publicly would expose the aggregate's live object graph and
607
+ * let nested mutation bypass behavior, validation, versioning, and
608
+ * dirty tracking. Concrete entities should expose business-meaningful queries or
609
+ * detached immutable DTOs. Snapshot projection belongs to an adapter-owned
610
+ * `SnapshotModel`; persistence code captures an aggregate with
611
+ * `captureAggregateSnapshot(model, aggregate, snapshotAt)` rather than
612
+ * asking the entity to create its own persistence memento.
613
+ */
614
+ protected get state(): TState;
615
+ /**
616
+ * The state is `protected` so that only the subclass can modify it.
617
+ * Ordinary entity behavior must use {@link setState}; direct assignment
618
+ * skips instance-bound validation. Kit event-sourcing internals use direct
619
+ * assignment deliberately because historical evolution must not run
620
+ * today's decision validator.
621
+ */
622
+ protected _state: TState;
623
+ private readonly _deepFreezeState;
624
+ private readonly validateState;
625
+ /**
626
+ * **State ownership.** Plain-object and array states are shallow-copied
627
+ * before the freeze, so the caller's own object stays mutable. A CLASS
628
+ * INSTANCE passed as state is an ownership transfer: it is frozen
629
+ * in place (a copy would strip its prototype). Do not keep mutating
630
+ * the instance after handing it to the entity. The same contract
631
+ * applies to {@link setState}. With
632
+ * {@link EntityConfig.deepFreezeState} enabled, the ownership transfer
633
+ * widens to the whole graph: NESTED objects are frozen in place too.
634
+ *
635
+ * @throws HostileStateKeyError when a plain-object, null-prototype,
636
+ * or array state carries an own `"__proto__"` data key; validate and
637
+ * strip untrusted input at the boundary.
638
+ */
639
+ protected constructor(id: TId, initialState: TState, config?: EntityConfig<TState>);
640
+ /**
641
+ * Freezes a state value according to this entity's configured freeze
642
+ * mode: the default shallow freeze, or `deepFreeze` when
643
+ * {@link EntityConfig.deepFreezeState} was enabled at construction.
644
+ * Infrastructure-style subclass code that deliberately assigns
645
+ * `this._state` directly must freeze through this method, not
646
+ * `freezeShallow`, or the opt-in silently degrades to shallow for that
647
+ * path. Ordinary domain behavior should use {@link setState} instead.
648
+ */
649
+ protected freezeState(value: TState): TState;
650
+ /**
651
+ * Sets the state of the entity.
652
+ * This is a convenience method for state mutations.
653
+ * Automatically validates `newState` with the instance-bound
654
+ * {@link EntityConfig.validateState} function.
655
+ *
656
+ * Plain-object and array states are shallow-copied before the freeze
657
+ * (the caller's object stays mutable); a class-instance state is an
658
+ * ownership transfer and is frozen in place; see the constructor.
659
+ *
660
+ * @param newState - The new state
661
+ * @throws HostileStateKeyError when the state carries an own
662
+ * `"__proto__"` data key; the previous state is kept.
663
+ */
664
+ protected setState(newState: TState): void;
665
+ }
666
+ /**
667
+ * Shallow-freezes `value` when it's a non-null object or array, so that
668
+ * direct property writes throw in strict mode. Returns the value as-is for
669
+ * primitives. Used internally by `Entity` (via `freezeState`, which picks
670
+ * shallow or deep per the `deepFreezeState` config) to prevent outside
671
+ * mutation of state read through the `state` getter without paying the
672
+ * cost of a deep clone on every read.
673
+ *
674
+ * Subclass code that assigns `this._state` directly should freeze through
675
+ * the protected `freezeState(value)` method rather than calling this
676
+ * helper, so the configured freeze mode is honored. The export remains
677
+ * for consumers using it as a standalone utility.
678
+ */
679
+ declare function freezeShallow<T>(value: T): T;
680
+ /**
681
+ * Checks if two entities have the same ID.
682
+ * Works with any object that has an 'id' property.
683
+ *
684
+ * @param a - First entity
685
+ * @param b - Second entity
686
+ * @returns true if both entities have the same ID, false otherwise
687
+ *
688
+ * @example
689
+ * ```typescript
690
+ * const item1: OrderItem = { id: itemId1, productId: "prod-1", quantity: 2 };
691
+ * const item2: OrderItem = { id: itemId2, productId: "prod-2", quantity: 1 };
692
+ *
693
+ * sameEntity(item1, item2); // false
694
+ * sameEntity(item1, item1); // true
695
+ * ```
696
+ */
697
+ declare function sameEntity<TId extends Id<string>>(a: Identifiable<TId>, b: Identifiable<TId>): boolean;
698
+ /**
699
+ * Finds an entity by ID in a collection.
700
+ * Returns undefined if not found.
701
+ *
702
+ * @param entities - Array of entities to search
703
+ * @param id - The ID to search for
704
+ * @returns The entity if found, undefined otherwise
705
+ *
706
+ * @example
707
+ * ```typescript
708
+ * const items: OrderItem[] = [
709
+ * { id: itemId1, productId: "prod-1", quantity: 2 },
710
+ * { id: itemId2, productId: "prod-2", quantity: 1 }
711
+ * ];
712
+ *
713
+ * const item = findEntityById(items, itemId1);
714
+ * // item is { id: itemId1, productId: "prod-1", quantity: 2 }
715
+ * ```
716
+ */
717
+ declare function findEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId): T | undefined;
718
+ /**
719
+ * Checks if an entity with the given ID exists in the collection.
720
+ *
721
+ * @param entities - Array of entities to search
722
+ * @param id - The ID to check for
723
+ * @returns true if an entity with the ID exists, false otherwise
724
+ *
725
+ * @example
726
+ * ```typescript
727
+ * const items: OrderItem[] = [
728
+ * { id: itemId1, productId: "prod-1", quantity: 2 }
729
+ * ];
730
+ *
731
+ * hasEntityId(items, itemId1); // true
732
+ * hasEntityId(items, itemId2); // false
733
+ * ```
734
+ */
735
+ declare function hasEntityId<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId): boolean;
736
+ /**
737
+ * Removes an entity with the given ID from the collection. Returns the
738
+ * ORIGINAL array when the id is absent (structural sharing for the
739
+ * reference-based dirty tracking; see `updateEntityById`), otherwise a
740
+ * new array without the entity.
741
+ *
742
+ * @param entities - Array of entities
743
+ * @param id - The ID of the entity to remove
744
+ * @returns A new array without the entity with the given ID
745
+ *
746
+ * @example
747
+ * ```typescript
748
+ * const items: OrderItem[] = [
749
+ * { id: itemId1, productId: "prod-1", quantity: 2 },
750
+ * { id: itemId2, productId: "prod-2", quantity: 1 }
751
+ * ];
752
+ *
753
+ * const updated = removeEntityById(items, itemId1);
754
+ * // updated is [{ id: itemId2, productId: "prod-2", quantity: 1 }]
755
+ * ```
756
+ */
757
+ declare function removeEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId): ReadonlyArray<T>;
758
+ /**
759
+ * Updates an entity with the given ID in the collection.
760
+ * Returns a new array with the updated entity.
761
+ * Structural sharing for adapter-owned persistence projections: returns
762
+ * the ORIGINAL array when nothing changed (no match, or the element kept
763
+ * its reference), so a partial-write adapter can skip the untouched
764
+ * collection; a new array only when an
765
+ * element reference actually changed. The result is `ReadonlyArray<T>`:
766
+ * it may BE the (possibly frozen) input; spread it if you need a mutable
767
+ * copy.
768
+ *
769
+ * @param entities - Array of entities
770
+ * @param id - The ID of the entity to update
771
+ * @param updater - Function that takes the entity and returns the updated entity
772
+ * @returns A new array with the updated entity
773
+ *
774
+ * @example
775
+ * ```typescript
776
+ * const items: OrderItem[] = [
777
+ * { id: itemId1, productId: "prod-1", quantity: 2 }
778
+ * ];
779
+ *
780
+ * const updated = updateEntityById(items, itemId1, (item) => ({
781
+ * ...item,
782
+ * quantity: item.quantity + 1
783
+ * }));
784
+ * // updated is [{ id: itemId1, productId: "prod-1", quantity: 3 }]
785
+ * ```
786
+ */
787
+ declare function updateEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId, updater: (entity: T) => T): ReadonlyArray<T>;
788
+ /**
789
+ * Replaces an entity with the given ID in the collection.
790
+ * Returns a new array with the replaced entity.
791
+ * Structural sharing for adapter-owned persistence projections: returns
792
+ * the ORIGINAL array when nothing changed (no match, or the element kept
793
+ * its reference), so a partial-write adapter can skip the untouched
794
+ * collection; a new array only when an
795
+ * element reference actually changed. The result is `ReadonlyArray<T>`:
796
+ * it may BE the (possibly frozen) input; spread it if you need a mutable
797
+ * copy.
798
+ *
799
+ * @param entities - Array of entities
800
+ * @param id - The ID of the entity to replace
801
+ * @param replacement - The replacement entity
802
+ * @returns A new array with the replaced entity
803
+ *
804
+ * @example
805
+ * ```typescript
806
+ * const items: OrderItem[] = [
807
+ * { id: itemId1, productId: "prod-1", quantity: 2 }
808
+ * ];
809
+ *
810
+ * const updated = replaceEntityById(items, itemId1, {
811
+ * id: itemId1,
812
+ * productId: "prod-1",
813
+ * quantity: 5
814
+ * });
815
+ * ```
816
+ */
817
+ declare function replaceEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId, replacement: T): ReadonlyArray<T>;
818
+ /**
819
+ * Extracts all IDs from a collection of entities.
820
+ *
821
+ * @param entities - Array of entities
822
+ * @returns Array of entity IDs
823
+ *
824
+ * @example
825
+ * ```typescript
826
+ * const items: OrderItem[] = [
827
+ * { id: itemId1, productId: "prod-1", quantity: 2 },
828
+ * { id: itemId2, productId: "prod-2", quantity: 1 }
829
+ * ];
830
+ *
831
+ * const ids = entityIds(items);
832
+ * // ids is [itemId1, itemId2]
833
+ * ```
834
+ */
835
+ declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>): TId[];
836
+ //#endregion
837
+ //#region src/aggregate/base-aggregate.d.ts
838
+ /** Construction options shared by state-stored and event-sourced aggregates. */
839
+ type AggregateConfig<TState = unknown> = EntityConfig<TState>;
840
+ /**
841
+ * Shared base for both `AggregateRoot` (state-stored) and
842
+ * `EventSourcedAggregate`. Carries the lifecycle machinery that's
843
+ * identical across the two flavours: current version, pending-event
844
+ * tracking, the kit-internal post-commit acknowledgement capability,
845
+ * the `markRestored` post-load marker, and the `createEvent` helper
846
+ * that auto-injects `aggregateId` + `aggregateType` on every event the
847
+ * aggregate emits. The application shell records the pending decisions
848
+ * with `recordPendingEvents` before persistence.
849
+ *
850
+ * Consumers do NOT extend this class directly; extend
851
+ * `AggregateRoot` for state-stored aggregates or
852
+ * `EventSourcedAggregate` for event-sourced ones. The split between
853
+ * those two reflects the canonical Vernon §8 (state-stored) /
854
+ * Vernon §11 + Greg Young (event-sourced) distinction in how state
855
+ * is represented; the lifecycle machinery is the same for both.
856
+ *
857
+ * @template TState - The type of the aggregate state
858
+ * @template TId - The aggregate root identifier
859
+ * @template TEvent - The domain-event union. Defaults to `never` so
860
+ * aggregates without a declared event type cannot emit events
861
+ * (emitting any event becomes a compile error).
862
+ */
863
+ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId, TEvent> {
864
+ /**
865
+ * The aggregate's domain type as a string, used to populate
866
+ * `aggregateType` on events created via {@link createEvent}.
867
+ *
868
+ * Subclasses MUST declare this as a string literal:
869
+ *
870
+ * ```ts
871
+ * class Order extends AggregateRoot<OrderState, OrderId, OrderEvent> {
872
+ * protected readonly aggregateType = "Order";
873
+ * }
874
+ * ```
875
+ *
876
+ * The string is *the* identifier downstream consumers (outbox
877
+ * dispatchers, projection handlers, audit logs) use to route by
878
+ * aggregate kind. Use the same canonical name across your system;
879
+ * matching the class name is the obvious choice, but the value
880
+ * comes from this explicit declaration, not `constructor.name`
881
+ * (which is fragile under minification, bundler transforms, and
882
+ * subclass renaming).
883
+ */
884
+ protected abstract readonly aggregateType: string;
885
+ private _version;
886
+ /**
887
+ * Version the persistence layer last confirmed for this instance:
888
+ * `undefined` until the aggregate is reconstituted (`markRestored`) or a
889
+ * commit is acknowledged. Kit-internal via the lifecycle capability; it
890
+ * grounds the `withCommit` unique-cursor guard so an eventful commit that
891
+ * did not advance beyond the persisted row is rejected deterministically.
892
+ */
893
+ private _persistedVersion;
894
+ private _pendingEvents;
895
+ protected constructor(id: TId, initialState: TState, config?: AggregateConfig<TState>);
896
+ private acknowledgePendingEvents;
897
+ /**
898
+ * Post-commit cleanup for the deleted disposition. The row is gone, so
899
+ * there is no persisted version to advance: stamping the marker from the
900
+ * live instance would make a later legitimate re-enrollment of this
901
+ * instance trip the unique-cursor guard for a row that does not exist.
902
+ */
903
+ private discardPendingEventsAfterDeletion;
904
+ private stripAcknowledgedPrefix;
905
+ get version(): Version;
906
+ /**
907
+ * Read-only list of domain events recorded on this aggregate that
908
+ * have not yet been flushed to the outbox / persistence layer.
909
+ */
910
+ get pendingEvents(): ReadonlyArray<PendingDomainEvent<TEvent>>;
911
+ /**
912
+ * Count-only accessor for internal aggregate paths: the public
913
+ * {@link pendingEvents} getter allocates and freezes
914
+ * a defensive copy per read, which a length check does not need.
915
+ */
916
+ protected get pendingEventCount(): number;
917
+ protected setVersion(version: Version): void;
918
+ /**
919
+ * Manually bumps the aggregate version. Used by state-stored
920
+ * aggregates' `setState()` / `commit()` paths and by the
921
+ * event-sourced replay path after each applied event.
922
+ */
923
+ protected bumpVersion(): void;
924
+ /**
925
+ * **Lifecycle marker, Post-Load.** Syncs both `_version` and
926
+ * the current version to the stored version. Used by
927
+ * `reconstitute(...)` factories to assemble an in-memory aggregate
928
+ * from a persisted row.
929
+ *
930
+ * The Factory-vs-Reconstitution distinction (Vernon §11) is honoured
931
+ * structurally: reconstitution stays inside the aggregate factory while
932
+ * post-commit acknowledgement belongs to application commit orchestration.
933
+ *
934
+ * If you override this, call `super.markRestored(version)` so the current
935
+ * domain version remains aligned with the reconstituted facts.
936
+ *
937
+ * @param version - The version the row currently holds in the DB
938
+ *
939
+ * @example
940
+ * ```ts
941
+ * static reconstitute(id: OrderId, state: OrderState, version: Version): Order {
942
+ * const order = new Order(id, state);
943
+ * order.markRestored(version);
944
+ * return order;
945
+ * }
946
+ * ```
947
+ */
948
+ protected markRestored(version: Version): void;
949
+ /**
950
+ * Appends a domain event to the pending list. Prefer the higher-level
951
+ * `AggregateRoot.commit()` (state-stored) or `EventSourcedAggregate.apply()`
952
+ * (event-sourced) call sites, both of which wrap `addDomainEvent` in the
953
+ * canonical record-AFTER-mutation order (Vernon §8). Calling
954
+ * `addDomainEvent` directly is appropriate only after a version-advancing
955
+ * state mutation, or while constructing a never-persisted aggregate.
956
+ * An event-only commit on an already-persisted aggregate has no unique
957
+ * cursor and `withCommit` rejects it; use `commit(currentState, event)`.
958
+ */
959
+ protected addDomainEvent(event: PendingDomainEvent<TEvent>): void;
960
+ /**
961
+ * Immutability gate for every recording path: only events minted by
962
+ * the kit's constructors (`createDomainEvent`,
963
+ * `createDomainEventFromFacts`, `createEvent`) pass,
964
+ * checked against the constructor's internal, unforgeable mint
965
+ * marker. Minted implies deeply frozen with defensively copied
966
+ * payload and metadata, a guarantee no frozen-ness probe can
967
+ * establish (a shallow-frozen literal with mutable nested data
968
+ * would fool it). O(1): one WeakSet lookup.
969
+ */
970
+ protected assertMintedEvent(event: PendingDomainEvent<TEvent>): void;
971
+ /**
972
+ * Creates the immutable business fact accepted by this aggregate without
973
+ * reading a clock, generating an id, or attaching tracing metadata.
974
+ *
975
+ * The application shell records pending events after the domain operation
976
+ * and before persistence. Payload schema version stays here, next to the
977
+ * concrete event producer, rather than in shell-owned recording data.
978
+ */
979
+ protected createEvent<E extends TEvent>(type: E["type"], payload: E["payload"], options?: Omit<CreateUncommittedDomainEventOptions, "aggregateId" | "aggregateType">): UncommittedDomainEventOf<E>;
980
+ }
981
+ //#endregion
982
+ //#region src/aggregate/aggregate-root.d.ts
983
+ /**
984
+ * OO-first Aggregate Root for state-stored domain models.
985
+ *
986
+ * The aggregate owns identity, valid domain state, behavior, its current
987
+ * domain version, and pending domain events. It deliberately does not own a
988
+ * database baseline or dirty-key bookkeeping. A repository adapter defines
989
+ * its persistence projection through `PersistenceModel`; the Unit of Work
990
+ * retains that opaque baseline and derives the adapter's change set at flush.
991
+ */
992
+ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never> extends BaseAggregate<TState, TId, TEvent> {
993
+ /**
994
+ * Changes state and records the resulting facts in record-after-mutation
995
+ * order. Validation and event mint checks run before the transition becomes
996
+ * observable, so a rejected decision records nothing.
997
+ */
998
+ protected commit(newState: TState, events?: PendingDomainEvent<TEvent> | readonly PendingDomainEvent<TEvent>[]): void;
999
+ /** Every normal domain-state transition advances the OCC version. */
1000
+ protected setState(newState: TState): void;
1001
+ /**
1002
+ * Replaces loss-tolerant derived state without advancing the domain version.
1003
+ *
1004
+ * This is intentionally loud: concurrent writers may overwrite such a
1005
+ * change. Keep business facts on the normal `setState`/`commit` path.
1006
+ */
1007
+ protected setStateWithoutVersionBump(newState: TState): void;
1008
+ }
1009
+ //#endregion
1010
+ //#region src/events/json-value.d.ts
1011
+ /** A primitive value represented without loss by JSON. */
1012
+ type JsonPrimitive = boolean | null | number | string;
1013
+ /** A recursively JSON-safe value. Runtime validation rejects lossy shapes. */
1014
+ type JsonValue = JsonPrimitive | ReadonlyArray<JsonValue> | {
1015
+ readonly [key: string]: JsonValue;
1016
+ };
1017
+ /** A JSON-safe object. */
1018
+ type JsonObject = {
1019
+ readonly [key: string]: JsonValue;
1020
+ };
1021
+ //#endregion
1022
+ //#region src/app/command.d.ts
1023
+ /**
1024
+ * Marker interface for Commands.
1025
+ * Commands represent write operations that change system state.
1026
+ * They should be immutable and contain all data needed to perform the operation.
1027
+ *
1028
+ * This interface can be used as a type marker even when using external frameworks
1029
+ * (e.g., RabbitMQ, AWS SQS) to ensure type safety across different bus implementations.
1030
+ *
1031
+ * @example
1032
+ * ```typescript
1033
+ * type CreateOrderCommand = Command & {
1034
+ * type: "CreateOrder";
1035
+ * customerId: string;
1036
+ * items: OrderItem[];
1037
+ * };
1038
+ * ```
1039
+ *
1040
+ * @example Using with external frameworks (RabbitMQ, etc.)
1041
+ * ```typescript
1042
+ * // Define command using Command marker
1043
+ * type CreateOrderCommand = Command & {
1044
+ * type: "CreateOrder";
1045
+ * customerId: string;
1046
+ * };
1047
+ *
1048
+ * // Handler can be typed with CommandHandler even for external frameworks
1049
+ * const handler: CommandHandler<CreateOrderCommand, OrderId> = async (cmd) => {
1050
+ * // ... handler logic
1051
+ * return ok(orderId);
1052
+ * };
1053
+ *
1054
+ * // The consumer owns this runtime decoder. It checks byte and collection
1055
+ * // ceilings, parses to unknown, allow-lists fields, and constructs domain types.
1056
+ * declare function decodeCreateOrderCommand(
1057
+ * body: Uint8Array,
1058
+ * principal: AuthenticatedPrincipal,
1059
+ * ): Result<CreateOrderCommand, InvalidCommand>;
1060
+ * declare function decodeMessageId(
1061
+ * value: unknown,
1062
+ * ): Result<string, InvalidTransportMetadata>;
1063
+ * declare function createOrderDeliveryKey(messageId: string): string;
1064
+ *
1065
+ * // This application service invokes the handler through withIdempotentCommit.
1066
+ * // createOrderDeliveryKey scopes the message id by consumer. The service
1067
+ * // fingerprints the complete CreateOrder intention and commits that claim,
1068
+ * // the aggregate write, outbox entries, and outcome together.
1069
+ * declare function executeIdempotentCreateOrder(
1070
+ * deliveryKey: string,
1071
+ * command: CreateOrderCommand,
1072
+ * ): Promise<Result<OrderId, string>>;
1073
+ *
1074
+ * // Register with RabbitMQ or another external bus.
1075
+ * rabbitMQChannel.consume("order.commands", async (message) => {
1076
+ * const messageId = decodeMessageId(message.properties.messageId);
1077
+ * if (messageId.isErr()) {
1078
+ * rabbitMQChannel.reject(message, false); // missing identity: dead-letter
1079
+ * return;
1080
+ * }
1081
+ * const deliveryKey = createOrderDeliveryKey(messageId.value);
1082
+ * const principal = authenticateProducer(message.properties.headers);
1083
+ * const decoded = decodeCreateOrderCommand(message.content, principal);
1084
+ * if (decoded.isErr()) {
1085
+ * rabbitMQChannel.reject(message, false); // invalid input: dead-letter, do not retry
1086
+ * return;
1087
+ * }
1088
+ * const outcome = await executeIdempotentCreateOrder(
1089
+ * deliveryKey,
1090
+ * decoded.value,
1091
+ * );
1092
+ * await recordCommandOutcome(deliveryKey, outcome);
1093
+ * rabbitMQChannel.ack(message);
1094
+ * });
1095
+ * ```
1096
+ */
1097
+ interface Command {
1098
+ readonly type: string;
1099
+ }
1100
+ /**
1101
+ * Versioned Published Language for a command that crosses a process or
1102
+ * Bounded-Context boundary. Unlike a local {@link Command}, its payload is
1103
+ * JSON-safe data rather than a domain object graph. Map value objects to their
1104
+ * wire DTOs at this boundary; for example, use `MoneyDto` instead of `Money`.
1105
+ */
1106
+ interface PublishedCommand<TType extends string = string, TPayload extends JsonValue = JsonValue> extends Command {
1107
+ readonly type: TType;
1108
+ readonly version: number;
1109
+ readonly payload: TPayload;
1110
+ }
1111
+ /**
1112
+ * Handler for executing commands.
1113
+ * Commands return Result for explicit error handling.
1114
+ * Commands may modify system state. When a caller can retry or a broker can
1115
+ * redeliver, the application service must enforce idempotency; this handler
1116
+ * type alone does not provide it.
1117
+ *
1118
+ * This type can be used to mark handlers even when using external frameworks
1119
+ * (e.g., RabbitMQ, AWS SQS, Kafka) to ensure type safety and consistency.
1120
+ *
1121
+ * @template C - The command type (must extend Command)
1122
+ * @template R - The result type
1123
+ * @template E - The error channel type. Defaults to `string`; widen it (e.g.
1124
+ * to a `DomainError` union) to carry typed failures through the bus.
1125
+ *
1126
+ * @example
1127
+ * ```typescript
1128
+ * const handler: CommandHandler<CreateOrderCommand, OrderId> = async (cmd) => {
1129
+ * const order = Order.create(cmd.customerId, cmd.items);
1130
+ * repository.add(order);
1131
+ * return ok(order.id);
1132
+ * };
1133
+ * ```
1134
+ *
1135
+ * @example Using with external frameworks
1136
+ * ```typescript
1137
+ * // Handler typed with CommandHandler for type safety
1138
+ * const createOrderHandler: CommandHandler<CreateOrderCommand, OrderId> = async (cmd) => {
1139
+ * // ... handler logic
1140
+ * return ok(orderId);
1141
+ * };
1142
+ *
1143
+ * // The broker adapter validates before calling the application handler.
1144
+ * rabbitMQChannel.consume("commands", async (msg) => {
1145
+ * const messageId = decodeMessageId(msg.properties.messageId);
1146
+ * if (messageId.isErr()) {
1147
+ * rabbitMQChannel.reject(msg, false);
1148
+ * return;
1149
+ * }
1150
+ * const deliveryKey = createOrderDeliveryKey(messageId.value);
1151
+ * const principal = authenticateProducer(msg.properties.headers);
1152
+ * const decoded = decodeCreateOrderCommand(msg.content, principal);
1153
+ * if (decoded.isErr()) {
1154
+ * rabbitMQChannel.reject(msg, false); // malformed or over limit
1155
+ * return;
1156
+ * }
1157
+ * // executeIdempotentCreateOrder invokes createOrderHandler through the same
1158
+ * // atomic withIdempotentCommit boundary described in the first example.
1159
+ * const outcome = await executeIdempotentCreateOrder(
1160
+ * deliveryKey,
1161
+ * decoded.value,
1162
+ * );
1163
+ * await recordCommandOutcome(deliveryKey, outcome);
1164
+ * rabbitMQChannel.ack(msg);
1165
+ * });
1166
+ * ```
1167
+ */
1168
+ type CommandHandler<C extends Command, R, E = string> = (cmd: C) => Promise<Result<R, E>>;
1169
+ //#endregion
1170
+ //#region src/utils/execution.d.ts
1171
+ /** Cancellation and deadline controls for one bounded shell operation. */
1172
+ interface ExecutionContext {
1173
+ /** Cooperative cancellation for the in-flight operation. */
1174
+ readonly signal: AbortSignal;
1175
+ /** Absolute Unix epoch millisecond at which the shell stops waiting. */
1176
+ readonly deadlineAt: number;
1177
+ }
1178
+ //#endregion
1179
+ //#region src/events/ports.d.ts
1180
+ /**
1181
+ * Event handler function type for subscribing to domain events. The execution
1182
+ * context carries the publication's cooperative cancellation and deadline;
1183
+ * those runtime controls belong to the imperative shell, never to the domain
1184
+ * event itself.
1185
+ *
1186
+ * @template Evt - The type of domain event
1187
+ */
1188
+ type EventHandler<Evt> = (event: Evt, context: ExecutionContext) => Promise<void> | void;
1189
+ /** Controls one bounded in-process event publication. */
1190
+ interface PublishOptions {
1191
+ /** Owner/request cancellation propagated to every event handler. */
1192
+ readonly signal?: AbortSignal;
1193
+ /** Maximum time to await the complete publication. Default `30000`ms. */
1194
+ readonly timeoutMs?: number;
1195
+ }
1196
+ /**
1197
+ * Event Bus interface for publishing and subscribing to domain events.
1198
+ * Supports multiple subscribers per event type (pub/sub pattern).
1199
+ *
1200
+ * @template Evt - The type of domain events
1201
+ *
1202
+ * @example
1203
+ * ```typescript
1204
+ * const bus = new EventBus<OrderEvent>();
1205
+ *
1206
+ * // Subscribe to specific event types
1207
+ * bus.subscribe("OrderCreated", async (event) => {
1208
+ * await sendEmail(event.payload.customerId);
1209
+ * });
1210
+ *
1211
+ * bus.subscribe("OrderShipped", async (event) => {
1212
+ * await updateInventory(event.payload.orderId);
1213
+ * });
1214
+ *
1215
+ * // Publish events
1216
+ * await bus.publish([orderCreatedEvent, orderShippedEvent]);
1217
+ * ```
1218
+ */
1219
+ interface EventBus<Evt extends AnyDomainEvent> {
1220
+ /**
1221
+ * Publishes events to all subscribed handlers.
1222
+ *
1223
+ * **Ordering & parallelism contract:**
1224
+ *
1225
+ * 1. **Events run in input order.** `publish([a, b, c])` dispatches `a`,
1226
+ * awaits all of its handlers, then dispatches `b`, and so on. The
1227
+ * library never reorders or parallelises across events.
1228
+ * 2. **Handlers within a single event run in parallel.** All handlers
1229
+ * subscribed to `event.type` are awaited via `Promise.allSettled`:
1230
+ * none of them sees the others' errors and none is skipped if a
1231
+ * peer fails.
1232
+ * 3. **Errors are collected and thrown AFTER everything dispatches.**
1233
+ * If one handler throws, remaining handlers for that event still
1234
+ * run, and remaining events in the batch still publish. Once
1235
+ * `publish` reaches the end of the batch it throws: the single
1236
+ * error directly if there was one, or an `AggregateError`
1237
+ * ("Multiple event handlers failed") containing every captured
1238
+ * error otherwise. Callers that need fail-fast semantics should
1239
+ * publish events one at a time and not rely on batch atomicity.
1240
+ *
1241
+ * The contract is intentionally simple and in-process. For
1242
+ * cross-process delivery (RabbitMQ, Kafka, etc.), use the `Outbox`
1243
+ * port and a dedicated dispatcher.
1244
+ *
1245
+ * @param events - Array of events to publish
1246
+ * @param options - Owner cancellation and publication timeout
1247
+ */
1248
+ publish: (events: ReadonlyArray<Evt>, options?: PublishOptions) => Promise<void>;
1249
+ /**
1250
+ * Subscribes a handler to a specific event type.
1251
+ * Multiple handlers can subscribe to the same event type.
1252
+ *
1253
+ * @param eventType - The event type to subscribe to
1254
+ * @param handler - The handler function to call when events of this type are published
1255
+ * @returns A function to unsubscribe the handler
1256
+ *
1257
+ * @example
1258
+ * ```typescript
1259
+ * const unsubscribe = bus.subscribe("OrderCreated", async (event) => {
1260
+ * console.log("Order created:", event.payload.orderId);
1261
+ * });
1262
+ *
1263
+ * // Later: unsubscribe
1264
+ * unsubscribe();
1265
+ * ```
1266
+ */
1267
+ subscribe: <K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
1268
+ type: K;
1269
+ }>>) => () => void;
1270
+ /**
1271
+ * Subscribes a handler to EVERY event type: the subscription for
1272
+ * cross-cutting consumers (audit log, metrics, dev logging,
1273
+ * forward-all) that would otherwise have to enumerate the union's
1274
+ * event types and silently miss every type added later.
1275
+ *
1276
+ * Catch-all handlers run in the SAME `Promise.allSettled` batch as
1277
+ * the event's typed handlers, so the publish contract is unchanged:
1278
+ * awaited delivery, no handler skipped when a peer fails, errors
1279
+ * collected and thrown after the batch, events in input order.
1280
+ *
1281
+ * Deliberately minimal: no predicate subscriptions (filter in your
1282
+ * handler; it is one line) and no glob/topic patterns (topic routing
1283
+ * belongs to broker sinks: Kafka topics, JetStream subjects).
1284
+ *
1285
+ * @param handler - Called with every published event, typed as the
1286
+ * full event union; narrow via `event.type` in the handler
1287
+ * @returns A function to unsubscribe the handler
1288
+ *
1289
+ * @example
1290
+ * ```typescript
1291
+ * const unsubscribe = bus.subscribeAll(async (event) => {
1292
+ * await auditLog.append(event.type, event.eventId, event.payload);
1293
+ * });
1294
+ * ```
1295
+ */
1296
+ subscribeAll: (handler: EventHandler<Evt>) => () => void;
1297
+ /**
1298
+ * Subscribes to the next occurrence of an event type.
1299
+ * Returns a Promise that resolves with the event data.
1300
+ * Automatically unsubscribes after the first event.
1301
+ *
1302
+ * @param eventType - The event type to wait for
1303
+ * @returns A Promise that resolves with the event
1304
+ *
1305
+ * @example
1306
+ * ```typescript
1307
+ * const event = await bus.once("OrderCreated");
1308
+ * console.log("Order created:", event.payload.orderId);
1309
+ * ```
1310
+ */
1311
+ once: <K extends Evt["type"]>(eventType: K, options?: OnceOptions) => Promise<Extract<Evt, {
1312
+ type: K;
1313
+ }>>;
1314
+ }
1315
+ /**
1316
+ * Options for `EventBus.once()`. Both fields are optional; without them
1317
+ * `once()` waits forever (the historical behaviour).
1318
+ */
1319
+ interface OnceOptions {
1320
+ /**
1321
+ * Aborts the wait. When `signal` fires, `once()` rejects with
1322
+ * `signal.reason` (or a generic abort error if none was supplied) and
1323
+ * the internal subscription is removed.
1324
+ */
1325
+ signal?: AbortSignal;
1326
+ /**
1327
+ * Rejects with a timeout error after this many milliseconds if no event
1328
+ * has arrived. The internal subscription and timer are cleaned up
1329
+ * regardless of which path settles the promise.
1330
+ */
1331
+ timeoutMs?: number;
1332
+ }
1333
+ /**
1334
+ * Gap-proof position finalized by the event source at the persistence
1335
+ * boundary. It is deliberately separate from `DomainEvent`: these values
1336
+ * describe a stored commit, not the business fact itself.
1337
+ */
1338
+ interface CommitPosition {
1339
+ /** Aggregate OCC version reached by this eventful commit. */
1340
+ readonly aggregateVersion: number;
1341
+ /** Zero-based event index inside this aggregate commit. */
1342
+ readonly commitSequence: number;
1343
+ /** Total number of events emitted by this aggregate commit. */
1344
+ readonly commitSize: number;
1345
+ /**
1346
+ * Aggregate version of the immediately preceding EVENTFUL commit for this
1347
+ * qualified aggregate source, or `null` when this is its first eventful
1348
+ * commit. State-only persistence is intentionally absent from this chain.
1349
+ *
1350
+ * The outbox/event-store adapter owns this value. It must read and advance
1351
+ * the source head atomically with inserting the committed event envelope;
1352
+ * application orchestration cannot derive it from the Unit of Work's OCC
1353
+ * receipt because state-only commits are intentionally absent here.
1354
+ */
1355
+ readonly previousEventfulAggregateVersion: number | null;
1356
+ }
1357
+ /**
1358
+ * Commit information known by the application transaction before the outbox
1359
+ * source has linked this eventful commit to its predecessor.
1360
+ */
1361
+ type EventCommitCandidatePosition = Omit<CommitPosition, "previousEventfulAggregateVersion">;
1362
+ /**
1363
+ * A bare domain event prepared for the transactional outbox. The outbox source
1364
+ * owns the predecessor link and turns this candidate into a
1365
+ * {@link CommittedDomainEvent} when it persists the record.
1366
+ */
1367
+ interface EventCommitCandidate<Evt extends AnyDomainEvent> {
1368
+ readonly event: Evt;
1369
+ readonly source: AggregateAddress;
1370
+ readonly position: EventCommitCandidatePosition;
1371
+ }
1372
+ /**
1373
+ * A domain event enriched after persistence has established its source and
1374
+ * commit position. Outboxes and projectors consume this envelope; in-process
1375
+ * domain handlers continue to consume the bare {@link DomainEvent} value.
1376
+ */
1377
+ interface CommittedDomainEvent<Evt extends AnyDomainEvent> {
1378
+ readonly event: Evt;
1379
+ readonly source: AggregateAddress;
1380
+ readonly position: CommitPosition;
1381
+ }
1382
+ /**
1383
+ * One pending event in the outbox plus the opaque id the implementation
1384
+ * needs to ack it via `markDispatched`. The library does not prescribe
1385
+ * what `dispatchId` looks like: an implementation can reuse the event's
1386
+ * own `eventId`, generate its own UUID, use the row's auto-increment
1387
+ * primary key, or whatever the storage layer prefers.
1388
+ */
1389
+ interface OutboxRecord<Evt extends AnyDomainEvent> extends CommittedDomainEvent<Evt> {
1390
+ dispatchId: string;
1391
+ /**
1392
+ * Failed delivery attempts so far. Populated by implementations that
1393
+ * track dispatch failures (see {@link DispatchTrackingOutbox});
1394
+ * plain `Outbox` implementations may omit it.
1395
+ */
1396
+ attempts?: number;
1397
+ }
1398
+ /** A record that exhausted its delivery attempts; see {@link DispatchTrackingOutbox.deadLetters}. */
1399
+ interface DeadLetterRecord<Evt extends AnyDomainEvent> extends CommittedDomainEvent<Evt> {
1400
+ dispatchId: string;
1401
+ /** Failed delivery attempts when the record was dead-lettered. */
1402
+ attempts: number;
1403
+ /** Human-readable rendering of the last delivery error, if recorded. */
1404
+ lastError?: string;
1405
+ }
1406
+ /**
1407
+ * Write half of the transactional outbox: the only outbox capability the
1408
+ * write side (`withCommit`, `UnitOfWork`) depends on. Persisting the
1409
+ * events atomically with the aggregate state is the kit's guarantee;
1410
+ * DELIVERY is a separate, replaceable concern.
1411
+ *
1412
+ * Implement ONLY this interface to plug in an external delivery
1413
+ * solution: `add()` writes into that solution's outbox storage inside
1414
+ * the ambient transaction, and its own listener (polling or
1415
+ * WAL/CDC-based, such as a Debezium-style connector, a delivery
1416
+ * library, or a broker-native outbox) owns delivery entirely. The
1417
+ * kit-side poll surface ({@link Outbox}) is then never involved. See
1418
+ * the outbox guide, "External dispatchers".
1419
+ */
1420
+ interface OutboxWriter<Evt extends AnyDomainEvent> {
1421
+ /**
1422
+ * Finalizes and persists event commit candidates. Called from inside
1423
+ * `withCommit`'s transactional callback, atomically with the aggregate
1424
+ * write.
1425
+ *
1426
+ * For every qualified aggregate source, the adapter must serialize source
1427
+ * advancement, read its last eventful aggregate version, write that value as
1428
+ * `previousEventfulAggregateVersion` on every event in the candidate's
1429
+ * commit, and advance the source head to `aggregateVersion` in the SAME
1430
+ * transaction. A state-only aggregate commit does not call `add()` and must
1431
+ * therefore not advance this event-source head.
1432
+ *
1433
+ * A qualified source position `(aggregateType, aggregateId,
1434
+ * aggregateVersion, commitSequence)` MUST identify one immutable event. All
1435
+ * candidates for the same aggregate commit MUST also agree on `commitSize`.
1436
+ * Enforce both constraints before advancing the source head; a conflicting
1437
+ * retry must reject without replacing the stored event or changing the head.
1438
+ *
1439
+ * **Idempotency:** implementations should dedupe on
1440
+ * `candidate.event.eventId`. `withCommit` itself does not retry, but the
1441
+ * surrounding use case (a queue consumer, an HTTP retry, a transactional
1442
+ * outbox-dispatcher loop) may legitimately invoke the same write more than
1443
+ * once. A unique-key constraint on `(eventId)` in the outbox table is the
1444
+ * standard implementation; the source-head update and dedupe decision must
1445
+ * share the transaction. Idempotency applies only to an exact candidate
1446
+ * retry: the same event ID, qualified source, aggregate version, commit
1447
+ * sequence, and commit size. Reusing an `eventId` for another source or
1448
+ * position is a caller bug: adapters that retain the conflicting record
1449
+ * should reject it rather than replace or silently reinterpret it as a retry.
1450
+ */
1451
+ add: (events: ReadonlyArray<EventCommitCandidate<Evt>>) => Promise<void>;
1452
+ }
1453
+ /**
1454
+ * Transactional outbox port: the bridge between the write-side
1455
+ * transaction and the (out-of-band) event dispatcher.
1456
+ *
1457
+ * Lifecycle:
1458
+ * 1. `add()` inside the write transaction (`withCommit` calls this) so
1459
+ * events persist atomically with the aggregate state
1460
+ * ({@link OutboxWriter}, the only part the write side needs).
1461
+ * 2. An outbox dispatcher (the kit's `OutboxDispatcher` or your own)
1462
+ * polls `getPending()` and forwards the events to subscribers /
1463
+ * external brokers.
1464
+ * 3. After successful dispatch, the dispatcher calls `markDispatched()`
1465
+ * with the records' `dispatchId`s so they don't come back next poll.
1466
+ *
1467
+ * `markDispatched` is required to be idempotent: calling it with an id
1468
+ * that's already marked is a no-op, not an error. This lets the
1469
+ * dispatcher safely retry on partial-failure.
1470
+ *
1471
+ * **Competing dispatcher instances** are an adapter contract, not a
1472
+ * dispatcher feature: a transactional implementation that should
1473
+ * support several concurrent pollers must make `getPending` claim the
1474
+ * returned records (`FOR UPDATE SKIP LOCKED` or equivalent). Without
1475
+ * claiming, run one logical dispatcher per outbox.
1476
+ *
1477
+ * The bundled dispatcher supplies an {@link ExecutionContext} to every poll-side
1478
+ * operation. Production adapters MUST pass its signal to native I/O or enforce
1479
+ * a native timeout no later than `deadlineAt`; the shell can bound its wait but
1480
+ * cannot terminate a promise that ignores cancellation. A timed-out write has
1481
+ * an unknown outcome. Acknowledgements must remain idempotent when they complete
1482
+ * late; a late failure update may count its original delivery attempt and must
1483
+ * still no-op after the record was dispatched.
1484
+ */
1485
+ interface Outbox<Evt extends AnyDomainEvent> extends OutboxWriter<Evt> {
1486
+ /**
1487
+ * Returns up to `limit` outbox records that have not yet been
1488
+ * dispatched, **in the order `add()` persisted them** (commit order).
1489
+ * The ordering is part of the port contract: `withCommit` promises
1490
+ * subscribers per-aggregate causal order, and a sequential dispatcher
1491
+ * can only honor that promise when this read is ordered. SQL-backed
1492
+ * implementations need a monotonic position column (an auto-increment
1493
+ * primary key works) and an `ORDER BY` on it; a bare `SELECT` returns
1494
+ * rows in storage order, not insertion order. The dispatcher polls
1495
+ * this on a schedule. When `limit` is omitted, the implementation
1496
+ * decides on a default page size. The bundled dispatcher always supplies
1497
+ * `context`; it is optional only so existing adapters remain assignable.
1498
+ */
1499
+ getPending: (limit?: number, context?: ExecutionContext) => Promise<ReadonlyArray<OutboxRecord<Evt>>>;
1500
+ /**
1501
+ * Marks the given dispatch records as delivered so subsequent
1502
+ * `getPending` calls don't return them. Must be idempotent on
1503
+ * already-marked ids, including a late completion after the caller's
1504
+ * storage deadline. The bundled dispatcher always supplies `context`.
1505
+ */
1506
+ markDispatched: (dispatchIds: ReadonlyArray<string>, context?: ExecutionContext) => Promise<void>;
1507
+ }
1508
+ /**
1509
+ * Optional extension of {@link Outbox} for dispatchers that track
1510
+ * delivery failures. Without failure tracking, a poison message (an
1511
+ * event whose delivery always throws) is redelivered forever: it comes
1512
+ * back from every `getPending` poll, blocks per-aggregate ordering
1513
+ * behind it, and burns the dispatcher's cycles. This extension gives
1514
+ * the dispatcher a bounded-retry story: report each failed delivery via
1515
+ * {@link markFailed}; the implementation moves records past its
1516
+ * attempt ceiling to a dead-letter set that `getPending` no longer
1517
+ * returns, and {@link deadLetters} exposes them for alerting, manual
1518
+ * inspection, and redelivery (deliver by hand, then ack via
1519
+ * `markDispatched`, which also clears dead-lettered records).
1520
+ *
1521
+ * See the outbox guide's dispatcher recipe for the retry-then-dead-letter
1522
+ * loop this port shape supports.
1523
+ */
1524
+ interface DispatchTrackingOutbox<Evt extends AnyDomainEvent> extends Outbox<Evt> {
1525
+ /**
1526
+ * Records one failed delivery attempt for the given record:
1527
+ * increments its attempt count (surfaced as
1528
+ * {@link OutboxRecord.attempts}) and, once the implementation's
1529
+ * ceiling is reached, moves the record to the dead-letter set.
1530
+ * A no-op for unknown or already-dispatched ids (a late failure
1531
+ * report after a successful retry must not resurrect the record).
1532
+ * Returns the exact dead-letter record only on the call that performs
1533
+ * that transition; retries below the ceiling and no-ops return
1534
+ * `undefined`. This lets productive pollers emit an immediate signal
1535
+ * without scanning the durable dead-letter set after every failure. A late
1536
+ * completion may count that original delivery attempt; it must still no-op if
1537
+ * the record was dispatched in the meantime. The bundled dispatcher never
1538
+ * reissues the same store call and always supplies `context`.
1539
+ */
1540
+ markFailed: (dispatchId: string, error?: unknown, context?: ExecutionContext) => Promise<DeadLetterRecord<Evt> | undefined>;
1541
+ /**
1542
+ * Records that exhausted their delivery attempts. They no longer
1543
+ * come back from `getPending`; wire this to durable alerting and
1544
+ * reconciliation so poison messages surface even if the poller stops
1545
+ * between this store transition and its immediate observer callback.
1546
+ */
1547
+ deadLetters: () => Promise<ReadonlyArray<DeadLetterRecord<Evt>>>;
1548
+ }
1549
+ //#endregion
1550
+ //#region src/app/command-outbox.d.ts
1551
+ /**
1552
+ * Business relationships and technical trace context selected explicitly for
1553
+ * an outgoing command. Correlation/conversation explain the business flow;
1554
+ * W3C Trace Context connects technical spans.
1555
+ */
1556
+ interface CommandMessageRelationships {
1557
+ /** Groups messages that belong to one operation or trace. */
1558
+ readonly correlationId?: string;
1559
+ /** Groups every message in one long-running business interaction. */
1560
+ readonly conversationId?: string;
1561
+ /** W3C Trace Context parent for technical distributed tracing. */
1562
+ readonly traceparent?: string;
1563
+ /** Optional vendor trace state associated with `traceparent`. */
1564
+ readonly tracestate?: string;
1565
+ }
1566
+ /**
1567
+ * Application-owned Published Language produced from one private domain or
1568
+ * process event. `destination` names one receiver contract; it is deliberately
1569
+ * required because a command is an instruction, not a broadcast fact.
1570
+ *
1571
+ * The command carries a stable schema `version` and JSON-safe `payload`.
1572
+ * Domain value objects are translated to wire DTOs by the mapper before this
1573
+ * boundary.
1574
+ */
1575
+ interface CommandMessageContent<C extends PublishedCommand> extends CommandMessageRelationships {
1576
+ readonly destination: string;
1577
+ readonly command: C;
1578
+ }
1579
+ /**
1580
+ * Immutable, JSON-safe command envelope stored for later at-least-once
1581
+ * delivery.
1582
+ *
1583
+ * `causationId` always identifies the private event whose accepted decision
1584
+ * requested this command. The mapper cannot replace it with a weaker
1585
+ * correlation. Consumer-produced events should in turn use `messageId` as
1586
+ * their causation id.
1587
+ */
1588
+ interface DurableCommandMessage<C extends PublishedCommand> extends CommandMessageContent<C> {
1589
+ readonly messageId: string;
1590
+ readonly recordedAt: string;
1591
+ readonly causationId: string;
1592
+ }
1593
+ /**
1594
+ * Receipt for the private event that requested one command batch. It retains
1595
+ * commit identity and ordering without putting the private event or its
1596
+ * payload into the command outbox.
1597
+ */
1598
+ interface CommandCommitOriginCandidate {
1599
+ readonly eventId: string;
1600
+ readonly source: AggregateAddress;
1601
+ readonly position: EventCommitCandidatePosition;
1602
+ }
1603
+ /**
1604
+ * One private process-event commit and the exact commands it requested.
1605
+ * `messages` may be empty: the receipt still advances the originating source
1606
+ * and makes an exact retry distinguishable from a missing commit.
1607
+ */
1608
+ interface CommandOutboxCommitCandidate<C extends PublishedCommand> {
1609
+ readonly origin: CommandCommitOriginCandidate;
1610
+ readonly messages: ReadonlyArray<DurableCommandMessage<C>>;
1611
+ }
1612
+ /**
1613
+ * Write port for a dedicated transactional command outbox.
1614
+ *
1615
+ * The adapter is bound to the same ambient transaction as the aggregate or
1616
+ * event-stream repository. It must persist the complete input atomically,
1617
+ * retain input order, deduplicate exact retries by `origin.eventId`, and reject
1618
+ * a reused origin id whose source, position, or messages differ. It also owns
1619
+ * the durable source cursor represented by `origin.position`; an empty command
1620
+ * batch still advances that cursor.
1621
+ *
1622
+ * Delivery is out of band and at least once. A consumer therefore uses
1623
+ * `message.messageId` as its idempotency key and acknowledges only after the
1624
+ * command result has been stored.
1625
+ */
1626
+ interface CommandOutboxWriter<C extends PublishedCommand> {
1627
+ add(commits: ReadonlyArray<CommandOutboxCommitCandidate<C>>): Promise<void>;
1628
+ }
1629
+ /** Maps one private accepted event to zero or more addressed commands. */
1630
+ type CommandOutboxMapper<Evt extends AnyDomainEvent, C extends PublishedCommand> = (event: Evt) => ReadonlyArray<CommandMessageContent<C>>;
1631
+ /**
1632
+ * Adapts a dedicated command outbox to the event-candidate write port consumed
1633
+ * by `withCommit`.
1634
+ *
1635
+ * Mapping happens inside the transaction, before the command outbox write.
1636
+ * The private event is used only at this boundary and is reduced to an origin
1637
+ * receipt. The helper never publishes it or copies its payload implicitly; the
1638
+ * application mapper selects and translates the data that belongs in the
1639
+ * versioned Published Language. The route rejects values JSON would lose or
1640
+ * change before it calls the adapter.
1641
+ * Every command gets a stable id derived from the event id and its zero-based
1642
+ * order, so an exact transaction retry produces the same rows.
1643
+ *
1644
+ * Omit `withCommit`'s in-process `bus` for private process events. Participants
1645
+ * consume the durable command messages from their explicitly named
1646
+ * destinations, while event-stream replay only rebuilds process state.
1647
+ */
1648
+ declare function routeEventsToCommandOutbox<C extends PublishedCommand, Evt extends AnyDomainEvent = AnyDomainEvent>(outbox: CommandOutboxWriter<C>, mapper: CommandOutboxMapper<Evt, C>): OutboxWriter<Evt>;
1649
+ //#endregion
1650
+ //#region src/repo/scope.d.ts
1651
+ /** Options passed to {@link TransactionScope.transactional}. */
1652
+ interface TransactionalOptions {
1653
+ /**
1654
+ * Cooperative-cancellation signal forwarded from `withCommit` /
1655
+ * `UnitOfWork.run`. The kit does not interrupt an in-flight query
1656
+ * itself: it pre-checks `aborted` before opening the transaction and
1657
+ * exposes the signal for the work callback to poll. A scope whose
1658
+ * driver supports cancellation (passing the signal to the query, an
1659
+ * interactive-transaction timeout) SHOULD honor it to abort work
1660
+ * already in progress; scopes that ignore it stay correct, just not
1661
+ * eagerly cancellable.
1662
+ */
1663
+ readonly signal?: AbortSignal;
1664
+ }
1665
+ /**
1666
+ * Transaction-scope abstraction.
1667
+ *
1668
+ * Wraps a block of work so it runs inside the persistence layer's native
1669
+ * transaction (Postgres `BEGIN`/`COMMIT`, Mongo session, Drizzle / Prisma
1670
+ * `$transaction`, etc.). The block commits when the callback resolves
1671
+ * and rolls back if it throws.
1672
+ *
1673
+ * `TCtx` is the persistence layer's transaction handle: Drizzle's `tx`,
1674
+ * Prisma's `tx`, Mongo's session, etc. The scope opens the transaction
1675
+ * and passes the handle to `fn`; the use case binds its repositories to
1676
+ * that handle (typically by constructing a tx-scoped repo from the ctx).
1677
+ *
1678
+ * No default for `TCtx`: every implementor names their context type
1679
+ * explicitly. For genuinely context-free scopes (in-memory tests, naive
1680
+ * no-tx scopes) use `TransactionScope<undefined>`: that's a conscious
1681
+ * "there is nothing meaningful here" statement, not an accidental
1682
+ * `unknown` fallback.
1683
+ *
1684
+ * Intentionally minimal: the scope itself does no change tracking and
1685
+ * no commit-time flush. Those concerns live in the layers above: a
1686
+ * repository adapter derives its change set, `withCommit` orchestrates the
1687
+ * event lifecycle, and `UnitOfWork` owns tracking and atomic flush. See
1688
+ * "TransactionScope stays minimal; the Unit of Work lives above it" in
1689
+ * docs/guide/design-decisions.md.
1690
+ *
1691
+ * @example Drizzle implementation
1692
+ * ```typescript
1693
+ * class DrizzleScope implements TransactionScope<DrizzleTx> {
1694
+ * constructor(private db: DrizzleDb) {}
1695
+ * async transactional<T>(fn: (tx: DrizzleTx) => Promise<T>): Promise<T> {
1696
+ * return this.db.transaction((tx) => fn(tx));
1697
+ * }
1698
+ * }
1699
+ * ```
1700
+ *
1701
+ * @example Use site: bind repos to the live transaction
1702
+ * ```typescript
1703
+ * await scope.transactional(async (tx) => {
1704
+ * // Construct tx-bound repos from ctx (your factory / DI of choice)
1705
+ * const orderRepository = makeOrderRepository(tx);
1706
+ *
1707
+ * const order = await orderRepository.getById(orderId);
1708
+ * order.confirm();
1709
+ * orderRepository.update(order);
1710
+ * });
1711
+ * ```
1712
+ *
1713
+ * Repository contracts take the id or aggregate only: the tx handle
1714
+ * is wired into a concrete repository at construction time, not threaded
1715
+ * through every call. Different ORMs have different idioms for that
1716
+ * (constructor injection, factory functions, `withTx` chains); pick one
1717
+ * and keep it consistent.
1718
+ */
1719
+ interface TransactionScope<TCtx> {
1720
+ transactional<T>(fn: (ctx: TCtx) => Promise<T>, options?: TransactionalOptions): Promise<T>;
1721
+ }
1722
+ //#endregion
1723
+ //#region src/app/handler.d.ts
1724
+ /** Dependencies for {@link withCommit}. */
1725
+ interface WithCommitDeps<Evt extends AnyDomainEvent, TCtx> {
1726
+ /**
1727
+ * The write half of the outbox: `withCommit` only ever calls `add()`.
1728
+ * Pass a full `Outbox` for the kit's poll-based dispatch, or a bare
1729
+ * `OutboxWriter` backed by an external delivery solution.
1730
+ *
1731
+ * Required on purpose, while `bus` is optional: the bus is the
1732
+ * best-effort in-process fast path, the outbox is the delivery
1733
+ * guarantee. Running without delivery reliability is a decision, not
1734
+ * a default; make it explicit with
1735
+ * `outboxWriterAcceptingEventLoss()`.
1736
+ */
1737
+ outbox: OutboxWriter<Evt>;
1738
+ bus?: EventBus<Evt>;
1739
+ scope: TransactionScope<TCtx>;
1740
+ /**
1741
+ * Observer for post-commit `bus.publish` failures. Called with the
1742
+ * error and the events that were published. Must not be relied on
1743
+ * for delivery: the outbox dispatcher is the reliable path.
1744
+ */
1745
+ onPublishError?: (error: unknown, events: ReadonlyArray<Evt>) => void;
1746
+ /**
1747
+ * Application-shell observer invoked for each successfully acknowledged
1748
+ * saved aggregate, after every commit record has completed its internal
1749
+ * acknowledgement attempt. Deleted aggregates do not trigger it. `version`
1750
+ * is the commit-time value captured before any observer runs. Observer
1751
+ * failures are reported through `onPersistError` and never turn an already
1752
+ * committed write into an apparent failure. The execution context carries
1753
+ * owner cancellation and the configured post-commit deadline.
1754
+ */
1755
+ onPersisted?: (aggregate: IAggregateRoot<Id<string>, Evt>, version: Version, context: ExecutionContext) => void | Promise<void>;
1756
+ /**
1757
+ * Observer for post-commit persistence failures: either the internal
1758
+ * acknowledgement/disposal step or the application-shell `onPersisted`
1759
+ * observer. Called once per failure with the error and affected aggregate.
1760
+ * Symmetric with {@link onPublishError}: the
1761
+ * transaction has already committed, so the failure must NOT reject the
1762
+ * write; without this observer it would otherwise vanish silently. The
1763
+ * hook is an observer only: if it throws, its error is swallowed so the
1764
+ * post-commit invariant holds, and the loop continues the remaining
1765
+ * post-commit work.
1766
+ */
1767
+ onPersistError?: (error: unknown, aggregate: IAggregateRoot<Id<string>, Evt>) => void;
1768
+ /**
1769
+ * Total time allotted to the complete post-commit application phase:
1770
+ * every application observer followed by in-process bus publication shares
1771
+ * one absolute deadline. Callbacks that have not started when the deadline is
1772
+ * reached are skipped and reported as timeouts. Defaults to `30000`ms.
1773
+ * Timing out or aborting these best-effort operations is reported through the
1774
+ * matching error observer and never rejects an already committed write.
1775
+ */
1776
+ postCommitTimeoutMs?: number;
1777
+ /**
1778
+ * Cooperative-cancellation signal. If already aborted, `withCommit`
1779
+ * rejects with the signal's `reason` BEFORE opening the transaction.
1780
+ * Otherwise the signal is forwarded to `scope.transactional`, where a
1781
+ * cancellation-aware scope can abort an in-flight query. The kit does
1782
+ * not race the work promise: aborting does not kill a running query
1783
+ * unless the scope honors the signal.
1784
+ */
1785
+ signal?: AbortSignal;
1786
+ }
1787
+ declare const aggregateCommitTokenBrand: unique symbol;
1788
+ /**
1789
+ * Opaque receipt that one aggregate was explicitly enrolled in the current
1790
+ * {@link withCommit} invocation. Tokens are minted only by the invocation's
1791
+ * {@link CommitEnrollment} capability and are bound to that invocation at
1792
+ * runtime; a forged token or one retained from an earlier call is rejected
1793
+ * inside the transaction.
1794
+ */
1795
+ interface AggregateCommitToken<Evt extends AnyDomainEvent = AnyDomainEvent> {
1796
+ readonly [aggregateCommitTokenBrand]: Evt;
1797
+ }
1798
+ /**
1799
+ * Invocation-scoped enrollment capability handed to a {@link withCommit}
1800
+ * callback. Call `enrollSaved` only for an aggregate participating in the
1801
+ * repository write, and return every resulting token in `commits`. Omitting
1802
+ * any token rejects the transaction: an enrolled write may not commit without
1803
+ * its event harvest and post-commit acknowledgement. Enrollable instances
1804
+ * must extend `AggregateRoot` or `EventSourcedAggregate`; structural
1805
+ * `IAggregateRoot` lookalikes have no internal lifecycle capability and fail
1806
+ * before commit.
1807
+ */
1808
+ interface CommitEnrollment<Evt extends AnyDomainEvent> {
1809
+ enrollSaved(aggregate: IAggregateRoot<Id<string>, Evt>, options?: CommitEnrollmentOptions): AggregateCommitToken<Evt>;
1810
+ /**
1811
+ * Enroll an aggregate whose row is deleted by the current transaction.
1812
+ * Its events are harvested and discarded after commit, but the saved-only
1813
+ * application `onPersisted` observer is not called.
1814
+ */
1815
+ enrollDeleted(aggregate: IAggregateRoot<Id<string>, Evt>, options?: CommitEnrollmentOptions): AggregateCommitToken<Evt>;
1816
+ }
1817
+ /** OCC baseline associated with one exact commit enrollment. */
1818
+ interface CommitEnrollmentOptions {
1819
+ /** Absent for a new aggregate; captured at load for update or removal. */
1820
+ readonly expectedVersion?: Version;
1821
+ }
1822
+ /** The resolved value of a {@link withCommit} work callback. */
1823
+ interface WithCommitWorkResult<Evt extends AnyDomainEvent, R> {
1824
+ result: R;
1825
+ /**
1826
+ * Commit tokens returned by the invocation's enrollment capability.
1827
+ * Every token minted during the callback must appear at least once.
1828
+ * Naked aggregates are intentionally not accepted: touching an aggregate
1829
+ * does not prove that its repository write participated in the transaction.
1830
+ */
1831
+ commits: ReadonlyArray<AggregateCommitToken<Evt>>;
1832
+ }
1833
+ /**
1834
+ * Helper for executing a write Use Case inside a transaction scope.
1835
+ *
1836
+ * The use-case callback receives an invocation-scoped enrollment capability
1837
+ * and returns opaque commit tokens for the repository writes that completed
1838
+ * in the transaction. `withCommit` owns the post-commit lifecycle (harvest,
1839
+ * outbox, mark-persisted, publish). A naked aggregate is not commit evidence:
1840
+ * merely touching or constructing one must never make it look persisted.
1841
+ *
1842
+ * **Trust boundary.** A token proves invocation-local enrollment, not that the
1843
+ * kit inspected a database write; a generic transaction helper cannot observe
1844
+ * adapter internals. Repository code must enroll only writes participating in
1845
+ * this transaction. `UnitOfWork` centralizes that rule in repository methods.
1846
+ * The opaque, scoped token prevents accidental aggregate smuggling and stale
1847
+ * reuse; it is not a security boundary against code that deliberately lies to
1848
+ * its own persistence capability.
1849
+ *
1850
+ * Order of operations:
1851
+ * 1. `fn(ctx, enrollment)` runs inside `scope.transactional(...)`; domain
1852
+ * mutations + repo writes happen here. After a repository write has
1853
+ * enrolled an aggregate, the callback includes that opaque token in its
1854
+ * `commits` result. Tokens are invocation-bound: forged or stale tokens
1855
+ * fail before harvest. `ctx` is whatever transaction handle the `scope`
1856
+ * exposes (Drizzle `tx`, Prisma `tx`, Mongo session, or `undefined` for
1857
+ * context-free scopes).
1858
+ * 2. **Still inside the transaction**, `withCommit` harvests every
1859
+ * aggregate's `pendingEvents` and writes them via `outbox.add` (so
1860
+ * events persist atomically with the state change). Skipped when no
1861
+ * events were recorded. Each bare domain event is composed into an
1862
+ * `EventCommitCandidate` carrying its aggregate source and the commit
1863
+ * facts known by the application. The outbox source atomically links
1864
+ * that candidate to the preceding eventful commit and persists the
1865
+ * resulting `CommittedDomainEvent`. The domain event itself is never
1866
+ * stamped or copied.
1867
+ *
1868
+ * **Harvest order.** Events are concatenated in the order
1869
+ * tokens appear in the returned `commits` array, then in
1870
+ * each aggregate's `pendingEvents` order (insertion order via
1871
+ * `apply` / `commit` / `addDomainEvent`). So tokens for `[a, b]`
1872
+ * with `a` emitting `[e1, e2]` and `b` emitting `[e3]` produces
1873
+ * `outbox.add([envelope(e1), envelope(e2), envelope(e3)])` and
1874
+ * `bus.publish([e1, e2, e3])` in that exact order.
1875
+ *
1876
+ * **Two ordering guarantees, not one.** Within a single aggregate
1877
+ * the order is *causal*: events are recorded in the order the
1878
+ * domain methods ran, and subscribers (handlers, projections,
1879
+ * replay) MUST process them in that order. Across aggregates the
1880
+ * order in this batch is deterministic but *not* a domain
1881
+ * guarantee. Greg Young / Vernon IDDD §10: aggregates are
1882
+ * independent consistency boundaries; events across them are
1883
+ * eventually consistent. Subscribers should NOT engineer
1884
+ * dependencies on cross-aggregate ordering; use
1885
+ * `EventMetadata.causationId` to express true causation, or a
1886
+ * process manager to coordinate. The in-process EventBus delivers
1887
+ * this batch in order, sequential outbox-dispatchers preserve it
1888
+ * too, but parallel dispatchers or message brokers may reorder
1889
+ * across aggregates at delivery time.
1890
+ * 3. The transaction commits.
1891
+ * 4. **After** the commit, a non-exported capability acknowledges every
1892
+ * saved enrollment and discards pending events for deleted enrollments.
1893
+ * Only after the complete commit set is clean does the optional
1894
+ * application-shell `onPersisted(aggregate, version, context)` observer run for
1895
+ * saved aggregates. Deleted rows never trigger that observer.
1896
+ * 5. `bus.publish(events)` fires for the in-process fast path (skipped
1897
+ * when no events or no `bus` is wired).
1898
+ *
1899
+ * Publishing AFTER commit prevents the classic "publish before commit"
1900
+ * footgun: in-process subscribers can never react to events from a
1901
+ * transaction that later rolled back. If `bus.publish` itself throws, the
1902
+ * outbox still holds the events and an outbox-dispatcher will deliver
1903
+ * them (eventual consistency).
1904
+ *
1905
+ * **A `bus.publish` failure never rejects `withCommit`.** Once the
1906
+ * transaction has committed, the write succeeded; surfacing a subscriber
1907
+ * failure as a rejection would hand the caller a use-case failure for a
1908
+ * committed write (a typical caller retries, double-executing it). The
1909
+ * in-process fast path is best-effort by design; the error is reported to
1910
+ * the optional `onPublishError(error, events)` hook (wire it to your
1911
+ * logger/metrics) and otherwise dropped; delivery is still guaranteed via
1912
+ * the outbox. The hook is an observer: if it throws, its error is
1913
+ * swallowed so the post-commit invariant holds.
1914
+ * The complete application-observer and bus-publication phase shares one
1915
+ * absolute `postCommitTimeoutMs` budget (30 seconds by default); later callbacks
1916
+ * are not started once it expires. A timeout or owner abort is reported
1917
+ * through the same observer paths and never changes the committed result.
1918
+ *
1919
+ * If the transaction rolls back, no acknowledgement occurs: the aggregate
1920
+ * keeps its pending events, so the caller can retry or discard the instance.
1921
+ *
1922
+ * Enrollment captures an exact version and event batch. Re-enrolling the same
1923
+ * aggregate after it changes rejects. `UnitOfWork` additionally seals the
1924
+ * adapter persistence projection and rejects later mutation before flush. For
1925
+ * direct `withCommit` use, make domain decisions first, write, and enroll last.
1926
+ *
1927
+ * **Duplicate enrollment is idempotent by reference.** Enrolling the same
1928
+ * instance repeatedly returns the same token, and a repeated token in
1929
+ * `commits` is harvested once. A repeat call that omits `expectedVersion`
1930
+ * makes no OCC assertion; only a supplied value that contradicts the
1931
+ * enrollment-time baseline rejects. Each event lands in the outbox exactly once
1932
+ * and post-commit acknowledgement runs exactly once. Two
1933
+ * *different* instances with the same logical id cannot be detected
1934
+ * at this layer; that is a Repository contract violation (failure to
1935
+ * maintain Fowler's Identity Map per Unit of Work). See
1936
+ * `docs/guide/repository.md` → "Identity Map: one instance per
1937
+ * aggregate per Unit of Work" for the requirement on repository
1938
+ * implementations that makes this dedupe sound.
1939
+ *
1940
+ * @example Tx-bound repos (Drizzle, Prisma, Mongo, …)
1941
+ * ```typescript
1942
+ * const result = await withCommit({ outbox, bus, scope }, async (tx, enrollment) => {
1943
+ * const orderRepository = makeOrderRepository(tx); // your factory binds tx to the repo
1944
+ * const order = await orderRepository.getById(orderId);
1945
+ * order.confirm();
1946
+ * await persistOrder(tx, order); // low-level adapter write
1947
+ * const commit = enrollment.enrollSaved(order); // attest the repository write
1948
+ * return { result: order.id, commits: [commit] };
1949
+ * });
1950
+ * ```
1951
+ */
1952
+ declare function withCommit<Evt extends AnyDomainEvent, R, TCtx>(deps: WithCommitDeps<Evt, TCtx>, fn: (ctx: TCtx, enrollment: CommitEnrollment<Evt>) => Promise<WithCommitWorkResult<Evt, R>>): Promise<R>;
1953
+ //#endregion
1954
+ //#region src/app/idempotency.d.ts
1955
+ /**
1956
+ * Result of `IdempotencyStore.claim()`: this execution owns the key and must
1957
+ * run the command (`claimed`), a previous execution completed and its outcome
1958
+ * is replayed (`completed`), or an expired staged outcome needs evidence from
1959
+ * the authoritative write model (`reconciliation-required`).
1960
+ *
1961
+ * The two FAILURE answers are thrown, not returned, following the kit's
1962
+ * error posture: a concurrent unfinished execution throws
1963
+ * `IdempotencyInFlightError` (retryable), and the same key arriving
1964
+ * with a different fingerprint throws `IdempotencyKeyReuseError`
1965
+ * (not retryable).
1966
+ */
1967
+ interface IdempotencyLease {
1968
+ /** Adapter-clock expiry as a canonical ISO-8601 timestamp. */
1969
+ readonly expiresAt: string;
1970
+ /** Delay after which the wrapper should renew this lease. */
1971
+ readonly renewAfterMs: number;
1972
+ }
1973
+ /** Store-minted ownership receipt for one successful claim. */
1974
+ interface IdempotencyClaimHandle {
1975
+ readonly key: string;
1976
+ /** Unique across ownership generations for this key; treat as opaque. */
1977
+ readonly token: string;
1978
+ /** Absent for a transactional store; required for a leased store. */
1979
+ readonly lease?: IdempotencyLease;
1980
+ }
1981
+ /** Receipt for an expired staged outcome that needs authoritative evidence. */
1982
+ interface IdempotencyReconciliation {
1983
+ readonly key: string;
1984
+ readonly fingerprint: string;
1985
+ readonly token: string;
1986
+ readonly expiredAt: string;
1987
+ }
1988
+ type IdempotencyReconciliationDecision = "committed" | "not-committed" | "unknown";
1989
+ type IdempotencyClaim = {
1990
+ readonly status: "claimed";
1991
+ readonly claim: IdempotencyClaimHandle;
1992
+ } | {
1993
+ readonly status: "completed";
1994
+ readonly outcome: unknown;
1995
+ } | {
1996
+ readonly status: "reconciliation-required";
1997
+ readonly reconciliation: IdempotencyReconciliation;
1998
+ };
1999
+ /**
2000
+ * Driven port for command idempotency and message-inbox deduplication.
2001
+ *
2002
+ * The store keeps one record per idempotency key: the key, a
2003
+ * fingerprint of the command that first claimed it, and, once the
2004
+ * execution completed, the stored outcome. The intended integration is
2005
+ * the SINGLE-TRANSACTION pattern via {@link withIdempotentCommit}: the
2006
+ * record is written in the same transaction as the aggregate and the
2007
+ * outbox, so a rollback releases the claim and there is no crash window
2008
+ * between claim and commit.
2009
+ *
2010
+ * Adapter contract (mirror of the repository/event-store delegation
2011
+ * model): the adapter maps its store's native signals onto the kit's
2012
+ * errors instead of leaking driver errors:
2013
+ *
2014
+ * - unique-constraint conflict from a CONCURRENT uncommitted claim ->
2015
+ * `IdempotencyInFlightError` (retryable; a retry replays the outcome
2016
+ * or claims fresh),
2017
+ * - existing COMPLETED record with the same fingerprint -> return
2018
+ * `{ status: "completed", outcome }`,
2019
+ * - existing record with a DIFFERENT fingerprint ->
2020
+ * `IdempotencyKeyReuseError`.
2021
+ *
2022
+ * **Transactional vs leased non-transactional stores.** A transactional
2023
+ * adapter (the record lives in the same database as the aggregate)
2024
+ * gets the commit boundary for free: `complete` is atomic with the
2025
+ * command's commit, a rollback releases everything, and `confirm` /
2026
+ * `abandon` / `renew` / `reconcile` are no-ops. This remains the recommended
2027
+ * production pattern and the only family that proves atomic command effect +
2028
+ * idempotency completion without reconciliation.
2029
+ *
2030
+ * A NON-transactional store (the in-memory reference, a separate durable
2031
+ * store) cannot see commits or rollbacks. Every fresh claim therefore returns
2032
+ * a store-minted token and bounded lease. The wrapper renews it while the
2033
+ * transaction runs; `complete`, `renew`, `confirm`, `abandon`, and `reconcile`
2034
+ * compare the token so a stale owner cannot mutate a successor claim. An
2035
+ * expired PENDING claim may be replaced. An expired STAGED outcome is never
2036
+ * replayed or released automatically: `claim` returns
2037
+ * `reconciliation-required`, and the application must consult the source of
2038
+ * truth. `unknown` keeps it blocked.
2039
+ *
2040
+ * A lease is coordination, not a security or exactly-once boundary. To return
2041
+ * `not-committed` safely, the source transaction must persist an idempotency
2042
+ * key or claim token (available as the callback's `execution` argument), or
2043
+ * offer equivalent durable fencing proving the old transaction cannot still
2044
+ * commit. Without that evidence, return `unknown`. A database row merely being
2045
+ * absent while an old transaction may still be in flight is not proof.
2046
+ * A takeover can overlap briefly with the stale worker, so `fn` must keep
2047
+ * irreversible external side effects out of the transaction. Persist an
2048
+ * outbox record and deliver after commit; token fencing can stop the stale
2049
+ * database commit, but it cannot undo an HTTP call already sent.
2050
+ *
2051
+ * The same store doubles as a message INBOX: use the message id as the
2052
+ * key and a constant fingerprint; a duplicate delivery replays the
2053
+ * stored (possibly `undefined`) outcome instead of re-running the
2054
+ * handler.
2055
+ *
2056
+ * The stored outcome must be PLAIN, serialisable data (the same
2057
+ * discipline as snapshots and event payloads): the record round-trips
2058
+ * through the adapter's storage, so class instances would silently lose
2059
+ * their prototype.
2060
+ *
2061
+ * @template TCtx - The transaction context the surrounding scope
2062
+ * exposes (Drizzle `tx`, Prisma `tx`, `undefined` for context-free
2063
+ * scopes). `claim` and `complete` run inside that transaction.
2064
+ */
2065
+ interface IdempotencyStore<TCtx = unknown> {
2066
+ /**
2067
+ * Claims the key for this execution, atomically with respect to
2068
+ * concurrent claimers (`INSERT ... ON CONFLICT` or equivalent).
2069
+ * Returns `claimed` when this execution owns the key, or
2070
+ * `completed` with the stored outcome when a previous execution
2071
+ * already finished under the same key and fingerprint. Throws
2072
+ * `IdempotencyInFlightError` / `IdempotencyKeyReuseError` for the
2073
+ * failure answers (see the port docs). A live staged outcome is in-flight;
2074
+ * after its lease expires it returns `reconciliation-required`, never a
2075
+ * replay or fresh claim.
2076
+ */
2077
+ claim(ctx: TCtx, key: string, fingerprint: string): Promise<IdempotencyClaim>;
2078
+ /**
2079
+ * Stores the outcome for a key this execution claimed, in the same
2080
+ * transaction as the command's writes. On a transactional store the
2081
+ * commit makes it durable and replayable; on a non-transactional
2082
+ * store the outcome is only STAGED until {@link confirm} runs.
2083
+ * Throws `IdempotencyCompletionWithoutClaimError` when no claim exists, and
2084
+ * `IdempotencyClaimLostError` when the receipt is stale, already settled, or
2085
+ * expired. A stale completion must fail before the source transaction can
2086
+ * commit.
2087
+ */
2088
+ complete(ctx: TCtx, claim: IdempotencyClaimHandle, outcome: unknown): Promise<void>;
2089
+ /**
2090
+ * Extends a non-transactional claim's lease and returns its new timing.
2091
+ * The update is compare-and-set on key + token. A transactional adapter
2092
+ * implements this as a no-op returning `undefined`; the wrapper never calls
2093
+ * it for a claim without a lease.
2094
+ */
2095
+ renew(claim: IdempotencyClaimHandle): Promise<IdempotencyLease | undefined>;
2096
+ /**
2097
+ * Finalizes a staged outcome AFTER the surrounding transaction
2098
+ * committed. Called by {@link withIdempotentCommit} post-commit on
2099
+ * every fresh execution. A transactional adapter implements this as
2100
+ * a no-op (the commit already finalized the record). Idempotent:
2101
+ * confirming an already-confirmed receipt is a no-op. A missing or stale
2102
+ * receipt is also a no-op and must never confirm its successor.
2103
+ */
2104
+ confirm(claim: IdempotencyClaimHandle): Promise<void>;
2105
+ /**
2106
+ * Releases a claim whose attempt did not commit: a pending claim or
2107
+ * a staged, unconfirmed outcome. Called by
2108
+ * {@link withIdempotentCommit} once per failed attempt, best-effort.
2109
+ * A transactional adapter implements this as a no-op: the rollback
2110
+ * already removed the row, and the method must be SAFE to call when
2111
+ * the commit outcome is unknown; it never releases a confirmed
2112
+ * record. A stale receipt is a no-op and must never release its successor.
2113
+ */
2114
+ abandon(claim: IdempotencyClaimHandle): Promise<void>;
2115
+ /**
2116
+ * Resolves an EXPIRED staged outcome after the application consulted its
2117
+ * authoritative write model. `committed` makes the staged result replayable;
2118
+ * `not-committed` releases it for a fresh execution. `unknown` is
2119
+ * intentionally not accepted here: uncertainty must preserve the record.
2120
+ * The receipt is compare-and-set so a stale reconciler cannot settle a newer
2121
+ * owner. Transactional adapters implement this as a no-op because they never
2122
+ * return `reconciliation-required`.
2123
+ */
2124
+ reconcile(reconciliation: IdempotencyReconciliation, decision: Exclude<IdempotencyReconciliationDecision, "unknown">): Promise<void>;
2125
+ }
2126
+ /** Identifies one logical command execution for {@link withIdempotentCommit}. */
2127
+ interface IdempotentCommitRequest {
2128
+ /**
2129
+ * The idempotency key: client-supplied header, message id, or a key
2130
+ * derived from actor + intention. One key names one logical command.
2131
+ */
2132
+ readonly key: string;
2133
+ /**
2134
+ * Fingerprint of the command's content (a hash or canonical string
2135
+ * of the request payload). Detects the same key being reused for a
2136
+ * DIFFERENT command, which is rejected instead of replayed.
2137
+ */
2138
+ readonly fingerprint: string;
2139
+ }
2140
+ /**
2141
+ * Outcome of {@link withIdempotentCommit}: `replayed: false` carries the
2142
+ * fresh result of this execution; `replayed: true` carries the stored
2143
+ * outcome of the previous execution with the same key and fingerprint.
2144
+ * The replayed value is typed `R` on the strength of the fingerprint
2145
+ * match: the same command was executed, so the stored outcome has the
2146
+ * shape this command produces, provided the adapter round-trips plain
2147
+ * data faithfully.
2148
+ */
2149
+ interface IdempotentCommitResult<R> {
2150
+ readonly replayed: boolean;
2151
+ readonly result: R;
2152
+ }
2153
+ /** Claim identity visible to work that persists a source-of-truth marker. */
2154
+ interface IdempotentExecution extends IdempotentCommitRequest {
2155
+ readonly claimToken: string;
2156
+ }
2157
+ interface IdempotencyOperationErrorContext {
2158
+ readonly operation: "abandon" | "confirm" | "renew";
2159
+ readonly key: string;
2160
+ readonly token: string;
2161
+ }
2162
+ interface WithIdempotentCommitDeps<Evt extends AnyDomainEvent, TCtx> extends WithCommitDeps<Evt, TCtx> {
2163
+ idempotency: IdempotencyStore<TCtx>;
2164
+ /**
2165
+ * Source-of-truth decision for an expired staged outcome. The callback must
2166
+ * return `committed` only when the command effect is durably visible, and
2167
+ * `not-committed` only when a durable marker proves the attempt cannot still
2168
+ * commit. `unknown` keeps the key blocked.
2169
+ */
2170
+ reconcileIdempotency?: (reconciliation: IdempotencyReconciliation, ctx: TCtx) => Promise<IdempotencyReconciliationDecision>;
2171
+ /**
2172
+ * Observer for best-effort post-commit confirm, rollback abandon, and a
2173
+ * secondary heartbeat failure masked by the primary work error.
2174
+ */
2175
+ onIdempotencyError?: (error: unknown, context: IdempotencyOperationErrorContext) => void;
2176
+ }
2177
+ /**
2178
+ * {@link withCommit} with command idempotency: the duplicate-safe write
2179
+ * path for retryable deliveries (client retries, at-least-once
2180
+ * messages, scheduler re-runs).
2181
+ *
2182
+ * Order of operations:
2183
+ * 1. Inside the transaction, `store.claim(ctx, key, fingerprint)` runs
2184
+ * FIRST. A completed execution short-circuits without touching the domain.
2185
+ * An expired staged outcome invokes `reconcileIdempotency`; `committed`
2186
+ * replays it, `not-committed` releases and claims fresh, and `unknown` (or
2187
+ * no callback) throws `IdempotencyReconciliationRequiredError` without
2188
+ * changing the store.
2189
+ * 2. A fresh claim carries an opaque ownership token. For a leased store the
2190
+ * wrapper renews it at `renewAfterMs` until the transaction callback is
2191
+ * ready to commit. A renewal failure rejects before commit and releases
2192
+ * the claim. `fn(ctx, enrollment, execution)` receives the same token so a
2193
+ * source-side marker can make later reconciliation conclusive.
2194
+ * 3. `store.complete(ctx, claim, fn's result)` stages or completes the outcome
2195
+ * in the same transaction as aggregate writes and outbox. The enrollment
2196
+ * capability is sealed and its token array copied before `complete` can
2197
+ * yield, so leaked callback state cannot change the harvest receipt.
2198
+ * 4. After commit, `store.confirm(claim)` finalizes a leased store's staged
2199
+ * outcome; it is a no-op for transactional stores. A failure cannot reject
2200
+ * an already committed write, so it is sent to `onIdempotencyError` and the
2201
+ * record later enters reconciliation after lease expiry.
2202
+ * 5. Any pre-commit failure releases that exact token through
2203
+ * `store.abandon(claim)` before leaving the transactional region. A stale
2204
+ * abandon cannot release a successor. Secondary abandon/renew failures are
2205
+ * observable but never mask the primary error.
2206
+ *
2207
+ * Composes with `RetryingTransactionScope`: a retryable failure inside
2208
+ * one attempt releases that attempt's claim, and the retry either
2209
+ * executes fresh or, when a concurrent execution completed meanwhile,
2210
+ * replays its confirmed outcome. A concurrent duplicate while the first
2211
+ * execution is still running surfaces as `IdempotencyInFlightError`
2212
+ * (retryable); unwrapped, map it to a conflict/retry-later application
2213
+ * outcome.
2214
+ *
2215
+ * The stored outcome is `fn`'s `result` value; it must be plain, serialisable
2216
+ * data (see {@link IdempotencyStore}). Transactional storage remains the
2217
+ * production default. Leases make the non-transactional family recoverable;
2218
+ * they do not manufacture an atomic exactly-once boundary across two stores.
2219
+ */
2220
+ declare function withIdempotentCommit<Evt extends AnyDomainEvent, R, TCtx>(deps: WithIdempotentCommitDeps<Evt, TCtx>, request: IdempotentCommitRequest, fn: (ctx: TCtx, enrollment: CommitEnrollment<Evt>, execution: IdempotentExecution) => Promise<WithCommitWorkResult<Evt, R>>): Promise<IdempotentCommitResult<R>>;
2221
+ //#endregion
2222
+ //#region src/deadlines/deadline-store.d.ts
2223
+ /**
2224
+ * One deadline due for delivery, as returned by
2225
+ * {@link DeadlineStore.due}. `deliveryId` identifies this scheduled
2226
+ * INCARNATION of the deadline, not the `(scope, key)` address: a
2227
+ * reschedule replaces the incarnation, and acknowledging a stale
2228
+ * incarnation must never consume the new one (see
2229
+ * {@link DeadlineStore.markDelivered}).
2230
+ */
2231
+ interface DueDeadline<TPayload = unknown> {
2232
+ /** Opaque per-incarnation id used for `markDelivered`/`markFailed`. */
2233
+ readonly deliveryId: string;
2234
+ /** The namespace half of the address, e.g. a process or policy name. */
2235
+ readonly scope: string;
2236
+ /** The instance half of the address, e.g. a saga or reservation id. */
2237
+ readonly key: string;
2238
+ /** When the deadline was due. */
2239
+ readonly dueAt: Date;
2240
+ /** The payload handed back as the input; plain data only. */
2241
+ readonly payload: TPayload;
2242
+ /** Failed delivery attempts so far (see `markFailed`). */
2243
+ readonly attempts: number;
2244
+ }
2245
+ /** A deadline that exhausted its delivery attempts; see {@link DeadlineStore.deadLetters}. */
2246
+ interface DeadLetterDeadline<TPayload = unknown> extends DueDeadline<TPayload> {
2247
+ /** Human-readable rendering of the last delivery error, if recorded. */
2248
+ readonly lastError?: string;
2249
+ }
2250
+ /**
2251
+ * Driven port for durable deadlines: timeout-as-input. A process that
2252
+ * waits ("if PaymentReceived has not arrived in 30 minutes,
2253
+ * compensate"; "release the reservation hold after 15 minutes";
2254
+ * "expire the offer at month's end") schedules a deadline, and a poll
2255
+ * loop later DELIVERS it as an input to whatever owns the decision, a
2256
+ * saga aggregate, a use case, a policy. The store never executes
2257
+ * consumer code; firing a deadline means handing back a record.
2258
+ *
2259
+ * Deliberately general-purpose and deliberately small. This is not a
2260
+ * scheduler framework and not a cron abstraction: there is no
2261
+ * recurrence, no execution engine, and the poll loop belongs to the
2262
+ * consumer (the outbox guide's `drainOnce` pattern fits; the deadlines
2263
+ * guide shows the wiring).
2264
+ *
2265
+ * Addressing is the `(scope, key)` pair, so one table serves every
2266
+ * waiting process in an application: `scope` names the policy
2267
+ * ("checkout-saga", "reservation-hold"), `key` the instance. There is
2268
+ * at most ONE pending deadline per address; `schedule` on an existing
2269
+ * address replaces it (that IS the reschedule operation), and each
2270
+ * scheduling gets a fresh `deliveryId`, so acknowledgements of a
2271
+ * replaced incarnation cannot consume its successor.
2272
+ *
2273
+ * Two sides, two transactional postures, the same split as the outbox:
2274
+ *
2275
+ * - **`schedule` and `cancel` are write-side calls** and must join the
2276
+ * ambient write transaction (use a tx-bound store instance inside
2277
+ * `withCommit`'s callback, exactly like an outbox adapter). This is
2278
+ * a correctness rule, not a preference: state that says "waiting for
2279
+ * payment" committed without its deadline is a process that never
2280
+ * wakes up, and a deadline scheduled for a rolled-back state change
2281
+ * is a ghost input.
2282
+ * - **`due`, `markDelivered`, `markFailed`, and `deadLetters` are the
2283
+ * poll surface** and run out of band, in the consumer's loop.
2284
+ *
2285
+ * Delivery is at-least-once: a crash between processing and
2286
+ * `markDelivered` redelivers, so consumers make deadline handling
2287
+ * idempotent (the idempotency store with the `deliveryId` as key is
2288
+ * the ready-made answer). Deadlines have no cross-key ordering
2289
+ * obligations, so unlike the outbox a poison deadline blocks only
2290
+ * itself; bounded retries still matter, which is why failure tracking
2291
+ * is part of the port rather than an extension: report failed
2292
+ * deliveries via `markFailed`, and the store dead-letters a deadline
2293
+ * past its attempt ceiling.
2294
+ *
2295
+ * Run one logical poller per store unless your adapter's `due` claims
2296
+ * records for competing pollers; the same rule as the outbox
2297
+ * dispatcher.
2298
+ *
2299
+ * The bundled processor supplies an `ExecutionContext` to every poll-side
2300
+ * operation. Production adapters MUST pass its signal to native I/O or enforce
2301
+ * a native timeout no later than `deadlineAt`; the shell can bound its wait but
2302
+ * cannot terminate a promise that ignores cancellation. A timed-out write has
2303
+ * an unknown outcome. Acknowledgements must remain idempotent when they complete
2304
+ * late; a late failure update may count its original delivery attempt and must
2305
+ * still no-op after the incarnation was delivered or replaced.
2306
+ *
2307
+ * Verify an adapter with `createDeadlineStoreContractTests` from
2308
+ * `@shirudo/ddd-kit/testing`; `InMemoryDeadlineStore` is the
2309
+ * reference.
2310
+ *
2311
+ * @template TPayload - The payload shape carried from `schedule` to
2312
+ * delivery; plain, serializable data (the same discipline as event
2313
+ * payloads and snapshots)
2314
+ */
2315
+ interface DeadlineStore<TPayload = unknown> {
2316
+ /**
2317
+ * Schedules (or reschedules) the deadline at `(scope, key)`: at most
2318
+ * one pending deadline exists per address, and scheduling an
2319
+ * occupied address replaces its due time, payload, attempt count,
2320
+ * and incarnation. Called inside the write transaction.
2321
+ */
2322
+ schedule(deadline: {
2323
+ scope: string;
2324
+ key: string;
2325
+ dueAt: Date;
2326
+ payload: TPayload;
2327
+ }): Promise<void>;
2328
+ /**
2329
+ * Removes the pending deadline at `(scope, key)`; a no-op when none
2330
+ * exists (the awaited input arrived in time and the wait is over).
2331
+ * Called inside the write transaction.
2332
+ */
2333
+ cancel(scope: string, key: string): Promise<void>;
2334
+ /**
2335
+ * Up to `limit` deadlines with `dueAt <= now` that are neither
2336
+ * delivered nor dead-lettered, ordered by `dueAt` (earliest first;
2337
+ * ties in scheduling order). A `limit` of `0` is legal and yields an
2338
+ * empty page (poll loops computing a remaining capacity may pass
2339
+ * it). `now` is a parameter on purpose: the poll loop owns the
2340
+ * clock, which keeps adapters deterministic and tests free of real
2341
+ * time. The bundled processor always supplies `context`; it is optional only
2342
+ * so existing adapters remain assignable.
2343
+ */
2344
+ due(now: Date, limit: number, context?: ExecutionContext): Promise<ReadonlyArray<DueDeadline<TPayload>>>;
2345
+ /**
2346
+ * Acknowledges delivered incarnations so they stop coming back.
2347
+ * Idempotent on already-acknowledged and unknown ids, and a no-op
2348
+ * for ids of REPLACED incarnations (a late ack after a reschedule
2349
+ * must not consume the successor). Also clears a dead-lettered
2350
+ * incarnation (manual redelivery, then ack). It remains idempotent if the
2351
+ * operation completes after the caller timed out. The bundled processor
2352
+ * always supplies `context`.
2353
+ */
2354
+ markDelivered(deliveryIds: ReadonlyArray<string>, context?: ExecutionContext): Promise<void>;
2355
+ /**
2356
+ * Records one failed delivery attempt for the incarnation:
2357
+ * increments its `attempts` and, once the store's ceiling is
2358
+ * reached, moves it to the dead-letter set that `due` no longer
2359
+ * returns. A no-op for unknown, delivered, or replaced ids.
2360
+ * Returns the exact dead-letter record only on the call that performs
2361
+ * that transition; retries below the ceiling and no-ops return
2362
+ * `undefined`. A late completion may count that original delivery attempt; it
2363
+ * must still no-op if the incarnation was delivered or replaced in the
2364
+ * meantime. The bundled processor never reissues the same store call and
2365
+ * always supplies `context`.
2366
+ */
2367
+ markFailed(deliveryId: string, error?: unknown, context?: ExecutionContext): Promise<DeadLetterDeadline<TPayload> | undefined>;
2368
+ /**
2369
+ * Deadlines that exhausted their delivery attempts. Wire this to durable
2370
+ * alerting and reconciliation: a growing set means processes that stopped
2371
+ * waking up, and the poller can stop between the store transition and its
2372
+ * immediate observer callback.
2373
+ */
2374
+ deadLetters(): Promise<ReadonlyArray<DeadLetterDeadline<TPayload>>>;
2375
+ }
2376
+ //#endregion
2377
+ //#region src/projections/ports.d.ts
2378
+ /**
2379
+ * A projection's gap-proof cursor into one aggregate's commit chain.
2380
+ * `aggregateVersion` plus `commitSequence` orders events; `commitSize`
2381
+ * proves the current commit is complete; `previousEventfulAggregateVersion`
2382
+ * links the next commit to the eventful predecessor. `withCommit` supplies
2383
+ * the current commit facts; the event source finalizes the predecessor on the
2384
+ * surrounding `CommittedDomainEvent`.
2385
+ *
2386
+ * A source MUST map exactly one immutable receipt to each qualified position:
2387
+ * one `eventId`, one `commitSize`, and one eventful predecessor. Custom
2388
+ * envelopes may translate another store's cursor into these fields, but
2389
+ * changing any part of an already observed receipt destroys the proof and is
2390
+ * a source-adapter bug.
2391
+ */
2392
+ type ProjectionPosition = CommitPosition;
2393
+ /**
2394
+ * Durable receipt for the last event one projection applied from an aggregate
2395
+ * stream. The position answers "how far?"; `lastAppliedEventId` identifies the
2396
+ * event at exactly that watermark. Together they let the projector distinguish
2397
+ * a true watermark redelivery from a source changing the event identity,
2398
+ * commit cardinality, or predecessor at the same position. Older positions
2399
+ * still rely on the source's immutable-receipt-per-position contract because a
2400
+ * checkpoint deliberately retains no full history.
2401
+ */
2402
+ interface ProjectionCheckpoint {
2403
+ readonly position: ProjectionPosition;
2404
+ readonly lastAppliedEventId: string;
2405
+ }
2406
+ /**
2407
+ * `true` when `candidate` comes strictly after `reference` in the
2408
+ * per-aggregate tuple order (higher version, or same version and higher
2409
+ * commit sequence). This comparison alone does not prove continuity;
2410
+ * the projector checks the boundary fields before advancing.
2411
+ */
2412
+ declare function isPositionAfter(candidate: ProjectionPosition, reference: ProjectionPosition): boolean;
2413
+ /**
2414
+ * Driven port for projection checkpoints: the per-`(projection,
2415
+ * aggregateType, aggregateId)` watermark receipt that makes a projection
2416
+ * idempotent and rebuild-safe. The {@link
2417
+ * ProjectionCheckpointStore.withCheckpointLocks} callback and every
2418
+ * {@link ProjectionCheckpointStore.load} / {@link
2419
+ * ProjectionCheckpointStore.save} it contains run inside the SAME transaction
2420
+ * as the read-model update (the `Projector` guarantees the pairing); the store
2421
+ * itself is a dumb last-write-wins record, monotonicity is the projector's job.
2422
+ *
2423
+ * Production adapters put the checkpoint table in the same database
2424
+ * as the read model, so update and checkpoint commit atomically: a
2425
+ * checkpoint without its update loses events, an update without its
2426
+ * checkpoint replays work. Verify an adapter with
2427
+ * `createProjectionCheckpointStoreContractTests` from
2428
+ * `@shirudo/ddd-kit/testing`.
2429
+ *
2430
+ * @template TCtx - The transaction context of the ambient
2431
+ * `TransactionScope` (a knex trx, a drizzle tx, a pg client)
2432
+ */
2433
+ interface ProjectionCheckpointStore<TCtx = unknown> {
2434
+ /**
2435
+ * Runs the complete checkpoint read / read-model update / checkpoint save
2436
+ * critical section with exclusive access to every supplied
2437
+ * `(projection, aggregateType, aggregateId)` key.
2438
+ *
2439
+ * Exclusivity MUST cover keys for which no checkpoint row exists yet. A
2440
+ * plain `SELECT ... FOR UPDATE` against the checkpoint table is therefore
2441
+ * insufficient at genesis: use transaction-scoped advisory/key locks, or
2442
+ * first materialize durable lock rows and lock those. Acquire multiple keys
2443
+ * in a deterministic order to avoid deadlocks, and keep database locks until
2444
+ * the surrounding transaction commits or rolls back. On entry, `work` must
2445
+ * observe checkpoint commits made by the preceding lock holder; choose the
2446
+ * transaction isolation level accordingly, or surface and retry a
2447
+ * serialization conflict instead of applying against a stale snapshot.
2448
+ *
2449
+ * Implementations may serialize more than the requested keys, but never
2450
+ * less. The callback is non-reentrant for an overlapping key set.
2451
+ */
2452
+ withCheckpointLocks<R>(ctx: TCtx, projection: string, addresses: ReadonlyArray<AggregateAddress>, work: () => Promise<R>): Promise<R>;
2453
+ /**
2454
+ * The stored watermark receipt for `(projection, address)`, or `undefined`
2455
+ * when this projection has never applied an event of that
2456
+ * aggregate. Called inside the projector's transaction.
2457
+ */
2458
+ load(ctx: TCtx, projection: string, address: AggregateAddress): Promise<ProjectionCheckpoint | undefined>;
2459
+ /**
2460
+ * Persists the watermark receipt, overwriting a previous one (last write
2461
+ * wins; the projector only calls this with advancing checkpoints).
2462
+ * Called inside the projector's transaction, after the read-model
2463
+ * update it accounts for.
2464
+ */
2465
+ save(ctx: TCtx, projection: string, address: AggregateAddress, checkpoint: ProjectionCheckpoint): Promise<void>;
2466
+ /**
2467
+ * The wait-for-version building block: `true` when the stored
2468
+ * watermark for `(projection, address)` is at or past
2469
+ * `position`. Runs OUTSIDE any transaction (a query-side poll).
2470
+ *
2471
+ * Pass the position of the LAST event your commit emitted: all
2472
+ * events of one commit share the `aggregateVersion`, so comparing
2473
+ * on the version alone would report "reached" while later events
2474
+ * of the same commit are still unapplied.
2475
+ */
2476
+ hasReached(projection: string, address: AggregateAddress, position: ProjectionPosition): Promise<boolean>;
2477
+ /**
2478
+ * Deletes every checkpoint of `projection` (other projections'
2479
+ * checkpoints are untouched): the rebuild entry point. Called
2480
+ * inside the rebuild transaction, together with the projection's
2481
+ * `truncate`, so a rebuild starts from a consistent zero.
2482
+ */
2483
+ reset(ctx: TCtx, projection: string): Promise<void>;
2484
+ }
2485
+ /**
2486
+ * One projection: the consumer-owned mapping from events to ONE read
2487
+ * model (one table/view per projection; run several `Projector`s for
2488
+ * several read shapes). The kit owns the mechanics around it
2489
+ * (cursor skip, atomic checkpointing, rebuild); the handler owns the
2490
+ * read-model writes.
2491
+ *
2492
+ * The projector feed MUST contain every committed envelope for each aggregate
2493
+ * address it carries, including event types this read model does not use.
2494
+ * Handle those events as explicit no-ops in `apply`: the projector still
2495
+ * advances their cursor. Filtering a broker subscription by event type drops
2496
+ * positions from the source chain and turns the next commit into a real gap.
2497
+ * For correctness-critical read models, use `projectionFromHandlers` to make
2498
+ * every event in the declared union a compile-time handler-or-ignore decision;
2499
+ * implement this interface directly when intentionally partial routing is the
2500
+ * better fit.
2501
+ */
2502
+ interface Projection<Evt extends AnyDomainEvent, TCtx = unknown> {
2503
+ /**
2504
+ * Stable unique name; keys the checkpoints. Renaming it orphans the
2505
+ * old checkpoints and replays everything under the new name.
2506
+ */
2507
+ name: string;
2508
+ /**
2509
+ * Applies ONE event's read-model change inside the ambient
2510
+ * transaction. The projector's cursor already filtered duplicates
2511
+ * and stale events, so plain writes are safe; route on
2512
+ * `event.type` and handle creates, updates, deletes, corrections,
2513
+ * and tombstones explicitly (an upsert-only handler silently
2514
+ * retains stale rows). For a known event type this projection does not use,
2515
+ * return without writing; that explicit no-op still consumes and checkpoints
2516
+ * the envelope's source position.
2517
+ *
2518
+ * MUST be side-effect-free beyond the read model: no mails, no
2519
+ * external calls, no commands. A rebuild replays every event; side
2520
+ * effects would fire again.
2521
+ */
2522
+ apply(ctx: TCtx, event: Evt): Promise<void>;
2523
+ /**
2524
+ * Optional: clears the read model, called by `Projector.reset()`
2525
+ * in the same transaction as the checkpoint reset, so a rebuild
2526
+ * never observes a half-cleared state. Without it, truncating the
2527
+ * read model before a rebuild is the caller's responsibility.
2528
+ */
2529
+ truncate?(ctx: TCtx): Promise<void>;
2530
+ }
2531
+ //#endregion
2532
+ //#region src/repo/event-store.d.ts
2533
+ /** Options for {@link EventStore.append}. */
2534
+ interface EventStoreAppendOptions {
2535
+ /**
2536
+ * The stream version the writer loaded (its optimistic-concurrency
2537
+ * baseline): the number of events the stream held when the aggregate
2538
+ * was reconstituted. `0` for a brand-new stream. `UnitOfWork` captures
2539
+ * this value when the adapter returns a loaded aggregate; it is not stored
2540
+ * on the aggregate itself.
2541
+ */
2542
+ readonly expectedVersion: number;
2543
+ }
2544
+ /** Options for {@link EventStore.readStream}. */
2545
+ interface ReadStreamOptions {
2546
+ /**
2547
+ * Maximum number of events returned by this page. Required so callers
2548
+ * cannot accidentally materialize an unbounded stream. Must be a positive
2549
+ * safe integer. An adapter may return fewer events, but must return at least
2550
+ * one while unread events remain inside the requested window.
2551
+ */
2552
+ readonly limit: number;
2553
+ /**
2554
+ * Return only events AFTER this stream position (1-based event count),
2555
+ * the snapshot catch-up read: `readStream(stream, { fromVersion:
2556
+ * snapshot.version, limit: 256 })` yields the next page passed to
2557
+ * `aggregate.loadFromHistory`. Defaults to `0` (the first
2558
+ * stream page).
2559
+ * Must be a non-negative safe integer when present.
2560
+ */
2561
+ readonly fromVersion?: number;
2562
+ /**
2563
+ * Return events only THROUGH this stream position (inclusive, 1-based
2564
+ * event count). Together with `fromVersion`, this describes the interval
2565
+ * `(fromVersion, toVersion]`. Defaults to the actual stream head.
2566
+ * `0` therefore returns an empty window; a value beyond the head clamps
2567
+ * to the head; and `fromVersion >= toVersion` is an empty interval, not
2568
+ * an error.
2569
+ * Must be a non-negative safe integer when present.
2570
+ */
2571
+ readonly toVersion?: number;
2572
+ }
2573
+ /**
2574
+ * State returned by {@link EventStore.readStream}.
2575
+ *
2576
+ * `lastVersion` is always the actual stream head (the event count), independent
2577
+ * of the requested read window and page limit. `exists: true` implies
2578
+ * `lastVersion >= 1`: an
2579
+ * existing stream has at least one event, while metadata or tombstones without
2580
+ * events must be reported as `exists: false`. A missing stream is therefore
2581
+ * distinguishable from an existing stream whose requested window is empty.
2582
+ * Snapshot-backed repositories use that distinction to reject a snapshot whose
2583
+ * version lies beyond the current authoritative stream head.
2584
+ */
2585
+ type StreamReadResult<Evt extends AnyDomainEvent> = {
2586
+ readonly exists: false;
2587
+ readonly lastVersion: 0;
2588
+ readonly events: readonly [];
2589
+ } | {
2590
+ readonly exists: true;
2591
+ readonly lastVersion: number;
2592
+ readonly events: ReadonlyArray<Evt>;
2593
+ };
2594
+ /**
2595
+ * Driven port for event-sourced aggregate persistence: an append-only
2596
+ * store with one stream per aggregate. Each stream is addressed by the
2597
+ * qualified tuple `(aggregateType, aggregateId)`, because aggregate ids
2598
+ * are type-scoped rather than globally unique.
2599
+ *
2600
+ * The kit ships the port, the OCC error contract, `InMemoryEventStore`
2601
+ * as the reference implementation, and the event-sourced repository
2602
+ * contract suites (`createEventStoreContractTests` and
2603
+ * `createEsRepositoryContractTests` from `@shirudo/ddd-kit/testing`).
2604
+ * Your adapter implements this port against a real store and must pass
2605
+ * those suites. Like the state-stored repository contract, its optimistic
2606
+ * concurrency and key isolation are testable adapter contracts, not kit
2607
+ * guarantees.
2608
+ *
2609
+ * Repository usage (see the event-sourcing guide):
2610
+ *
2611
+ * ```ts
2612
+ * private stream(id: OrderId): AggregateAddress<OrderId> {
2613
+ * return { aggregateType: "Order", aggregateId: id };
2614
+ * }
2615
+ *
2616
+ * async findById(id: OrderId): Promise<Order | undefined> {
2617
+ * const cached = this.tracking.identityMap.get(Order, id);
2618
+ * if (cached) return cached;
2619
+ * const address = this.stream(id);
2620
+ * const order = Order.reconstitute(id); // bare instance, no events
2621
+ * let fromVersion = 0;
2622
+ * let targetVersion: number | undefined;
2623
+ * for (;;) {
2624
+ * const page = await this.eventStore.readStream(address, {
2625
+ * fromVersion,
2626
+ * toVersion: targetVersion,
2627
+ * limit: 256,
2628
+ * });
2629
+ * if (!page.exists) return undefined;
2630
+ * targetVersion ??= page.lastVersion; // pin the first observed head
2631
+ * if (fromVersion === targetVersion) break;
2632
+ * if (page.events.length === 0) {
2633
+ * throw new NonProgressingEventStreamPageError({
2634
+ * ...address,
2635
+ * fromVersion,
2636
+ * targetVersion,
2637
+ * });
2638
+ * }
2639
+ * const result = order.loadFromHistory(page.events);
2640
+ * if (result.isErr()) throw result.error; // corrupt stream
2641
+ * fromVersion += page.events.length;
2642
+ * }
2643
+ * return this.tracking.trackLoaded(order);
2644
+ * }
2645
+ *
2646
+ * flush(write: AggregatePersistenceWrite<Order, number | undefined>) {
2647
+ * return this.eventStore.append(this.stream(write.aggregateId), write.events, {
2648
+ * expectedVersion: write.expectedVersion ?? 0,
2649
+ * });
2650
+ * }
2651
+ * ```
2652
+ *
2653
+ * `flush` appends the exact event batch registered by `add` or `update`;
2654
+ * `withCommit`
2655
+ * separately composes them into outbox envelopes. The event store's own
2656
+ * stream position remains the ordering authority for replay.
2657
+ *
2658
+ * The exact appended event batch is acknowledged only after the surrounding
2659
+ * transaction commits. Rollback leaves it pending.
2660
+ */
2661
+ interface EventStore<Evt extends AnyDomainEvent> {
2662
+ /**
2663
+ * Atomically appends `events` to the stream, guarded by optimistic
2664
+ * concurrency: the append succeeds only when the stream currently
2665
+ * holds exactly `options.expectedVersion` events.
2666
+ *
2667
+ * Contract for implementations:
2668
+ *
2669
+ * 1. **OCC:** on a version mismatch (stale writer, duplicate create
2670
+ * racing on `expectedVersion: 0`, or an expectedVersion ahead of
2671
+ * the stream), throw `ConcurrencyConflictError` from
2672
+ * `@shirudo/ddd-kit` carrying the expected and actual stream
2673
+ * versions; map your store's native conflict signal to it instead
2674
+ * of letting a raw driver error escape. One sanctioned exception:
2675
+ * an adapter that can DISTINGUISH the duplicate-create race
2676
+ * (`expectedVersion: 0` against a stream that already exists,
2677
+ * typically a unique violation on the first position) may throw
2678
+ * `DuplicateAggregateError` for that case instead, matching the
2679
+ * state-stored insert path. It is deliberately NOT retryable:
2680
+ * replaying the same append cannot succeed; the use case resolves
2681
+ * the create race (load the existing aggregate, or surface HTTP
2682
+ * 409). The contract suite accepts both errors for this race.
2683
+ * 2. **Atomicity:** all events land or none do; a rejected append
2684
+ * leaves the stream untouched.
2685
+ * 3. **Qualified identity:** `(aggregateType, aggregateId)` is the
2686
+ * storage key. Equal raw ids under different aggregate types are
2687
+ * independent streams. Use both columns in every primary/unique
2688
+ * key, OCC predicate, and read predicate.
2689
+ * 4. **Order:** events are stored in the given array order, appended
2690
+ * after the existing stream tail.
2691
+ * 5. **Append-only:** stored events are never edited or deleted;
2692
+ * corrections are new (compensating) events.
2693
+ * 6. **Replay integrity:** reads order by the persisted stream position
2694
+ * and reject duplicate or non-contiguous positions where the backing
2695
+ * store exposes them. The portable contract suite proves observable
2696
+ * append order and slicing; because the port cannot inject malformed
2697
+ * physical rows, adapters add a store-specific corruption fixture that
2698
+ * proves the duplicate/gap rejection. The repository then calls
2699
+ * `loadFromHistory`, whose replay guard rejects any event carrying an
2700
+ * aggregate type or id that contradicts this stream key.
2701
+ *
2702
+ * An empty `events` array is a no-op; implementations resolve without
2703
+ * touching the store (an ES repository skips `append` for aggregates
2704
+ * without pending events anyway).
2705
+ *
2706
+ * Treat `aggregateType` as a stable technical stream category. If two
2707
+ * bounded contexts share one physical store and reuse a domain name,
2708
+ * qualify it at the source (`sales.order`, `fulfillment.order`). Renaming
2709
+ * it changes the stream key and therefore requires a data migration.
2710
+ */
2711
+ append(stream: AggregateAddress, events: ReadonlyArray<Evt>, options: EventStoreAppendOptions): Promise<void>;
2712
+ /**
2713
+ * Reads one bounded page of the qualified stream in append order. An unknown stream returns
2714
+ * `{ exists: false, lastVersion: 0, events: [] }`; an existing stream keeps
2715
+ * `exists: true` even when its requested window is empty. An existing stream
2716
+ * has at least one event, so `exists: true` implies `lastVersion >= 1`;
2717
+ * metadata or tombstones without events must be reported as absent.
2718
+ * `lastVersion` is always the actual stream head. `options.limit` is
2719
+ * mandatory and caps the returned array. An adapter may return fewer than
2720
+ * the requested limit, but if the requested window still contains unread
2721
+ * events it must return a non-empty contiguous prefix so callers can make
2722
+ * progress. `options.fromVersion`
2723
+ * excludes positions at or below its 1-based event count;
2724
+ * `options.toVersion` includes positions through its count, so both bounds
2725
+ * describe `(fromVersion, toVersion]`. `toVersion: 0` and inverted ranges
2726
+ * return an empty existing window, while a bound beyond the head clamps to
2727
+ * the head. This distinction is load-bearing for snapshot catch-up and
2728
+ * point-in-time reconstruction: a repository can verify the requested
2729
+ * historical window against the authoritative head. `limit` must be a
2730
+ * positive safe integer; present bounds must be non-negative safe integers.
2731
+ * Invalid options reject with `RangeError` before querying storage.
2732
+ *
2733
+ * Each page's `exists`, `lastVersion`, and `events` must describe one
2734
+ * consistent view of the stream. Multiple page reads are not one database
2735
+ * snapshot: pin the first page's `lastVersion` as `toVersion` on every
2736
+ * continuation, then advance `fromVersion` by the number of events actually
2737
+ * returned. Because streams are append-only, that yields a stable prefix
2738
+ * even if new events arrive while replay is in progress. The returned
2739
+ * event array is owned by the caller; implementations must not hand out
2740
+ * mutable live internal state.
2741
+ */
2742
+ readStream(stream: AggregateAddress, options: ReadStreamOptions): Promise<StreamReadResult<Evt>>;
2743
+ }
2744
+ //#endregion
2745
+ //#region src/repo/snapshot-store.d.ts
2746
+ /**
2747
+ * Driven port for aggregate snapshot persistence: the storage half of
2748
+ * the snapshot-plus-recent-events load path for event-sourced aggregates.
2749
+ * `SnapshotModel` owns projection, migration, and reconstitution;
2750
+ * `EventStore.readStream` supplies the catch-up tail to `loadFromHistory`.
2751
+ *
2752
+ * **A snapshot is derived data, never authority.** The stream remains
2753
+ * the source of truth; a snapshot only shortens replay. That shapes
2754
+ * the port:
2755
+ *
2756
+ * - **Transaction-free by design.** Unlike the outbox or the
2757
+ * idempotency store, saving a snapshot does NOT belong in the write
2758
+ * transaction: write it after the commit, out of band, on whatever
2759
+ * cadence your policy picks. A lost save costs replay time, not
2760
+ * correctness; a stale snapshot is caught up by the event tail.
2761
+ * - **Latest only.** One snapshot per `(aggregateType, aggregateId)`;
2762
+ * `save` replaces the previous one. Snapshot history has no reader
2763
+ * in this load path.
2764
+ * - **WHEN to snapshot is policy and stays with the consumer** (every
2765
+ * N events after commit is the usual shape); the kit ships the port,
2766
+ * not the policy.
2767
+ *
2768
+ * Contract for implementations (verified by
2769
+ * `createSnapshotStoreContractTests` from `@shirudo/ddd-kit/testing`):
2770
+ * the snapshot round-trips verbatim (`state` as plain data,
2771
+ * `version`, `snapshotAt` with millisecond fidelity, `schemaVersion`
2772
+ * including its absence), loads return detached copies (never live
2773
+ * internal state), and keys are isolated per aggregate type AND id
2774
+ * (one table may serve every aggregate type).
2775
+ *
2776
+ * @template TState - The adapter-owned snapshot DTO shape; a store shared
2777
+ * across aggregate types is a
2778
+ * `SnapshotStore<unknown>` with typed views per repository
2779
+ */
2780
+ interface SnapshotStore<TState = unknown> {
2781
+ /**
2782
+ * The latest snapshot for the aggregate, or `undefined` when none
2783
+ * exists. The repository falls back to a full replay then.
2784
+ */
2785
+ load(address: AggregateAddress): Promise<AggregateSnapshot<TState> | undefined>;
2786
+ /**
2787
+ * Persists `snapshot` as the new latest for the aggregate,
2788
+ * replacing any previous one. Called AFTER the write transaction
2789
+ * committed (see the port docs); a single-row upsert is the
2790
+ * standard implementation.
2791
+ */
2792
+ save(address: AggregateAddress, snapshot: AggregateSnapshot<TState>): Promise<void>;
2793
+ /**
2794
+ * Removes the aggregate's snapshot; a no-op when none exists. The
2795
+ * two callers: the schema-migration fallback (a
2796
+ * `SnapshotSchemaMismatchError` or corrupt snapshot during adapter
2797
+ * reconstitution discards the snapshot and refolds from the full stream)
2798
+ * and erasure (a snapshot duplicates aggregate state and follows
2799
+ * the same retention rules). When erasing, delete the snapshot
2800
+ * BEFORE the event stream: the reverse order has a crash window in
2801
+ * which a stale snapshot resurrects the erased aggregate on the
2802
+ * snapshot load path; snapshot-first degrades to a full replay.
2803
+ */
2804
+ delete(address: AggregateAddress): Promise<void>;
2805
+ }
2806
+ //#endregion
2807
+ export { OutboxWriter as $, ClockFactory as $t, WithCommitWorkResult as A, AnyDomainEvent as At, DurableCommandMessage as B, EventIdFactory as Bt, IdempotentExecution as C, updateEntityById as Ct, CommitEnrollment as D, IEventSourcedAggregate as Dt, AggregateCommitToken as E, IAggregateRoot as Et, CommandMessageContent as F, CreateUncommittedDomainEventOptions as Ft, DispatchTrackingOutbox as G, copyMetadata as Gt, CommitPosition as H, PendingDomainEvent as Ht, CommandMessageRelationships as I, DomainEvent as It, EventCommitCandidatePosition as J, createDomainEventFromFacts as Jt, EventBus as K, createDomainEvent as Kt, CommandOutboxCommitCandidate as L, DomainEventFactory as Lt, TransactionScope as M, CreateDomainEventFromFactsOptions as Mt, TransactionalOptions as N, CreateDomainEventOptions as Nt, CommitEnrollmentOptions as O, Version as Ot, CommandCommitOriginCandidate as P, CreateDomainEventStampOptions as Pt, OutboxRecord as Q, recordDomainEvent as Qt, CommandOutboxMapper as R, DomainEventFactoryOptions as Rt, IdempotentCommitResult as S, sameEntity as St, withIdempotentCommit as T, AggregateSnapshot as Tt, CommittedDomainEvent as U, UncommittedDomainEvent as Ut, routeEventsToCommandOutbox as V, EventMetadata as Vt, DeadLetterRecord as W, UncommittedDomainEventOf as Wt, OnceOptions as X, defaultDomainEventFactory as Xt, EventHandler as Y, createUncommittedDomainEvent as Yt, Outbox as Z, mergeMetadata as Zt, IdempotencyOperationErrorContext as _, findEntityById as _t, StreamReadResult as a, JsonObject as at, IdempotencyStore as b, removeEntityById as bt, ProjectionCheckpointStore as c, AggregateRoot as ct, DeadLetterDeadline as d, Entity as dt, Id as en, PublishOptions as et, DeadlineStore as f, EntityConfig as ft, IdempotencyLease as g, entityIds as gt, IdempotencyClaimHandle as h, StateValidator as ht, ReadStreamOptions as i, PublishedCommand as it, withCommit as j, AnyUncommittedDomainEvent as jt, WithCommitDeps as k, sameVersion as kt, ProjectionPosition as l, AggregateConfig as lt, IdempotencyClaim as m, Identifiable as mt, EventStore as n, Command as nt, Projection as o, JsonPrimitive as ot, DueDeadline as p, IEntity as pt, EventCommitCandidate as q, createDomainEventFactory as qt, EventStoreAppendOptions as r, CommandHandler as rt, ProjectionCheckpoint as s, JsonValue as st, SnapshotStore as t, IdGenerator as tn, ExecutionContext as tt, isPositionAfter as u, BaseAggregate as ut, IdempotencyReconciliation as v, freezeShallow as vt, WithIdempotentCommitDeps as w, AggregateAddress as wt, IdempotentCommitRequest as x, replaceEntityById as xt, IdempotencyReconciliationDecision as y, hasEntityId as yt, CommandOutboxWriter as z, DomainEventStamp as zt };
2808
+ //# sourceMappingURL=snapshot-store.d.ts.map