@shirudo/ddd-kit 3.0.0-rc.13 → 3.0.0-rc.14

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.
@@ -193,15 +193,16 @@ type ClockFactory = () => Date;
193
193
  /**
194
194
  * Factory function producing a fresh, unique event identifier for each call.
195
195
  *
196
- * The library ships a default that uses Web Crypto `crypto.randomUUID()`
197
- * (works on Node 19+, modern browsers in secure contexts, Deno, Bun,
198
- * Cloudflare Workers, Vercel Edge, and any runtime that implements Web
199
- * Crypto). Note that `crypto.randomUUID()` returns **UUID v4** (purely
200
- * random); for production event stores prefer a **time-ordered** id
201
- * format (UUID v7 / ULID / KSUID) so B-tree indexes on the eventId
202
- * column stay clustered and `ORDER BY eventId` matches creation order.
203
- * Supply one to {@link createDomainEventFactory} to use UUID v7, ULID,
204
- * KSUID, or another collision-safe format without mutating module state.
196
+ * The library ships a default that creates a time-ordered **UUID v7** from
197
+ * Web Crypto random bits (works on Node 19+, modern browsers in secure
198
+ * contexts, Deno, Bun, Cloudflare Workers, Vercel Edge, and any runtime that
199
+ * implements Web Crypto). A B-tree index on the eventId column stays
200
+ * clustered. The id is no ordering authority: ids from several processes
201
+ * interleave within one millisecond, and clock skew can invert them. Order
202
+ * events by aggregate version or stream position. Supply another generator
203
+ * to {@link createDomainEventFactory} to
204
+ * use ULID, KSUID, or another collision-safe format without mutating module
205
+ * state.
205
206
  */
206
207
  type EventIdFactory = () => string;
207
208
  /**
@@ -266,8 +267,8 @@ interface DomainEvent<T extends string, P = void> {
266
267
  /**
267
268
  * Unique identifier for this specific event instance. Used by idempotent
268
269
  * consumers, outbox dispatch tracking, and as the target of
269
- * `metadata.causationId`. Convenience constructors default to
270
- * `crypto.randomUUID()`; strict construction requires the caller to supply it.
270
+ * `metadata.causationId`. Convenience constructors default to a UUID v7;
271
+ * strict construction requires the caller to supply it.
271
272
  */
272
273
  readonly eventId: string;
273
274
  /**
@@ -291,7 +292,7 @@ interface DomainEvent<T extends string, P = void> {
291
292
  */
292
293
  readonly payload: P;
293
294
  /**
294
- * Timestamp when the accepted fact was recorded by the application shell.
295
+ * Timestamp when the application shell stamped the accepted fact.
295
296
  * Put business-relevant time in the payload under a domain name.
296
297
  */
297
298
  readonly occurredAt: Date;
