@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.
- package/README.md +1 -1
- package/dist/chunks/kit-errors.d.ts +7 -7
- package/dist/chunks/kit-errors.js +8 -8
- package/dist/chunks/kit-errors.js.map +1 -1
- package/dist/chunks/read-stream-pages.js +52 -4
- package/dist/chunks/read-stream-pages.js.map +1 -1
- package/dist/chunks/snapshot-store.d.ts +73 -19
- package/dist/index.d.ts +16 -36
- package/dist/index.js +68 -45
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +38 -12
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
197
|
-
* (works on Node 19+, modern browsers in secure
|
|
198
|
-
* Cloudflare Workers, Vercel Edge, and any runtime that
|
|
199
|
-
* Crypto).
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* KSUID, or another collision-safe format without mutating module
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
2392
|
-
*
|
|
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
|
|
2603
|
-
*
|
|
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 =
|
|
638
|
-
const 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 "${
|
|
643
|
-
if (aggregateId !== registeredId || aggregateType !== registeredType) throw new EventCollectionError(`withCommit: event "${
|
|
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.`,
|
|
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
|
|
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
|
-
|
|
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
|
|
4453
|
-
*
|
|
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
|
|
4772
|
-
*
|
|
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
|
-
|
|
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)}`);
|