@shirudo/ddd-kit 3.0.0-rc.12 → 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 +13 -8
- package/dist/chunks/kit-errors.d.ts +30 -31
- package/dist/chunks/kit-errors.js +56 -57
- package/dist/chunks/kit-errors.js.map +1 -1
- package/dist/chunks/read-stream-pages.js +142 -1368
- package/dist/chunks/read-stream-pages.js.map +1 -1
- package/dist/chunks/snapshot-store.d.ts +150 -146
- package/dist/chunks/value-object.js +1275 -0
- package/dist/chunks/value-object.js.map +1 -0
- package/dist/index.d.ts +133 -413
- package/dist/index.js +252 -897
- package/dist/index.js.map +1 -1
- package/dist/money.d.ts +3 -3
- package/dist/money.js +3 -3
- package/dist/money.js.map +1 -1
- package/dist/state-machine.d.ts +227 -0
- package/dist/state-machine.js +673 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/testing.d.ts +4 -4
- package/dist/testing.js +47 -20
- package/dist/testing.js.map +1 -1
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { $ as isDomainErrorLike, A as MissingHandlerError, B as
|
|
2
|
-
import {
|
|
1
|
+
import { $ as isDomainErrorLike, A as MissingHandlerError, B as ReplayTargetMismatchError, C as InvalidEventStreamPageError, D as MisattributedEventError, E as KitWiringError, F as ProjectionOrderViolationError, G as UnprojectableEventError, H as SnapshotSchemaMismatchError, I as ProjectionReceiptViolationError, J as UnreplayableAggregateError, K as UnregisteredChangesError, L as PrototypePollutionError, M as PendingEventLimitExceededError, N as ProjectionGapError, O as MissingEntityIdError, P as ProjectionIdentityViolationError, Q as describeAggregateIdentity, R as ReentrantEventRecordingError, S as InvalidCommandMessageError, T as InvalidVersionError, U as SnapshotVersionNotRestoredError, V as SnapshotCorruptedError, W as UnmanagedInstanceError, X as attributeConflictIntent, Y as assertNoOwnProtoKey, Z as copyAggregateIdentity, _ as IdempotencyInFlightError, a as DirectStateMutationError, b as InMemoryCapacityExceededError, c as DuplicateEventIdError, d as EventCollectionError, et as isInfrastructureErrorLike, f as FoldReturnedNoStateError, g as IdempotencyCompletionWithoutClaimError, h as IdempotencyClaimLostError, i as ConcurrencyConflictError, j as PendingEventBatchMismatchError, k as MissingFoldError, l as DuplicateHandlerRegistrationError, m as HandBuiltEventError, n as AggregateNotFoundError, nt as rewriteErrorMessage, o as DomainError, p as ForeignEventError, q as UnregisteredHandlerError, r as CapabilityRegistryConflictError, s as DuplicateAggregateError, t as AggregateDeletedError, tt as isWiringErrorLike, u as ErrorMapperFailedError, v as IdempotencyKeyReuseError, w as InvalidIntegrationMessageError, x as InfrastructureError, y as IdempotencyReconciliationRequiredError, z as ReplayRejectedError } from "./chunks/kit-errors.js";
|
|
2
|
+
import { a as valueObjectEqualsExcept, c as applyCooperativeBrand, d as deepEqual, i as valueObjectEquals, l as hasCooperativeBrand, n as deepFreeze, o as valueObjectOrError, r as valueObject, t as ValueObject, u as deepOmit, v as isWeakMap } from "./chunks/value-object.js";
|
|
3
|
+
import { A as copyFreezableState, C as defaultDomainEventFactory, D as stampDomainEvent, E as mergeMetadata, O as DomainEventValidationError, S as createStampedDomainEvent, T as isUnstampedDomainEvent, _ as adoptUnstampedDomainEvent, a as isDispatchTrackingOutbox, b as createDomainEvent, c as runBoundedExecution, d as assertPositiveInteger, f as assertPositiveSafeInteger, g as adoptStampedDomainEvent, h as waitRejectingOnAbort, i as assertPageWithinWindow, j as copyPlainState, k as SnapshotTimeValidationError, l as MAX_TIMER_DELAY_MS, m as abortReason, n as readStreamPages, o as DEFAULT_EXECUTION_TIMEOUT_MS, p as assertTimerDelay, r as assertPageNotEmpty, s as ownerSignalOf, t as pinTargetVersion, u as assertNonNegativeSafeInteger, v as buildUnstampedDomainEvent, w as isStampedDomainEvent, x as createDomainEventFactory, y as copyMetadata } from "./chunks/read-stream-pages.js";
|
|
3
4
|
import { err, ok } from "@shirudo/result";
|
|
4
5
|
import { ValidationError, someChainRetryable } from "@shirudo/base-error";
|
|
5
6
|
|
|
@@ -403,40 +404,76 @@ function captureObserverFunctions(context, observers, required) {
|
|
|
403
404
|
return Object.freeze(captured);
|
|
404
405
|
}
|
|
405
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
|
+
|
|
406
443
|
//#endregion
|
|
407
444
|
//#region src/application/cqrs/handler.ts
|
|
408
445
|
/**
|
|
409
446
|
* True when the aggregate's live version or pending-event batch no longer
|
|
410
|
-
* matches its
|
|
411
|
-
* gate and the
|
|
412
|
-
* or events recorded after
|
|
447
|
+
* matches its registration-time snapshot. Shared by the duplicate-registration
|
|
448
|
+
* gate and the collection-time recheck: both must reject the same divergence,
|
|
449
|
+
* or events recorded after registration would be silently dropped.
|
|
413
450
|
*/
|
|
414
|
-
function
|
|
451
|
+
function registrationDiverged(record) {
|
|
415
452
|
const pending = record.aggregate.pendingEvents;
|
|
416
453
|
return record.aggregate.version !== record.version || pending.length !== record.events.length || record.events.some((event, index) => event !== pending[index]);
|
|
417
454
|
}
|
|
418
455
|
/** One token registry per transactional callback attempt. */
|
|
419
|
-
function createCommitTokenScope() {
|
|
456
|
+
function createCommitTokenScope(stamp) {
|
|
420
457
|
const recordsByToken = /* @__PURE__ */ new WeakMap();
|
|
421
458
|
const tokensByAggregate = /* @__PURE__ */ new WeakMap();
|
|
422
|
-
let
|
|
459
|
+
let issuedTokenCount = 0;
|
|
423
460
|
let open = true;
|
|
424
|
-
const
|
|
425
|
-
if (!open) throw new
|
|
461
|
+
const register = (aggregate, disposition, options) => {
|
|
462
|
+
if (!open) throw new EventCollectionError("withCommit: commit registration was used after its work callback settled. Await every repository write and return its token before leaving the callback.");
|
|
426
463
|
const existing = tokensByAggregate.get(aggregate);
|
|
427
464
|
if (existing) {
|
|
428
465
|
const record = recordsByToken.get(existing);
|
|
429
|
-
if (!record) throw new
|
|
430
|
-
if (record.disposition === "deleted" && disposition === "saved") throw new
|
|
431
|
-
if (options?.expectedVersion !== void 0 && options.expectedVersion !== record.expectedVersion) throw new
|
|
432
|
-
if (
|
|
466
|
+
if (!record) throw new EventCollectionError("withCommit: internal commit-token registry is inconsistent.");
|
|
467
|
+
if (record.disposition === "deleted" && disposition === "saved") throw new EventCollectionError(`withCommit: aggregate ${describeAggregateIdentity(aggregate.aggregateIdentity)} was registered as saved after it was registered as deleted in the same transaction.`);
|
|
468
|
+
if (options?.expectedVersion !== void 0 && options.expectedVersion !== record.expectedVersion) throw new EventCollectionError(`withCommit: aggregate ${describeAggregateIdentity(aggregate.aggregateIdentity)} was re-registered with expectedVersion ${String(options.expectedVersion)}, but its registration recorded ${String(record.expectedVersion)}. Duplicate registration must assert the same OCC baseline or none.`);
|
|
469
|
+
if (registrationDiverged(record)) throw new EventCollectionError(`withCommit: aggregate ${describeAggregateIdentity(aggregate.aggregateIdentity)} changed after its commit batch was registered. Register persistence intent last.`);
|
|
433
470
|
if (disposition === "deleted") record.disposition = "deleted";
|
|
434
471
|
return existing;
|
|
435
472
|
}
|
|
436
|
-
const eventLifecycle = requirePendingEventLifecycleCapability(aggregate, "withCommit
|
|
473
|
+
const eventLifecycle = requirePendingEventLifecycleCapability(aggregate, "withCommit registration");
|
|
437
474
|
const token = Object.freeze(Object.create(null));
|
|
475
|
+
stamp(aggregate, "withCommit registration");
|
|
438
476
|
const events = aggregate.pendingEvents;
|
|
439
|
-
for (const event of events) if (!isRecordedDomainEvent(event)) throw new EventHarvestError(`withCommit: event "${event.type}" has not been recorded. Call recordPendingEvents(aggregate, createStamp) in the application shell before persistence or outbox harvest.`, event.type);
|
|
440
477
|
const persistedVersion = eventLifecycle.persistedVersion();
|
|
441
478
|
tokensByAggregate.set(aggregate, token);
|
|
442
479
|
recordsByToken.set(token, {
|
|
@@ -448,32 +485,32 @@ function createCommitTokenScope() {
|
|
|
448
485
|
persistedVersion,
|
|
449
486
|
events
|
|
450
487
|
});
|
|
451
|
-
|
|
488
|
+
issuedTokenCount += 1;
|
|
452
489
|
return token;
|
|
453
490
|
};
|
|
454
491
|
return {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
492
|
+
registration: Object.freeze({
|
|
493
|
+
registerSaved: (aggregate, options) => register(aggregate, "saved", options),
|
|
494
|
+
registerRemoved: (aggregate, options) => register(aggregate, "deleted", options)
|
|
458
495
|
}),
|
|
459
496
|
close: () => {
|
|
460
497
|
open = false;
|
|
461
498
|
},
|
|
462
499
|
resolve: (tokens) => {
|
|
463
|
-
if (!Array.isArray(tokens)) throw new
|
|
500
|
+
if (!Array.isArray(tokens)) throw new EventCollectionError("withCommit: the work callback must return `commits` containing tokens from the current registration capability. Naked aggregate arrays are not commit evidence.");
|
|
464
501
|
const seen = /* @__PURE__ */ new Set();
|
|
465
502
|
const records = [];
|
|
466
503
|
for (const token of tokens) {
|
|
467
|
-
if (token === null || typeof token !== "object" && typeof token !== "function") throw new
|
|
504
|
+
if (token === null || typeof token !== "object" && typeof token !== "function") throw new EventCollectionError("withCommit: a commit token was not issued by this callback's registration capability. Forged and stale tokens are rejected.");
|
|
468
505
|
const tokenObject = token;
|
|
469
506
|
const record = recordsByToken.get(tokenObject);
|
|
470
|
-
if (!record) throw new
|
|
507
|
+
if (!record) throw new EventCollectionError("withCommit: a commit token was not issued by this callback's registration capability. Forged and stale tokens are rejected.");
|
|
471
508
|
if (seen.has(tokenObject)) continue;
|
|
472
509
|
seen.add(tokenObject);
|
|
473
|
-
if (
|
|
510
|
+
if (registrationDiverged(record)) throw new EventCollectionError(`withCommit: aggregate ${describeAggregateIdentity(record.aggregate.aggregateIdentity)} changed after its commit batch was registered; events recorded after registration are not part of the attested write and would be silently dropped. Make domain decisions first, write, and register last.`);
|
|
474
511
|
records.push(record);
|
|
475
512
|
}
|
|
476
|
-
if (seen.size !==
|
|
513
|
+
if (seen.size !== issuedTokenCount) throw new EventCollectionError("withCommit: every token issued by the current registration capability must be returned in `commits`. If a registered write must not commit, throw so the transaction rolls back.");
|
|
477
514
|
return records;
|
|
478
515
|
}
|
|
479
516
|
};
|
|
@@ -481,29 +518,29 @@ function createCommitTokenScope() {
|
|
|
481
518
|
/**
|
|
482
519
|
* Helper for executing a write Use Case inside a transaction scope.
|
|
483
520
|
*
|
|
484
|
-
* The use-case callback receives an invocation-scoped
|
|
521
|
+
* The use-case callback receives an invocation-scoped registration capability
|
|
485
522
|
* and returns opaque commit tokens for the repository writes that completed
|
|
486
|
-
* in the transaction. `withCommit` owns the post-commit lifecycle (
|
|
523
|
+
* in the transaction. `withCommit` owns the post-commit lifecycle (collection,
|
|
487
524
|
* outbox, mark-persisted, publish). A naked aggregate is not commit evidence:
|
|
488
525
|
* merely touching or constructing one must never make it look persisted.
|
|
489
526
|
*
|
|
490
|
-
* **Trust boundary.** A token proves invocation-local
|
|
527
|
+
* **Trust boundary.** A token proves invocation-local registration, not that the
|
|
491
528
|
* kit inspected a database write; a generic transaction helper cannot observe
|
|
492
|
-
* adapter internals. Repository code must
|
|
529
|
+
* adapter internals. Repository code must register only writes participating in
|
|
493
530
|
* this transaction. `UnitOfWork` centralizes that rule in repository methods.
|
|
494
531
|
* The opaque, scoped token prevents accidental aggregate smuggling and stale
|
|
495
532
|
* reuse; it is not a security boundary against code that deliberately lies to
|
|
496
533
|
* its own persistence capability.
|
|
497
534
|
*
|
|
498
535
|
* Order of operations:
|
|
499
|
-
* 1. `fn(ctx,
|
|
536
|
+
* 1. `fn(ctx, registration)` runs inside `scope.transactional(...)`; domain
|
|
500
537
|
* mutations + repo writes happen here. After a repository write has
|
|
501
|
-
*
|
|
538
|
+
* registered an aggregate, the callback includes that opaque token in its
|
|
502
539
|
* `commits` result. Tokens are invocation-bound: forged or stale tokens
|
|
503
|
-
* fail before
|
|
540
|
+
* fail before collection. `ctx` is whatever transaction handle the `scope`
|
|
504
541
|
* exposes (Drizzle `tx`, Prisma `tx`, Mongo session, or `undefined` for
|
|
505
542
|
* context-free scopes).
|
|
506
|
-
* 2. **Still inside the transaction**, `withCommit`
|
|
543
|
+
* 2. **Still inside the transaction**, `withCommit` collects every
|
|
507
544
|
* aggregate's `pendingEvents` and writes them via `outbox.add` (so
|
|
508
545
|
* events persist atomically with the state change). Skipped when no
|
|
509
546
|
* events were recorded. Each bare domain event is composed into an
|
|
@@ -513,7 +550,7 @@ function createCommitTokenScope() {
|
|
|
513
550
|
* resulting `CommittedDomainEvent`. The domain event itself is never
|
|
514
551
|
* stamped or copied.
|
|
515
552
|
*
|
|
516
|
-
* **
|
|
553
|
+
* **Collection order.** Events are concatenated in the order
|
|
517
554
|
* tokens appear in the returned `commits` array, then in
|
|
518
555
|
* each aggregate's `pendingEvents` order (insertion order via
|
|
519
556
|
* `apply` / `commit` / `addDomainEvent`). So tokens for `[a, b]`
|
|
@@ -537,7 +574,7 @@ function createCommitTokenScope() {
|
|
|
537
574
|
* across aggregates at delivery time.
|
|
538
575
|
* 3. The transaction commits.
|
|
539
576
|
* 4. **After** the commit, a non-exported capability acknowledges every
|
|
540
|
-
* saved
|
|
577
|
+
* saved registration and discards pending events for deleted registrations.
|
|
541
578
|
* Only after the complete commit set is clean does the optional
|
|
542
579
|
* application-shell `onPersisted(aggregate, version, context)` observer run for
|
|
543
580
|
* saved aggregates. Deleted rows never trigger that observer.
|
|
@@ -567,16 +604,16 @@ function createCommitTokenScope() {
|
|
|
567
604
|
* If the transaction rolls back, no acknowledgement occurs: the aggregate
|
|
568
605
|
* keeps its pending events, so the caller can retry or discard the instance.
|
|
569
606
|
*
|
|
570
|
-
*
|
|
607
|
+
* Registration captures an exact version and event batch. Re-registering the same
|
|
571
608
|
* aggregate after it changes rejects. `UnitOfWork` additionally seals the
|
|
572
|
-
* adapter persistence
|
|
573
|
-
* direct `withCommit` use, make domain decisions first, write, and
|
|
609
|
+
* adapter persistence capture and rejects later mutation before flush. For
|
|
610
|
+
* direct `withCommit` use, make domain decisions first, write, and register last.
|
|
574
611
|
*
|
|
575
|
-
* **Duplicate
|
|
612
|
+
* **Duplicate registration is idempotent by reference.** Registering the same
|
|
576
613
|
* instance repeatedly returns the same token, and a repeated token in
|
|
577
|
-
* `commits` is
|
|
614
|
+
* `commits` is collected once. A repeat call that omits `expectedVersion`
|
|
578
615
|
* makes no OCC assertion; only a supplied value that contradicts the
|
|
579
|
-
*
|
|
616
|
+
* registration-time baseline rejects. Each event lands in the outbox exactly once
|
|
580
617
|
* and post-commit acknowledgement runs exactly once. Two
|
|
581
618
|
* *different* instances with the same logical id cannot be detected
|
|
582
619
|
* at this layer; that is a Repository contract violation (failure to
|
|
@@ -587,19 +624,19 @@ function createCommitTokenScope() {
|
|
|
587
624
|
*
|
|
588
625
|
* @example Tx-bound repos (Drizzle, Prisma, Mongo, …)
|
|
589
626
|
* ```typescript
|
|
590
|
-
* const result = await withCommit({ outbox, bus, scope }, async (tx,
|
|
627
|
+
* const result = await withCommit({ outbox, bus, scope }, async (tx, registration) => {
|
|
591
628
|
* const orderRepository = makeOrderRepository(tx); // your factory binds tx to the repo
|
|
592
629
|
* const order = await orderRepository.getById(orderId);
|
|
593
630
|
* order.confirm();
|
|
594
631
|
* await persistOrder(tx, order); // low-level adapter write
|
|
595
|
-
* const commit =
|
|
632
|
+
* const commit = registration.registerSaved(order); // attest the repository write
|
|
596
633
|
* return { result: order.id, commits: [commit] };
|
|
597
634
|
* });
|
|
598
635
|
* ```
|
|
599
636
|
*/
|
|
600
637
|
async function withCommit(deps, fn) {
|
|
601
|
-
return withCheckedCommit(deps, async (ctx,
|
|
602
|
-
const { result, commits } = await fn(ctx,
|
|
638
|
+
return withCheckedCommit(deps, async (ctx, registration) => {
|
|
639
|
+
const { result, commits } = await fn(ctx, registration);
|
|
603
640
|
return {
|
|
604
641
|
result,
|
|
605
642
|
commits
|
|
@@ -608,46 +645,48 @@ async function withCommit(deps, fn) {
|
|
|
608
645
|
}
|
|
609
646
|
/**
|
|
610
647
|
* {@link withCommit} with a check per attempt before the commit. The scope
|
|
611
|
-
* 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.
|
|
612
651
|
*/
|
|
613
652
|
async function withCheckedCommit(deps, fn, onAttemptStart) {
|
|
614
653
|
const postCommitTimeoutMs = deps.postCommitTimeoutMs ?? 3e4;
|
|
615
654
|
assertTimerDelay("withCommit", "postCommitTimeoutMs", postCommitTimeoutMs);
|
|
616
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);
|
|
617
658
|
const { result, commitRecords, events } = await deps.scope.transactional(async (ctx) => {
|
|
618
|
-
const tokenScope = createCommitTokenScope();
|
|
659
|
+
const tokenScope = createCommitTokenScope(stamp);
|
|
619
660
|
let fnResult;
|
|
620
661
|
try {
|
|
621
|
-
fnResult = await fn(ctx, tokenScope.
|
|
662
|
+
fnResult = await fn(ctx, tokenScope.registration, stamp);
|
|
622
663
|
} finally {
|
|
623
664
|
tokenScope.close();
|
|
624
665
|
}
|
|
625
666
|
try {
|
|
626
667
|
const commitRecords = tokenScope.resolve(fnResult.commits);
|
|
627
668
|
const candidates = commitRecords.flatMap((record) => {
|
|
628
|
-
const
|
|
629
|
-
if (record.events.length > 0 && record.persistedVersion !== void 0 && record.version <= record.persistedVersion) throw new
|
|
630
|
-
const
|
|
631
|
-
const
|
|
669
|
+
const registered = record.aggregate.aggregateIdentity;
|
|
670
|
+
if (record.events.length > 0 && record.persistedVersion !== void 0 && record.version <= record.persistedVersion) throw new EventCollectionError(`withCommit: aggregate ${describeAggregateIdentity(registered)} recorded events but did not advance its version beyond the persisted version (${String(record.persistedVersion)}). An eventful commit needs a unique cursor; use StateStoredAggregate.setState(currentState, event) instead of addDomainEvent(event) alone.`);
|
|
671
|
+
const registeredId = registered.aggregateId;
|
|
672
|
+
const registeredType = registered.aggregateType;
|
|
632
673
|
return record.events.map((event, index) => {
|
|
633
|
-
if (!isRecordedDomainEvent(event)) throw new EventHarvestError(`withCommit: event "${event.type}" has not been recorded. Call recordPendingEvents(aggregate, createStamp) in the application shell before persistence or outbox harvest.`, event.type);
|
|
634
|
-
const recordedEvent = event;
|
|
635
674
|
const commitSize = record.events.length;
|
|
636
|
-
const aggregateId =
|
|
637
|
-
const aggregateType =
|
|
675
|
+
const aggregateId = event.aggregateId;
|
|
676
|
+
const aggregateType = event.aggregateType;
|
|
638
677
|
const missing = [];
|
|
639
678
|
if (!aggregateId) missing.push("aggregateId");
|
|
640
679
|
if (!aggregateType) missing.push("aggregateType");
|
|
641
|
-
if (!aggregateId || !aggregateType) throw new
|
|
642
|
-
if (aggregateId !==
|
|
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({
|
|
643
682
|
aggregateType,
|
|
644
683
|
aggregateId
|
|
645
|
-
})} but was
|
|
646
|
-
aggregateType:
|
|
647
|
-
aggregateId:
|
|
648
|
-
})}. The aggregate base classes
|
|
684
|
+
})} but was registered under ${describeAggregateIdentity({
|
|
685
|
+
aggregateType: registeredType,
|
|
686
|
+
aggregateId: registeredId
|
|
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);
|
|
649
688
|
return Object.freeze({
|
|
650
|
-
event
|
|
689
|
+
event,
|
|
651
690
|
source: Object.freeze({
|
|
652
691
|
aggregateId,
|
|
653
692
|
aggregateType
|
|
@@ -660,12 +699,12 @@ async function withCheckedCommit(deps, fn, onAttemptStart) {
|
|
|
660
699
|
});
|
|
661
700
|
});
|
|
662
701
|
});
|
|
663
|
-
const endedSources = commitRecords.filter((record) => record.disposition === "deleted").map((record) =>
|
|
702
|
+
const endedSources = commitRecords.filter((record) => record.disposition === "deleted").map((record) => copyAggregateIdentity(record.aggregate.aggregateIdentity));
|
|
664
703
|
if (candidates.length > 0) await deps.outbox.add(Object.freeze(candidates));
|
|
665
704
|
if (endedSources.length > 0) await deps.outbox.endEventSources(Object.freeze(endedSources));
|
|
666
705
|
fnResult.checkBeforeCommit?.();
|
|
667
706
|
if (candidates.length > 0 || endedSources.length > 0) {
|
|
668
|
-
for (const record of commitRecords) if (
|
|
707
|
+
for (const record of commitRecords) if (registrationDiverged(record)) throw new EventCollectionError(`withCommit: aggregate ${describeAggregateIdentity(record.aggregate.aggregateIdentity)} changed while its outbox write ran. Await every change inside the work callback.`);
|
|
669
708
|
}
|
|
670
709
|
return {
|
|
671
710
|
result: fnResult.result,
|
|
@@ -1406,7 +1445,7 @@ function positiveSafeInteger(value) {
|
|
|
1406
1445
|
*
|
|
1407
1446
|
* On its own, the store is the reference for the leased family: it cannot
|
|
1408
1447
|
* see commits or rollbacks. Claims and staged outcomes carry bounded leases,
|
|
1409
|
-
* while every mutation compares the store-
|
|
1448
|
+
* while every mutation compares the store-issued token. An expired pending
|
|
1410
1449
|
* claim may be replaced; an expired staged outcome cannot be guessed away and
|
|
1411
1450
|
* instead returns `reconciliation-required`. Only an authoritative
|
|
1412
1451
|
* `committed` / `not-committed` decision can settle it.
|
|
@@ -1669,20 +1708,20 @@ function startLeaseHeartbeat(store, claim) {
|
|
|
1669
1708
|
failure: () => heartbeatFailure
|
|
1670
1709
|
};
|
|
1671
1710
|
}
|
|
1672
|
-
function
|
|
1711
|
+
function scopeWorkRegistration(parent) {
|
|
1673
1712
|
let open = true;
|
|
1674
1713
|
const assertOpen = () => {
|
|
1675
|
-
if (!open) throw new
|
|
1714
|
+
if (!open) throw new EventCollectionError("withIdempotentCommit: commit registration was used after the user work callback settled. Await every repository write before returning from the callback.");
|
|
1676
1715
|
};
|
|
1677
1716
|
return {
|
|
1678
|
-
|
|
1679
|
-
|
|
1717
|
+
registration: Object.freeze({
|
|
1718
|
+
registerSaved: (...args) => {
|
|
1680
1719
|
assertOpen();
|
|
1681
|
-
return parent.
|
|
1720
|
+
return parent.registerSaved(...args);
|
|
1682
1721
|
},
|
|
1683
|
-
|
|
1722
|
+
registerRemoved: (...args) => {
|
|
1684
1723
|
assertOpen();
|
|
1685
|
-
return parent.
|
|
1724
|
+
return parent.registerRemoved(...args);
|
|
1686
1725
|
}
|
|
1687
1726
|
}),
|
|
1688
1727
|
close: () => {
|
|
@@ -1705,12 +1744,12 @@ function scopeWorkEnrollment(parent) {
|
|
|
1705
1744
|
* 2. A fresh claim carries an opaque ownership token. For a leased store the
|
|
1706
1745
|
* wrapper renews it at `renewAfterMs` until the transaction callback is
|
|
1707
1746
|
* ready to commit. A renewal failure rejects before commit and releases
|
|
1708
|
-
* the claim. `fn(ctx,
|
|
1747
|
+
* the claim. `fn(ctx, registration, execution)` receives the same token so a
|
|
1709
1748
|
* source-side marker can make later reconciliation conclusive.
|
|
1710
1749
|
* 3. `store.complete(ctx, claim, fn's result)` stages or completes the outcome
|
|
1711
|
-
* in the same transaction as aggregate writes and outbox. The
|
|
1750
|
+
* in the same transaction as aggregate writes and outbox. The registration
|
|
1712
1751
|
* capability is sealed and its token array copied before `complete` can
|
|
1713
|
-
* yield, so leaked callback state cannot change the
|
|
1752
|
+
* yield, so leaked callback state cannot change the collection receipt.
|
|
1714
1753
|
* 4. After commit, `store.confirm(claim)` finalizes a leased store's staged
|
|
1715
1754
|
* outcome; it is a no-op for transactional stores. A failure cannot reject
|
|
1716
1755
|
* an already committed write, so it is sent to `onIdempotencyError` and the
|
|
@@ -1798,7 +1837,7 @@ async function withIdempotentCommit(deps, request, fn) {
|
|
|
1798
1837
|
const outcome = await withCommit({
|
|
1799
1838
|
...deps,
|
|
1800
1839
|
scope
|
|
1801
|
-
}, async (ctx,
|
|
1840
|
+
}, async (ctx, registration) => {
|
|
1802
1841
|
let claim = await store.claim(ctx, request.key, request.fingerprint);
|
|
1803
1842
|
if (claim.status === "reconciliation-required") {
|
|
1804
1843
|
const decision = deps.reconcileIdempotency ? await deps.reconcileIdempotency(claim.reconciliation, ctx) : "unknown";
|
|
@@ -1817,16 +1856,16 @@ async function withIdempotentCommit(deps, request, fn) {
|
|
|
1817
1856
|
};
|
|
1818
1857
|
attempt.claim = claim.claim;
|
|
1819
1858
|
attempt.heartbeat = startLeaseHeartbeat(store, claim.claim);
|
|
1820
|
-
const
|
|
1859
|
+
const workRegistration = scopeWorkRegistration(registration);
|
|
1821
1860
|
let work;
|
|
1822
1861
|
try {
|
|
1823
|
-
work = await fn(ctx,
|
|
1862
|
+
work = await fn(ctx, workRegistration.registration, {
|
|
1824
1863
|
key: request.key,
|
|
1825
1864
|
fingerprint: request.fingerprint,
|
|
1826
1865
|
claimToken: claim.claim.token
|
|
1827
1866
|
});
|
|
1828
1867
|
} finally {
|
|
1829
|
-
|
|
1868
|
+
workRegistration.close();
|
|
1830
1869
|
}
|
|
1831
1870
|
const result = work.result;
|
|
1832
1871
|
const commits = Array.isArray(work.commits) ? Object.freeze([...work.commits]) : work.commits;
|
|
@@ -1841,7 +1880,7 @@ async function withIdempotentCommit(deps, request, fn) {
|
|
|
1841
1880
|
});
|
|
1842
1881
|
if (!outcome.replayed) {
|
|
1843
1882
|
const committedClaim = attempt.claim;
|
|
1844
|
-
if (!committedClaim) throw new
|
|
1883
|
+
if (!committedClaim) throw new EventCollectionError("withIdempotentCommit: a fresh result committed without its claim receipt.");
|
|
1845
1884
|
try {
|
|
1846
1885
|
await store.confirm(committedClaim);
|
|
1847
1886
|
} catch (confirmError) {
|
|
@@ -2438,7 +2477,7 @@ var RepositoryErrorMappingFailedError = class extends KitWiringError {
|
|
|
2438
2477
|
mapperCause;
|
|
2439
2478
|
constructor(options) {
|
|
2440
2479
|
super("REPOSITORY_ERROR_MAPPING_FAILED", `The repository error mapper failed for ${options.intent} of aggregate ${describeAggregateIdentity(options.identity)}. The original persistence failure is preserved as cause; the mapper failure is available as mapperCause.`, options.persistenceError);
|
|
2441
|
-
this.identity =
|
|
2480
|
+
this.identity = copyAggregateIdentity(options.identity);
|
|
2442
2481
|
this.intent = options.intent;
|
|
2443
2482
|
this.mapperCause = options.mapperError;
|
|
2444
2483
|
}
|
|
@@ -2463,7 +2502,7 @@ var InvalidFlushStatementError = class extends KitWiringError {
|
|
|
2463
2502
|
classifierCause;
|
|
2464
2503
|
constructor(options) {
|
|
2465
2504
|
super("INVALID_FLUSH_STATEMENT", flushStatementReasonMessage(options), options.cause);
|
|
2466
|
-
this.identity =
|
|
2505
|
+
this.identity = copyAggregateIdentity(options.identity);
|
|
2467
2506
|
this.intent = options.intent ?? null;
|
|
2468
2507
|
this.reason = options.reason;
|
|
2469
2508
|
this.received = options.received;
|
|
@@ -2510,7 +2549,7 @@ var AggregateTrackingError = class extends KitWiringError {
|
|
|
2510
2549
|
registeredIntent;
|
|
2511
2550
|
constructor(options) {
|
|
2512
2551
|
super("AGGREGATE_TRACKING", trackingReasonMessage(options));
|
|
2513
|
-
this.identity =
|
|
2552
|
+
this.identity = copyAggregateIdentity(options.identity);
|
|
2514
2553
|
this.operation = options.operation;
|
|
2515
2554
|
this.reason = options.reason;
|
|
2516
2555
|
this.registeredIntent = options.registeredIntent;
|
|
@@ -2543,10 +2582,10 @@ function trackingReasonMessage(options) {
|
|
|
2543
2582
|
* A `CommitError` is the **potentially transient** post-completion
|
|
2544
2583
|
* failure (a commit-time serialization failure is the classic case), so
|
|
2545
2584
|
* it is the one a retrying caller should consider re-running. The
|
|
2546
|
-
* deterministic post-completion failure, a
|
|
2585
|
+
* deterministic post-completion failure, a collection-guard violation (an
|
|
2547
2586
|
* event missing `aggregateId` / `aggregateType`, or an eventful persisted
|
|
2548
2587
|
* aggregate that did not advance its version), is a programming bug and surfaces as
|
|
2549
|
-
* {@link
|
|
2588
|
+
* {@link EventCollectionError} instead, which does NOT extend
|
|
2550
2589
|
* `InfrastructureError`, so it stays out of retry paths by construction.
|
|
2551
2590
|
*/
|
|
2552
2591
|
var CommitError = class extends InfrastructureError {
|
|
@@ -2598,29 +2637,6 @@ function wiringErrorInCauseChain(error) {
|
|
|
2598
2637
|
return findInCauseChain(error, (link) => isWiringErrorLike(link) ? link : void 0);
|
|
2599
2638
|
}
|
|
2600
2639
|
|
|
2601
|
-
//#endregion
|
|
2602
|
-
//#region src/domain/aggregate/pending-event-recording.ts
|
|
2603
|
-
const { registry: capabilities$1, require } = createGlobalCapabilityRegistry(Symbol.for("@shirudo/ddd-kit/pending-event-recording-registry/v2"));
|
|
2604
|
-
/** Resolves the recording capability or throws `UnmanagedInstanceError`. */
|
|
2605
|
-
function requirePendingEventRecordingCapability(aggregate, operation) {
|
|
2606
|
-
return require(aggregate, operation, "aggregate");
|
|
2607
|
-
}
|
|
2608
|
-
function registerPendingEventRecordingCapability(aggregate, capability) {
|
|
2609
|
-
capabilities$1.set(aggregate, Object.freeze(capability));
|
|
2610
|
-
}
|
|
2611
|
-
|
|
2612
|
-
//#endregion
|
|
2613
|
-
//#region src/application/unit-of-work/record-pending-events.ts
|
|
2614
|
-
function recordPendingEvents(aggregate, source, stampOptions) {
|
|
2615
|
-
const capability = requirePendingEventRecordingCapability(aggregate, "recordPendingEvents");
|
|
2616
|
-
const shared = {
|
|
2617
|
-
occurredAt: stampOptions?.occurredAt,
|
|
2618
|
-
metadata: stampOptions?.metadata
|
|
2619
|
-
};
|
|
2620
|
-
const createStamp = typeof source === "function" ? source : () => source.createStamp(shared);
|
|
2621
|
-
return capability.record((event, index) => createStamp(event, index));
|
|
2622
|
-
}
|
|
2623
|
-
|
|
2624
2640
|
//#endregion
|
|
2625
2641
|
//#region src/application/unit-of-work/repository-facade.ts
|
|
2626
2642
|
/**
|
|
@@ -2836,7 +2852,7 @@ function deleteRepositoryFacadeProperty(state, target, property) {
|
|
|
2836
2852
|
* `AggregatePersistence` implementations: two `findById(id)` calls in the same
|
|
2837
2853
|
* unit of work MUST return the same instance, because commit-token
|
|
2838
2854
|
* write registration dedupes by JavaScript object identity. Two instances for
|
|
2839
|
-
* one logical aggregate can otherwise produce two tokens, two
|
|
2855
|
+
* one logical aggregate can otherwise produce two tokens, two collections,
|
|
2840
2856
|
* and two post-commit lifecycle calls.
|
|
2841
2857
|
*
|
|
2842
2858
|
* Storage is two-level (per-type stores created lazily), so
|
|
@@ -2903,7 +2919,7 @@ var IdentityMap = class {
|
|
|
2903
2919
|
* - Registering a DIFFERENT instance for an occupied type+id throws:
|
|
2904
2920
|
* that is precisely the identity-map violation this class exists
|
|
2905
2921
|
* to prevent (the repository hydrated twice instead of checking
|
|
2906
|
-
* {@link get} first), and letting it pass would double-
|
|
2922
|
+
* {@link get} first), and letting it pass would double-collect
|
|
2907
2923
|
* events downstream.
|
|
2908
2924
|
* - Registering a type+id that was {@link delete}d in this unit of
|
|
2909
2925
|
* work throws `AggregateDeletedError`: deletion is final within
|
|
@@ -2918,7 +2934,7 @@ var IdentityMap = class {
|
|
|
2918
2934
|
this._stores.set(type, store);
|
|
2919
2935
|
}
|
|
2920
2936
|
const existing = store.get(id);
|
|
2921
|
-
if (existing !== void 0 && existing !== aggregate) throw new Error(`IdentityMap: a different instance is already registered for ${describeAggregateIdentity(aggregate.aggregateIdentity)}. Check get() before hydrating: two live instances of one aggregate break the one-instance-per-unit-of-work contract that exactly-once event
|
|
2937
|
+
if (existing !== void 0 && existing !== aggregate) throw new Error(`IdentityMap: a different instance is already registered for ${describeAggregateIdentity(aggregate.aggregateIdentity)}. Check get() before hydrating: two live instances of one aggregate break the one-instance-per-unit-of-work contract that exactly-once event collection relies on.`);
|
|
2922
2938
|
store.set(id, aggregate);
|
|
2923
2939
|
if (aggregate !== null && typeof aggregate === "object" && !this._pendingAtRegistration.has(aggregate)) {
|
|
2924
2940
|
const pending = pendingEventCountOf(aggregate);
|
|
@@ -2929,7 +2945,7 @@ var IdentityMap = class {
|
|
|
2929
2945
|
* Registered instances that have recorded MORE pending events than they
|
|
2930
2946
|
* carried when first registered (loaded). Used by the unit of work's
|
|
2931
2947
|
* end-of-run guard: an aggregate that gained events after load but was
|
|
2932
|
-
* never
|
|
2948
|
+
* never registered would silently drop them. A read-only load, or a
|
|
2933
2949
|
* reconstitution that already carried events, shows no increase and is
|
|
2934
2950
|
* not reported.
|
|
2935
2951
|
*/
|
|
@@ -3008,7 +3024,7 @@ function insertPersistenceBaseline(model) {
|
|
|
3008
3024
|
return createBaselineToken(model, void 0, "new");
|
|
3009
3025
|
}
|
|
3010
3026
|
/**
|
|
3011
|
-
* Captures the aggregate's current adapter
|
|
3027
|
+
* Captures the aggregate's current adapter capture using the capability
|
|
3012
3028
|
* carried by an existing baseline. Used to seal persistence-last registration.
|
|
3013
3029
|
*/
|
|
3014
3030
|
function recapturePersistenceBaseline(baseline, aggregate) {
|
|
@@ -3020,7 +3036,7 @@ function recapturePersistenceBaseline(baseline, aggregate) {
|
|
|
3020
3036
|
});
|
|
3021
3037
|
}
|
|
3022
3038
|
/**
|
|
3023
|
-
* Recaptures the adapter
|
|
3039
|
+
* Recaptures the adapter capture and reports whether it drifted from the
|
|
3024
3040
|
* baseline's stored capture, using the model's `captureEquals` when supplied
|
|
3025
3041
|
* and structural deep equality otherwise.
|
|
3026
3042
|
*
|
|
@@ -3031,8 +3047,8 @@ function recapturePersistenceBaseline(baseline, aggregate) {
|
|
|
3031
3047
|
* question independent of the model's diffing strategy. A `"new"` lifecycle
|
|
3032
3048
|
* baseline has no stored capture and never reports drift.
|
|
3033
3049
|
*/
|
|
3034
|
-
function
|
|
3035
|
-
const capability = capabilityFor(baseline, "
|
|
3050
|
+
function persistenceCaptureDrifted(baseline, aggregate) {
|
|
3051
|
+
const capability = capabilityFor(baseline, "persistenceCaptureDrifted");
|
|
3036
3052
|
if (capability.lifecycle === "new") return false;
|
|
3037
3053
|
return !capability.captureEquals(capability.baseline, capability.capture(aggregate));
|
|
3038
3054
|
}
|
|
@@ -3076,7 +3092,8 @@ function capabilityFor(baseline, operation) {
|
|
|
3076
3092
|
* the public API.
|
|
3077
3093
|
*/
|
|
3078
3094
|
var Session = class {
|
|
3079
|
-
|
|
3095
|
+
commitRegistration;
|
|
3096
|
+
stamp;
|
|
3080
3097
|
_registeredWrites = [];
|
|
3081
3098
|
_commitTokens = /* @__PURE__ */ new Set();
|
|
3082
3099
|
_identityMap = new IdentityMap();
|
|
@@ -3092,15 +3109,16 @@ var Session = class {
|
|
|
3092
3109
|
/**
|
|
3093
3110
|
* True from the start of the flush until the last check before the
|
|
3094
3111
|
* commit ran or the run ended. A write registered in that window would
|
|
3095
|
-
* be committed and
|
|
3112
|
+
* be committed and collected without its flush, so it is rejected, and
|
|
3096
3113
|
* the first rejection also fails the attempt, even when the caller
|
|
3097
3114
|
* swallows it. After the window, a registration reports a closed unit of
|
|
3098
3115
|
* work: nothing can fail the attempt any more.
|
|
3099
3116
|
*/
|
|
3100
3117
|
_lateRegistrationsFailAttempt = false;
|
|
3101
3118
|
_registrationDuringFlush;
|
|
3102
|
-
constructor(
|
|
3103
|
-
this.
|
|
3119
|
+
constructor(commitRegistration, stamp) {
|
|
3120
|
+
this.commitRegistration = commitRegistration;
|
|
3121
|
+
this.stamp = stamp;
|
|
3104
3122
|
}
|
|
3105
3123
|
get identityMap() {
|
|
3106
3124
|
this.assertOpen("tracking.identityMap");
|
|
@@ -3195,7 +3213,7 @@ var Session = class {
|
|
|
3195
3213
|
throw error;
|
|
3196
3214
|
}
|
|
3197
3215
|
}
|
|
3198
|
-
/** Two live instances of one identity would each flush and
|
|
3216
|
+
/** Two live instances of one identity would each flush and collect. */
|
|
3199
3217
|
assertIdentityNotTaken(aggregate, definition) {
|
|
3200
3218
|
const tracked = this._identityMap.get(definition.aggregate, aggregate.id);
|
|
3201
3219
|
if (tracked === void 0 || tracked === aggregate) return;
|
|
@@ -3221,8 +3239,9 @@ var Session = class {
|
|
|
3221
3239
|
const loaded = this.loadedEntryFor(aggregate, "remove", definition);
|
|
3222
3240
|
this.registerWrite(loaded, "remove", definition);
|
|
3223
3241
|
}
|
|
3224
|
-
/** Registers persistence intent and commit
|
|
3242
|
+
/** Registers persistence intent and commit registration as one operation. */
|
|
3225
3243
|
registerWrite(entry, intent, definition) {
|
|
3244
|
+
if (entry.registration === void 0) this.stamp(entry.aggregate, `repository.${intent}`);
|
|
3226
3245
|
const newlyRegistered = this.registerIntent(entry, intent);
|
|
3227
3246
|
try {
|
|
3228
3247
|
if (intent === "remove") this.registerRemovedCommit(entry.aggregate, definition, entry.expectedVersion);
|
|
@@ -3279,7 +3298,7 @@ var Session = class {
|
|
|
3279
3298
|
this._registeredWrites.push(entry);
|
|
3280
3299
|
return true;
|
|
3281
3300
|
}
|
|
3282
|
-
/** Restores the pre-registration state when commit
|
|
3301
|
+
/** Restores the pre-registration state when commit registration rejects. */
|
|
3283
3302
|
rollbackIntentRegistration(entry) {
|
|
3284
3303
|
const index = this._registeredWrites.lastIndexOf(entry);
|
|
3285
3304
|
if (index >= 0) this._registeredWrites.splice(index, 1);
|
|
@@ -3289,7 +3308,7 @@ var Session = class {
|
|
|
3289
3308
|
const registration = entry.registration;
|
|
3290
3309
|
if (registration === void 0) return;
|
|
3291
3310
|
const currentEvents = entry.aggregate.pendingEvents;
|
|
3292
|
-
const persistenceChanged =
|
|
3311
|
+
const persistenceChanged = persistenceCaptureDrifted(registration.baseline, entry.aggregate);
|
|
3293
3312
|
const sameEvents = currentEvents.length === registration.events.length && currentEvents.every((event, index) => event === registration.events[index]);
|
|
3294
3313
|
if (registration.version !== entry.aggregate.version || !sameEvents || persistenceChanged) throw new AggregateTrackingError({
|
|
3295
3314
|
identity: entry.aggregate.aggregateIdentity,
|
|
@@ -3301,13 +3320,13 @@ var Session = class {
|
|
|
3301
3320
|
registerSavedCommit(aggregate, definition, expectedVersion) {
|
|
3302
3321
|
this.assertOpen("repository.add/update");
|
|
3303
3322
|
if (this.isRemovedInstance(aggregate) || this._identityMap.isDeleted(definition.aggregate, aggregate.id)) throw new AggregateDeletedError({ identity: aggregate.aggregateIdentity });
|
|
3304
|
-
const token = this.
|
|
3323
|
+
const token = this.commitRegistration.registerSaved(aggregate, { expectedVersion });
|
|
3305
3324
|
this._commitTokens.add(token);
|
|
3306
3325
|
return token;
|
|
3307
3326
|
}
|
|
3308
3327
|
registerRemovedCommit(aggregate, definition, expectedVersion) {
|
|
3309
3328
|
this.assertOpen("repository.remove");
|
|
3310
|
-
const token = this.
|
|
3329
|
+
const token = this.commitRegistration.registerRemoved(aggregate, { expectedVersion });
|
|
3311
3330
|
this._identityMap.delete(definition.aggregate, aggregate.id);
|
|
3312
3331
|
this._commitTokens.add(token);
|
|
3313
3332
|
return token;
|
|
@@ -3326,7 +3345,7 @@ var Session = class {
|
|
|
3326
3345
|
this.assertUnchangedAfterRegistration(entry, "commit");
|
|
3327
3346
|
continue;
|
|
3328
3347
|
}
|
|
3329
|
-
if (entry.lifecycle === "loaded" && (entry.aggregate.version !== entry.expectedVersion ||
|
|
3348
|
+
if (entry.lifecycle === "loaded" && (entry.aggregate.version !== entry.expectedVersion || persistenceCaptureDrifted(entry.baseline, entry.aggregate))) throw new UnregisteredChangesError({
|
|
3330
3349
|
identity: entry.aggregate.aggregateIdentity,
|
|
3331
3350
|
appendOnly: entry.definition.appendOnly === true
|
|
3332
3351
|
});
|
|
@@ -3334,7 +3353,7 @@ var Session = class {
|
|
|
3334
3353
|
for (const instance of this._identityMap.instancesWithNewPendingEvents()) {
|
|
3335
3354
|
if (instance !== null && typeof instance === "object" && this.registrationOf(instance) !== void 0) continue;
|
|
3336
3355
|
const aggregate = instance;
|
|
3337
|
-
throw new
|
|
3356
|
+
throw new UnregisteredChangesError({
|
|
3338
3357
|
identity: aggregate.aggregateIdentity,
|
|
3339
3358
|
appendOnly: this._trackingByAggregate.get(aggregate)?.definition.appendOnly === true
|
|
3340
3359
|
});
|
|
@@ -3524,7 +3543,7 @@ function defineRepository() {
|
|
|
3524
3543
|
const builder = (definition) => {
|
|
3525
3544
|
const branded = { ...definition };
|
|
3526
3545
|
assertRepositoryDefinitionMembers(branded);
|
|
3527
|
-
|
|
3546
|
+
applyCooperativeBrand(branded, repositoryDefinitionBrand);
|
|
3528
3547
|
return Object.freeze(branded);
|
|
3529
3548
|
};
|
|
3530
3549
|
return builder;
|
|
@@ -3535,7 +3554,7 @@ function defineRepository() {
|
|
|
3535
3554
|
* transaction and either persist completely or not at all.
|
|
3536
3555
|
*
|
|
3537
3556
|
* Built ON TOP of `withCommit` - the commit orchestration (event
|
|
3538
|
-
*
|
|
3557
|
+
* collection into the outbox inside the transaction, internal acknowledgement
|
|
3539
3558
|
* after the commit, best-effort in-process publish last) is inherited,
|
|
3540
3559
|
* not reimplemented. What this layer adds:
|
|
3541
3560
|
*
|
|
@@ -3603,7 +3622,7 @@ var UnitOfWork = class {
|
|
|
3603
3622
|
* Execute one unit of work: open the transaction, hand the callback
|
|
3604
3623
|
* tx-bound repositories, commit on resolve, roll back on throw,
|
|
3605
3624
|
* run the post-commit lifecycle (acknowledge, observe, publish) for every
|
|
3606
|
-
*
|
|
3625
|
+
* registered aggregate. Returns the callback's result.
|
|
3607
3626
|
*/
|
|
3608
3627
|
async run(work, options) {
|
|
3609
3628
|
if (options?.signal?.aborted) throw abortReason(options.signal, "UnitOfWork.run aborted before opening a transaction");
|
|
@@ -3621,10 +3640,12 @@ var UnitOfWork = class {
|
|
|
3621
3640
|
onPersisted: this.deps.onPersisted,
|
|
3622
3641
|
onPersistError: this.deps.onPersistError,
|
|
3623
3642
|
postCommitTimeoutMs: this.deps.postCommitTimeoutMs,
|
|
3624
|
-
signal: options?.signal
|
|
3625
|
-
|
|
3643
|
+
signal: options?.signal,
|
|
3644
|
+
eventStampFactory: this.deps.eventStampFactory,
|
|
3645
|
+
stampOptions: options?.stampOptions
|
|
3646
|
+
}, async (tx, registration, stamp) => {
|
|
3626
3647
|
session?.close();
|
|
3627
|
-
const s = new Session(
|
|
3648
|
+
const s = new Session(registration, stamp);
|
|
3628
3649
|
session = s;
|
|
3629
3650
|
sessions.push(s);
|
|
3630
3651
|
const current = startAttempt();
|
|
@@ -3712,7 +3733,7 @@ function startAttempt() {
|
|
|
3712
3733
|
* {@link RollbackError}.
|
|
3713
3734
|
* - `workCompleted`: the callback finished; the failure is post-completion.
|
|
3714
3735
|
* The checks run in this order. First, a wiring error in the rejection (a
|
|
3715
|
-
*
|
|
3736
|
+
* collection-guard violation, or an aggregate that changed after its
|
|
3716
3737
|
* registration) is a deterministic programming bug and passes through; it
|
|
3717
3738
|
* does NOT extend `InfrastructureError`, so a retry-on-Infrastructure
|
|
3718
3739
|
* handler skips it. It is thrown inside `scope.transactional()`, so a
|
|
@@ -4097,7 +4118,7 @@ var Entity = class {
|
|
|
4097
4118
|
* `TState` publicly would expose the aggregate's live object graph and
|
|
4098
4119
|
* let nested mutation bypass behavior, validation, versioning, and
|
|
4099
4120
|
* dirty tracking. Concrete entities should expose business-meaningful queries or
|
|
4100
|
-
*
|
|
4121
|
+
* immutable DTOs (`deepFreeze(copyPlainState(this.state))` for a
|
|
4101
4122
|
* plain-data state). Snapshot projection belongs to the persistence
|
|
4102
4123
|
* adapter, which captures the aggregate from outside through those
|
|
4103
4124
|
* queries and DTOs rather than asking the entity to create its own
|
|
@@ -4130,7 +4151,7 @@ var Entity = class {
|
|
|
4130
4151
|
* widens to the whole graph: NESTED objects are frozen in place too.
|
|
4131
4152
|
*
|
|
4132
4153
|
* @throws MissingEntityIdError when `id` is not a non-blank string.
|
|
4133
|
-
* @throws
|
|
4154
|
+
* @throws PrototypePollutionError when a plain-object, null-prototype,
|
|
4134
4155
|
* or array state carries an own `"__proto__"` data key; validate and
|
|
4135
4156
|
* strip untrusted input at the boundary.
|
|
4136
4157
|
*/
|
|
@@ -4154,7 +4175,7 @@ var Entity = class {
|
|
|
4154
4175
|
* ownership transfer and is frozen in place; see the constructor.
|
|
4155
4176
|
*
|
|
4156
4177
|
* @param newState - The new state
|
|
4157
|
-
* @throws
|
|
4178
|
+
* @throws PrototypePollutionError when the state carries an own
|
|
4158
4179
|
* `"__proto__"` data key; the previous state is kept.
|
|
4159
4180
|
*/
|
|
4160
4181
|
setState(newState) {
|
|
@@ -4215,15 +4236,15 @@ function freezeShallow(value) {
|
|
|
4215
4236
|
* @internal Shared by the kit's own entity subclasses; not part of the
|
|
4216
4237
|
* public API.
|
|
4217
4238
|
*/
|
|
4218
|
-
function
|
|
4239
|
+
function assertStateHasNoOwnProtoKey(state, subject) {
|
|
4219
4240
|
if (state === null || typeof state !== "object") return;
|
|
4220
4241
|
if (Array.isArray(state)) {
|
|
4221
|
-
|
|
4242
|
+
assertNoOwnProtoKey(state, subject);
|
|
4222
4243
|
return;
|
|
4223
4244
|
}
|
|
4224
4245
|
const proto = Object.getPrototypeOf(state);
|
|
4225
4246
|
if (proto !== Object.prototype && proto !== null) return;
|
|
4226
|
-
|
|
4247
|
+
assertNoOwnProtoKey(state, subject);
|
|
4227
4248
|
}
|
|
4228
4249
|
/**
|
|
4229
4250
|
* Returns a shallow copy for plain objects and arrays so the subsequent
|
|
@@ -4235,7 +4256,7 @@ function assertStateHasNoHostileOwnKey(state, subject) {
|
|
|
4235
4256
|
*/
|
|
4236
4257
|
function shallowCopyOwned(value) {
|
|
4237
4258
|
if (value === null || typeof value !== "object") return value;
|
|
4238
|
-
|
|
4259
|
+
assertStateHasNoOwnProtoKey(value, "Entity state");
|
|
4239
4260
|
if (Array.isArray(value)) {
|
|
4240
4261
|
const copy = [...value];
|
|
4241
4262
|
for (const key of Reflect.ownKeys(value)) {
|
|
@@ -4344,7 +4365,7 @@ function removeEntityById(entities, id) {
|
|
|
4344
4365
|
/**
|
|
4345
4366
|
* Updates an entity with the given ID in the collection.
|
|
4346
4367
|
* Returns a new array with the updated entity.
|
|
4347
|
-
* Structural sharing for adapter-owned persistence
|
|
4368
|
+
* Structural sharing for adapter-owned persistence captures: returns
|
|
4348
4369
|
* the ORIGINAL array when nothing changed (no match, or the element kept
|
|
4349
4370
|
* its reference), so a partial-write adapter can skip the untouched
|
|
4350
4371
|
* collection; a new array only when an
|
|
@@ -4383,7 +4404,7 @@ function updateEntityById(entities, id, updater) {
|
|
|
4383
4404
|
/**
|
|
4384
4405
|
* Replaces an entity with the given ID in the collection.
|
|
4385
4406
|
* Returns a new array with the replaced entity.
|
|
4386
|
-
* Structural sharing for adapter-owned persistence
|
|
4407
|
+
* Structural sharing for adapter-owned persistence captures: returns
|
|
4387
4408
|
* the ORIGINAL array when nothing changed (no match, or the element kept
|
|
4388
4409
|
* its reference), so a partial-write adapter can skip the untouched
|
|
4389
4410
|
* collection; a new array only when an
|
|
@@ -4448,8 +4469,8 @@ function entityIds(entities) {
|
|
|
4448
4469
|
* tracking, the kit-internal post-commit acknowledgement capability,
|
|
4449
4470
|
* the `markReconstituted` post-load marker, and the `createEvent` helper
|
|
4450
4471
|
* that auto-injects `aggregateId` + `aggregateType` on every event the
|
|
4451
|
-
* aggregate emits. The
|
|
4452
|
-
*
|
|
4472
|
+
* aggregate emits. The commit stamps the pending decisions when it
|
|
4473
|
+
* registers the aggregate, before persistence.
|
|
4453
4474
|
*
|
|
4454
4475
|
* Consumers do NOT extend this class directly; extend
|
|
4455
4476
|
* `StateStoredAggregate` for state-stored aggregates or
|
|
@@ -4509,24 +4530,24 @@ var BaseAggregate = class extends Entity {
|
|
|
4509
4530
|
registerPendingEventRecordingCapability(this, { record: (createStamp) => this.recordPendingDecisions(createStamp) });
|
|
4510
4531
|
}
|
|
4511
4532
|
/**
|
|
4512
|
-
* Stamps every
|
|
4513
|
-
* and time from `createStamp`, passes an already
|
|
4533
|
+
* Stamps every unstamped decision in the pending list with identity
|
|
4534
|
+
* and time from `createStamp`, passes an already stamped event through,
|
|
4514
4535
|
* and replaces the list atomically. Three guards run before the
|
|
4515
|
-
* assignment: an event that no kit constructor
|
|
4516
|
-
* ({@link
|
|
4536
|
+
* assignment: an event that no kit constructor built is rejected
|
|
4537
|
+
* ({@link HandBuiltEventError}); a stamp provider that recorded a new
|
|
4517
4538
|
* decision on this aggregate mid-map is rejected
|
|
4518
4539
|
* ({@link ReentrantEventRecordingError}); and two decisions that
|
|
4519
4540
|
* received one eventId are rejected ({@link DuplicateEventIdError}).
|
|
4520
|
-
* When a guard fires, every decision stays
|
|
4541
|
+
* When a guard fires, every decision stays unstamped.
|
|
4521
4542
|
*/
|
|
4522
4543
|
recordPendingDecisions(createStamp) {
|
|
4523
4544
|
const stamped = this._pendingEvents;
|
|
4524
4545
|
const stampedCount = stamped.length;
|
|
4525
4546
|
const recorded = stamped.map((event, index) => {
|
|
4526
4547
|
const candidate = event;
|
|
4527
|
-
if (
|
|
4528
|
-
if (!
|
|
4529
|
-
return
|
|
4548
|
+
if (isStampedDomainEvent(candidate)) return candidate;
|
|
4549
|
+
if (!isUnstampedDomainEvent(candidate)) throw new HandBuiltEventError(event.type);
|
|
4550
|
+
return stampDomainEvent(candidate, createStamp(candidate, index));
|
|
4530
4551
|
});
|
|
4531
4552
|
if (this._pendingEvents !== stamped || this._pendingEvents.length !== stampedCount) throw new ReentrantEventRecordingError({ identity: this.aggregateIdentity });
|
|
4532
4553
|
const seenEventIds = /* @__PURE__ */ new Set();
|
|
@@ -4549,7 +4570,7 @@ var BaseAggregate = class extends Entity {
|
|
|
4549
4570
|
/**
|
|
4550
4571
|
* Post-commit cleanup for the deleted disposition. The row is gone, so
|
|
4551
4572
|
* there is no persisted version to advance: stamping the marker from the
|
|
4552
|
-
* live instance would make a later legitimate re-
|
|
4573
|
+
* live instance would make a later legitimate re-registration of this
|
|
4553
4574
|
* instance trip the unique-cursor guard for a row that does not exist.
|
|
4554
4575
|
*/
|
|
4555
4576
|
discardPendingEventsAfterDeletion(events) {
|
|
@@ -4636,12 +4657,12 @@ var BaseAggregate = class extends Entity {
|
|
|
4636
4657
|
this._persistedVersion = restored;
|
|
4637
4658
|
}
|
|
4638
4659
|
/**
|
|
4639
|
-
* Appends a domain event to the pending list. The event must be
|
|
4660
|
+
* Appends a domain event to the pending list. The event must be kit-built
|
|
4640
4661
|
* by a kit constructor; a missing `aggregateId` or `aggregateType` is
|
|
4641
4662
|
* stamped from this aggregate, and an aggregate identity that names another
|
|
4642
4663
|
* aggregate throws {@link MisattributedEventError} before anything is
|
|
4643
4664
|
* recorded. Each append is one fact. A decision appended twice becomes
|
|
4644
|
-
* two facts with distinct ids. A
|
|
4665
|
+
* two facts with distinct ids. A stamped event that is already pending
|
|
4645
4666
|
* throws {@link DuplicateEventIdError}, and a list at `maxPendingEvents`
|
|
4646
4667
|
* throws {@link PendingEventLimitExceededError}, before anything is
|
|
4647
4668
|
* recorded. Prefer the higher-level `StateStoredAggregate.setState()`
|
|
@@ -4654,23 +4675,23 @@ var BaseAggregate = class extends Entity {
|
|
|
4654
4675
|
* application shell rejects it; use `setState(currentState, event)`.
|
|
4655
4676
|
*/
|
|
4656
4677
|
addDomainEvent(event) {
|
|
4657
|
-
const stamped = this.
|
|
4678
|
+
const stamped = this.attributeNewEvent(event);
|
|
4658
4679
|
this.assertEventIdsNotPending([stamped]);
|
|
4659
4680
|
this.assertPendingEventLimit(1);
|
|
4660
4681
|
this.appendStampedEvent(stamped);
|
|
4661
4682
|
}
|
|
4662
4683
|
/**
|
|
4663
4684
|
* One identity per fact, checked before the change becomes observable:
|
|
4664
|
-
* a
|
|
4685
|
+
* a stamped event whose `eventId` is already pending, or appears twice
|
|
4665
4686
|
* in one batch, throws {@link DuplicateEventIdError}. A decision has no
|
|
4666
4687
|
* `eventId` yet, so a stamp provider that reuses one is caught at
|
|
4667
4688
|
* recording instead.
|
|
4668
4689
|
*/
|
|
4669
4690
|
assertEventIdsNotPending(batch) {
|
|
4670
4691
|
const pendingIds = /* @__PURE__ */ new Set();
|
|
4671
|
-
for (const pending of this._pendingEvents) if (
|
|
4692
|
+
for (const pending of this._pendingEvents) if (isStampedDomainEvent(pending)) pendingIds.add(pending.eventId);
|
|
4672
4693
|
for (const event of batch) {
|
|
4673
|
-
if (!
|
|
4694
|
+
if (!isStampedDomainEvent(event)) continue;
|
|
4674
4695
|
if (pendingIds.has(event.eventId)) throw new DuplicateEventIdError({
|
|
4675
4696
|
identity: this.aggregateIdentity,
|
|
4676
4697
|
eventId: event.eventId
|
|
@@ -4698,7 +4719,7 @@ var BaseAggregate = class extends Entity {
|
|
|
4698
4719
|
}
|
|
4699
4720
|
/**
|
|
4700
4721
|
* Appends an event that the caller already passed through
|
|
4701
|
-
* {@link
|
|
4722
|
+
* {@link attributeNewEvent}, {@link assertEventIdsNotPending} and
|
|
4702
4723
|
* {@link assertPendingEventLimit}. `setState()` and `apply()` run the
|
|
4703
4724
|
* three before the state moves and append afterwards, so each gate
|
|
4704
4725
|
* runs once per event.
|
|
@@ -4719,15 +4740,15 @@ var BaseAggregate = class extends Entity {
|
|
|
4719
4740
|
* present-but-foreign `aggregateId` / `aggregateType` is a wiring bug and
|
|
4720
4741
|
* throws {@link MisattributedEventError}; missing fields are filled in
|
|
4721
4742
|
* from the aggregate, so a recorded event always carries its full
|
|
4722
|
-
* aggregate identity and can never fail the
|
|
4723
|
-
* later. The
|
|
4724
|
-
* runs first, so
|
|
4743
|
+
* aggregate identity and can never fail the collection or the replay guard
|
|
4744
|
+
* later. The kit-built check
|
|
4745
|
+
* runs first, so a hand-built event fails before anything else. The
|
|
4725
4746
|
* stamped copy is frozen like the original (payload and metadata are
|
|
4726
4747
|
* shared, already deep-frozen by the constructors); a fully stamped
|
|
4727
4748
|
* event is returned as is.
|
|
4728
4749
|
*/
|
|
4729
|
-
|
|
4730
|
-
this.
|
|
4750
|
+
attributeNewEvent(event) {
|
|
4751
|
+
this.assertKitBuiltEvent(event);
|
|
4731
4752
|
const { aggregateId, aggregateType } = event;
|
|
4732
4753
|
const idForeign = aggregateId !== void 0 && aggregateId !== this.id;
|
|
4733
4754
|
const typeForeign = aggregateType !== void 0 && aggregateType !== this.aggregateType;
|
|
@@ -4748,32 +4769,31 @@ var BaseAggregate = class extends Entity {
|
|
|
4748
4769
|
aggregateId: this.id,
|
|
4749
4770
|
aggregateType: this.aggregateType
|
|
4750
4771
|
};
|
|
4751
|
-
return
|
|
4772
|
+
return isStampedDomainEvent(event) ? adoptStampedDomainEvent(copy) : adoptUnstampedDomainEvent(copy);
|
|
4752
4773
|
}
|
|
4753
4774
|
/**
|
|
4754
|
-
*
|
|
4755
|
-
* constructor produced passes. The kit marks two shapes: a
|
|
4756
|
-
* event carries the
|
|
4757
|
-
* `
|
|
4758
|
-
* brand (
|
|
4759
|
-
* helper). Either brand implies deeply frozen with defensively copied
|
|
4775
|
+
* Kit-built check for every recording path: only an event that a kit
|
|
4776
|
+
* constructor produced passes. The kit marks two shapes: a stamped
|
|
4777
|
+
* event carries the stamped brand (`createDomainEvent`,
|
|
4778
|
+
* `createStampedDomainEvent`) and a decision carries the unstamped
|
|
4779
|
+
* brand (the aggregate `createEvent` helper). Either brand implies deeply frozen with defensively copied
|
|
4760
4780
|
* payload and metadata, a guarantee no frozen-ness probe can establish
|
|
4761
4781
|
* (a shallow-frozen literal with mutable nested data would fool it).
|
|
4762
4782
|
* O(1): one brand check per shape.
|
|
4763
4783
|
*/
|
|
4764
|
-
|
|
4765
|
-
if (!
|
|
4784
|
+
assertKitBuiltEvent(event) {
|
|
4785
|
+
if (!isStampedDomainEvent(event) && !isUnstampedDomainEvent(event)) throw new HandBuiltEventError(event.type);
|
|
4766
4786
|
}
|
|
4767
4787
|
/**
|
|
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
|
-
*
|
|
4773
|
-
* concrete event producer, rather than in shell-owned
|
|
4791
|
+
* The commit stamps pending events when it registers the aggregate,
|
|
4792
|
+
* before persistence. Payload schema version stays here, next to the
|
|
4793
|
+
* concrete event producer, rather than in shell-owned stamp data.
|
|
4774
4794
|
*/
|
|
4775
4795
|
createEvent(type, payload, options) {
|
|
4776
|
-
return
|
|
4796
|
+
return buildUnstampedDomainEvent(type, payload, {
|
|
4777
4797
|
...options,
|
|
4778
4798
|
aggregateId: this.id,
|
|
4779
4799
|
aggregateType: this.aggregateType
|
|
@@ -4785,7 +4805,7 @@ var BaseAggregate = class extends Entity {
|
|
|
4785
4805
|
* `EventSourcedAggregate.replayHistory`: a target carrying unflushed
|
|
4786
4806
|
* `pendingEvents` throws {@link UnreplayableAggregateError} BEFORE anything
|
|
4787
4807
|
* moves. A restore advances the aggregate's current version, so unflushed
|
|
4788
|
-
* events recorded against the old version would later be
|
|
4808
|
+
* events recorded against the old version would later be collected claiming
|
|
4789
4809
|
* a version baseline they were never part of. When the discard is
|
|
4790
4810
|
* deliberate, discard this dirty instance and reconstitute a fresh aggregate
|
|
4791
4811
|
* instead of mutating persistence lifecycle state publicly.
|
|
@@ -4793,8 +4813,8 @@ var BaseAggregate = class extends Entity {
|
|
|
4793
4813
|
* Deliberately a module-level function, not a class method: it MUST not be
|
|
4794
4814
|
* overridable by consumer subclasses (a no-op override would silently
|
|
4795
4815
|
* disable the guard at every call site). The callers pass the count of
|
|
4796
|
-
* the private list; the commit
|
|
4797
|
-
* getter, so a subclass that overrides the getter changes the
|
|
4816
|
+
* the private list; the commit collection reads the public `pendingEvents`
|
|
4817
|
+
* getter, so a subclass that overrides the getter changes the collection,
|
|
4798
4818
|
* not this guard.
|
|
4799
4819
|
*
|
|
4800
4820
|
* @internal Shared by the aggregate flavours in this package; not part of
|
|
@@ -4926,12 +4946,12 @@ var EventSourcedAggregate = class extends BaseAggregate {
|
|
|
4926
4946
|
* @param event - The domain event to apply
|
|
4927
4947
|
*/
|
|
4928
4948
|
apply(event) {
|
|
4929
|
-
const stamped = this.
|
|
4949
|
+
const stamped = this.attributeNewEvent(event);
|
|
4930
4950
|
this.assertEventIdsNotPending([stamped]);
|
|
4931
4951
|
this.assertPendingEventLimit(1);
|
|
4932
4952
|
this.validateEvent(stamped);
|
|
4933
4953
|
const next = freezeEntityState(this, this.fold(stamped));
|
|
4934
|
-
|
|
4954
|
+
assertStateHasNoOwnProtoKey(next, "Aggregate state");
|
|
4935
4955
|
assertStateInvariant(this, next);
|
|
4936
4956
|
this.bumpVersion();
|
|
4937
4957
|
storeTrustedState(this, next);
|
|
@@ -4959,7 +4979,7 @@ var EventSourcedAggregate = class extends BaseAggregate {
|
|
|
4959
4979
|
* stream corruption) after the all-or-nothing rollback. History
|
|
4960
4980
|
* events without the optional aggregate identity fields pass unchecked (the
|
|
4961
4981
|
* fields are optional on the event shape); NEW events are covered
|
|
4962
|
-
* by the stricter `
|
|
4982
|
+
* by the stricter `attributeNewEvent` on the apply path.
|
|
4963
4983
|
*/
|
|
4964
4984
|
assertReplayedEventBelongsHere(event) {
|
|
4965
4985
|
const idMismatch = event.aggregateId !== void 0 && event.aggregateId !== this.id;
|
|
@@ -5020,7 +5040,7 @@ var EventSourcedAggregate = class extends BaseAggregate {
|
|
|
5020
5040
|
this.assertReplayedEventBelongsHere(event);
|
|
5021
5041
|
storeTrustedState(this, this.fold(event));
|
|
5022
5042
|
}
|
|
5023
|
-
|
|
5043
|
+
assertStateHasNoOwnProtoKey(this.state, "Aggregate state");
|
|
5024
5044
|
this.markReconstituted(startVersion + history.length);
|
|
5025
5045
|
} catch (e) {
|
|
5026
5046
|
storeTrustedState(this, previousState);
|
|
@@ -5057,20 +5077,20 @@ function reconstituteAggregateFromHistory(createReplayTarget, history) {
|
|
|
5057
5077
|
* The aggregate owns identity, valid domain state, behavior, its current
|
|
5058
5078
|
* domain version, and pending domain events. It deliberately does not own a
|
|
5059
5079
|
* database baseline or dirty-key bookkeeping. A repository adapter defines
|
|
5060
|
-
* its own persistence
|
|
5080
|
+
* its own persistence capture; the application shell retains that opaque
|
|
5061
5081
|
* baseline and derives the adapter's change set at flush.
|
|
5062
5082
|
*/
|
|
5063
5083
|
var StateStoredAggregate = class extends BaseAggregate {
|
|
5064
5084
|
/**
|
|
5065
5085
|
* Replaces the state, advances the OCC version, and records the events
|
|
5066
|
-
* of the change, in that order. State validation, the
|
|
5086
|
+
* of the change, in that order. State validation, the kit-built check,
|
|
5067
5087
|
* the aggregate identity check of the event, the pending event-id check,
|
|
5068
5088
|
* and the pending event limit check run before the change becomes
|
|
5069
5089
|
* observable, so a rejected decision records nothing and moves nothing.
|
|
5070
5090
|
* Without events the call is a plain versioned state change.
|
|
5071
5091
|
*/
|
|
5072
5092
|
setState(newState, events = []) {
|
|
5073
|
-
const stamped = (Array.isArray(events) ? events : [events]).map((event) => this.
|
|
5093
|
+
const stamped = (Array.isArray(events) ? events : [events]).map((event) => this.attributeNewEvent(event));
|
|
5074
5094
|
this.assertEventIdsNotPending(stamped);
|
|
5075
5095
|
this.assertPendingEventLimit(stamped.length);
|
|
5076
5096
|
const next = this.nextVersion();
|
|
@@ -5229,684 +5249,16 @@ var NotSpecification = class extends Specification {
|
|
|
5229
5249
|
};
|
|
5230
5250
|
|
|
5231
5251
|
//#endregion
|
|
5232
|
-
//#region src/domain/
|
|
5233
|
-
/** No transition is defined for the input in the current state. */
|
|
5234
|
-
var InvalidDomainTransitionError = class extends DomainError {
|
|
5235
|
-
state;
|
|
5236
|
-
inputType;
|
|
5237
|
-
constructor(state, inputType) {
|
|
5238
|
-
super({
|
|
5239
|
-
code: "INVALID_DOMAIN_TRANSITION",
|
|
5240
|
-
message: `No domain transition from "${state}" on "${inputType}".`
|
|
5241
|
-
});
|
|
5242
|
-
this.state = state;
|
|
5243
|
-
this.inputType = inputType;
|
|
5244
|
-
}
|
|
5245
|
-
};
|
|
5246
|
-
/** A defined transition was rejected by its domain guard. */
|
|
5247
|
-
var DomainTransitionGuardRejectedError = class extends DomainError {
|
|
5248
|
-
state;
|
|
5249
|
-
inputType;
|
|
5250
|
-
constructor(state, inputType) {
|
|
5251
|
-
super({
|
|
5252
|
-
code: "DOMAIN_TRANSITION_GUARD_REJECTED",
|
|
5253
|
-
message: `Domain transition guard rejected "${inputType}" from "${state}".`
|
|
5254
|
-
});
|
|
5255
|
-
this.state = state;
|
|
5256
|
-
this.inputType = inputType;
|
|
5257
|
-
}
|
|
5258
|
-
};
|
|
5259
|
-
/** The machine definition violates its runtime contract. */
|
|
5260
|
-
var InvalidDomainMachineDefinitionError = class extends KitWiringError {
|
|
5261
|
-
constructor(message, cause) {
|
|
5262
|
-
super("INVALID_DOMAIN_MACHINE_DEFINITION", message, cause);
|
|
5263
|
-
}
|
|
5264
|
-
};
|
|
5265
|
-
/** Context contains unsupported or unsafe runtime data. */
|
|
5266
|
-
var InvalidDomainMachineContextError = class extends KitWiringError {
|
|
5267
|
-
constructor(message, cause) {
|
|
5268
|
-
super("INVALID_DOMAIN_MACHINE_CONTEXT", message, cause);
|
|
5269
|
-
}
|
|
5270
|
-
};
|
|
5271
|
-
/** A supplied or produced snapshot is malformed or violates invariants. */
|
|
5272
|
-
var InvalidDomainMachineSnapshotError = class extends KitWiringError {
|
|
5273
|
-
constructor(message, cause) {
|
|
5274
|
-
super("INVALID_DOMAIN_MACHINE_SNAPSHOT", message, cause);
|
|
5275
|
-
}
|
|
5276
|
-
};
|
|
5277
|
-
/** An input is malformed or contains unsupported runtime data. */
|
|
5278
|
-
var InvalidDomainMachineInputError = class extends KitWiringError {
|
|
5279
|
-
constructor(message, cause) {
|
|
5280
|
-
super("INVALID_DOMAIN_MACHINE_INPUT", message, cause);
|
|
5281
|
-
}
|
|
5282
|
-
};
|
|
5283
|
-
/** A guard returned a value other than `boolean` or `DomainError`. */
|
|
5284
|
-
var InvalidDomainTransitionGuardResultError = class extends KitWiringError {
|
|
5285
|
-
constructor(message, cause) {
|
|
5286
|
-
super("INVALID_DOMAIN_TRANSITION_GUARD_RESULT", message, cause);
|
|
5287
|
-
}
|
|
5288
|
-
};
|
|
5289
|
-
/** A reducer returned a malformed result or unsupported output data. */
|
|
5290
|
-
var InvalidDomainTransitionResultError = class extends KitWiringError {
|
|
5291
|
-
constructor(message, cause) {
|
|
5292
|
-
super("INVALID_DOMAIN_TRANSITION_RESULT", message, cause);
|
|
5293
|
-
}
|
|
5294
|
-
};
|
|
5295
|
-
/** A callback attempted to evaluate the same stateful machine recursively. */
|
|
5296
|
-
var ReentrantDomainStateMachineEvaluationError = class extends KitWiringError {
|
|
5297
|
-
constructor() {
|
|
5298
|
-
super("REENTRANT_DOMAIN_STATE_MACHINE_EVALUATION", "Domain state machine callbacks cannot evaluate the same machine.");
|
|
5299
|
-
}
|
|
5300
|
-
};
|
|
5301
|
-
|
|
5302
|
-
//#endregion
|
|
5303
|
-
//#region src/domain/state-machine/machine-data.ts
|
|
5304
|
-
const DOMAIN_MACHINE_DATA_MAX_DEPTH = 256;
|
|
5305
|
-
const DOMAIN_MACHINE_DATA_MAX_NODES = 1e4;
|
|
5306
|
-
const DOMAIN_MACHINE_DATA_MAX_PROPERTIES = 1e5;
|
|
5307
|
-
function copyDomainMachineOutputs(outputs) {
|
|
5308
|
-
try {
|
|
5309
|
-
const copiedOutputs = cloneDomainMachineDataValue(outputs ?? [], createDomainTransitionOutputError);
|
|
5310
|
-
return deepFreeze(copiedOutputs);
|
|
5311
|
-
} catch (cause) {
|
|
5312
|
-
if (cause instanceof InvalidDomainTransitionResultError) throw cause;
|
|
5313
|
-
throw new InvalidDomainTransitionResultError("Domain transition result outputs must contain cloneable, deeply immutable data.", cause);
|
|
5314
|
-
}
|
|
5315
|
-
}
|
|
5316
|
-
function copyDomainMachineInput(input) {
|
|
5317
|
-
try {
|
|
5318
|
-
return deepFreeze(cloneDomainMachineDataValue(input, createDomainMachineInputError));
|
|
5319
|
-
} catch (cause) {
|
|
5320
|
-
if (cause instanceof InvalidDomainMachineInputError) throw cause;
|
|
5321
|
-
throw new InvalidDomainMachineInputError("Domain machine input must contain cloneable, deeply immutable data.", cause);
|
|
5322
|
-
}
|
|
5323
|
-
}
|
|
5324
|
-
function copyDomainMachineContext(context) {
|
|
5325
|
-
try {
|
|
5326
|
-
return deepFreeze(cloneDomainMachineDataValue(context, createDomainMachineContextError));
|
|
5327
|
-
} catch (cause) {
|
|
5328
|
-
if (cause instanceof InvalidDomainMachineContextError) throw cause;
|
|
5329
|
-
throw new InvalidDomainMachineContextError("Domain machine context must contain cloneable, deeply immutable data.", cause);
|
|
5330
|
-
}
|
|
5331
|
-
}
|
|
5332
|
-
function createDomainMachineContextError(message, cause) {
|
|
5333
|
-
return new InvalidDomainMachineContextError(message, cause);
|
|
5334
|
-
}
|
|
5335
|
-
function createDomainMachineInputError(message, cause) {
|
|
5336
|
-
return new InvalidDomainMachineInputError(message, cause);
|
|
5337
|
-
}
|
|
5338
|
-
function createDomainTransitionOutputError(message, cause) {
|
|
5339
|
-
return new InvalidDomainTransitionResultError(message, cause);
|
|
5340
|
-
}
|
|
5341
|
-
function cloneDomainMachineDataValue(value, errorFactory, seen = /* @__PURE__ */ new WeakMap(), traversal = {
|
|
5342
|
-
nodes: 0,
|
|
5343
|
-
properties: 0
|
|
5344
|
-
}, depth = 0) {
|
|
5345
|
-
if (typeof value === "function") throw errorFactory("Domain machine data cannot contain function values.");
|
|
5346
|
-
if (value === null || typeof value !== "object") return value;
|
|
5347
|
-
const source = value;
|
|
5348
|
-
if (depth > DOMAIN_MACHINE_DATA_MAX_DEPTH) throw errorFactory(`Domain machine data exceeds the maximum depth of ${DOMAIN_MACHINE_DATA_MAX_DEPTH}.`);
|
|
5349
|
-
const existing = seen.get(source);
|
|
5350
|
-
if (existing !== void 0) return existing;
|
|
5351
|
-
traversal.nodes += 1;
|
|
5352
|
-
if (traversal.nodes > DOMAIN_MACHINE_DATA_MAX_NODES) throw errorFactory(`Domain machine data contains more than ${DOMAIN_MACHINE_DATA_MAX_NODES.toLocaleString("en-US")} object nodes.`);
|
|
5353
|
-
const toStringTagDescriptor = findPropertyDescriptor(source, Symbol.toStringTag);
|
|
5354
|
-
if (toStringTagDescriptor !== void 0 && !("value" in toStringTagDescriptor)) throw errorFactory("Domain machine data cannot contain accessor properties.");
|
|
5355
|
-
if (Array.isArray(value)) {
|
|
5356
|
-
if (!isIntrinsicArrayPrototype(Object.getPrototypeOf(value))) throw errorFactory("Domain machine data cannot contain custom Array instances.");
|
|
5357
|
-
const cloned = new Array(value.length);
|
|
5358
|
-
seen.set(source, cloned);
|
|
5359
|
-
for (const key of readDomainMachineDataKeys(source, errorFactory, traversal)) {
|
|
5360
|
-
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
|
5361
|
-
if (!descriptor) continue;
|
|
5362
|
-
if (!("value" in descriptor)) throw errorFactory("Domain machine data cannot contain accessor properties.");
|
|
5363
|
-
if (key === "length") continue;
|
|
5364
|
-
descriptor.value = cloneDomainMachineDataValue(descriptor.value, errorFactory, seen, traversal, depth + 1);
|
|
5365
|
-
Object.defineProperty(cloned, key, descriptor);
|
|
5366
|
-
}
|
|
5367
|
-
return cloned;
|
|
5368
|
-
}
|
|
5369
|
-
const prototype = Object.getPrototypeOf(source);
|
|
5370
|
-
if (prototype !== null && !isIntrinsicObjectPrototype(prototype)) throw errorFactory("Domain machine data cannot contain custom class instances.");
|
|
5371
|
-
const tag = Object.prototype.toString.call(source);
|
|
5372
|
-
if (isBuiltInObject(source, tag) || ArrayBuffer.isView(source)) throw errorFactory(`Domain machine data cannot contain ${tag.slice(8, -1)} object values.`);
|
|
5373
|
-
const cloned = Object.create(prototype === null ? null : Object.prototype);
|
|
5374
|
-
seen.set(source, cloned);
|
|
5375
|
-
for (const key of readDomainMachineDataKeys(source, errorFactory, traversal)) {
|
|
5376
|
-
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
|
5377
|
-
if (!descriptor) continue;
|
|
5378
|
-
if (!("value" in descriptor)) throw errorFactory("Domain machine data cannot contain accessor properties.");
|
|
5379
|
-
descriptor.value = cloneDomainMachineDataValue(descriptor.value, errorFactory, seen, traversal, depth + 1);
|
|
5380
|
-
Object.defineProperty(cloned, key, descriptor);
|
|
5381
|
-
}
|
|
5382
|
-
return cloned;
|
|
5383
|
-
}
|
|
5384
|
-
function readDomainMachineDataKeys(value, errorFactory, traversal) {
|
|
5385
|
-
const keys = Reflect.ownKeys(value);
|
|
5386
|
-
traversal.properties += keys.length;
|
|
5387
|
-
if (traversal.properties > DOMAIN_MACHINE_DATA_MAX_PROPERTIES) throw errorFactory(`Domain machine data contains more than ${DOMAIN_MACHINE_DATA_MAX_PROPERTIES.toLocaleString("en-US")} own properties.`);
|
|
5388
|
-
return keys;
|
|
5389
|
-
}
|
|
5390
|
-
function isRecord(value) {
|
|
5391
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5392
|
-
}
|
|
5393
|
-
function isPlainRecord(value) {
|
|
5394
|
-
if (!isRecord(value)) return false;
|
|
5395
|
-
const prototype = Object.getPrototypeOf(value);
|
|
5396
|
-
return prototype === null || isIntrinsicObjectPrototype(prototype);
|
|
5397
|
-
}
|
|
5398
|
-
function isIntrinsicArrayPrototype(prototype) {
|
|
5399
|
-
if (prototype === null || !Array.isArray(prototype)) return false;
|
|
5400
|
-
if (!isIntrinsicConstructorPrototype(prototype, "Array")) return false;
|
|
5401
|
-
const parentPrototype = Object.getPrototypeOf(prototype);
|
|
5402
|
-
return parentPrototype !== null && isIntrinsicObjectPrototype(parentPrototype);
|
|
5403
|
-
}
|
|
5404
|
-
function isIntrinsicObjectPrototype(prototype) {
|
|
5405
|
-
return Object.getPrototypeOf(prototype) === null && isIntrinsicConstructorPrototype(prototype, "Object");
|
|
5406
|
-
}
|
|
5407
|
-
function hasOwn(value, key) {
|
|
5408
|
-
return Object.hasOwn(value, key);
|
|
5409
|
-
}
|
|
5410
|
-
|
|
5411
|
-
//#endregion
|
|
5412
|
-
//#region src/domain/state-machine/definition.ts
|
|
5413
|
-
const DOMAIN_MACHINE_DEFINITION_KEYS = /* @__PURE__ */ new Set([
|
|
5414
|
-
"initial",
|
|
5415
|
-
"initialContext",
|
|
5416
|
-
"validateSnapshot",
|
|
5417
|
-
"states"
|
|
5418
|
-
]);
|
|
5419
|
-
const DOMAIN_MACHINE_STATE_NODE_KEYS = /* @__PURE__ */ new Set([
|
|
5420
|
-
"terminal",
|
|
5421
|
-
"validateContext",
|
|
5422
|
-
"on"
|
|
5423
|
-
]);
|
|
5424
|
-
const DOMAIN_MACHINE_TRANSITION_KEYS = /* @__PURE__ */ new Set([
|
|
5425
|
-
"target",
|
|
5426
|
-
"guard",
|
|
5427
|
-
"reduce"
|
|
5428
|
-
]);
|
|
5429
|
-
function copyDomainMachineDefinition(definition) {
|
|
5430
|
-
const states = readDomainMachineDefinitionProperty(definition, "states");
|
|
5431
|
-
const copiedStates = Object.create(null);
|
|
5432
|
-
for (const state of Object.keys(states)) {
|
|
5433
|
-
const node = readDomainMachineDefinitionProperty(states, state);
|
|
5434
|
-
const transitions = readOptionalDomainMachineDefinitionProperty(node, "on");
|
|
5435
|
-
const copiedTransitions = Object.create(null);
|
|
5436
|
-
for (const inputType of Object.keys(transitions ?? {})) {
|
|
5437
|
-
const transition = readDomainMachineDefinitionProperty(transitions, inputType);
|
|
5438
|
-
if (transition) {
|
|
5439
|
-
const copiedTransition = Object.freeze({
|
|
5440
|
-
target: readDomainMachineDefinitionProperty(transition, "target"),
|
|
5441
|
-
guard: readOptionalDomainMachineDefinitionProperty(transition, "guard"),
|
|
5442
|
-
reduce: readOptionalDomainMachineDefinitionProperty(transition, "reduce")
|
|
5443
|
-
});
|
|
5444
|
-
Object.defineProperty(copiedTransitions, inputType, {
|
|
5445
|
-
value: copiedTransition,
|
|
5446
|
-
enumerable: true
|
|
5447
|
-
});
|
|
5448
|
-
}
|
|
5449
|
-
}
|
|
5450
|
-
Object.defineProperty(copiedStates, state, {
|
|
5451
|
-
value: Object.freeze({
|
|
5452
|
-
terminal: readOptionalDomainMachineDefinitionProperty(node, "terminal"),
|
|
5453
|
-
validateContext: readOptionalDomainMachineDefinitionProperty(node, "validateContext"),
|
|
5454
|
-
on: Object.freeze(copiedTransitions)
|
|
5455
|
-
}),
|
|
5456
|
-
enumerable: true
|
|
5457
|
-
});
|
|
5458
|
-
}
|
|
5459
|
-
return Object.freeze({
|
|
5460
|
-
initial: readDomainMachineDefinitionProperty(definition, "initial"),
|
|
5461
|
-
initialContext: readDomainMachineDefinitionProperty(definition, "initialContext"),
|
|
5462
|
-
validateSnapshot: readOptionalDomainMachineDefinitionProperty(definition, "validateSnapshot"),
|
|
5463
|
-
states: Object.freeze(copiedStates)
|
|
5464
|
-
});
|
|
5465
|
-
}
|
|
5466
|
-
/**
|
|
5467
|
-
* Registry of definitions produced by `prepareDomainMachineDefinition`:
|
|
5468
|
-
* validated, defensively copied, deeply frozen. The runtime membership
|
|
5469
|
-
* proof lives in this module-private WeakSet (the stable copies are
|
|
5470
|
-
* frozen and must stay pure data, so no runtime brand property); the
|
|
5471
|
-
* compile-time proof is the required type brand on
|
|
5472
|
-
* {@link PreparedDomainMachineDefinition}. Only
|
|
5473
|
-
* `prepareDomainMachineDefinition` below adds to the set, so membership
|
|
5474
|
-
* always implies validated + copied + frozen.
|
|
5475
|
-
*/
|
|
5476
|
-
const preparedDefinitions = /* @__PURE__ */ new WeakSet();
|
|
5477
|
-
/**
|
|
5478
|
-
* The entry-point normalization every pure function and the
|
|
5479
|
-
* `DomainStateMachine` constructor share: a prepared definition passes
|
|
5480
|
-
* through untouched (already validated, copied, frozen); anything else
|
|
5481
|
-
* pays the documented per-call validate-and-copy.
|
|
5482
|
-
*/
|
|
5483
|
-
function ensureStableDomainMachineDefinition(definition) {
|
|
5484
|
-
if (preparedDefinitions.has(definition)) return definition;
|
|
5485
|
-
validateDomainMachineDefinition(definition);
|
|
5486
|
-
const stable = copyDomainMachineDefinition(definition);
|
|
5487
|
-
validateDomainMachineDefinition(stable);
|
|
5488
|
-
return stable;
|
|
5489
|
-
}
|
|
5490
|
-
/**
|
|
5491
|
-
* Validates and stabilizes a machine definition ONCE, for repeated use
|
|
5492
|
-
* with the pure functions. Without it, `transitionDomainState` and
|
|
5493
|
-
* `canTransitionDomainState` re-validate and defensively re-copy the
|
|
5494
|
-
* WHOLE definition on every call (the documented safety of the raw
|
|
5495
|
-
* path); on a hot dispatch path that is avoidable O(definition) work.
|
|
5496
|
-
* The `DomainStateMachine` class does the equivalent once in its
|
|
5497
|
-
* constructor; this export brings the same amortization to pure-API
|
|
5498
|
-
* users:
|
|
5499
|
-
*
|
|
5500
|
-
* ```ts
|
|
5501
|
-
* const prepared = prepareDomainMachineDefinition(orderLifecycle);
|
|
5502
|
-
* // per dispatch: no re-validation, no definition copy
|
|
5503
|
-
* const outcome = transitionDomainState(prepared, snapshot, input);
|
|
5504
|
-
* ```
|
|
5505
|
-
*
|
|
5506
|
-
* The returned definition is a deeply frozen copy, isolated from later
|
|
5507
|
-
* mutation of the input object. Preparing an already-prepared
|
|
5508
|
-
* definition returns it unchanged.
|
|
5509
|
-
*/
|
|
5510
|
-
function prepareDomainMachineDefinition(definition) {
|
|
5511
|
-
const stable = ensureStableDomainMachineDefinition(definition);
|
|
5512
|
-
preparedDefinitions.add(stable);
|
|
5513
|
-
return stable;
|
|
5514
|
-
}
|
|
5515
|
-
function getTransition(definition, state, input) {
|
|
5516
|
-
const transitions = definition.states[state].on;
|
|
5517
|
-
if (!transitions || !hasOwn(transitions, input.type)) return void 0;
|
|
5518
|
-
return transitions[input.type];
|
|
5519
|
-
}
|
|
5520
|
-
function validateDomainMachineDefinition(definition) {
|
|
5521
|
-
const candidate = definition;
|
|
5522
|
-
if (!isPlainRecord(candidate)) throw new InvalidDomainMachineDefinitionError("Domain machine definition must be a plain object.");
|
|
5523
|
-
assertDomainMachineDefinitionDataProperties(candidate, DOMAIN_MACHINE_DEFINITION_KEYS);
|
|
5524
|
-
const initial = readDomainMachineDefinitionProperty(candidate, "initial");
|
|
5525
|
-
if (typeof initial !== "string") throw new InvalidDomainMachineDefinitionError("Domain machine initial state must be a string data property.");
|
|
5526
|
-
if (typeof readDomainMachineDefinitionProperty(candidate, "initialContext") !== "function") throw new InvalidDomainMachineDefinitionError("Domain machine initialContext must be a function data property.");
|
|
5527
|
-
const validateSnapshot = readOptionalDomainMachineDefinitionProperty(candidate, "validateSnapshot");
|
|
5528
|
-
if (validateSnapshot !== void 0 && typeof validateSnapshot !== "function") throw new InvalidDomainMachineDefinitionError("Domain machine validateSnapshot must be a function data property.");
|
|
5529
|
-
const statesCandidate = readDomainMachineDefinitionProperty(candidate, "states");
|
|
5530
|
-
if (!isPlainRecord(statesCandidate)) throw new InvalidDomainMachineDefinitionError("Domain machine states must be a plain object data property.");
|
|
5531
|
-
const states = statesCandidate;
|
|
5532
|
-
assertDomainMachineDefinitionEntryMap(states, "state");
|
|
5533
|
-
if (!hasOwn(states, initial)) throw new InvalidDomainMachineDefinitionError(`Initial domain machine state "${initial}" is not defined.`);
|
|
5534
|
-
for (const state of Object.keys(states)) {
|
|
5535
|
-
const node = readDomainMachineDefinitionProperty(states, state);
|
|
5536
|
-
if (!isPlainRecord(node)) throw new InvalidDomainMachineDefinitionError(`Domain machine state "${state}" must be a plain object data property.`);
|
|
5537
|
-
assertDomainMachineDefinitionDataProperties(node, DOMAIN_MACHINE_STATE_NODE_KEYS);
|
|
5538
|
-
const terminal = readOptionalDomainMachineDefinitionProperty(node, "terminal");
|
|
5539
|
-
if (terminal !== void 0 && typeof terminal !== "boolean") throw new InvalidDomainMachineDefinitionError(`Domain machine state "${state}" terminal flag must be a boolean.`);
|
|
5540
|
-
const validateContext = readOptionalDomainMachineDefinitionProperty(node, "validateContext");
|
|
5541
|
-
if (validateContext !== void 0 && typeof validateContext !== "function") throw new InvalidDomainMachineDefinitionError(`Domain machine state "${state}" validateContext must be a function.`);
|
|
5542
|
-
const transitions = readOptionalDomainMachineDefinitionProperty(node, "on");
|
|
5543
|
-
if (transitions !== void 0 && !isPlainRecord(transitions)) throw new InvalidDomainMachineDefinitionError(`Domain machine state "${state}" transitions must be a plain object.`);
|
|
5544
|
-
if (isPlainRecord(transitions)) assertDomainMachineDefinitionEntryMap(transitions, "input");
|
|
5545
|
-
const inputTypes = Object.keys(transitions ?? {});
|
|
5546
|
-
if (terminal === true && inputTypes.length > 0) throw new InvalidDomainMachineDefinitionError(`Terminal domain machine state "${state}" cannot declare transitions.`);
|
|
5547
|
-
for (const inputType of inputTypes) {
|
|
5548
|
-
const transition = readDomainMachineDefinitionProperty(transitions, inputType);
|
|
5549
|
-
if (!isPlainRecord(transition)) throw new InvalidDomainMachineDefinitionError(`Domain transition from "${state}" on "${inputType}" must be a plain object.`);
|
|
5550
|
-
assertDomainMachineDefinitionDataProperties(transition, DOMAIN_MACHINE_TRANSITION_KEYS);
|
|
5551
|
-
const target = readDomainMachineDefinitionProperty(transition, "target");
|
|
5552
|
-
if (typeof target !== "string") throw new InvalidDomainMachineDefinitionError(`Domain transition from "${state}" on "${inputType}" must target a string state.`);
|
|
5553
|
-
if (!hasOwn(states, target)) throw new InvalidDomainMachineDefinitionError(`Domain transition from "${state}" on "${inputType}" targets unknown state "${target}".`);
|
|
5554
|
-
const guard = readOptionalDomainMachineDefinitionProperty(transition, "guard");
|
|
5555
|
-
if (guard !== void 0 && typeof guard !== "function") throw new InvalidDomainMachineDefinitionError(`Domain transition from "${state}" on "${inputType}" guard must be a function.`);
|
|
5556
|
-
const reduce = readOptionalDomainMachineDefinitionProperty(transition, "reduce");
|
|
5557
|
-
if (reduce !== void 0 && typeof reduce !== "function") throw new InvalidDomainMachineDefinitionError(`Domain transition from "${state}" on "${inputType}" reduce must be a function.`);
|
|
5558
|
-
}
|
|
5559
|
-
}
|
|
5560
|
-
}
|
|
5561
|
-
function assertDomainMachineDefinitionDataProperties(value, allowedKeys) {
|
|
5562
|
-
for (const key of Reflect.ownKeys(value)) {
|
|
5563
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
5564
|
-
if (descriptor !== void 0 && !("value" in descriptor)) throw new InvalidDomainMachineDefinitionError("Domain machine definition must contain data properties only.");
|
|
5565
|
-
if (allowedKeys !== void 0 && !allowedKeys.has(key)) throw new InvalidDomainMachineDefinitionError(`Domain machine definition contains unknown property "${String(key)}".`);
|
|
5566
|
-
}
|
|
5567
|
-
}
|
|
5568
|
-
function assertDomainMachineDefinitionEntryMap(value, entryName) {
|
|
5569
|
-
assertDomainMachineDefinitionDataProperties(value);
|
|
5570
|
-
for (const key of Reflect.ownKeys(value)) {
|
|
5571
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
5572
|
-
if (typeof key !== "string" || descriptor?.enumerable !== true) throw new InvalidDomainMachineDefinitionError(`Domain machine ${entryName} names must be enumerable string properties.`);
|
|
5573
|
-
}
|
|
5574
|
-
}
|
|
5575
|
-
function readDomainMachineDefinitionProperty(value, key) {
|
|
5576
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
5577
|
-
if (descriptor === void 0 || !("value" in descriptor)) throw new InvalidDomainMachineDefinitionError("Domain machine definition must contain data properties only.");
|
|
5578
|
-
return descriptor.value;
|
|
5579
|
-
}
|
|
5580
|
-
function readOptionalDomainMachineDefinitionProperty(value, key) {
|
|
5581
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
5582
|
-
if (descriptor === void 0) return void 0;
|
|
5583
|
-
if (!("value" in descriptor)) throw new InvalidDomainMachineDefinitionError("Domain machine definition must contain data properties only.");
|
|
5584
|
-
return descriptor.value;
|
|
5585
|
-
}
|
|
5586
|
-
|
|
5587
|
-
//#endregion
|
|
5588
|
-
//#region src/domain/state-machine/snapshot.ts
|
|
5589
|
-
const DOMAIN_TRANSITION_RESULT_KEYS = /* @__PURE__ */ new Set(["context", "outputs"]);
|
|
5590
|
-
function prepareDomainMachineSnapshot(definition, snapshot) {
|
|
5591
|
-
validateDomainMachineSnapshot(definition, snapshot);
|
|
5592
|
-
const preparedSnapshot = createDomainMachineSnapshot(snapshot);
|
|
5593
|
-
validateDomainMachineSnapshot(definition, preparedSnapshot);
|
|
5594
|
-
validateDomainMachineSnapshotInvariant(definition, preparedSnapshot);
|
|
5595
|
-
return preparedSnapshot;
|
|
5596
|
-
}
|
|
5597
|
-
function createDomainMachineSnapshotFromPreparedContext(state, context) {
|
|
5598
|
-
return Object.freeze({
|
|
5599
|
-
state,
|
|
5600
|
-
context
|
|
5601
|
-
});
|
|
5602
|
-
}
|
|
5603
|
-
function createDomainMachineSnapshot(snapshot) {
|
|
5604
|
-
return Object.freeze({
|
|
5605
|
-
state: readDomainMachineSnapshotState(snapshot),
|
|
5606
|
-
context: copyDomainMachineContext(readDomainMachineSnapshotContext(snapshot))
|
|
5607
|
-
});
|
|
5608
|
-
}
|
|
5609
|
-
function validateDomainMachineSnapshot(definition, snapshot) {
|
|
5610
|
-
if (!isRecord(snapshot)) throw new InvalidDomainMachineSnapshotError("Domain machine snapshot must be an object.");
|
|
5611
|
-
const state = readDomainMachineSnapshotState(snapshot);
|
|
5612
|
-
readDomainMachineSnapshotContext(snapshot);
|
|
5613
|
-
if (!hasOwn(definition.states, state)) throw new InvalidDomainMachineSnapshotError(`Domain machine snapshot state "${state}" is not defined.`);
|
|
5614
|
-
}
|
|
5615
|
-
function validateDomainMachineSnapshotInvariant(definition, snapshot) {
|
|
5616
|
-
const stateNode = definition.states[snapshot.state];
|
|
5617
|
-
if (stateNode.validateContext !== void 0) {
|
|
5618
|
-
const validContext = stateNode.validateContext({
|
|
5619
|
-
state: snapshot.state,
|
|
5620
|
-
context: snapshot.context
|
|
5621
|
-
});
|
|
5622
|
-
if (typeof validContext !== "boolean") throw new InvalidDomainMachineDefinitionError(`Domain machine state "${snapshot.state}" validateContext must return a boolean.`);
|
|
5623
|
-
if (!validContext) throw new InvalidDomainMachineSnapshotError(`Domain machine snapshot violates the context invariant for state "${snapshot.state}".`);
|
|
5624
|
-
}
|
|
5625
|
-
if (definition.validateSnapshot === void 0) return;
|
|
5626
|
-
const valid = definition.validateSnapshot(snapshot);
|
|
5627
|
-
if (typeof valid !== "boolean") throw new InvalidDomainMachineDefinitionError("Domain machine validateSnapshot must return a boolean.");
|
|
5628
|
-
if (!valid) throw new InvalidDomainMachineSnapshotError(`Domain machine snapshot violates invariants for state "${snapshot.state}".`);
|
|
5629
|
-
}
|
|
5630
|
-
function readDomainMachineSnapshotState(snapshot) {
|
|
5631
|
-
const stateDescriptor = Object.getOwnPropertyDescriptor(snapshot, "state");
|
|
5632
|
-
if (stateDescriptor === void 0 || !("value" in stateDescriptor) || typeof stateDescriptor.value !== "string") throw new InvalidDomainMachineSnapshotError("Domain machine snapshot state must be a string data property.");
|
|
5633
|
-
return stateDescriptor.value;
|
|
5634
|
-
}
|
|
5635
|
-
function readDomainMachineSnapshotContext(snapshot) {
|
|
5636
|
-
const contextDescriptor = Object.getOwnPropertyDescriptor(snapshot, "context");
|
|
5637
|
-
if (contextDescriptor === void 0 || !("value" in contextDescriptor)) throw new InvalidDomainMachineSnapshotError("Domain machine snapshot context must be present as a data property.");
|
|
5638
|
-
return contextDescriptor.value;
|
|
5639
|
-
}
|
|
5640
|
-
function validateDomainMachineInput(input) {
|
|
5641
|
-
if (!isDomainMachineInput(input)) throw new InvalidDomainMachineInputError("Domain machine input must be an object with a string type.");
|
|
5642
|
-
}
|
|
5643
|
-
function isDomainMachineInput(input) {
|
|
5644
|
-
if (!isRecord(input)) return false;
|
|
5645
|
-
const typeDescriptor = Object.getOwnPropertyDescriptor(input, "type");
|
|
5646
|
-
return typeDescriptor !== void 0 && "value" in typeDescriptor && typeof typeDescriptor.value === "string";
|
|
5647
|
-
}
|
|
5648
|
-
function resolveDomainTransitionGuardResult(result) {
|
|
5649
|
-
if (typeof result === "boolean") return { allowed: result };
|
|
5650
|
-
if (result instanceof DomainError) return {
|
|
5651
|
-
allowed: false,
|
|
5652
|
-
rejection: result
|
|
5653
|
-
};
|
|
5654
|
-
throw new InvalidDomainTransitionGuardResultError("Domain transition guard must return a boolean or DomainError.");
|
|
5655
|
-
}
|
|
5656
|
-
function validateDomainTransitionResult(result) {
|
|
5657
|
-
if (result === void 0) return;
|
|
5658
|
-
if (!isPlainRecord(result)) throw new InvalidDomainTransitionResultError("Domain transition result must be a plain object when returned.");
|
|
5659
|
-
for (const key of Reflect.ownKeys(result)) {
|
|
5660
|
-
if (!DOMAIN_TRANSITION_RESULT_KEYS.has(key)) throw new InvalidDomainTransitionResultError(`Domain transition result contains unknown property "${String(key)}".`);
|
|
5661
|
-
const descriptor = Object.getOwnPropertyDescriptor(result, key);
|
|
5662
|
-
if (descriptor !== void 0 && !("value" in descriptor)) throw new InvalidDomainTransitionResultError("Domain transition result must contain data properties only.");
|
|
5663
|
-
}
|
|
5664
|
-
const outputs = readDomainTransitionResultOutputs(result);
|
|
5665
|
-
if (outputs !== void 0 && !Array.isArray(outputs)) throw new InvalidDomainTransitionResultError("Domain transition result outputs must be an array when provided.");
|
|
5666
|
-
}
|
|
5667
|
-
function readDomainTransitionResultContext(result) {
|
|
5668
|
-
if (result === void 0) return { hasContext: false };
|
|
5669
|
-
const contextDescriptor = Object.getOwnPropertyDescriptor(result, "context");
|
|
5670
|
-
if (contextDescriptor === void 0) return { hasContext: false };
|
|
5671
|
-
if (!("value" in contextDescriptor)) throw new InvalidDomainTransitionResultError("Domain transition result context must be a data property when provided.");
|
|
5672
|
-
return {
|
|
5673
|
-
hasContext: true,
|
|
5674
|
-
context: contextDescriptor.value
|
|
5675
|
-
};
|
|
5676
|
-
}
|
|
5677
|
-
function readDomainTransitionResultOutputs(result) {
|
|
5678
|
-
if (result === void 0) return void 0;
|
|
5679
|
-
const outputsDescriptor = Object.getOwnPropertyDescriptor(result, "outputs");
|
|
5680
|
-
if (outputsDescriptor === void 0) return void 0;
|
|
5681
|
-
if (!("value" in outputsDescriptor)) throw new InvalidDomainTransitionResultError("Domain transition result outputs must be a data property when provided.");
|
|
5682
|
-
return outputsDescriptor.value;
|
|
5683
|
-
}
|
|
5684
|
-
|
|
5685
|
-
//#endregion
|
|
5686
|
-
//#region src/domain/state-machine/transition.ts
|
|
5687
|
-
/** Creates and validates a fresh initial snapshot from a machine definition. */
|
|
5688
|
-
function createInitialDomainMachineSnapshot(definition) {
|
|
5689
|
-
return createInitialDomainMachineSnapshotFromPrepared(ensureStableDomainMachineDefinition(definition));
|
|
5690
|
-
}
|
|
5691
|
-
function createInitialDomainMachineSnapshotFromPrepared(definition) {
|
|
5692
|
-
const snapshot = createDomainMachineSnapshot({
|
|
5693
|
-
state: definition.initial,
|
|
5694
|
-
context: definition.initialContext()
|
|
5695
|
-
});
|
|
5696
|
-
validateDomainMachineSnapshotInvariant(definition, snapshot);
|
|
5697
|
-
return snapshot;
|
|
5698
|
-
}
|
|
5699
|
-
/**
|
|
5700
|
-
* Checks whether an input currently has an allowed transition.
|
|
5701
|
-
*
|
|
5702
|
-
* Returns `false` for missing transitions, terminal states, rejected guards,
|
|
5703
|
-
* and inputs without an own string `type` property. Invalid payload data for a
|
|
5704
|
-
* matching transition and broken guard code still throw structured errors.
|
|
5705
|
-
*/
|
|
5706
|
-
function canTransitionDomainState(definition, snapshot, input) {
|
|
5707
|
-
const stableDefinition = ensureStableDomainMachineDefinition(definition);
|
|
5708
|
-
return canTransitionPreparedDomainState(stableDefinition, prepareDomainMachineSnapshot(stableDefinition, snapshot), input);
|
|
5709
|
-
}
|
|
5710
|
-
function canTransitionPreparedDomainState(definition, snapshot, input) {
|
|
5711
|
-
if (!isDomainMachineInput(input)) return false;
|
|
5712
|
-
if (definition.states[snapshot.state].terminal === true) return false;
|
|
5713
|
-
const transition = getTransition(definition, snapshot.state, input);
|
|
5714
|
-
if (!transition) return false;
|
|
5715
|
-
const currentInput = copyDomainMachineInput(input);
|
|
5716
|
-
if (!transition.guard) return true;
|
|
5717
|
-
const guardResult = transition.guard({
|
|
5718
|
-
state: snapshot.state,
|
|
5719
|
-
context: snapshot.context,
|
|
5720
|
-
input: currentInput
|
|
5721
|
-
});
|
|
5722
|
-
return resolveDomainTransitionGuardResult(guardResult).allowed;
|
|
5723
|
-
}
|
|
5724
|
-
/**
|
|
5725
|
-
* Applies one input without mutating the input definition or snapshot.
|
|
5726
|
-
*
|
|
5727
|
-
* @throws {@link InvalidDomainTransitionError} when no transition is defined.
|
|
5728
|
-
* @throws {@link DomainTransitionGuardRejectedError} when its guard rejects.
|
|
5729
|
-
* @throws A concrete `DomainError` returned by a rejecting guard.
|
|
5730
|
-
*/
|
|
5731
|
-
function transitionDomainState(definition, snapshot, input) {
|
|
5732
|
-
const stableDefinition = ensureStableDomainMachineDefinition(definition);
|
|
5733
|
-
return transitionPreparedDomainState(stableDefinition, prepareDomainMachineSnapshot(stableDefinition, snapshot), input);
|
|
5734
|
-
}
|
|
5735
|
-
function transitionPreparedDomainState(definition, snapshot, input) {
|
|
5736
|
-
validateDomainMachineInput(input);
|
|
5737
|
-
const from = snapshot.state;
|
|
5738
|
-
const transition = definition.states[from].terminal === true ? void 0 : getTransition(definition, from, input);
|
|
5739
|
-
if (!transition) throw new InvalidDomainTransitionError(from, input.type);
|
|
5740
|
-
const currentInput = copyDomainMachineInput(input);
|
|
5741
|
-
const guardResult = transition.guard === void 0 ? true : transition.guard({
|
|
5742
|
-
state: from,
|
|
5743
|
-
context: snapshot.context,
|
|
5744
|
-
input: currentInput
|
|
5745
|
-
});
|
|
5746
|
-
const guardDecision = resolveDomainTransitionGuardResult(guardResult);
|
|
5747
|
-
if (!guardDecision.allowed) {
|
|
5748
|
-
if (guardDecision.rejection !== void 0) throw guardDecision.rejection;
|
|
5749
|
-
throw new DomainTransitionGuardRejectedError(from, currentInput.type);
|
|
5750
|
-
}
|
|
5751
|
-
const result = transition.reduce?.({
|
|
5752
|
-
state: from,
|
|
5753
|
-
context: snapshot.context,
|
|
5754
|
-
input: currentInput
|
|
5755
|
-
});
|
|
5756
|
-
validateDomainTransitionResult(result);
|
|
5757
|
-
const contextResult = readDomainTransitionResultContext(result);
|
|
5758
|
-
const nextContext = contextResult.hasContext ? contextResult.context : snapshot.context;
|
|
5759
|
-
const nextSnapshot = nextContext === snapshot.context ? createDomainMachineSnapshotFromPreparedContext(transition.target, snapshot.context) : createDomainMachineSnapshot({
|
|
5760
|
-
state: transition.target,
|
|
5761
|
-
context: nextContext
|
|
5762
|
-
});
|
|
5763
|
-
validateDomainMachineSnapshotInvariant(definition, nextSnapshot);
|
|
5764
|
-
return Object.freeze({
|
|
5765
|
-
from,
|
|
5766
|
-
to: transition.target,
|
|
5767
|
-
snapshot: nextSnapshot,
|
|
5768
|
-
outputs: copyDomainMachineOutputs(readDomainTransitionResultOutputs(result))
|
|
5769
|
-
});
|
|
5770
|
-
}
|
|
5771
|
-
|
|
5772
|
-
//#endregion
|
|
5773
|
-
//#region src/domain/state-machine/analyzer.ts
|
|
5774
|
-
/**
|
|
5775
|
-
* Inspects the declarative transition graph without executing definition callbacks.
|
|
5776
|
-
* Guarded edges are treated as possible edges, so diagnostics never claim more
|
|
5777
|
-
* runtime reachability than the static graph can prove.
|
|
5778
|
-
*/
|
|
5779
|
-
function analyzeDomainMachineDefinition(definition) {
|
|
5780
|
-
const stableDefinition = ensureStableDomainMachineDefinition(definition);
|
|
5781
|
-
const states = Object.keys(stableDefinition.states).sort(compareStrings);
|
|
5782
|
-
const outgoing = /* @__PURE__ */ new Map();
|
|
5783
|
-
const incoming = /* @__PURE__ */ new Map();
|
|
5784
|
-
const transitions = [];
|
|
5785
|
-
for (const state of states) {
|
|
5786
|
-
outgoing.set(state, []);
|
|
5787
|
-
incoming.set(state, []);
|
|
5788
|
-
}
|
|
5789
|
-
for (const state of states) {
|
|
5790
|
-
const stateTransitions = stableDefinition.states[state].on;
|
|
5791
|
-
const inputTypes = Object.keys(stateTransitions ?? {}).sort(compareStrings);
|
|
5792
|
-
for (const inputType of inputTypes) {
|
|
5793
|
-
const transition = stateTransitions?.[inputType];
|
|
5794
|
-
if (transition === void 0) continue;
|
|
5795
|
-
outgoing.get(state)?.push(transition.target);
|
|
5796
|
-
incoming.get(transition.target)?.push(state);
|
|
5797
|
-
transitions.push(Object.freeze({
|
|
5798
|
-
state,
|
|
5799
|
-
inputType,
|
|
5800
|
-
target: transition.target,
|
|
5801
|
-
guarded: transition.guard !== void 0
|
|
5802
|
-
}));
|
|
5803
|
-
}
|
|
5804
|
-
}
|
|
5805
|
-
const structurallyReachable = visitGraph([stableDefinition.initial], outgoing);
|
|
5806
|
-
const statesWithTerminalPath = visitGraph(states.filter((state) => stableDefinition.states[state].terminal === true), incoming);
|
|
5807
|
-
const diagnostics = [];
|
|
5808
|
-
for (const state of states) if (!structurallyReachable.has(state)) diagnostics.push(Object.freeze({
|
|
5809
|
-
code: "unreachable-state",
|
|
5810
|
-
state
|
|
5811
|
-
}));
|
|
5812
|
-
for (const state of states) if (stableDefinition.states[state].terminal !== true && outgoing.get(state)?.length === 0) diagnostics.push(Object.freeze({
|
|
5813
|
-
code: "structural-dead-end",
|
|
5814
|
-
state
|
|
5815
|
-
}));
|
|
5816
|
-
for (const state of states) if (!statesWithTerminalPath.has(state)) diagnostics.push(Object.freeze({
|
|
5817
|
-
code: "no-terminal-path",
|
|
5818
|
-
state
|
|
5819
|
-
}));
|
|
5820
|
-
return Object.freeze({
|
|
5821
|
-
diagnostics: Object.freeze(diagnostics),
|
|
5822
|
-
transitions: Object.freeze(transitions),
|
|
5823
|
-
structurallyReachableStates: Object.freeze(states.filter((state) => structurallyReachable.has(state))),
|
|
5824
|
-
statesWithTerminalPath: Object.freeze(states.filter((state) => statesWithTerminalPath.has(state)))
|
|
5825
|
-
});
|
|
5826
|
-
}
|
|
5827
|
-
function visitGraph(startStates, edges) {
|
|
5828
|
-
const visited = /* @__PURE__ */ new Set();
|
|
5829
|
-
const pending = [...startStates];
|
|
5830
|
-
while (pending.length > 0) {
|
|
5831
|
-
const state = pending.pop();
|
|
5832
|
-
if (state === void 0 || visited.has(state)) continue;
|
|
5833
|
-
visited.add(state);
|
|
5834
|
-
for (const next of edges.get(state) ?? []) if (!visited.has(next)) pending.push(next);
|
|
5835
|
-
}
|
|
5836
|
-
return visited;
|
|
5837
|
-
}
|
|
5838
|
-
function compareStrings(left, right) {
|
|
5839
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
5840
|
-
}
|
|
5841
|
-
|
|
5842
|
-
//#endregion
|
|
5843
|
-
//#region src/domain/state-machine/domain-state-machine.ts
|
|
5844
|
-
/**
|
|
5845
|
-
* Stateful convenience wrapper around the pure domain transition functions.
|
|
5846
|
-
*
|
|
5847
|
-
* Persist {@link snapshot}, not the machine instance. Pass a restored snapshot
|
|
5848
|
-
* to the second constructor overload to validate and reconstitute a machine.
|
|
5849
|
-
*/
|
|
5850
|
-
var DomainStateMachine = class {
|
|
5851
|
-
definition;
|
|
5852
|
-
#snapshot;
|
|
5853
|
-
#evaluating = false;
|
|
5854
|
-
constructor(definition, ...snapshotInput) {
|
|
5855
|
-
this.definition = ensureStableDomainMachineDefinition(definition);
|
|
5856
|
-
const [suppliedSnapshot] = snapshotInput;
|
|
5857
|
-
if (suppliedSnapshot !== void 0) this.#snapshot = prepareDomainMachineSnapshot(this.definition, suppliedSnapshot);
|
|
5858
|
-
else this.#snapshot = createInitialDomainMachineSnapshotFromPrepared(this.definition);
|
|
5859
|
-
}
|
|
5860
|
-
/** Returns a defensive, deeply frozen copy of the current snapshot. */
|
|
5861
|
-
get snapshot() {
|
|
5862
|
-
return createDomainMachineSnapshot(this.#snapshot);
|
|
5863
|
-
}
|
|
5864
|
-
/** Current named control state. */
|
|
5865
|
-
get state() {
|
|
5866
|
-
return this.#snapshot.state;
|
|
5867
|
-
}
|
|
5868
|
-
/** Current deeply readonly context. */
|
|
5869
|
-
get context() {
|
|
5870
|
-
return this.#snapshot.context;
|
|
5871
|
-
}
|
|
5872
|
-
/** Whether the current state permanently forbids outgoing transitions. */
|
|
5873
|
-
isTerminal() {
|
|
5874
|
-
return this.definition.states[this.state].terminal === true;
|
|
5875
|
-
}
|
|
5876
|
-
/** Checks a transition without changing the current snapshot. */
|
|
5877
|
-
can(input) {
|
|
5878
|
-
return this.evaluate(() => canTransitionPreparedDomainState(this.definition, this.#snapshot, input));
|
|
5879
|
-
}
|
|
5880
|
-
/** Applies an input and advances the current snapshot on success. */
|
|
5881
|
-
dispatch(input) {
|
|
5882
|
-
return this.evaluate(() => {
|
|
5883
|
-
const result = transitionPreparedDomainState(this.definition, this.#snapshot, input);
|
|
5884
|
-
this.#snapshot = result.snapshot;
|
|
5885
|
-
return result;
|
|
5886
|
-
});
|
|
5887
|
-
}
|
|
5888
|
-
evaluate(operation) {
|
|
5889
|
-
if (this.#evaluating) throw new ReentrantDomainStateMachineEvaluationError();
|
|
5890
|
-
this.#evaluating = true;
|
|
5891
|
-
try {
|
|
5892
|
-
return operation();
|
|
5893
|
-
} finally {
|
|
5894
|
-
this.#evaluating = false;
|
|
5895
|
-
}
|
|
5896
|
-
}
|
|
5897
|
-
};
|
|
5898
|
-
|
|
5899
|
-
//#endregion
|
|
5900
|
-
//#region src/domain/value-object/vo-validated.ts
|
|
5252
|
+
//#region src/domain/value-object/value-object-or-issues.ts
|
|
5901
5253
|
/**
|
|
5902
5254
|
* Builds an immutable value object while collecting **all** validation
|
|
5903
5255
|
* violations into a single {@link ValidationError}, instead of failing on the
|
|
5904
5256
|
* first one. This is the Result-first, multi-error counterpart to
|
|
5905
|
-
* `
|
|
5257
|
+
* `valueObjectOrError` (which returns a single string message).
|
|
5906
5258
|
*
|
|
5907
5259
|
* The `validate` callback receives a fresh `ValidationError` to push field
|
|
5908
5260
|
* issues onto (via `addIssue` / `addIssues`) and the raw input. When no issue
|
|
5909
|
-
* was recorded the input is frozen into a `
|
|
5261
|
+
* was recorded the input is frozen into a `ValueObjectOf<T>` and returned as `Ok`;
|
|
5910
5262
|
* otherwise the populated `ValidationError` is returned as `Err`.
|
|
5911
5263
|
*
|
|
5912
5264
|
* `ValidationError` comes from `@shirudo/base-error`; import it from there to
|
|
@@ -5916,7 +5268,7 @@ var DomainStateMachine = class {
|
|
|
5916
5268
|
*
|
|
5917
5269
|
* @example
|
|
5918
5270
|
* ```ts
|
|
5919
|
-
* const result =
|
|
5271
|
+
* const result = valueObjectOrIssues(
|
|
5920
5272
|
* { email, age },
|
|
5921
5273
|
* (issues, m) => {
|
|
5922
5274
|
* if (!isEmail(m.email))
|
|
@@ -5929,10 +5281,10 @@ var DomainStateMachine = class {
|
|
|
5929
5281
|
* // result.isErr() → result.error.publicIssues() has both violations
|
|
5930
5282
|
* ```
|
|
5931
5283
|
*/
|
|
5932
|
-
function
|
|
5284
|
+
function valueObjectOrIssues(t, validate, message = "Validation failed") {
|
|
5933
5285
|
const issues = new ValidationError(message);
|
|
5934
5286
|
validate(issues, t);
|
|
5935
|
-
return issues.hasIssues() ? err(issues) : ok(
|
|
5287
|
+
return issues.hasIssues() ? err(issues) : ok(valueObject(t));
|
|
5936
5288
|
}
|
|
5937
5289
|
|
|
5938
5290
|
//#endregion
|
|
@@ -6041,7 +5393,7 @@ var PublishChainTracker = class {
|
|
|
6041
5393
|
stateBySignal = /* @__PURE__ */ new WeakMap();
|
|
6042
5394
|
syncWindowState;
|
|
6043
5395
|
constructor(store) {
|
|
6044
|
-
if (store !== void 0 && (typeof store.run !== "function" || typeof store.getStore !== "function")) throw new TypeError("
|
|
5396
|
+
if (store !== void 0 && (typeof store.run !== "function" || typeof store.getStore !== "function")) throw new TypeError("InProcessEventBus.chainStore must provide run and getStore");
|
|
6045
5397
|
this.store = store;
|
|
6046
5398
|
}
|
|
6047
5399
|
/** Depth, path and enclosing state that a publication on `signal` inherits. */
|
|
@@ -6152,7 +5504,7 @@ function toHandlerError(reason) {
|
|
|
6152
5504
|
/** Bound for one publish chain. Real nesting stays far below this. */
|
|
6153
5505
|
const DEFAULT_MAX_PUBLISH_DEPTH = 32;
|
|
6154
5506
|
/** Marks the catch-all subscriptions in the reported-once set. */
|
|
6155
|
-
const CATCH_ALL = Symbol("
|
|
5507
|
+
const CATCH_ALL = Symbol("InProcessEventBus.subscribeAll");
|
|
6156
5508
|
/**
|
|
6157
5509
|
* Subscriptions for one event type above which the bus reports a leak. A
|
|
6158
5510
|
* fan-out of projections stays far below this.
|
|
@@ -6166,7 +5518,7 @@ const DEFAULT_MAX_SUBSCRIPTIONS_PER_EVENT_TYPE = 32;
|
|
|
6166
5518
|
*
|
|
6167
5519
|
* @example
|
|
6168
5520
|
* ```typescript
|
|
6169
|
-
* const bus = new
|
|
5521
|
+
* const bus = new InProcessEventBus<OrderEvent>();
|
|
6170
5522
|
*
|
|
6171
5523
|
* bus.subscribe("OrderCreated", async (event) => {
|
|
6172
5524
|
* await sendEmail(event.payload.customerId);
|
|
@@ -6226,7 +5578,7 @@ const DEFAULT_MAX_SUBSCRIPTIONS_PER_EVENT_TYPE = 32;
|
|
|
6226
5578
|
* the signal.
|
|
6227
5579
|
*
|
|
6228
5580
|
*/
|
|
6229
|
-
var
|
|
5581
|
+
var InProcessEventBus = class {
|
|
6230
5582
|
handlers = /* @__PURE__ */ new Map();
|
|
6231
5583
|
catchAllHandlers = [];
|
|
6232
5584
|
maxPublishDepth;
|
|
@@ -6237,12 +5589,12 @@ var EventBusImpl = class {
|
|
|
6237
5589
|
observers;
|
|
6238
5590
|
reportedAt = /* @__PURE__ */ new Map();
|
|
6239
5591
|
constructor(options = {}) {
|
|
6240
|
-
if (options.maxPublishDepth !== void 0) assertPositiveInteger("
|
|
6241
|
-
if (options.maxSubscriptionsPerEventType !== void 0) assertPositiveInteger("
|
|
5592
|
+
if (options.maxPublishDepth !== void 0) assertPositiveInteger("InProcessEventBus", "maxPublishDepth", options.maxPublishDepth);
|
|
5593
|
+
if (options.maxSubscriptionsPerEventType !== void 0) assertPositiveInteger("InProcessEventBus", "maxSubscriptionsPerEventType", options.maxSubscriptionsPerEventType);
|
|
6242
5594
|
this.chain = new PublishChainTracker(options.chainStore);
|
|
6243
5595
|
this.maxPublishDepth = options.maxPublishDepth ?? DEFAULT_MAX_PUBLISH_DEPTH;
|
|
6244
5596
|
this.maxSubscriptionsPerEventType = options.maxSubscriptionsPerEventType ?? DEFAULT_MAX_SUBSCRIPTIONS_PER_EVENT_TYPE;
|
|
6245
|
-
this.observers = options.observers === void 0 ? void 0 : captureObserverFunctions("
|
|
5597
|
+
this.observers = options.observers === void 0 ? void 0 : captureObserverFunctions("InProcessEventBus", options.observers, [
|
|
6246
5598
|
"onSubscriptionThresholdExceeded",
|
|
6247
5599
|
"onHandlerError",
|
|
6248
5600
|
"onPublishAborted"
|
|
@@ -6515,7 +5867,7 @@ function decodeIntegrationMessage(serialized) {
|
|
|
6515
5867
|
}
|
|
6516
5868
|
}
|
|
6517
5869
|
/**
|
|
6518
|
-
* Composes a validated public message into a
|
|
5870
|
+
* Composes a validated public message into a kit-built local projector input.
|
|
6519
5871
|
* Relationship headers become local event metadata. The public JSON schema is
|
|
6520
5872
|
* retained; producer-private domain types are not reconstructed.
|
|
6521
5873
|
*/
|
|
@@ -6668,9 +6020,9 @@ function outboxWriterAcceptingEventLoss() {
|
|
|
6668
6020
|
* another aggregate version. Without an `InMemoryTransactionScope`, it keeps
|
|
6669
6021
|
* the first add of a rolled-back attempt. Dead-lettered and acknowledged
|
|
6670
6022
|
* retries must match the complete original candidate receipt. Reusing an `eventId` for another source or commit position
|
|
6671
|
-
* throws {@link
|
|
6023
|
+
* throws {@link EventCollectionError} while the pending, dead-letter, or bounded
|
|
6672
6024
|
* dispatched receipt still proves the collision. `add` also throws
|
|
6673
|
-
* {@link
|
|
6025
|
+
* {@link EventCollectionError} for a malformed candidate: a part that is not an
|
|
6674
6026
|
* object, an `eventId` or `type` that is not an own string data property, a
|
|
6675
6027
|
* source field that is not a string, or a position that is not a valid
|
|
6676
6028
|
* commit position. Insertion order is preserved:
|
|
@@ -6685,7 +6037,7 @@ function outboxWriterAcceptingEventLoss() {
|
|
|
6685
6037
|
* To link future eventful commits, the implementation also retains one
|
|
6686
6038
|
* source cursor per qualified aggregate after dispatch. `endEventSources`
|
|
6687
6039
|
* marks the cursor of a removed aggregate as ended, and a new event of that
|
|
6688
|
-
* source then throws {@link
|
|
6040
|
+
* source then throws {@link EventCollectionError}. Consequently a
|
|
6689
6041
|
* long-lived instance is bounded only when `maxRecords` and `maxSources` are
|
|
6690
6042
|
* configured, plus `maxRetainedDispatchedEventIds`; use a durable adapter with
|
|
6691
6043
|
* an explicit source-head lifecycle and an event-id unique key for unbounded
|
|
@@ -6717,10 +6069,10 @@ function outboxWriterAcceptingEventLoss() {
|
|
|
6717
6069
|
*
|
|
6718
6070
|
* @example
|
|
6719
6071
|
* ```ts
|
|
6720
|
-
* import { InMemoryOutbox,
|
|
6072
|
+
* import { InMemoryOutbox, InProcessEventBus } from "@shirudo/ddd-kit";
|
|
6721
6073
|
*
|
|
6722
6074
|
* const outbox = new InMemoryOutbox<OrderEvent>();
|
|
6723
|
-
* const bus = new
|
|
6075
|
+
* const bus = new InProcessEventBus<OrderEvent>();
|
|
6724
6076
|
*
|
|
6725
6077
|
* const uow = makeOrderUnitOfWork({ outbox, bus });
|
|
6726
6078
|
* await uow.run(async ({ repositories }) => {
|
|
@@ -7022,7 +6374,7 @@ var InMemoryOutbox = class {
|
|
|
7022
6374
|
if (this.pending.has(event.eventId) || this.dead.has(event.eventId) || this.dispatchedEventIds.has(event.eventId)) return;
|
|
7023
6375
|
const cursor = this.sourceCursors.get(sourceKey);
|
|
7024
6376
|
if (cursor !== void 0 && (position.aggregateVersion < cursor.aggregateVersion || position.aggregateVersion === cursor.aggregateVersion && cursor.eventIdsBySequence.has(position.commitSequence))) return;
|
|
7025
|
-
throw new
|
|
6377
|
+
throw new EventCollectionError(`InMemoryOutbox rejected event "${event.eventId}" for ${describeAggregateIdentity(source)}: the aggregate was removed, so its event source ended. The kit does not support an aggregate that is created again under a removed identity. Give the new aggregate a new id.`, event.type);
|
|
7026
6378
|
}
|
|
7027
6379
|
/**
|
|
7028
6380
|
* Event ids of the batch that the source head names at their position
|
|
@@ -7091,7 +6443,7 @@ function assertSameCandidateReceipt(event, received, recorded) {
|
|
|
7091
6443
|
}
|
|
7092
6444
|
/**
|
|
7093
6445
|
* The lenient variant for PENDING records only: an outbox without an
|
|
7094
|
-
* `InMemoryTransactionScope` cannot observe a rollback, so a re-
|
|
6446
|
+
* `InMemoryTransactionScope` cannot observe a rollback, so a re-collected
|
|
7095
6447
|
* event may legitimately arrive at a new aggregateVersion. Index and commit
|
|
7096
6448
|
* cardinality stay immutable.
|
|
7097
6449
|
*/
|
|
@@ -7100,7 +6452,7 @@ function assertSameCandidateReceiptAllowingVersionRefresh(event, received, recor
|
|
|
7100
6452
|
}
|
|
7101
6453
|
function assertReceiptShape(event, received, recorded, allowAggregateVersionRefresh) {
|
|
7102
6454
|
if ((allowAggregateVersionRefresh || received.aggregateVersion === recorded.aggregateVersion) && received.commitSequence === recorded.commitSequence && received.commitSize === recorded.commitSize) return;
|
|
7103
|
-
throw new
|
|
6455
|
+
throw new EventCollectionError(`InMemoryOutbox rejected event "${event.eventId}": its commit candidate changed from (${recorded.aggregateVersion}, ${recorded.commitSequence}; commitSize=${recorded.commitSize}) to (${received.aggregateVersion}, ${received.commitSequence}; commitSize=${received.commitSize}). An exact redelivery must keep its source position immutable.`, event.type);
|
|
7104
6456
|
}
|
|
7105
6457
|
/**
|
|
7106
6458
|
* The fields of a candidate that the outbox reads, read once and checked.
|
|
@@ -7112,7 +6464,7 @@ function assertReceiptShape(event, received, recorded, allowAggregateVersionRefr
|
|
|
7112
6464
|
function ownCandidate(candidate, index) {
|
|
7113
6465
|
let subject = `the candidate at index ${index}`;
|
|
7114
6466
|
let eventType;
|
|
7115
|
-
const rejection = (field, expected, observed) => new
|
|
6467
|
+
const rejection = (field, expected, observed) => new EventCollectionError(`InMemoryOutbox rejected ${subject}: ${field} must be ${expected}, but it is ${observed}. Pass the candidates that withCommit builds.`, eventType);
|
|
7116
6468
|
if (typeof candidate !== "object" || candidate === null) throw rejection("the candidate", "an object", describeRejected(candidate));
|
|
7117
6469
|
const { event, source, position } = candidate;
|
|
7118
6470
|
if (typeof event !== "object" || event === null) throw rejection("event", "an object", describeRejected(event));
|
|
@@ -7162,17 +6514,17 @@ function refill(map, entries) {
|
|
|
7162
6514
|
for (const [key, value] of entries) map.set(key, value);
|
|
7163
6515
|
}
|
|
7164
6516
|
function commitSizeMismatchError(event, source, position, recordedCommitSize) {
|
|
7165
|
-
return new
|
|
6517
|
+
return new EventCollectionError(`InMemoryOutbox rejected event "${event.eventId}" for ${describeAggregateIdentity(source)}: aggregate version ${position.aggregateVersion} was already recorded with commitSize ${recordedCommitSize}, not ${position.commitSize}. An aggregate that was removed and created again under the same identity causes this too; the kit does not support that, so give the new aggregate a new id.`, event.type);
|
|
7166
6518
|
}
|
|
7167
6519
|
function positionOwnedError(event, source, position, positionOwner) {
|
|
7168
|
-
return new
|
|
6520
|
+
return new EventCollectionError(`InMemoryOutbox rejected event "${event.eventId}" for ${describeAggregateIdentity(source)}: source position (${position.aggregateVersion}, ${position.commitSequence}) is already owned by event "${positionOwner}". One qualified source position must identify exactly one immutable event. An aggregate that was removed and created again under the same identity causes this too; the kit does not support that, so give the new aggregate a new id.`, event.type);
|
|
7169
6521
|
}
|
|
7170
6522
|
function staleHeadError(event, source, position, staleHeadVersion) {
|
|
7171
|
-
return new
|
|
6523
|
+
return new EventCollectionError(`InMemoryOutbox rejected stale event "${event.eventId}" for ${describeAggregateIdentity(source)} at aggregate version ${position.aggregateVersion}: the event-source head is already ${staleHeadVersion}. There are two possible causes. First, the aggregate was removed and created again under the same identity. The kit does not support this, because the versions of the new aggregate restart below the head. Give the new aggregate a new id. Second, this is a retry of an event whose dispatched-id receipt expired. Use a durable outbox with a transactional eventId unique key for unbounded idempotency.`, event.type);
|
|
7172
6524
|
}
|
|
7173
6525
|
function assertSameEventSource(event, received, recorded) {
|
|
7174
6526
|
if (received.aggregateType === recorded.aggregateType && received.aggregateId === recorded.aggregateId) return;
|
|
7175
|
-
throw new
|
|
6527
|
+
throw new EventCollectionError(`InMemoryOutbox rejected eventId collision for "${event.eventId}": it already belongs to ${describeAggregateIdentity(recorded)}, but was received for ${describeAggregateIdentity(received)}. An eventId must identify one immutable event across all aggregate sources.`, event.type);
|
|
7176
6528
|
}
|
|
7177
6529
|
|
|
7178
6530
|
//#endregion
|
|
@@ -7433,7 +6785,10 @@ var OutboxDispatcher = class extends PollLoop {
|
|
|
7433
6785
|
function copyForStorage(event, index, stream) {
|
|
7434
6786
|
const subject = `InMemoryEventStore.append (stream ${describeAggregateIdentity(stream)}): events[${index}]`;
|
|
7435
6787
|
if (event === null || typeof event !== "object") throw new TypeError(`${subject} is not an event object`);
|
|
7436
|
-
|
|
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;
|
|
7437
6792
|
}
|
|
7438
6793
|
function assertStreamPosition(name, value) {
|
|
7439
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)}`);
|
|
@@ -7492,7 +6847,7 @@ var InMemoryEventStore = class {
|
|
|
7492
6847
|
async append(stream, events, options) {
|
|
7493
6848
|
const batch = Array.from(events);
|
|
7494
6849
|
if (batch.length === 0) return;
|
|
7495
|
-
const identity =
|
|
6850
|
+
const identity = copyAggregateIdentity(stream);
|
|
7496
6851
|
const expectedVersion = options.expectedVersion;
|
|
7497
6852
|
const key = encodeAggregateIdentity(identity);
|
|
7498
6853
|
this.existingStreamForAppend(identity, key, expectedVersion, batch.length);
|
|
@@ -7770,7 +7125,7 @@ function defaultSleep(ms, signal) {
|
|
|
7770
7125
|
*
|
|
7771
7126
|
* **Classification is by error, not by guesswork.** Only errors the
|
|
7772
7127
|
* `isRetryable` predicate accepts are retried; everything else (a
|
|
7773
|
-
* `DomainError`, `
|
|
7128
|
+
* `DomainError`, `EventCollectionError`, `UnregisteredChangesError`,
|
|
7774
7129
|
* `DuplicateAggregateError`, a non-Error throw) surfaces immediately.
|
|
7775
7130
|
* After `maxAttempts` the last error is rethrown unchanged, so a caller
|
|
7776
7131
|
* can still match `ConcurrencyConflictError` and map it to HTTP 409.
|
|
@@ -7936,12 +7291,12 @@ var InMemorySnapshotStore = class {
|
|
|
7936
7291
|
//#region src/persistence/snapshot-store/snapshot-model.ts
|
|
7937
7292
|
/** Type-inference helper for declaring an adapter-owned snapshot model. */
|
|
7938
7293
|
function defineSnapshotModel(model) {
|
|
7939
|
-
const
|
|
7940
|
-
assertSnapshotModel(
|
|
7941
|
-
return
|
|
7294
|
+
const copy = Object.freeze({ ...model });
|
|
7295
|
+
assertSnapshotModel(copy);
|
|
7296
|
+
return copy;
|
|
7942
7297
|
}
|
|
7943
7298
|
/**
|
|
7944
|
-
* Captures
|
|
7299
|
+
* Captures an independent persistence envelope at an application-supplied time.
|
|
7945
7300
|
* The application decides when snapshotting is worthwhile; this function does
|
|
7946
7301
|
* not read a clock or perform I/O. The envelope is deep-frozen, its `state`
|
|
7947
7302
|
* and its `snapshotAt` included: a write into the captured state or a
|
|
@@ -7952,7 +7307,7 @@ function defineSnapshotModel(model) {
|
|
|
7952
7307
|
function captureAggregateSnapshot(model, aggregate, snapshotAt) {
|
|
7953
7308
|
assertSnapshotModel(model);
|
|
7954
7309
|
const recordedAt = copySnapshotAt(snapshotAt);
|
|
7955
|
-
const state =
|
|
7310
|
+
const state = copyPlainState(model.capture(aggregate));
|
|
7956
7311
|
return deepFreeze({
|
|
7957
7312
|
state,
|
|
7958
7313
|
version: aggregate.version,
|
|
@@ -7991,8 +7346,8 @@ function reconstituteAggregateFromSnapshot(model, id, snapshot) {
|
|
|
7991
7346
|
let aggregate;
|
|
7992
7347
|
try {
|
|
7993
7348
|
let state;
|
|
7994
|
-
if (storedSchemaVersion === model.schemaVersion) state =
|
|
7995
|
-
else if (model.migrate) state =
|
|
7349
|
+
if (storedSchemaVersion === model.schemaVersion) state = copyPlainState(snapshot.state);
|
|
7350
|
+
else if (model.migrate) state = copyPlainState(model.migrate(copyPlainState(snapshot.state), storedSchemaVersion));
|
|
7996
7351
|
else throw new SnapshotSchemaMismatchError({
|
|
7997
7352
|
identity: {
|
|
7998
7353
|
aggregateType: model.aggregateType,
|
|
@@ -8031,5 +7386,5 @@ function copySnapshotAt(snapshotAt) {
|
|
|
8031
7386
|
}
|
|
8032
7387
|
|
|
8033
7388
|
//#endregion
|
|
8034
|
-
export { AggregateDeletedError, AggregateNotFoundError, AggregateTrackingError, CapabilityRegistryConflictError, CommandBus, CommitError, ConcurrencyConflictError, DeadlineProcessor, DirectStateMutationError, DomainError, DomainEventValidationError,
|
|
7389
|
+
export { AggregateDeletedError, AggregateNotFoundError, AggregateTrackingError, CapabilityRegistryConflictError, CommandBus, CommitError, ConcurrencyConflictError, DeadlineProcessor, DirectStateMutationError, DomainError, DomainEventValidationError, DuplicateAggregateError, DuplicateEventIdError, DuplicateHandlerRegistrationError, Entity, ErrorMapperFailedError, EventBusClosedError, EventCollectionError, EventSourcedAggregate, FoldReturnedNoStateError, ForeignEventError, HandBuiltEventError, IdempotencyClaimLostError, IdempotencyCompletionWithoutClaimError, IdempotencyInFlightError, IdempotencyKeyReuseError, IdempotencyReconciliationRequiredError, IdentityMap, InMemoryCapacityExceededError, InMemoryDeadlineStore, InMemoryEventStore, InMemoryIdempotencyStore, InMemoryOutbox, InMemoryProjectionCheckpointStore, InMemorySnapshotStore, InMemoryTransactionScope, InProcessEventBus, InfrastructureError, InvalidCommandMessageError, InvalidEventStreamPageError, InvalidFlushStatementError, InvalidIntegrationMessageError, InvalidRepositoryAdapterError, InvalidRepositoryDefinitionError, InvalidVersionError, MisattributedEventError, MissingEntityIdError, MissingFoldError, MissingHandlerError, NestedUnitOfWorkError, OutboxDispatcher, PendingEventBatchMismatchError, PendingEventLimitExceededError, ProjectionGapError, ProjectionIdentityViolationError, ProjectionOrderViolationError, ProjectionReceiptViolationError, Projector, PrototypePollutionError, PublishDepthExceededError, QueryBus, ReentrantEventRecordingError, ReplayRejectedError, ReplayTargetMismatchError, RepositoryErrorMappingFailedError, RetryingTransactionScope, RollbackError, SnapshotCorruptedError, SnapshotSchemaMismatchError, SnapshotTimeValidationError, SnapshotVersionNotRestoredError, Specification, StateStoredAggregate, TransactionClosedError, UnitOfWork, UnmanagedInstanceError, UnprojectableEventError, UnregisteredChangesError, UnregisteredHandlerError, UnreplayableAggregateError, ValueObject, captureAggregateSnapshot, classifyConcurrencyConflict, copyMetadata, copyPlainState, createDomainEvent, createDomainEventFactory, createIntegrationMessage, createStampedDomainEvent, decodeIntegrationMessage, deepEqual, deepFreeze, deepOmit, defaultDomainEventFactory, defineRepository, defineSnapshotModel, domainErrorToResult, encodeIntegrationMessage, entityIds, eventBusSink, findEntityById, hasEntityId, ignoreProjectionEvent, integrationMessageToCommittedEvent, isDomainErrorLike, isInfrastructureErrorLike, isPositionAfter, isWiringErrorLike, mergeMetadata, outboxWriterAcceptingEventLoss, pinTargetVersion, projectionFromHandlers, readStreamPages, reconstituteAggregateFromHistory, reconstituteAggregateFromSnapshot, reconstituteAggregateFromStreamPages, removeEntityById, replaceEntityById, routeEventsToCommandOutbox, sameEntity, sameVersion, specification, stampPendingEvents, toVersion, updateEntityById, valueObject, valueObjectEquals, valueObjectEqualsExcept, valueObjectOrError, valueObjectOrIssues, versionedFlush, withCommit, withIdempotentCommit };
|
|
8035
7390
|
//# sourceMappingURL=index.js.map
|