@@ -355,7 +356,7 @@ interface CreateDomainEventOptions {
355
356
  /**
356
357
  * Override for the auto-generated `eventId`. Pass an existing id (for
357
358
  * replay, tests, or deterministic event sourcing) instead of letting the
358
- * factory call `crypto.randomUUID()`.
359
+ * factory generate a UUID v7.
359
360
  */
360
361
  eventId?: string;
361
362
  /**
@@ -402,7 +403,7 @@ interface CreateDomainEventStampOptions {
402
403
  }
403
404
  /** Dependencies captured by one immutable domain-event factory instance. */
404
405
  interface DomainEventFactoryOptions {
405
- /** Event-id generator. Defaults to Web Crypto `crypto.randomUUID()`. */
406
+ /** Event-id generator. Defaults to a UUID v7 from Web Crypto random bits. */
406
407
  readonly eventIdFactory?: EventIdFactory;
407
408
  /** Event-recording clock. Defaults to `() => new Date()`. */
408
409
  readonly clock?: ClockFactory;
@@ -442,7 +443,7 @@ interface DomainEventFactory {
442
443
  * The supplied functions are read once and captured by value. The returned
443
444
  * object is frozen, so another request, test, or library cannot replace its
444
445
  * policy. Its {@link DomainEventFactory.createStamp} method is the
445
- * application-shell bridge that records an accepted aggregate decision.
446
+ * application-shell bridge that stamps an accepted aggregate decision.
446
447
  * Passing the factory through `AggregateConfig`
447
448
  * enables the explicitly named convenience methods, whose defaults read time
448
449
  * and randomness.
@@ -453,8 +454,7 @@ interface DomainEventFactory {
453
454
  * eventIdFactory: () => uuidv7(),
454
455
  * clock: () => new Date(),
455
456
  * });
456
- * order.confirm();
457
- * stampPendingEvents(order, domainEvents);
457
+ * const uow = new UnitOfWork({ ...deps, eventStampFactory: domainEvents });
458
458
  * ```
459
459
  */
460
460
  declare function createDomainEventFactory(options?: DomainEventFactoryOptions): DomainEventFactory;
@@ -937,7 +937,7 @@ type Id<Tag extends string> = string & {
937
937
  * The kit makes no attempt to dedupe or detect collisions: a collision
938
938
  * silently overwrites earlier rows (under unique-key constraints) or
939
939
  * silently aliases two different entities (without them). Safe choices:
940
- * `crypto.randomUUID()` (UUIDv4, the default for events), ULID, UUIDv7,
940
+ * UUIDv7 (the default for events), `crypto.randomUUID()` (UUIDv4), ULID,
941
941
  * KSUID: all collision-resistant by design. Unsafe choices: `Date.now()`
942
942
  * alone (duplicates within the same millisecond), a process-local
943
943
  * counter without persistence (resets to 1 on restart, collides with
@@ -1396,6 +1396,43 @@ interface TransactionScope<TCtx> {
1396
1396
  transactional<T>(fn: (ctx: TCtx) => Promise<T>, options?: TransactionalOptions): Promise<T>;
1397
1397
  }
1398
1398
  //#endregion
1399
+ //#region src/application/unit-of-work/stamp-pending-events.d.ts
1400
+ /** Minimal shell role accepted by {@link stampPendingEvents}. */
1401
+ type DomainEventStampFactory = Pick<DomainEventFactory, "createStamp">;
1402
+ /** Per-decision stamp provider for metadata that depends on the event. */
1403
+ type DomainEventStampProvider<TEvent extends AnyDomainEvent> = (event: UnstampedDomainEventOf<TEvent>, index: number) => DomainEventStamp;
1404
+ /**
1405
+ * Stamp options that every decision of one stamping shares: a stamp time
1406
+ * and metadata such as the correlation id of the request. An `eventId` is per
1407
+ * fact, so the factory creates it for each decision.
1408
+ */
1409
+ type SharedDomainEventStampOptions = Omit<CreateDomainEventStampOptions, "eventId">;
1410
+ /**
1411
+ * Stamps every still-unstamped event accepted by an aggregate.
1412
+ *
1413
+ * The commit stamps every still-unstamped event when it registers the
1414
+ * aggregate, so a use case does not need this function. Call it first when
1415
+ * the metadata depends on the concrete decision, for an import, or for a test
1416
+ * that needs fixed ids. The commit keeps the stamps that this function set.
1417
+ *
1418
+ * The function is a command that also returns the stamped batch, a
1419
+ * deliberate exception to command-query separation: the caller hands the
1420
+ * batch on to persistence and needs no second read of `pendingEvents`.
1421
+ *
1422
+ * Stamping is atomic with respect to the aggregate's pending list: if stamp
1423
+ * creation or validation fails, every decision remains unstamped. A
1424
+ * successful second call returns the same event objects and does not read the
1425
+ * factory again, so an instance that a retried attempt registers again keeps
1426
+ * its event identities.
1427
+ *
1428
+ * Pass a `DomainEventFactory` (only its `createStamp` role is required) for one
1429
+ * uniform stamping policy. Add `stampOptions` for metadata that every
1430
+ * decision of the batch shares, such as the correlation id of the request.
1431
+ * Pass a callback instead when metadata depends on the concrete decision.
1432
+ */
1433
+ declare function stampPendingEvents<TId extends Id<string>, TEvent extends AnyDomainEvent>(aggregate: Aggregate<TId, TEvent>, factory: DomainEventStampFactory, stampOptions?: SharedDomainEventStampOptions): ReadonlyArray<TEvent>;
1434
+ declare function stampPendingEvents<TId extends Id<string>, TEvent extends AnyDomainEvent>(aggregate: Aggregate<TId, TEvent>, createStamp: DomainEventStampProvider<TEvent>): ReadonlyArray<TEvent>;
1435
+ //#endregion
1399
1436
  //#region src/application/cqrs/handler.d.ts
1400
1437
  /** Dependencies for {@link withCommit}. */
1401
1438
  interface WithCommitDeps<Evt extends AnyDomainEvent, TCtx> {
@@ -1459,6 +1496,19 @@ interface WithCommitDeps<Evt extends AnyDomainEvent, TCtx> {
1459
1496
  * unless the scope honors the signal.
1460
1497
  */
1461
1498
  signal?: AbortSignal;
1499
+ /**
1500
+ * Stamps every pending event that is still unstamped when its aggregate is
1501
+ * registered: the event id, the stamp time, and the metadata. Defaults to
1502
+ * `createDomainEventFactory()`, a UUID v7 from Web Crypto random bits and
1503
+ * the system clock. An event that is already stamped, for example by an
1504
+ * explicit `stampPendingEvents` call, keeps its stamp.
1505
+ */
1506
+ eventStampFactory?: DomainEventStampFactory;
1507
+ /**
1508
+ * Stamp options that every event stamped in this commit shares, for
1509
+ * example the correlation id of the request.
1510
+ */
1511
+ stampOptions?: SharedDomainEventStampOptions;
1462
1512
  }
1463
1513
  declare const aggregateCommitTokenBrand: unique symbol;
1464
1514
  /**
@@ -2667,6 +2717,10 @@ interface EventStore<Evt extends AnyDomainEvent> extends EventStreamReader<Evt>
2667
2717
  * proves the duplicate/gap rejection. The repository then calls
2668
2718
  * `replayHistory`, whose replay guard rejects any event carrying an
2669
2719
  * aggregate type or id that contradicts this stream key.
2720
+ * 7. **Stamped events only:** reject an event without an `eventId` with
2721
+ * a `TypeError`, before any write. A direct `withCommit` callback can
2722
+ * append before its registration stamps the events; this check makes
2723
+ * that mistake fail instead of storing ids that the outbox never sees.
2670
2724
  *
2671
2725
  * An empty `events` array is a no-op; implementations resolve without
2672
2726
  * touching the store (an ES repository skips `append` for aggregates
@@ -2825,5 +2879,5 @@ interface SnapshotStore<TState = unknown> {
2825
2879
  delete(identity: AggregateIdentity): Promise<void>;
2826
2880
  }
2827
2881
  //#endregion
2828
- export { Version as $, IdempotentCommitRequest as A, DomainEventFactoryOptions as At, CommitRegistrationOptions as B, createStampedDomainEvent as Bt, IdempotencyClaim as C, AnyUnstampedDomainEvent as Ct, IdempotencyReconciliation as D, CreateUnstampedDomainEventOptions as Dt, IdempotencyOperationErrorContext as E, CreateStampedDomainEventOptions as Et, DeadLetterDeadline as F, UnstampedDomainEvent as Ft, TransactionalOptions as G, Command as Gt, WithCommitWorkResult as H, mergeMetadata as Ht, DeadlineStore as I, UnstampedDomainEventOf as It, OnceOptions as J, JsonObject as Jt, EventBus as K, CommandHandler as Kt, DueDeadline as L, copyMetadata as Lt, IdempotentExecution as M, EventIdFactory as Mt, WithIdempotentCommitDeps as N, EventMetadata as Nt, IdempotencyReconciliationDecision as O, DomainEvent as Ot, withIdempotentCommit as P, PendingDomainEvent as Pt, ReplayableAggregate as Q, AggregateCommitToken as R, createDomainEvent as Rt, isPositionAfter as S, AnyDomainEvent as St, IdempotencyLease as T, CreateDomainEventStampOptions as Tt, withCommit as U, ClockFactory as Ut, WithCommitDeps as V, defaultDomainEventFactory as Vt, TransactionScope as W, AggregateIdentity as Wt, Aggregate as X, JsonValue as Xt, PublishOptions as Y, JsonPrimitive as Yt, AggregateSnapshot as Z, IdentityMap as _, ExecutionContext as _t, EventStoreAppendOptions as a, CommandMessageContent as at, ProjectionCheckpointStore as b, EventCommitCandidate as bt, StreamReadResult as c, CommandOutboxMapper as ct, RepositoryTracking as d, routeEventsToCommandOutbox as dt, sameVersion as et, UnitOfWorkIdentityMap as f, DeadLetterRecord as ft, AggregateClass as g, OutboxWriter as gt, PersistenceModel as h, OutboxRecord as ht, EventStore as i, CommandCommitOriginCandidate as it, IdempotentCommitResult as j, DomainEventStamp as jt, IdempotencyStore as k, DomainEventFactory as kt, AggregatePersistenceWrite as l, CommandOutboxWriter as lt, PersistenceLifecycle as m, Outbox as mt, ReplayableStreamPages as n, Id as nt, EventStreamReader as o, CommandMessageRelationships as ot, PersistenceChanges as p, DispatchTrackingOutbox as pt, EventHandler as q, PublishedCommand as qt, reconstituteAggregateFromStreamPages as r, IdGenerator as rt, ReadStreamOptions as s, CommandOutboxCommitCandidate as st, SnapshotStore as t, toVersion as tt, AggregateWriteIntent as u, DurableCommandMessage as ut, Projection as v, CommitPosition as vt, IdempotencyClaimHandle as w, CreateDomainEventOptions as wt, ProjectionPosition as x, EventCommitCandidatePosition as xt, ProjectionCheckpoint as y, CommittedDomainEvent as yt, CommitRegistration as z, createDomainEventFactory as zt };
2882
+ export { PublishOptions as $, JsonPrimitive as $t, IdempotentCommitRequest as A, CreateStampedDomainEventOptions as At, CommitRegistrationOptions as B, UnstampedDomainEventOf as Bt, IdempotencyClaim as C, CommittedDomainEvent as Ct, IdempotencyReconciliation as D, AnyUnstampedDomainEvent as Dt, IdempotencyOperationErrorContext as E, AnyDomainEvent as Et, DeadLetterDeadline as F, DomainEventStamp as Ft, DomainEventStampProvider as G, defaultDomainEventFactory as Gt, WithCommitWorkResult as H, createDomainEvent as Ht, DeadlineStore as I, EventIdFactory as It, TransactionScope as J, AggregateIdentity as Jt, SharedDomainEventStampOptions as K, mergeMetadata as Kt, DueDeadline as L, EventMetadata as Lt, IdempotentExecution as M, DomainEvent as Mt, WithIdempotentCommitDeps as N, DomainEventFactory as Nt, IdempotencyReconciliationDecision as O, CreateDomainEventOptions as Ot, withIdempotentCommit as P, DomainEventFactoryOptions as Pt, OnceOptions as Q, JsonObject as Qt, AggregateCommitToken as R, PendingDomainEvent as Rt, isPositionAfter as S, CommitPosition as St, IdempotencyLease as T, EventCommitCandidatePosition as Tt, withCommit as U, createDomainEventFactory as Ut, WithCommitDeps as V, copyMetadata as Vt, DomainEventStampFactory as W, createStampedDomainEvent as Wt, EventBus as X, CommandHandler as Xt, TransactionalOptions as Y, Command as Yt, EventHandler as Z, PublishedCommand as Zt, IdentityMap as _, DispatchTrackingOutbox as _t, EventStoreAppendOptions as a, toVersion as at, ProjectionCheckpointStore as b, OutboxWriter as bt, StreamReadResult as c, CommandCommitOriginCandidate as ct, RepositoryTracking as d, CommandOutboxCommitCandidate as dt, JsonValue as en, Aggregate as et, UnitOfWorkIdentityMap as f, CommandOutboxMapper as ft, AggregateClass as g, DeadLetterRecord as gt, PersistenceModel as h, routeEventsToCommandOutbox as ht, EventStore as i, sameVersion as it, IdempotentCommitResult as j, CreateUnstampedDomainEventOptions as jt, IdempotencyStore as k, CreateDomainEventStampOptions as kt, AggregatePersistenceWrite as l, CommandMessageContent as lt, PersistenceLifecycle as m, DurableCommandMessage as mt, ReplayableStreamPages as n, ReplayableAggregate as nt, EventStreamReader as o, Id as ot, PersistenceChanges as p, CommandOutboxWriter as pt, stampPendingEvents as q, ClockFactory as qt, reconstituteAggregateFromStreamPages as r, Version as rt, ReadStreamOptions as s, IdGenerator as st, SnapshotStore as t, AggregateSnapshot as tt, AggregateWriteIntent as u, CommandMessageRelationships as ut, Projection as v, Outbox as vt, IdempotencyClaimHandle as w, EventCommitCandidate as wt, ProjectionPosition as x, ExecutionContext as xt, ProjectionCheckpoint as y, OutboxRecord as yt, CommitRegistration as z, UnstampedDomainEvent as zt };
2829
2883
  //# sourceMappingURL=snapshot-store.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { $ as PendingEventLimitExceededErrorOptions, A as IdempotencyInFlightErrorOptions, B as InvalidEventStreamPageErrorOptions, C as FoldReturnedNoStateError, Ct as UnreplayableAggregateError, D as IdempotencyClaimLostErrorOptions, Dt as isWiringErrorLike, E as IdempotencyClaimLostError, Et as isInfrastructureErrorLike, F as InMemoryCapacityExceededError, G as KitWiringError, H as InvalidVersionError, I as InMemoryCapacityExceededErrorOptions, J as MissingFoldError, K as MisattributedEventError, L as InfrastructureError, M as IdempotencyKeyReuseErrorOptions, N as IdempotencyReconciliationRequiredError, O as IdempotencyCompletionWithoutClaimError, P as IdempotencyReconciliationRequiredErrorOptions, Q as PendingEventLimitExceededError, R as InvalidCommandMessageError, S as EventStreamPageReason, St as UnregisteredHandlerErrorOptions, T as HandBuiltEventError, Tt as isDomainErrorLike, U as KitErrorCode, V as InvalidIntegrationMessageError, W as KitErrorOptions, X as PendingEventBatchMismatchError, Y as MissingHandlerError, Z as PendingEventBatchMismatchErrorOptions, _ as DuplicateHandlerRegistrationError, _t as UnmanagedInstanceError, a as AggregateNotFoundErrorOptions, at as ReentrantEventRecordingError, b as ErrorMapperFailedErrorOptions, bt as UnregisteredChangesErrorOptions, c as ConcurrencyConflictErrorOptions, ct as ReplayRejectedErrorOptions, d as DirectStateMutationErrorOptions, dt as ReplayTargetMismatchReason, et as ProjectionGapError, f as DomainError, ft as SnapshotCorruptedError, g as DuplicateEventIdErrorOptions, gt as SnapshotVersionNotRestoredErrorOptions, h as DuplicateEventIdError, ht as SnapshotVersionNotRestoredError, i as AggregateNotFoundError, it as PrototypePollutionError, j as IdempotencyKeyReuseError, k as IdempotencyInFlightError, l as ConcurrencyConflictReason, lt as ReplayTargetMismatchError, m as DuplicateAggregateErrorOptions, mt as SnapshotSchemaMismatchErrorOptions, n as AggregateDeletedErrorOptions, nt as ProjectionOrderViolationError, o as CapabilityRegistryConflictError, ot as ReentrantEventRecordingErrorOptions, p as DuplicateAggregateError, pt as SnapshotSchemaMismatchError, q as MissingEntityIdError, r as AggregateIdentityMismatchOptions, rt as ProjectionReceiptViolationError, s as ConcurrencyConflictError, st as ReplayRejectedError, t as AggregateDeletedError, tt as ProjectionIdentityViolationError, u as DirectStateMutationError, ut as ReplayTargetMismatchErrorOptions, v as DuplicateHandlerRegistrationErrorOptions, vt as UnprojectableEventError, w as ForeignEventError, wt as UnreplayableAggregateErrorOptions, x as EventCollectionError, xt as UnregisteredHandlerError, y as ErrorMapperFailedError, yt as UnregisteredChangesError, z as InvalidEventStreamPageError } from "./chunks/kit-errors.js";
2
- import { $ as Version, A as IdempotentCommitRequest, At as DomainEventFactoryOptions, B as CommitRegistrationOptions, Bt as createStampedDomainEvent, C as IdempotencyClaim, Ct as AnyUnstampedDomainEvent, D as IdempotencyReconciliation, Dt as CreateUnstampedDomainEventOptions, E as IdempotencyOperationErrorContext, Et as CreateStampedDomainEventOptions, F as DeadLetterDeadline, Ft as UnstampedDomainEvent, G as TransactionalOptions, Gt as Command, H as WithCommitWorkResult, Ht as mergeMetadata, I as DeadlineStore, It as UnstampedDomainEventOf, J as OnceOptions, Jt as JsonObject, K as EventBus, Kt as CommandHandler, L as DueDeadline, Lt as copyMetadata, M as IdempotentExecution, Mt as EventIdFactory, N as WithIdempotentCommitDeps, Nt as EventMetadata, O as IdempotencyReconciliationDecision, Ot as DomainEvent, P as withIdempotentCommit, Pt as PendingDomainEvent, Q as ReplayableAggregate, R as AggregateCommitToken, Rt as createDomainEvent, S as isPositionAfter, St as AnyDomainEvent, T as IdempotencyLease, Tt as CreateDomainEventStampOptions, U as withCommit, Ut as ClockFactory, V as WithCommitDeps, Vt as defaultDomainEventFactory, W as TransactionScope, Wt as AggregateIdentity, X as Aggregate, Xt as JsonValue, Y as PublishOptions, Yt as JsonPrimitive, Z as AggregateSnapshot, _ as IdentityMap, _t as ExecutionContext, a as EventStoreAppendOptions, at as CommandMessageContent, b as ProjectionCheckpointStore, bt as EventCommitCandidate, c as StreamReadResult, ct as CommandOutboxMapper, d as RepositoryTracking, dt as routeEventsToCommandOutbox, et as sameVersion, f as UnitOfWorkIdentityMap, ft as DeadLetterRecord, g as AggregateClass, gt as OutboxWriter, h as PersistenceModel, ht as OutboxRecord, i as EventStore, it as CommandCommitOriginCandidate, j as IdempotentCommitResult, jt as DomainEventStamp, k as IdempotencyStore, kt as DomainEventFactory, l as AggregatePersistenceWrite, lt as CommandOutboxWriter, m as PersistenceLifecycle, mt as Outbox, n as ReplayableStreamPages, nt as Id, o as EventStreamReader, ot as CommandMessageRelationships, p as PersistenceChanges, pt as DispatchTrackingOutbox, q as EventHandler, qt as PublishedCommand, r as reconstituteAggregateFromStreamPages, rt as IdGenerator, s as ReadStreamOptions, st as CommandOutboxCommitCandidate, t as SnapshotStore, tt as toVersion, u as AggregateWriteIntent, ut as DurableCommandMessage, v as Projection, vt as CommitPosition, w as IdempotencyClaimHandle, wt as CreateDomainEventOptions, x as ProjectionPosition, xt as EventCommitCandidatePosition, y as ProjectionCheckpoint, yt as CommittedDomainEvent, z as CommitRegistration, zt as createDomainEventFactory } from "./chunks/snapshot-store.js";
2
+ import { $ as PublishOptions, $t as JsonPrimitive, A as IdempotentCommitRequest, At as CreateStampedDomainEventOptions, B as CommitRegistrationOptions, Bt as UnstampedDomainEventOf, C as IdempotencyClaim, Ct as CommittedDomainEvent, D as IdempotencyReconciliation, Dt as AnyUnstampedDomainEvent, E as IdempotencyOperationErrorContext, Et as AnyDomainEvent, F as DeadLetterDeadline, Ft as DomainEventStamp, G as DomainEventStampProvider, Gt as defaultDomainEventFactory, H as WithCommitWorkResult, Ht as createDomainEvent, I as DeadlineStore, It as EventIdFactory, J as TransactionScope, Jt as AggregateIdentity, K as SharedDomainEventStampOptions, Kt as mergeMetadata, L as DueDeadline, Lt as EventMetadata, M as IdempotentExecution, Mt as DomainEvent, N as WithIdempotentCommitDeps, Nt as DomainEventFactory, O as IdempotencyReconciliationDecision, Ot as CreateDomainEventOptions, P as withIdempotentCommit, Pt as DomainEventFactoryOptions, Q as OnceOptions, Qt as JsonObject, R as AggregateCommitToken, Rt as PendingDomainEvent, S as isPositionAfter, St as CommitPosition, T as IdempotencyLease, Tt as EventCommitCandidatePosition, U as withCommit, Ut as createDomainEventFactory, V as WithCommitDeps, Vt as copyMetadata, W as DomainEventStampFactory, Wt as createStampedDomainEvent, X as EventBus, Xt as CommandHandler, Y as TransactionalOptions, Yt as Command, Z as EventHandler, Zt as PublishedCommand, _ as IdentityMap, _t as DispatchTrackingOutbox, a as EventStoreAppendOptions, at as toVersion, b as ProjectionCheckpointStore, bt as OutboxWriter, c as StreamReadResult, ct as CommandCommitOriginCandidate, d as RepositoryTracking, dt as CommandOutboxCommitCandidate, en as JsonValue, et as Aggregate, f as UnitOfWorkIdentityMap, ft as CommandOutboxMapper, g as AggregateClass, gt as DeadLetterRecord, h as PersistenceModel, ht as routeEventsToCommandOutbox, i as EventStore, it as sameVersion, j as IdempotentCommitResult, jt as CreateUnstampedDomainEventOptions, k as IdempotencyStore, kt as CreateDomainEventStampOptions, l as AggregatePersistenceWrite, lt as CommandMessageContent, m as PersistenceLifecycle, mt as DurableCommandMessage, n as ReplayableStreamPages, nt as ReplayableAggregate, o as EventStreamReader, ot as Id, p as PersistenceChanges, pt as CommandOutboxWriter, q as stampPendingEvents, qt as ClockFactory, r as reconstituteAggregateFromStreamPages, rt as Version, s as ReadStreamOptions, st as IdGenerator, t as SnapshotStore, tt as AggregateSnapshot, u as AggregateWriteIntent, ut as CommandMessageRelationships, v as Projection, vt as Outbox, w as IdempotencyClaimHandle, wt as EventCommitCandidate, x as ProjectionPosition, xt as ExecutionContext, y as ProjectionCheckpoint, yt as OutboxRecord, z as CommitRegistration, zt as UnstampedDomainEvent } from "./chunks/snapshot-store.js";
3
3
  import { Result } from "@shirudo/result";
4
4
  import { ValidationError } from "@shirudo/base-error";
5
5
  //#region src/application/cqrs/internal/bus-internals.d.ts
@@ -1563,37 +1563,6 @@ declare class RollbackError extends InfrastructureError<"ROLLBACK_FAILED"> {
1563
1563
  constructor(cause: unknown, rollbackCause: unknown);
1564
1564
  }
1565
1565
  //#endregion
1566
- //#region src/application/unit-of-work/stamp-pending-events.d.ts
1567
- /** Minimal shell role accepted by {@link stampPendingEvents}. */
1568
- type DomainEventStampFactory = Pick<DomainEventFactory, "createStamp">;
1569
- /** Per-decision stamp provider for metadata that depends on the event. */
1570
- type DomainEventStampProvider<TEvent extends AnyDomainEvent> = (event: UnstampedDomainEventOf<TEvent>, index: number) => DomainEventStamp;
1571
- /**
1572
- * Stamp options that every decision of one stamping shares: a stamp time
1573
- * and metadata such as the correlation id of the request. An `eventId` is per
1574
- * fact, so the factory creates it for each decision.
1575
- */
1576
- type SharedDomainEventStampOptions = Omit<CreateDomainEventStampOptions, "eventId">;
1577
- /**
1578
- * Records every still-unstamped event accepted by an aggregate.
1579
- *
1580
- * The function is a command that also returns the recorded batch, a
1581
- * deliberate exception to command-query separation: the caller hands the
1582
- * batch on to persistence and needs no second read of `pendingEvents`.
1583
- *
1584
- * Recording is atomic with respect to the aggregate's pending list: if stamp
1585
- * creation or validation fails, every decision remains unstamped. A
1586
- * successful second call returns the same event objects and does not read the
1587
- * factory again, which keeps event identity stable across transaction retries.
1588
- *
1589
- * Pass a `DomainEventFactory` (only its `createStamp` role is required) for one
1590
- * uniform stamping policy. Add `stampOptions` for metadata that every
1591
- * decision of the batch shares, such as the correlation id of the request.
1592
- * Pass a callback instead when metadata depends on the concrete decision.
1593
- */
1594
- declare function stampPendingEvents<TId extends Id<string>, TEvent extends AnyDomainEvent>(aggregate: Aggregate<TId, TEvent>, factory: DomainEventStampFactory, stampOptions?: SharedDomainEventStampOptions): ReadonlyArray<TEvent>;
1595
- declare function stampPendingEvents<TId extends Id<string>, TEvent extends AnyDomainEvent>(aggregate: Aggregate<TId, TEvent>, createStamp: DomainEventStampProvider<TEvent>): ReadonlyArray<TEvent>;
1596
- //#endregion
1597
1566
  //#region src/application/unit-of-work/unit-of-work.d.ts
1598
1567
  /**
1599
1568
  * What the application work callback receives: repositories already bound to
@@ -1624,6 +1593,11 @@ interface RunOptions {
1624
1593
  * `TransactionScope`. Use `AbortSignal.timeout(ms)` for a deadline.
1625
1594
  */
1626
1595
  readonly signal?: AbortSignal;
1596
+ /**
1597
+ * Stamp options that every event stamped in this run shares, for example
1598
+ * the correlation id of the request.
1599
+ */
1600
+ readonly stampOptions?: SharedDomainEventStampOptions;
1627
1601
  }
1628
1602
  declare const repositoryDefinitionBrand: unique symbol;
1629
1603
  /** Adapter wiring accepted by {@link defineRepository}. */
@@ -1832,6 +1806,12 @@ interface UnitOfWorkDeps<Evt extends AnyDomainEvent, TCtx, TDefinitions extends
1832
1806
  * application phase. Default `30000`ms.
1833
1807
  */
1834
1808
  postCommitTimeoutMs?: number;
1809
+ /**
1810
+ * See `withCommit`: stamps every pending event that is still unstamped
1811
+ * when `add`, `update`, or `remove` registers its aggregate. Defaults to
1812
+ * `createDomainEventFactory()`.
1813
+ */
1814
+ eventStampFactory?: DomainEventStampFactory;
1835
1815
  repositories: CompatibleRepositoryDefinitions<Evt, TCtx, TDefinitions>;
1836
1816
  }
1837
1817
  /**
@@ -2388,8 +2368,8 @@ interface AggregateConfig<TState = unknown> extends EntityConfig<TState> {
2388
2368
  * tracking, the kit-internal post-commit acknowledgement capability,
2389
2369
  * the `markReconstituted` post-load marker, and the `createEvent` helper
2390
2370
  * that auto-injects `aggregateId` + `aggregateType` on every event the
2391
- * aggregate emits. The application shell stamps the pending decisions
2392
- * with `stampPendingEvents` before persistence.
2371
+ * aggregate emits. The commit stamps the pending decisions when it
2372
+ * registers the aggregate, before persistence.
2393
2373
  *
2394
2374
  * Consumers do NOT extend this class directly; extend
2395
2375
  * `StateStoredAggregate` for state-stored aggregates or
@@ -2599,8 +2579,8 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
2599
2579
  * Creates the immutable business fact accepted by this aggregate without
2600
2580
  * reading a clock, generating an id, or attaching tracing metadata.
2601
2581
  *
2602
- * The application shell stamps pending events after the domain operation
2603
- * and before persistence. Payload schema version stays here, next to the
2582
+ * The commit stamps pending events when it registers the aggregate,
2583
+ * before persistence. Payload schema version stays here, next to the
2604
2584
  * concrete event producer, rather than in shell-owned stamp data.
2605
2585
  */
2606
2586
  protected createEvent<E extends TEvent>(type: E["type"], payload: E["payload"], options?: Omit<CreateUnstampedDomainEventOptions, "aggregateId" | "aggregateType">): UnstampedDomainEventOf<E>;
package/dist/index.js CHANGED
@@ -404,6 +404,42 @@ function captureObserverFunctions(context, observers, required) {
404
404
  return Object.freeze(captured);
405
405
  }
406
406
 
407
+ //#endregion
408
+ //#region src/domain/aggregate/pending-event-recording.ts
409
+ const { registry: capabilities$1, require } = createGlobalCapabilityRegistry(Symbol.for("@shirudo/ddd-kit/pending-event-recording-registry/v2"));
410
+ /** Resolves the recording capability or throws `UnmanagedInstanceError`. */
411
+ function requirePendingEventRecordingCapability(aggregate, operation) {
412
+ return require(aggregate, operation, "aggregate");
413
+ }
414
+ function registerPendingEventRecordingCapability(aggregate, capability) {
415
+ capabilities$1.set(aggregate, Object.freeze(capability));
416
+ }
417
+
418
+ //#endregion
419
+ //#region src/application/unit-of-work/stamp-pending-events.ts
420
+ function stampPendingEvents(aggregate, source, stampOptions) {
421
+ return recordWith(requirePendingEventRecordingCapability(aggregate, "stampPendingEvents"), source, stampOptions);
422
+ }
423
+ /**
424
+ * Stamps the pending events that are still unstamped when the commit
425
+ * registers an aggregate. An aggregate whose pending events all carry a
426
+ * stamp stays untouched: an explicit {@link stampPendingEvents} call keeps
427
+ * its stamps, and an instance without the recording capability passes when
428
+ * it has nothing to stamp.
429
+ */
430
+ function stampAtRegistration(aggregate, factory, stampOptions, operation) {
431
+ if (aggregate.pendingEvents.every(isStampedDomainEvent)) return;
432
+ recordWith(requirePendingEventRecordingCapability(aggregate, operation), factory, stampOptions);
433
+ }
434
+ function recordWith(capability, source, stampOptions) {
435
+ const shared = {
436
+ occurredAt: stampOptions?.occurredAt,
437
+ metadata: stampOptions?.metadata
438
+ };
439
+ const createStamp = typeof source === "function" ? source : () => source.createStamp(shared);
440
+ return capability.record((event, index) => createStamp(event, index));
441
+ }
442
+
407
443
  //#endregion
408
444
  //#region src/application/cqrs/handler.ts
409
445
  /**
@@ -417,7 +453,7 @@ function registrationDiverged(record) {
417
453
  return record.aggregate.version !== record.version || pending.length !== record.events.length || record.events.some((event, index) => event !== pending[index]);
418
454
  }
419
455
  /** One token registry per transactional callback attempt. */
420
- function createCommitTokenScope() {
456
+ function createCommitTokenScope(stamp) {
421
457
  const recordsByToken = /* @__PURE__ */ new WeakMap();
422
458
  const tokensByAggregate = /* @__PURE__ */ new WeakMap();
423
459
  let issuedTokenCount = 0;
@@ -436,8 +472,8 @@ function createCommitTokenScope() {
436
472
  }
437
473
  const eventLifecycle = requirePendingEventLifecycleCapability(aggregate, "withCommit registration");
438
474
  const token = Object.freeze(Object.create(null));
475
+ stamp(aggregate, "withCommit registration");
439
476
  const events = aggregate.pendingEvents;
440
- for (const event of events) if (!isStampedDomainEvent(event)) throw new EventCollectionError(`withCommit: event "${event.type}" has not been stamped. Call stampPendingEvents(aggregate, createStamp) in the application shell before persistence or outbox collection.`, event.type);
441
477
  const persistedVersion = eventLifecycle.persistedVersion();
442
478
  tokensByAggregate.set(aggregate, token);
443
479
  recordsByToken.set(token, {
@@ -609,17 +645,21 @@ async function withCommit(deps, fn) {
609
645
  }
610
646
  /**
611
647
  * {@link withCommit} with a check per attempt before the commit. The scope
612
- * receives `onAttemptStart` in its transactional options.
648
+ * receives `onAttemptStart` in its transactional options. The callback also
649
+ * receives the stamping step of the registration, so that a caller that
650
+ * records the batch before it registers can stamp first.
613
651
  */
614
652
  async function withCheckedCommit(deps, fn, onAttemptStart) {
615
653
  const postCommitTimeoutMs = deps.postCommitTimeoutMs ?? 3e4;
616
654
  assertTimerDelay("withCommit", "postCommitTimeoutMs", postCommitTimeoutMs);
617
655
  if (deps.signal?.aborted) throw abortReason(deps.signal, "withCommit aborted before opening a transaction");
656
+ const eventStampFactory = deps.eventStampFactory ?? defaultDomainEventFactory;
657
+ const stamp = (aggregate, operation) => stampAtRegistration(aggregate, eventStampFactory, deps.stampOptions, operation);
618
658
  const { result, commitRecords, events } = await deps.scope.transactional(async (ctx) => {
619
- const tokenScope = createCommitTokenScope();
659
+ const tokenScope = createCommitTokenScope(stamp);
620
660
  let fnResult;
621
661
  try {
622
- fnResult = await fn(ctx, tokenScope.registration);
662
+ fnResult = await fn(ctx, tokenScope.registration, stamp);
623
663
  } finally {
624
664
  tokenScope.close();
625
665
  }
@@ -631,24 +671,22 @@ async function withCheckedCommit(deps, fn, onAttemptStart) {
631
671
  const registeredId = registered.aggregateId;
632
672
  const registeredType = registered.aggregateType;
633
673
  return record.events.map((event, index) => {
634
- if (!isStampedDomainEvent(event)) throw new EventCollectionError(`withCommit: event "${event.type}" has not been stamped. Call stampPendingEvents(aggregate, createStamp) in the application shell before persistence or outbox collection.`, event.type);
635
- const recordedEvent = event;
636
674
  const commitSize = record.events.length;
637
- const aggregateId = recordedEvent.aggregateId;
638
- const aggregateType = recordedEvent.aggregateType;
675
+ const aggregateId = event.aggregateId;
676
+ const aggregateType = event.aggregateType;
639
677
  const missing = [];
640
678
  if (!aggregateId) missing.push("aggregateId");
641
679
  if (!aggregateType) missing.push("aggregateType");
642
- if (!aggregateId || !aggregateType) throw new EventCollectionError(`withCommit: event "${recordedEvent.type}" is missing ${missing.join(" and ")}. Use this.createEvent(type, payload) inside aggregate methods instead of createDomainEvent(...); createEvent auto-injects aggregateId and aggregateType. Outbox dispatchers and projection handlers rely on the envelope source.`, recordedEvent.type);
643
- if (aggregateId !== registeredId || aggregateType !== registeredType) throw new EventCollectionError(`withCommit: event "${recordedEvent.type}" belongs to ${describeAggregateIdentity({
680
+ if (!aggregateId || !aggregateType) throw new EventCollectionError(`withCommit: event "${event.type}" is missing ${missing.join(" and ")}. Use this.createEvent(type, payload) inside aggregate methods instead of createDomainEvent(...); createEvent auto-injects aggregateId and aggregateType. Outbox dispatchers and projection handlers rely on the envelope source.`, event.type);
681
+ if (aggregateId !== registeredId || aggregateType !== registeredType) throw new EventCollectionError(`withCommit: event "${event.type}" belongs to ${describeAggregateIdentity({
644
682
  aggregateType,
645
683
  aggregateId
646
684
  })} but was registered under ${describeAggregateIdentity({
647
685
  aggregateType: registeredType,
648
686
  aggregateId: registeredId
649
- })}. The aggregate base classes attribute every event to the aggregate on every recording path; an instance from another package copy must stamp it the same way.`, recordedEvent.type);
687
+ })}. The aggregate base classes attribute every event to the aggregate on every recording path; an instance from another package copy must stamp it the same way.`, event.type);
650
688
  return Object.freeze({
651
- event: recordedEvent,
689
+ event,
652
690
  source: Object.freeze({
653
691
  aggregateId,
654
692
  aggregateType
@@ -2599,29 +2637,6 @@ function wiringErrorInCauseChain(error) {
2599
2637
  return findInCauseChain(error, (link) => isWiringErrorLike(link) ? link : void 0);
2600
2638
  }
2601
2639
 
2602
- //#endregion
2603
- //#region src/domain/aggregate/pending-event-recording.ts
2604
- const { registry: capabilities$1, require } = createGlobalCapabilityRegistry(Symbol.for("@shirudo/ddd-kit/pending-event-recording-registry/v2"));
2605
- /** Resolves the recording capability or throws `UnmanagedInstanceError`. */
2606
- function requirePendingEventRecordingCapability(aggregate, operation) {
2607
- return require(aggregate, operation, "aggregate");
2608
- }
2609
- function registerPendingEventRecordingCapability(aggregate, capability) {
2610
- capabilities$1.set(aggregate, Object.freeze(capability));
2611
- }
2612
-
2613
- //#endregion
2614
- //#region src/application/unit-of-work/stamp-pending-events.ts
2615
- function stampPendingEvents(aggregate, source, stampOptions) {
2616
- const capability = requirePendingEventRecordingCapability(aggregate, "stampPendingEvents");
2617
- const shared = {
2618
- occurredAt: stampOptions?.occurredAt,
2619
- metadata: stampOptions?.metadata
2620
- };
2621
- const createStamp = typeof source === "function" ? source : () => source.createStamp(shared);
2622
- return capability.record((event, index) => createStamp(event, index));
2623
- }
2624
-
2625
2640
  //#endregion
2626
2641
  //#region src/application/unit-of-work/repository-facade.ts
2627
2642
  /**
@@ -3078,6 +3093,7 @@ function capabilityFor(baseline, operation) {
3078
3093
  */
3079
3094
  var Session = class {
3080
3095
  commitRegistration;
3096
+ stamp;
3081
3097
  _registeredWrites = [];
3082
3098
  _commitTokens = /* @__PURE__ */ new Set();
3083
3099
  _identityMap = new IdentityMap();
@@ -3100,8 +3116,9 @@ var Session = class {
3100
3116
  */
3101
3117
  _lateRegistrationsFailAttempt = false;
3102
3118
  _registrationDuringFlush;
3103
- constructor(commitRegistration) {
3119
+ constructor(commitRegistration, stamp) {
3104
3120
  this.commitRegistration = commitRegistration;
3121
+ this.stamp = stamp;
3105
3122
  }
3106
3123
  get identityMap() {
3107
3124
  this.assertOpen("tracking.identityMap");
@@ -3224,6 +3241,7 @@ var Session = class {
3224
3241
  }
3225
3242
  /** Registers persistence intent and commit registration as one operation. */
3226
3243
  registerWrite(entry, intent, definition) {
3244
+ if (entry.registration === void 0) this.stamp(entry.aggregate, `repository.${intent}`);
3227
3245
  const newlyRegistered = this.registerIntent(entry, intent);
3228
3246
  try {
3229
3247
  if (intent === "remove") this.registerRemovedCommit(entry.aggregate, definition, entry.expectedVersion);
@@ -3622,10 +3640,12 @@ var UnitOfWork = class {
3622
3640
  onPersisted: this.deps.onPersisted,
3623
3641
  onPersistError: this.deps.onPersistError,
3624
3642
  postCommitTimeoutMs: this.deps.postCommitTimeoutMs,
3625
- signal: options?.signal
3626
- }, async (tx, registration) => {
3643
+ signal: options?.signal,
3644
+ eventStampFactory: this.deps.eventStampFactory,
3645
+ stampOptions: options?.stampOptions
3646
+ }, async (tx, registration, stamp) => {
3627
3647
  session?.close();
3628
- const s = new Session(registration);
3648
+ const s = new Session(registration, stamp);
3629
3649
  session = s;
3630
3650
  sessions.push(s);
3631
3651
  const current = startAttempt();
@@ -4449,8 +4469,8 @@ function entityIds(entities) {
4449
4469
  * tracking, the kit-internal post-commit acknowledgement capability,
4450
4470
  * the `markReconstituted` post-load marker, and the `createEvent` helper
4451
4471
  * that auto-injects `aggregateId` + `aggregateType` on every event the
4452
- * aggregate emits. The application shell stamps the pending decisions
4453
- * with `stampPendingEvents` before persistence.
4472
+ * aggregate emits. The commit stamps the pending decisions when it
4473
+ * registers the aggregate, before persistence.
4454
4474
  *
4455
4475
  * Consumers do NOT extend this class directly; extend
4456
4476
  * `StateStoredAggregate` for state-stored aggregates or
@@ -4768,8 +4788,8 @@ var BaseAggregate = class extends Entity {
4768
4788
  * Creates the immutable business fact accepted by this aggregate without
4769
4789
  * reading a clock, generating an id, or attaching tracing metadata.
4770
4790
  *
4771
- * The application shell stamps pending events after the domain operation
4772
- * and before persistence. Payload schema version stays here, next to the
4791
+ * The commit stamps pending events when it registers the aggregate,
4792
+ * before persistence. Payload schema version stays here, next to the
4773
4793
  * concrete event producer, rather than in shell-owned stamp data.
4774
4794
  */
4775
4795
  createEvent(type, payload, options) {
@@ -6765,7 +6785,10 @@ var OutboxDispatcher = class extends PollLoop {
6765
6785
  function copyForStorage(event, index, stream) {
6766
6786
  const subject = `InMemoryEventStore.append (stream ${describeAggregateIdentity(stream)}): events[${index}]`;
6767
6787
  if (event === null || typeof event !== "object") throw new TypeError(`${subject} is not an event object`);
6768
- return copyFreezableState(event, subject);
6788
+ const copy = copyFreezableState(event, subject);
6789
+ const eventId = copy.eventId;
6790
+ if (typeof eventId !== "string" || eventId === "") throw new TypeError(`${subject} has no eventId. Stamp the event before the append: the commit stamps it when a write registers its aggregate, and stampPendingEvents stamps it before that.`);
6791
+ return copy;
6769
6792
  }
6770
6793
  function assertStreamPosition(name, value) {
6771
6794
  if (value !== void 0 && (!Number.isSafeInteger(value) || value < 0)) throw new RangeError(`InMemoryEventStore: ${name} must be a non-negative safe integer, got ${String(value)}`);