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

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