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

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