@shirudo/ddd-kit 3.0.0-rc.11 → 3.0.0-rc.13
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 +25 -26
- package/dist/chunks/kit-errors.js +51 -52
- package/dist/chunks/kit-errors.js.map +1 -1
- package/dist/chunks/read-stream-pages.js +218 -1443
- package/dist/chunks/read-stream-pages.js.map +1 -1
- package/dist/chunks/snapshot-store.d.ts +123 -118
- package/dist/chunks/value-object.js +1275 -0
- package/dist/chunks/value-object.js.map +1 -0
- package/dist/index.d.ts +142 -395
- package/dist/index.js +200 -868
- 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 +12 -11
- 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
|
|
|
@@ -407,11 +408,11 @@ function captureObserverFunctions(context, observers, required) {
|
|
|
407
408
|
//#region src/application/cqrs/handler.ts
|
|
408
409
|
/**
|
|
409
410
|
* 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
|
|
411
|
+
* matches its registration-time snapshot. Shared by the duplicate-registration
|
|
412
|
+
* gate and the collection-time recheck: both must reject the same divergence,
|
|
413
|
+
* or events recorded after registration would be silently dropped.
|
|
413
414
|
*/
|
|
414
|
-
function
|
|
415
|
+
function registrationDiverged(record) {
|
|
415
416
|
const pending = record.aggregate.pendingEvents;
|
|
416
417
|
return record.aggregate.version !== record.version || pending.length !== record.events.length || record.events.some((event, index) => event !== pending[index]);
|
|
417
418
|
}
|
|
@@ -419,24 +420,24 @@ function enrollmentDiverged(record) {
|
|
|
419
420
|
function createCommitTokenScope() {
|
|
420
421
|
const recordsByToken = /* @__PURE__ */ new WeakMap();
|
|
421
422
|
const tokensByAggregate = /* @__PURE__ */ new WeakMap();
|
|
422
|
-
let
|
|
423
|
+
let issuedTokenCount = 0;
|
|
423
424
|
let open = true;
|
|
424
|
-
const
|
|
425
|
-
if (!open) throw new
|
|
425
|
+
const register = (aggregate, disposition, options) => {
|
|
426
|
+
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
427
|
const existing = tokensByAggregate.get(aggregate);
|
|
427
428
|
if (existing) {
|
|
428
429
|
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 (
|
|
430
|
+
if (!record) throw new EventCollectionError("withCommit: internal commit-token registry is inconsistent.");
|
|
431
|
+
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.`);
|
|
432
|
+
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.`);
|
|
433
|
+
if (registrationDiverged(record)) throw new EventCollectionError(`withCommit: aggregate ${describeAggregateIdentity(aggregate.aggregateIdentity)} changed after its commit batch was registered. Register persistence intent last.`);
|
|
433
434
|
if (disposition === "deleted") record.disposition = "deleted";
|
|
434
435
|
return existing;
|
|
435
436
|
}
|
|
436
|
-
const eventLifecycle = requirePendingEventLifecycleCapability(aggregate, "withCommit
|
|
437
|
+
const eventLifecycle = requirePendingEventLifecycleCapability(aggregate, "withCommit registration");
|
|
437
438
|
const token = Object.freeze(Object.create(null));
|
|
438
439
|
const events = aggregate.pendingEvents;
|
|
439
|
-
for (const event of events) if (!
|
|
440
|
+
for (const event of events) if (!isStampedDomainEvent(event)) throw new EventCollectionError(`withCommit: event "${event.type}" has not been stamped. Call stampPendingEvents(aggregate, createStamp) in the application shell before persistence or outbox collection.`, event.type);
|
|
440
441
|
const persistedVersion = eventLifecycle.persistedVersion();
|
|
441
442
|
tokensByAggregate.set(aggregate, token);
|
|
442
443
|
recordsByToken.set(token, {
|
|
@@ -448,32 +449,32 @@ function createCommitTokenScope() {
|
|
|
448
449
|
persistedVersion,
|
|
449
450
|
events
|
|
450
451
|
});
|
|
451
|
-
|
|
452
|
+
issuedTokenCount += 1;
|
|
452
453
|
return token;
|
|
453
454
|
};
|
|
454
455
|
return {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
456
|
+
registration: Object.freeze({
|
|
457
|
+
registerSaved: (aggregate, options) => register(aggregate, "saved", options),
|
|
458
|
+
registerRemoved: (aggregate, options) => register(aggregate, "deleted", options)
|
|
458
459
|
}),
|
|
459
460
|
close: () => {
|
|
460
461
|
open = false;
|
|
461
462
|
},
|
|
462
463
|
resolve: (tokens) => {
|
|
463
|
-
if (!Array.isArray(tokens)) throw new
|
|
464
|
+
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
465
|
const seen = /* @__PURE__ */ new Set();
|
|
465
466
|
const records = [];
|
|
466
467
|
for (const token of tokens) {
|
|
467
|
-
if (token === null || typeof token !== "object" && typeof token !== "function") throw new
|
|
468
|
+
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
469
|
const tokenObject = token;
|
|
469
470
|
const record = recordsByToken.get(tokenObject);
|
|
470
|
-
if (!record) throw new
|
|
471
|
+
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
472
|
if (seen.has(tokenObject)) continue;
|
|
472
473
|
seen.add(tokenObject);
|
|
473
|
-
if (
|
|
474
|
+
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
475
|
records.push(record);
|
|
475
476
|
}
|
|
476
|
-
if (seen.size !==
|
|
477
|
+
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
478
|
return records;
|
|
478
479
|
}
|
|
479
480
|
};
|
|
@@ -481,29 +482,29 @@ function createCommitTokenScope() {
|
|
|
481
482
|
/**
|
|
482
483
|
* Helper for executing a write Use Case inside a transaction scope.
|
|
483
484
|
*
|
|
484
|
-
* The use-case callback receives an invocation-scoped
|
|
485
|
+
* The use-case callback receives an invocation-scoped registration capability
|
|
485
486
|
* and returns opaque commit tokens for the repository writes that completed
|
|
486
|
-
* in the transaction. `withCommit` owns the post-commit lifecycle (
|
|
487
|
+
* in the transaction. `withCommit` owns the post-commit lifecycle (collection,
|
|
487
488
|
* outbox, mark-persisted, publish). A naked aggregate is not commit evidence:
|
|
488
489
|
* merely touching or constructing one must never make it look persisted.
|
|
489
490
|
*
|
|
490
|
-
* **Trust boundary.** A token proves invocation-local
|
|
491
|
+
* **Trust boundary.** A token proves invocation-local registration, not that the
|
|
491
492
|
* kit inspected a database write; a generic transaction helper cannot observe
|
|
492
|
-
* adapter internals. Repository code must
|
|
493
|
+
* adapter internals. Repository code must register only writes participating in
|
|
493
494
|
* this transaction. `UnitOfWork` centralizes that rule in repository methods.
|
|
494
495
|
* The opaque, scoped token prevents accidental aggregate smuggling and stale
|
|
495
496
|
* reuse; it is not a security boundary against code that deliberately lies to
|
|
496
497
|
* its own persistence capability.
|
|
497
498
|
*
|
|
498
499
|
* Order of operations:
|
|
499
|
-
* 1. `fn(ctx,
|
|
500
|
+
* 1. `fn(ctx, registration)` runs inside `scope.transactional(...)`; domain
|
|
500
501
|
* mutations + repo writes happen here. After a repository write has
|
|
501
|
-
*
|
|
502
|
+
* registered an aggregate, the callback includes that opaque token in its
|
|
502
503
|
* `commits` result. Tokens are invocation-bound: forged or stale tokens
|
|
503
|
-
* fail before
|
|
504
|
+
* fail before collection. `ctx` is whatever transaction handle the `scope`
|
|
504
505
|
* exposes (Drizzle `tx`, Prisma `tx`, Mongo session, or `undefined` for
|
|
505
506
|
* context-free scopes).
|
|
506
|
-
* 2. **Still inside the transaction**, `withCommit`
|
|
507
|
+
* 2. **Still inside the transaction**, `withCommit` collects every
|
|
507
508
|
* aggregate's `pendingEvents` and writes them via `outbox.add` (so
|
|
508
509
|
* events persist atomically with the state change). Skipped when no
|
|
509
510
|
* events were recorded. Each bare domain event is composed into an
|
|
@@ -513,7 +514,7 @@ function createCommitTokenScope() {
|
|
|
513
514
|
* resulting `CommittedDomainEvent`. The domain event itself is never
|
|
514
515
|
* stamped or copied.
|
|
515
516
|
*
|
|
516
|
-
* **
|
|
517
|
+
* **Collection order.** Events are concatenated in the order
|
|
517
518
|
* tokens appear in the returned `commits` array, then in
|
|
518
519
|
* each aggregate's `pendingEvents` order (insertion order via
|
|
519
520
|
* `apply` / `commit` / `addDomainEvent`). So tokens for `[a, b]`
|
|
@@ -537,7 +538,7 @@ function createCommitTokenScope() {
|
|
|
537
538
|
* across aggregates at delivery time.
|
|
538
539
|
* 3. The transaction commits.
|
|
539
540
|
* 4. **After** the commit, a non-exported capability acknowledges every
|
|
540
|
-
* saved
|
|
541
|
+
* saved registration and discards pending events for deleted registrations.
|
|
541
542
|
* Only after the complete commit set is clean does the optional
|
|
542
543
|
* application-shell `onPersisted(aggregate, version, context)` observer run for
|
|
543
544
|
* saved aggregates. Deleted rows never trigger that observer.
|
|
@@ -567,16 +568,16 @@ function createCommitTokenScope() {
|
|
|
567
568
|
* If the transaction rolls back, no acknowledgement occurs: the aggregate
|
|
568
569
|
* keeps its pending events, so the caller can retry or discard the instance.
|
|
569
570
|
*
|
|
570
|
-
*
|
|
571
|
+
* Registration captures an exact version and event batch. Re-registering the same
|
|
571
572
|
* aggregate after it changes rejects. `UnitOfWork` additionally seals the
|
|
572
|
-
* adapter persistence
|
|
573
|
-
* direct `withCommit` use, make domain decisions first, write, and
|
|
573
|
+
* adapter persistence capture and rejects later mutation before flush. For
|
|
574
|
+
* direct `withCommit` use, make domain decisions first, write, and register last.
|
|
574
575
|
*
|
|
575
|
-
* **Duplicate
|
|
576
|
+
* **Duplicate registration is idempotent by reference.** Registering the same
|
|
576
577
|
* instance repeatedly returns the same token, and a repeated token in
|
|
577
|
-
* `commits` is
|
|
578
|
+
* `commits` is collected once. A repeat call that omits `expectedVersion`
|
|
578
579
|
* makes no OCC assertion; only a supplied value that contradicts the
|
|
579
|
-
*
|
|
580
|
+
* registration-time baseline rejects. Each event lands in the outbox exactly once
|
|
580
581
|
* and post-commit acknowledgement runs exactly once. Two
|
|
581
582
|
* *different* instances with the same logical id cannot be detected
|
|
582
583
|
* at this layer; that is a Repository contract violation (failure to
|
|
@@ -587,19 +588,19 @@ function createCommitTokenScope() {
|
|
|
587
588
|
*
|
|
588
589
|
* @example Tx-bound repos (Drizzle, Prisma, Mongo, …)
|
|
589
590
|
* ```typescript
|
|
590
|
-
* const result = await withCommit({ outbox, bus, scope }, async (tx,
|
|
591
|
+
* const result = await withCommit({ outbox, bus, scope }, async (tx, registration) => {
|
|
591
592
|
* const orderRepository = makeOrderRepository(tx); // your factory binds tx to the repo
|
|
592
593
|
* const order = await orderRepository.getById(orderId);
|
|
593
594
|
* order.confirm();
|
|
594
595
|
* await persistOrder(tx, order); // low-level adapter write
|
|
595
|
-
* const commit =
|
|
596
|
+
* const commit = registration.registerSaved(order); // attest the repository write
|
|
596
597
|
* return { result: order.id, commits: [commit] };
|
|
597
598
|
* });
|
|
598
599
|
* ```
|
|
599
600
|
*/
|
|
600
601
|
async function withCommit(deps, fn) {
|
|
601
|
-
return withCheckedCommit(deps, async (ctx,
|
|
602
|
-
const { result, commits } = await fn(ctx,
|
|
602
|
+
return withCheckedCommit(deps, async (ctx, registration) => {
|
|
603
|
+
const { result, commits } = await fn(ctx, registration);
|
|
603
604
|
return {
|
|
604
605
|
result,
|
|
605
606
|
commits
|
|
@@ -618,19 +619,19 @@ async function withCheckedCommit(deps, fn, onAttemptStart) {
|
|
|
618
619
|
const tokenScope = createCommitTokenScope();
|
|
619
620
|
let fnResult;
|
|
620
621
|
try {
|
|
621
|
-
fnResult = await fn(ctx, tokenScope.
|
|
622
|
+
fnResult = await fn(ctx, tokenScope.registration);
|
|
622
623
|
} finally {
|
|
623
624
|
tokenScope.close();
|
|
624
625
|
}
|
|
625
626
|
try {
|
|
626
627
|
const commitRecords = tokenScope.resolve(fnResult.commits);
|
|
627
628
|
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
|
|
629
|
+
const registered = record.aggregate.aggregateIdentity;
|
|
630
|
+
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.`);
|
|
631
|
+
const registeredId = registered.aggregateId;
|
|
632
|
+
const registeredType = registered.aggregateType;
|
|
632
633
|
return record.events.map((event, index) => {
|
|
633
|
-
if (!
|
|
634
|
+
if (!isStampedDomainEvent(event)) throw new EventCollectionError(`withCommit: event "${event.type}" has not been stamped. Call stampPendingEvents(aggregate, createStamp) in the application shell before persistence or outbox collection.`, event.type);
|
|
634
635
|
const recordedEvent = event;
|
|
635
636
|
const commitSize = record.events.length;
|
|
636
637
|
const aggregateId = recordedEvent.aggregateId;
|
|
@@ -638,14 +639,14 @@ async function withCheckedCommit(deps, fn, onAttemptStart) {
|
|
|
638
639
|
const missing = [];
|
|
639
640
|
if (!aggregateId) missing.push("aggregateId");
|
|
640
641
|
if (!aggregateType) missing.push("aggregateType");
|
|
641
|
-
if (!aggregateId || !aggregateType) throw new
|
|
642
|
-
if (aggregateId !==
|
|
642
|
+
if (!aggregateId || !aggregateType) throw new EventCollectionError(`withCommit: event "${recordedEvent.type}" is missing ${missing.join(" and ")}. Use this.createEvent(type, payload) inside aggregate methods instead of createDomainEvent(...); createEvent auto-injects aggregateId and aggregateType. Outbox dispatchers and projection handlers rely on the envelope source.`, recordedEvent.type);
|
|
643
|
+
if (aggregateId !== registeredId || aggregateType !== registeredType) throw new EventCollectionError(`withCommit: event "${recordedEvent.type}" belongs to ${describeAggregateIdentity({
|
|
643
644
|
aggregateType,
|
|
644
645
|
aggregateId
|
|
645
|
-
})} but was
|
|
646
|
-
aggregateType:
|
|
647
|
-
aggregateId:
|
|
648
|
-
})}. The aggregate base classes
|
|
646
|
+
})} but was registered under ${describeAggregateIdentity({
|
|
647
|
+
aggregateType: registeredType,
|
|
648
|
+
aggregateId: registeredId
|
|
649
|
+
})}. The aggregate base classes attribute every event to the aggregate on every recording path; an instance from another package copy must stamp it the same way.`, recordedEvent.type);
|
|
649
650
|
return Object.freeze({
|
|
650
651
|
event: recordedEvent,
|
|
651
652
|
source: Object.freeze({
|
|
@@ -660,12 +661,12 @@ async function withCheckedCommit(deps, fn, onAttemptStart) {
|
|
|
660
661
|
});
|
|
661
662
|
});
|
|
662
663
|
});
|
|
663
|
-
const endedSources = commitRecords.filter((record) => record.disposition === "deleted").map((record) =>
|
|
664
|
+
const endedSources = commitRecords.filter((record) => record.disposition === "deleted").map((record) => copyAggregateIdentity(record.aggregate.aggregateIdentity));
|
|
664
665
|
if (candidates.length > 0) await deps.outbox.add(Object.freeze(candidates));
|
|
665
666
|
if (endedSources.length > 0) await deps.outbox.endEventSources(Object.freeze(endedSources));
|
|
666
667
|
fnResult.checkBeforeCommit?.();
|
|
667
668
|
if (candidates.length > 0 || endedSources.length > 0) {
|
|
668
|
-
for (const record of commitRecords) if (
|
|
669
|
+
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
670
|
}
|
|
670
671
|
return {
|
|
671
672
|
result: fnResult.result,
|
|
@@ -1406,7 +1407,7 @@ function positiveSafeInteger(value) {
|
|
|
1406
1407
|
*
|
|
1407
1408
|
* On its own, the store is the reference for the leased family: it cannot
|
|
1408
1409
|
* see commits or rollbacks. Claims and staged outcomes carry bounded leases,
|
|
1409
|
-
* while every mutation compares the store-
|
|
1410
|
+
* while every mutation compares the store-issued token. An expired pending
|
|
1410
1411
|
* claim may be replaced; an expired staged outcome cannot be guessed away and
|
|
1411
1412
|
* instead returns `reconciliation-required`. Only an authoritative
|
|
1412
1413
|
* `committed` / `not-committed` decision can settle it.
|
|
@@ -1669,20 +1670,20 @@ function startLeaseHeartbeat(store, claim) {
|
|
|
1669
1670
|
failure: () => heartbeatFailure
|
|
1670
1671
|
};
|
|
1671
1672
|
}
|
|
1672
|
-
function
|
|
1673
|
+
function scopeWorkRegistration(parent) {
|
|
1673
1674
|
let open = true;
|
|
1674
1675
|
const assertOpen = () => {
|
|
1675
|
-
if (!open) throw new
|
|
1676
|
+
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
1677
|
};
|
|
1677
1678
|
return {
|
|
1678
|
-
|
|
1679
|
-
|
|
1679
|
+
registration: Object.freeze({
|
|
1680
|
+
registerSaved: (...args) => {
|
|
1680
1681
|
assertOpen();
|
|
1681
|
-
return parent.
|
|
1682
|
+
return parent.registerSaved(...args);
|
|
1682
1683
|
},
|
|
1683
|
-
|
|
1684
|
+
registerRemoved: (...args) => {
|
|
1684
1685
|
assertOpen();
|
|
1685
|
-
return parent.
|
|
1686
|
+
return parent.registerRemoved(...args);
|
|
1686
1687
|
}
|
|
1687
1688
|
}),
|
|
1688
1689
|
close: () => {
|
|
@@ -1705,12 +1706,12 @@ function scopeWorkEnrollment(parent) {
|
|
|
1705
1706
|
* 2. A fresh claim carries an opaque ownership token. For a leased store the
|
|
1706
1707
|
* wrapper renews it at `renewAfterMs` until the transaction callback is
|
|
1707
1708
|
* ready to commit. A renewal failure rejects before commit and releases
|
|
1708
|
-
* the claim. `fn(ctx,
|
|
1709
|
+
* the claim. `fn(ctx, registration, execution)` receives the same token so a
|
|
1709
1710
|
* source-side marker can make later reconciliation conclusive.
|
|
1710
1711
|
* 3. `store.complete(ctx, claim, fn's result)` stages or completes the outcome
|
|
1711
|
-
* in the same transaction as aggregate writes and outbox. The
|
|
1712
|
+
* in the same transaction as aggregate writes and outbox. The registration
|
|
1712
1713
|
* capability is sealed and its token array copied before `complete` can
|
|
1713
|
-
* yield, so leaked callback state cannot change the
|
|
1714
|
+
* yield, so leaked callback state cannot change the collection receipt.
|
|
1714
1715
|
* 4. After commit, `store.confirm(claim)` finalizes a leased store's staged
|
|
1715
1716
|
* outcome; it is a no-op for transactional stores. A failure cannot reject
|
|
1716
1717
|
* an already committed write, so it is sent to `onIdempotencyError` and the
|
|
@@ -1798,7 +1799,7 @@ async function withIdempotentCommit(deps, request, fn) {
|
|
|
1798
1799
|
const outcome = await withCommit({
|
|
1799
1800
|
...deps,
|
|
1800
1801
|
scope
|
|
1801
|
-
}, async (ctx,
|
|
1802
|
+
}, async (ctx, registration) => {
|
|
1802
1803
|
let claim = await store.claim(ctx, request.key, request.fingerprint);
|
|
1803
1804
|
if (claim.status === "reconciliation-required") {
|
|
1804
1805
|
const decision = deps.reconcileIdempotency ? await deps.reconcileIdempotency(claim.reconciliation, ctx) : "unknown";
|
|
@@ -1817,16 +1818,16 @@ async function withIdempotentCommit(deps, request, fn) {
|
|
|
1817
1818
|
};
|
|
1818
1819
|
attempt.claim = claim.claim;
|
|
1819
1820
|
attempt.heartbeat = startLeaseHeartbeat(store, claim.claim);
|
|
1820
|
-
const
|
|
1821
|
+
const workRegistration = scopeWorkRegistration(registration);
|
|
1821
1822
|
let work;
|
|
1822
1823
|
try {
|
|
1823
|
-
work = await fn(ctx,
|
|
1824
|
+
work = await fn(ctx, workRegistration.registration, {
|
|
1824
1825
|
key: request.key,
|
|
1825
1826
|
fingerprint: request.fingerprint,
|
|
1826
1827
|
claimToken: claim.claim.token
|
|
1827
1828
|
});
|
|
1828
1829
|
} finally {
|
|
1829
|
-
|
|
1830
|
+
workRegistration.close();
|
|
1830
1831
|
}
|
|
1831
1832
|
const result = work.result;
|
|
1832
1833
|
const commits = Array.isArray(work.commits) ? Object.freeze([...work.commits]) : work.commits;
|
|
@@ -1841,7 +1842,7 @@ async function withIdempotentCommit(deps, request, fn) {
|
|
|
1841
1842
|
});
|
|
1842
1843
|
if (!outcome.replayed) {
|
|
1843
1844
|
const committedClaim = attempt.claim;
|
|
1844
|
-
if (!committedClaim) throw new
|
|
1845
|
+
if (!committedClaim) throw new EventCollectionError("withIdempotentCommit: a fresh result committed without its claim receipt.");
|
|
1845
1846
|
try {
|
|
1846
1847
|
await store.confirm(committedClaim);
|
|
1847
1848
|
} catch (confirmError) {
|
|
@@ -2438,7 +2439,7 @@ var RepositoryErrorMappingFailedError = class extends KitWiringError {
|
|
|
2438
2439
|
mapperCause;
|
|
2439
2440
|
constructor(options) {
|
|
2440
2441
|
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 =
|
|
2442
|
+
this.identity = copyAggregateIdentity(options.identity);
|
|
2442
2443
|
this.intent = options.intent;
|
|
2443
2444
|
this.mapperCause = options.mapperError;
|
|
2444
2445
|
}
|
|
@@ -2463,7 +2464,7 @@ var InvalidFlushStatementError = class extends KitWiringError {
|
|
|
2463
2464
|
classifierCause;
|
|
2464
2465
|
constructor(options) {
|
|
2465
2466
|
super("INVALID_FLUSH_STATEMENT", flushStatementReasonMessage(options), options.cause);
|
|
2466
|
-
this.identity =
|
|
2467
|
+
this.identity = copyAggregateIdentity(options.identity);
|
|
2467
2468
|
this.intent = options.intent ?? null;
|
|
2468
2469
|
this.reason = options.reason;
|
|
2469
2470
|
this.received = options.received;
|
|
@@ -2510,7 +2511,7 @@ var AggregateTrackingError = class extends KitWiringError {
|
|
|
2510
2511
|
registeredIntent;
|
|
2511
2512
|
constructor(options) {
|
|
2512
2513
|
super("AGGREGATE_TRACKING", trackingReasonMessage(options));
|
|
2513
|
-
this.identity =
|
|
2514
|
+
this.identity = copyAggregateIdentity(options.identity);
|
|
2514
2515
|
this.operation = options.operation;
|
|
2515
2516
|
this.reason = options.reason;
|
|
2516
2517
|
this.registeredIntent = options.registeredIntent;
|
|
@@ -2543,10 +2544,10 @@ function trackingReasonMessage(options) {
|
|
|
2543
2544
|
* A `CommitError` is the **potentially transient** post-completion
|
|
2544
2545
|
* failure (a commit-time serialization failure is the classic case), so
|
|
2545
2546
|
* it is the one a retrying caller should consider re-running. The
|
|
2546
|
-
* deterministic post-completion failure, a
|
|
2547
|
+
* deterministic post-completion failure, a collection-guard violation (an
|
|
2547
2548
|
* event missing `aggregateId` / `aggregateType`, or an eventful persisted
|
|
2548
2549
|
* aggregate that did not advance its version), is a programming bug and surfaces as
|
|
2549
|
-
* {@link
|
|
2550
|
+
* {@link EventCollectionError} instead, which does NOT extend
|
|
2550
2551
|
* `InfrastructureError`, so it stays out of retry paths by construction.
|
|
2551
2552
|
*/
|
|
2552
2553
|
var CommitError = class extends InfrastructureError {
|
|
@@ -2610,9 +2611,9 @@ function registerPendingEventRecordingCapability(aggregate, capability) {
|
|
|
2610
2611
|
}
|
|
2611
2612
|
|
|
2612
2613
|
//#endregion
|
|
2613
|
-
//#region src/application/unit-of-work/
|
|
2614
|
-
function
|
|
2615
|
-
const capability = requirePendingEventRecordingCapability(aggregate, "
|
|
2614
|
+
//#region src/application/unit-of-work/stamp-pending-events.ts
|
|
2615
|
+
function stampPendingEvents(aggregate, source, stampOptions) {
|
|
2616
|
+
const capability = requirePendingEventRecordingCapability(aggregate, "stampPendingEvents");
|
|
2616
2617
|
const shared = {
|
|
2617
2618
|
occurredAt: stampOptions?.occurredAt,
|
|
2618
2619
|
metadata: stampOptions?.metadata
|
|
@@ -2836,7 +2837,7 @@ function deleteRepositoryFacadeProperty(state, target, property) {
|
|
|
2836
2837
|
* `AggregatePersistence` implementations: two `findById(id)` calls in the same
|
|
2837
2838
|
* unit of work MUST return the same instance, because commit-token
|
|
2838
2839
|
* write registration dedupes by JavaScript object identity. Two instances for
|
|
2839
|
-
* one logical aggregate can otherwise produce two tokens, two
|
|
2840
|
+
* one logical aggregate can otherwise produce two tokens, two collections,
|
|
2840
2841
|
* and two post-commit lifecycle calls.
|
|
2841
2842
|
*
|
|
2842
2843
|
* Storage is two-level (per-type stores created lazily), so
|
|
@@ -2903,7 +2904,7 @@ var IdentityMap = class {
|
|
|
2903
2904
|
* - Registering a DIFFERENT instance for an occupied type+id throws:
|
|
2904
2905
|
* that is precisely the identity-map violation this class exists
|
|
2905
2906
|
* to prevent (the repository hydrated twice instead of checking
|
|
2906
|
-
* {@link get} first), and letting it pass would double-
|
|
2907
|
+
* {@link get} first), and letting it pass would double-collect
|
|
2907
2908
|
* events downstream.
|
|
2908
2909
|
* - Registering a type+id that was {@link delete}d in this unit of
|
|
2909
2910
|
* work throws `AggregateDeletedError`: deletion is final within
|
|
@@ -2918,7 +2919,7 @@ var IdentityMap = class {
|
|
|
2918
2919
|
this._stores.set(type, store);
|
|
2919
2920
|
}
|
|
2920
2921
|
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
|
|
2922
|
+
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
2923
|
store.set(id, aggregate);
|
|
2923
2924
|
if (aggregate !== null && typeof aggregate === "object" && !this._pendingAtRegistration.has(aggregate)) {
|
|
2924
2925
|
const pending = pendingEventCountOf(aggregate);
|
|
@@ -2929,7 +2930,7 @@ var IdentityMap = class {
|
|
|
2929
2930
|
* Registered instances that have recorded MORE pending events than they
|
|
2930
2931
|
* carried when first registered (loaded). Used by the unit of work's
|
|
2931
2932
|
* end-of-run guard: an aggregate that gained events after load but was
|
|
2932
|
-
* never
|
|
2933
|
+
* never registered would silently drop them. A read-only load, or a
|
|
2933
2934
|
* reconstitution that already carried events, shows no increase and is
|
|
2934
2935
|
* not reported.
|
|
2935
2936
|
*/
|
|
@@ -3008,7 +3009,7 @@ function insertPersistenceBaseline(model) {
|
|
|
3008
3009
|
return createBaselineToken(model, void 0, "new");
|
|
3009
3010
|
}
|
|
3010
3011
|
/**
|
|
3011
|
-
* Captures the aggregate's current adapter
|
|
3012
|
+
* Captures the aggregate's current adapter capture using the capability
|
|
3012
3013
|
* carried by an existing baseline. Used to seal persistence-last registration.
|
|
3013
3014
|
*/
|
|
3014
3015
|
function recapturePersistenceBaseline(baseline, aggregate) {
|
|
@@ -3020,7 +3021,7 @@ function recapturePersistenceBaseline(baseline, aggregate) {
|
|
|
3020
3021
|
});
|
|
3021
3022
|
}
|
|
3022
3023
|
/**
|
|
3023
|
-
* Recaptures the adapter
|
|
3024
|
+
* Recaptures the adapter capture and reports whether it drifted from the
|
|
3024
3025
|
* baseline's stored capture, using the model's `captureEquals` when supplied
|
|
3025
3026
|
* and structural deep equality otherwise.
|
|
3026
3027
|
*
|
|
@@ -3031,8 +3032,8 @@ function recapturePersistenceBaseline(baseline, aggregate) {
|
|
|
3031
3032
|
* question independent of the model's diffing strategy. A `"new"` lifecycle
|
|
3032
3033
|
* baseline has no stored capture and never reports drift.
|
|
3033
3034
|
*/
|
|
3034
|
-
function
|
|
3035
|
-
const capability = capabilityFor(baseline, "
|
|
3035
|
+
function persistenceCaptureDrifted(baseline, aggregate) {
|
|
3036
|
+
const capability = capabilityFor(baseline, "persistenceCaptureDrifted");
|
|
3036
3037
|
if (capability.lifecycle === "new") return false;
|
|
3037
3038
|
return !capability.captureEquals(capability.baseline, capability.capture(aggregate));
|
|
3038
3039
|
}
|
|
@@ -3076,7 +3077,7 @@ function capabilityFor(baseline, operation) {
|
|
|
3076
3077
|
* the public API.
|
|
3077
3078
|
*/
|
|
3078
3079
|
var Session = class {
|
|
3079
|
-
|
|
3080
|
+
commitRegistration;
|
|
3080
3081
|
_registeredWrites = [];
|
|
3081
3082
|
_commitTokens = /* @__PURE__ */ new Set();
|
|
3082
3083
|
_identityMap = new IdentityMap();
|
|
@@ -3092,15 +3093,15 @@ var Session = class {
|
|
|
3092
3093
|
/**
|
|
3093
3094
|
* True from the start of the flush until the last check before the
|
|
3094
3095
|
* commit ran or the run ended. A write registered in that window would
|
|
3095
|
-
* be committed and
|
|
3096
|
+
* be committed and collected without its flush, so it is rejected, and
|
|
3096
3097
|
* the first rejection also fails the attempt, even when the caller
|
|
3097
3098
|
* swallows it. After the window, a registration reports a closed unit of
|
|
3098
3099
|
* work: nothing can fail the attempt any more.
|
|
3099
3100
|
*/
|
|
3100
3101
|
_lateRegistrationsFailAttempt = false;
|
|
3101
3102
|
_registrationDuringFlush;
|
|
3102
|
-
constructor(
|
|
3103
|
-
this.
|
|
3103
|
+
constructor(commitRegistration) {
|
|
3104
|
+
this.commitRegistration = commitRegistration;
|
|
3104
3105
|
}
|
|
3105
3106
|
get identityMap() {
|
|
3106
3107
|
this.assertOpen("tracking.identityMap");
|
|
@@ -3195,7 +3196,7 @@ var Session = class {
|
|
|
3195
3196
|
throw error;
|
|
3196
3197
|
}
|
|
3197
3198
|
}
|
|
3198
|
-
/** Two live instances of one identity would each flush and
|
|
3199
|
+
/** Two live instances of one identity would each flush and collect. */
|
|
3199
3200
|
assertIdentityNotTaken(aggregate, definition) {
|
|
3200
3201
|
const tracked = this._identityMap.get(definition.aggregate, aggregate.id);
|
|
3201
3202
|
if (tracked === void 0 || tracked === aggregate) return;
|
|
@@ -3221,7 +3222,7 @@ var Session = class {
|
|
|
3221
3222
|
const loaded = this.loadedEntryFor(aggregate, "remove", definition);
|
|
3222
3223
|
this.registerWrite(loaded, "remove", definition);
|
|
3223
3224
|
}
|
|
3224
|
-
/** Registers persistence intent and commit
|
|
3225
|
+
/** Registers persistence intent and commit registration as one operation. */
|
|
3225
3226
|
registerWrite(entry, intent, definition) {
|
|
3226
3227
|
const newlyRegistered = this.registerIntent(entry, intent);
|
|
3227
3228
|
try {
|
|
@@ -3279,7 +3280,7 @@ var Session = class {
|
|
|
3279
3280
|
this._registeredWrites.push(entry);
|
|
3280
3281
|
return true;
|
|
3281
3282
|
}
|
|
3282
|
-
/** Restores the pre-registration state when commit
|
|
3283
|
+
/** Restores the pre-registration state when commit registration rejects. */
|
|
3283
3284
|
rollbackIntentRegistration(entry) {
|
|
3284
3285
|
const index = this._registeredWrites.lastIndexOf(entry);
|
|
3285
3286
|
if (index >= 0) this._registeredWrites.splice(index, 1);
|
|
@@ -3289,7 +3290,7 @@ var Session = class {
|
|
|
3289
3290
|
const registration = entry.registration;
|
|
3290
3291
|
if (registration === void 0) return;
|
|
3291
3292
|
const currentEvents = entry.aggregate.pendingEvents;
|
|
3292
|
-
const persistenceChanged =
|
|
3293
|
+
const persistenceChanged = persistenceCaptureDrifted(registration.baseline, entry.aggregate);
|
|
3293
3294
|
const sameEvents = currentEvents.length === registration.events.length && currentEvents.every((event, index) => event === registration.events[index]);
|
|
3294
3295
|
if (registration.version !== entry.aggregate.version || !sameEvents || persistenceChanged) throw new AggregateTrackingError({
|
|
3295
3296
|
identity: entry.aggregate.aggregateIdentity,
|
|
@@ -3301,13 +3302,13 @@ var Session = class {
|
|
|
3301
3302
|
registerSavedCommit(aggregate, definition, expectedVersion) {
|
|
3302
3303
|
this.assertOpen("repository.add/update");
|
|
3303
3304
|
if (this.isRemovedInstance(aggregate) || this._identityMap.isDeleted(definition.aggregate, aggregate.id)) throw new AggregateDeletedError({ identity: aggregate.aggregateIdentity });
|
|
3304
|
-
const token = this.
|
|
3305
|
+
const token = this.commitRegistration.registerSaved(aggregate, { expectedVersion });
|
|
3305
3306
|
this._commitTokens.add(token);
|
|
3306
3307
|
return token;
|
|
3307
3308
|
}
|
|
3308
3309
|
registerRemovedCommit(aggregate, definition, expectedVersion) {
|
|
3309
3310
|
this.assertOpen("repository.remove");
|
|
3310
|
-
const token = this.
|
|
3311
|
+
const token = this.commitRegistration.registerRemoved(aggregate, { expectedVersion });
|
|
3311
3312
|
this._identityMap.delete(definition.aggregate, aggregate.id);
|
|
3312
3313
|
this._commitTokens.add(token);
|
|
3313
3314
|
return token;
|
|
@@ -3326,7 +3327,7 @@ var Session = class {
|
|
|
3326
3327
|
this.assertUnchangedAfterRegistration(entry, "commit");
|
|
3327
3328
|
continue;
|
|
3328
3329
|
}
|
|
3329
|
-
if (entry.lifecycle === "loaded" && (entry.aggregate.version !== entry.expectedVersion ||
|
|
3330
|
+
if (entry.lifecycle === "loaded" && (entry.aggregate.version !== entry.expectedVersion || persistenceCaptureDrifted(entry.baseline, entry.aggregate))) throw new UnregisteredChangesError({
|
|
3330
3331
|
identity: entry.aggregate.aggregateIdentity,
|
|
3331
3332
|
appendOnly: entry.definition.appendOnly === true
|
|
3332
3333
|
});
|
|
@@ -3334,7 +3335,7 @@ var Session = class {
|
|
|
3334
3335
|
for (const instance of this._identityMap.instancesWithNewPendingEvents()) {
|
|
3335
3336
|
if (instance !== null && typeof instance === "object" && this.registrationOf(instance) !== void 0) continue;
|
|
3336
3337
|
const aggregate = instance;
|
|
3337
|
-
throw new
|
|
3338
|
+
throw new UnregisteredChangesError({
|
|
3338
3339
|
identity: aggregate.aggregateIdentity,
|
|
3339
3340
|
appendOnly: this._trackingByAggregate.get(aggregate)?.definition.appendOnly === true
|
|
3340
3341
|
});
|
|
@@ -3524,7 +3525,7 @@ function defineRepository() {
|
|
|
3524
3525
|
const builder = (definition) => {
|
|
3525
3526
|
const branded = { ...definition };
|
|
3526
3527
|
assertRepositoryDefinitionMembers(branded);
|
|
3527
|
-
|
|
3528
|
+
applyCooperativeBrand(branded, repositoryDefinitionBrand);
|
|
3528
3529
|
return Object.freeze(branded);
|
|
3529
3530
|
};
|
|
3530
3531
|
return builder;
|
|
@@ -3535,7 +3536,7 @@ function defineRepository() {
|
|
|
3535
3536
|
* transaction and either persist completely or not at all.
|
|
3536
3537
|
*
|
|
3537
3538
|
* Built ON TOP of `withCommit` - the commit orchestration (event
|
|
3538
|
-
*
|
|
3539
|
+
* collection into the outbox inside the transaction, internal acknowledgement
|
|
3539
3540
|
* after the commit, best-effort in-process publish last) is inherited,
|
|
3540
3541
|
* not reimplemented. What this layer adds:
|
|
3541
3542
|
*
|
|
@@ -3603,7 +3604,7 @@ var UnitOfWork = class {
|
|
|
3603
3604
|
* Execute one unit of work: open the transaction, hand the callback
|
|
3604
3605
|
* tx-bound repositories, commit on resolve, roll back on throw,
|
|
3605
3606
|
* run the post-commit lifecycle (acknowledge, observe, publish) for every
|
|
3606
|
-
*
|
|
3607
|
+
* registered aggregate. Returns the callback's result.
|
|
3607
3608
|
*/
|
|
3608
3609
|
async run(work, options) {
|
|
3609
3610
|
if (options?.signal?.aborted) throw abortReason(options.signal, "UnitOfWork.run aborted before opening a transaction");
|
|
@@ -3622,9 +3623,9 @@ var UnitOfWork = class {
|
|
|
3622
3623
|
onPersistError: this.deps.onPersistError,
|
|
3623
3624
|
postCommitTimeoutMs: this.deps.postCommitTimeoutMs,
|
|
3624
3625
|
signal: options?.signal
|
|
3625
|
-
}, async (tx,
|
|
3626
|
+
}, async (tx, registration) => {
|
|
3626
3627
|
session?.close();
|
|
3627
|
-
const s = new Session(
|
|
3628
|
+
const s = new Session(registration);
|
|
3628
3629
|
session = s;
|
|
3629
3630
|
sessions.push(s);
|
|
3630
3631
|
const current = startAttempt();
|
|
@@ -3712,7 +3713,7 @@ function startAttempt() {
|
|
|
3712
3713
|
* {@link RollbackError}.
|
|
3713
3714
|
* - `workCompleted`: the callback finished; the failure is post-completion.
|
|
3714
3715
|
* The checks run in this order. First, a wiring error in the rejection (a
|
|
3715
|
-
*
|
|
3716
|
+
* collection-guard violation, or an aggregate that changed after its
|
|
3716
3717
|
* registration) is a deterministic programming bug and passes through; it
|
|
3717
3718
|
* does NOT extend `InfrastructureError`, so a retry-on-Infrastructure
|
|
3718
3719
|
* handler skips it. It is thrown inside `scope.transactional()`, so a
|
|
@@ -4097,7 +4098,7 @@ var Entity = class {
|
|
|
4097
4098
|
* `TState` publicly would expose the aggregate's live object graph and
|
|
4098
4099
|
* let nested mutation bypass behavior, validation, versioning, and
|
|
4099
4100
|
* dirty tracking. Concrete entities should expose business-meaningful queries or
|
|
4100
|
-
*
|
|
4101
|
+
* immutable DTOs (`deepFreeze(copyPlainState(this.state))` for a
|
|
4101
4102
|
* plain-data state). Snapshot projection belongs to the persistence
|
|
4102
4103
|
* adapter, which captures the aggregate from outside through those
|
|
4103
4104
|
* queries and DTOs rather than asking the entity to create its own
|
|
@@ -4130,7 +4131,7 @@ var Entity = class {
|
|
|
4130
4131
|
* widens to the whole graph: NESTED objects are frozen in place too.
|
|
4131
4132
|
*
|
|
4132
4133
|
* @throws MissingEntityIdError when `id` is not a non-blank string.
|
|
4133
|
-
* @throws
|
|
4134
|
+
* @throws PrototypePollutionError when a plain-object, null-prototype,
|
|
4134
4135
|
* or array state carries an own `"__proto__"` data key; validate and
|
|
4135
4136
|
* strip untrusted input at the boundary.
|
|
4136
4137
|
*/
|
|
@@ -4154,7 +4155,7 @@ var Entity = class {
|
|
|
4154
4155
|
* ownership transfer and is frozen in place; see the constructor.
|
|
4155
4156
|
*
|
|
4156
4157
|
* @param newState - The new state
|
|
4157
|
-
* @throws
|
|
4158
|
+
* @throws PrototypePollutionError when the state carries an own
|
|
4158
4159
|
* `"__proto__"` data key; the previous state is kept.
|
|
4159
4160
|
*/
|
|
4160
4161
|
setState(newState) {
|
|
@@ -4215,15 +4216,15 @@ function freezeShallow(value) {
|
|
|
4215
4216
|
* @internal Shared by the kit's own entity subclasses; not part of the
|
|
4216
4217
|
* public API.
|
|
4217
4218
|
*/
|
|
4218
|
-
function
|
|
4219
|
+
function assertStateHasNoOwnProtoKey(state, subject) {
|
|
4219
4220
|
if (state === null || typeof state !== "object") return;
|
|
4220
4221
|
if (Array.isArray(state)) {
|
|
4221
|
-
|
|
4222
|
+
assertNoOwnProtoKey(state, subject);
|
|
4222
4223
|
return;
|
|
4223
4224
|
}
|
|
4224
4225
|
const proto = Object.getPrototypeOf(state);
|
|
4225
4226
|
if (proto !== Object.prototype && proto !== null) return;
|
|
4226
|
-
|
|
4227
|
+
assertNoOwnProtoKey(state, subject);
|
|
4227
4228
|
}
|
|
4228
4229
|
/**
|
|
4229
4230
|
* Returns a shallow copy for plain objects and arrays so the subsequent
|
|
@@ -4235,7 +4236,7 @@ function assertStateHasNoHostileOwnKey(state, subject) {
|
|
|
4235
4236
|
*/
|
|
4236
4237
|
function shallowCopyOwned(value) {
|
|
4237
4238
|
if (value === null || typeof value !== "object") return value;
|
|
4238
|
-
|
|
4239
|
+
assertStateHasNoOwnProtoKey(value, "Entity state");
|
|
4239
4240
|
if (Array.isArray(value)) {
|
|
4240
4241
|
const copy = [...value];
|
|
4241
4242
|
for (const key of Reflect.ownKeys(value)) {
|
|
@@ -4344,7 +4345,7 @@ function removeEntityById(entities, id) {
|
|
|
4344
4345
|
/**
|
|
4345
4346
|
* Updates an entity with the given ID in the collection.
|
|
4346
4347
|
* Returns a new array with the updated entity.
|
|
4347
|
-
* Structural sharing for adapter-owned persistence
|
|
4348
|
+
* Structural sharing for adapter-owned persistence captures: returns
|
|
4348
4349
|
* the ORIGINAL array when nothing changed (no match, or the element kept
|
|
4349
4350
|
* its reference), so a partial-write adapter can skip the untouched
|
|
4350
4351
|
* collection; a new array only when an
|
|
@@ -4383,7 +4384,7 @@ function updateEntityById(entities, id, updater) {
|
|
|
4383
4384
|
/**
|
|
4384
4385
|
* Replaces an entity with the given ID in the collection.
|
|
4385
4386
|
* Returns a new array with the replaced entity.
|
|
4386
|
-
* Structural sharing for adapter-owned persistence
|
|
4387
|
+
* Structural sharing for adapter-owned persistence captures: returns
|
|
4387
4388
|
* the ORIGINAL array when nothing changed (no match, or the element kept
|
|
4388
4389
|
* its reference), so a partial-write adapter can skip the untouched
|
|
4389
4390
|
* collection; a new array only when an
|
|
@@ -4448,8 +4449,8 @@ function entityIds(entities) {
|
|
|
4448
4449
|
* tracking, the kit-internal post-commit acknowledgement capability,
|
|
4449
4450
|
* the `markReconstituted` post-load marker, and the `createEvent` helper
|
|
4450
4451
|
* that auto-injects `aggregateId` + `aggregateType` on every event the
|
|
4451
|
-
* aggregate emits. The application shell
|
|
4452
|
-
* with `
|
|
4452
|
+
* aggregate emits. The application shell stamps the pending decisions
|
|
4453
|
+
* with `stampPendingEvents` before persistence.
|
|
4453
4454
|
*
|
|
4454
4455
|
* Consumers do NOT extend this class directly; extend
|
|
4455
4456
|
* `StateStoredAggregate` for state-stored aggregates or
|
|
@@ -4509,24 +4510,24 @@ var BaseAggregate = class extends Entity {
|
|
|
4509
4510
|
registerPendingEventRecordingCapability(this, { record: (createStamp) => this.recordPendingDecisions(createStamp) });
|
|
4510
4511
|
}
|
|
4511
4512
|
/**
|
|
4512
|
-
* Stamps every
|
|
4513
|
-
* and time from `createStamp`, passes an already
|
|
4513
|
+
* Stamps every unstamped decision in the pending list with identity
|
|
4514
|
+
* and time from `createStamp`, passes an already stamped event through,
|
|
4514
4515
|
* and replaces the list atomically. Three guards run before the
|
|
4515
|
-
* assignment: an event that no kit constructor
|
|
4516
|
-
* ({@link
|
|
4516
|
+
* assignment: an event that no kit constructor built is rejected
|
|
4517
|
+
* ({@link HandBuiltEventError}); a stamp provider that recorded a new
|
|
4517
4518
|
* decision on this aggregate mid-map is rejected
|
|
4518
4519
|
* ({@link ReentrantEventRecordingError}); and two decisions that
|
|
4519
4520
|
* received one eventId are rejected ({@link DuplicateEventIdError}).
|
|
4520
|
-
* When a guard fires, every decision stays
|
|
4521
|
+
* When a guard fires, every decision stays unstamped.
|
|
4521
4522
|
*/
|
|
4522
4523
|
recordPendingDecisions(createStamp) {
|
|
4523
4524
|
const stamped = this._pendingEvents;
|
|
4524
4525
|
const stampedCount = stamped.length;
|
|
4525
4526
|
const recorded = stamped.map((event, index) => {
|
|
4526
4527
|
const candidate = event;
|
|
4527
|
-
if (
|
|
4528
|
-
if (!
|
|
4529
|
-
return
|
|
4528
|
+
if (isStampedDomainEvent(candidate)) return candidate;
|
|
4529
|
+
if (!isUnstampedDomainEvent(candidate)) throw new HandBuiltEventError(event.type);
|
|
4530
|
+
return stampDomainEvent(candidate, createStamp(candidate, index));
|
|
4530
4531
|
});
|
|
4531
4532
|
if (this._pendingEvents !== stamped || this._pendingEvents.length !== stampedCount) throw new ReentrantEventRecordingError({ identity: this.aggregateIdentity });
|
|
4532
4533
|
const seenEventIds = /* @__PURE__ */ new Set();
|
|
@@ -4549,7 +4550,7 @@ var BaseAggregate = class extends Entity {
|
|
|
4549
4550
|
/**
|
|
4550
4551
|
* Post-commit cleanup for the deleted disposition. The row is gone, so
|
|
4551
4552
|
* there is no persisted version to advance: stamping the marker from the
|
|
4552
|
-
* live instance would make a later legitimate re-
|
|
4553
|
+
* live instance would make a later legitimate re-registration of this
|
|
4553
4554
|
* instance trip the unique-cursor guard for a row that does not exist.
|
|
4554
4555
|
*/
|
|
4555
4556
|
discardPendingEventsAfterDeletion(events) {
|
|
@@ -4636,12 +4637,12 @@ var BaseAggregate = class extends Entity {
|
|
|
4636
4637
|
this._persistedVersion = restored;
|
|
4637
4638
|
}
|
|
4638
4639
|
/**
|
|
4639
|
-
* Appends a domain event to the pending list. The event must be
|
|
4640
|
+
* Appends a domain event to the pending list. The event must be kit-built
|
|
4640
4641
|
* by a kit constructor; a missing `aggregateId` or `aggregateType` is
|
|
4641
4642
|
* stamped from this aggregate, and an aggregate identity that names another
|
|
4642
4643
|
* aggregate throws {@link MisattributedEventError} before anything is
|
|
4643
4644
|
* recorded. Each append is one fact. A decision appended twice becomes
|
|
4644
|
-
* two facts with distinct ids. A
|
|
4645
|
+
* two facts with distinct ids. A stamped event that is already pending
|
|
4645
4646
|
* throws {@link DuplicateEventIdError}, and a list at `maxPendingEvents`
|
|
4646
4647
|
* throws {@link PendingEventLimitExceededError}, before anything is
|
|
4647
4648
|
* recorded. Prefer the higher-level `StateStoredAggregate.setState()`
|
|
@@ -4654,23 +4655,23 @@ var BaseAggregate = class extends Entity {
|
|
|
4654
4655
|
* application shell rejects it; use `setState(currentState, event)`.
|
|
4655
4656
|
*/
|
|
4656
4657
|
addDomainEvent(event) {
|
|
4657
|
-
const stamped = this.
|
|
4658
|
+
const stamped = this.attributeNewEvent(event);
|
|
4658
4659
|
this.assertEventIdsNotPending([stamped]);
|
|
4659
4660
|
this.assertPendingEventLimit(1);
|
|
4660
4661
|
this.appendStampedEvent(stamped);
|
|
4661
4662
|
}
|
|
4662
4663
|
/**
|
|
4663
4664
|
* One identity per fact, checked before the change becomes observable:
|
|
4664
|
-
* a
|
|
4665
|
+
* a stamped event whose `eventId` is already pending, or appears twice
|
|
4665
4666
|
* in one batch, throws {@link DuplicateEventIdError}. A decision has no
|
|
4666
4667
|
* `eventId` yet, so a stamp provider that reuses one is caught at
|
|
4667
4668
|
* recording instead.
|
|
4668
4669
|
*/
|
|
4669
4670
|
assertEventIdsNotPending(batch) {
|
|
4670
4671
|
const pendingIds = /* @__PURE__ */ new Set();
|
|
4671
|
-
for (const pending of this._pendingEvents) if (
|
|
4672
|
+
for (const pending of this._pendingEvents) if (isStampedDomainEvent(pending)) pendingIds.add(pending.eventId);
|
|
4672
4673
|
for (const event of batch) {
|
|
4673
|
-
if (!
|
|
4674
|
+
if (!isStampedDomainEvent(event)) continue;
|
|
4674
4675
|
if (pendingIds.has(event.eventId)) throw new DuplicateEventIdError({
|
|
4675
4676
|
identity: this.aggregateIdentity,
|
|
4676
4677
|
eventId: event.eventId
|
|
@@ -4698,7 +4699,7 @@ var BaseAggregate = class extends Entity {
|
|
|
4698
4699
|
}
|
|
4699
4700
|
/**
|
|
4700
4701
|
* Appends an event that the caller already passed through
|
|
4701
|
-
* {@link
|
|
4702
|
+
* {@link attributeNewEvent}, {@link assertEventIdsNotPending} and
|
|
4702
4703
|
* {@link assertPendingEventLimit}. `setState()` and `apply()` run the
|
|
4703
4704
|
* three before the state moves and append afterwards, so each gate
|
|
4704
4705
|
* runs once per event.
|
|
@@ -4719,15 +4720,15 @@ var BaseAggregate = class extends Entity {
|
|
|
4719
4720
|
* present-but-foreign `aggregateId` / `aggregateType` is a wiring bug and
|
|
4720
4721
|
* throws {@link MisattributedEventError}; missing fields are filled in
|
|
4721
4722
|
* 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
|
|
4723
|
+
* aggregate identity and can never fail the collection or the replay guard
|
|
4724
|
+
* later. The kit-built check
|
|
4725
|
+
* runs first, so a hand-built event fails before anything else. The
|
|
4725
4726
|
* stamped copy is frozen like the original (payload and metadata are
|
|
4726
4727
|
* shared, already deep-frozen by the constructors); a fully stamped
|
|
4727
4728
|
* event is returned as is.
|
|
4728
4729
|
*/
|
|
4729
|
-
|
|
4730
|
-
this.
|
|
4730
|
+
attributeNewEvent(event) {
|
|
4731
|
+
this.assertKitBuiltEvent(event);
|
|
4731
4732
|
const { aggregateId, aggregateType } = event;
|
|
4732
4733
|
const idForeign = aggregateId !== void 0 && aggregateId !== this.id;
|
|
4733
4734
|
const typeForeign = aggregateType !== void 0 && aggregateType !== this.aggregateType;
|
|
@@ -4748,32 +4749,31 @@ var BaseAggregate = class extends Entity {
|
|
|
4748
4749
|
aggregateId: this.id,
|
|
4749
4750
|
aggregateType: this.aggregateType
|
|
4750
4751
|
};
|
|
4751
|
-
return
|
|
4752
|
+
return isStampedDomainEvent(event) ? adoptStampedDomainEvent(copy) : adoptUnstampedDomainEvent(copy);
|
|
4752
4753
|
}
|
|
4753
4754
|
/**
|
|
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
|
|
4755
|
+
* Kit-built check for every recording path: only an event that a kit
|
|
4756
|
+
* constructor produced passes. The kit marks two shapes: a stamped
|
|
4757
|
+
* event carries the stamped brand (`createDomainEvent`,
|
|
4758
|
+
* `createStampedDomainEvent`) and a decision carries the unstamped
|
|
4759
|
+
* brand (the aggregate `createEvent` helper). Either brand implies deeply frozen with defensively copied
|
|
4760
4760
|
* payload and metadata, a guarantee no frozen-ness probe can establish
|
|
4761
4761
|
* (a shallow-frozen literal with mutable nested data would fool it).
|
|
4762
4762
|
* O(1): one brand check per shape.
|
|
4763
4763
|
*/
|
|
4764
|
-
|
|
4765
|
-
if (!
|
|
4764
|
+
assertKitBuiltEvent(event) {
|
|
4765
|
+
if (!isStampedDomainEvent(event) && !isUnstampedDomainEvent(event)) throw new HandBuiltEventError(event.type);
|
|
4766
4766
|
}
|
|
4767
4767
|
/**
|
|
4768
4768
|
* Creates the immutable business fact accepted by this aggregate without
|
|
4769
4769
|
* reading a clock, generating an id, or attaching tracing metadata.
|
|
4770
4770
|
*
|
|
4771
|
-
* The application shell
|
|
4771
|
+
* The application shell stamps pending events after the domain operation
|
|
4772
4772
|
* and before persistence. Payload schema version stays here, next to the
|
|
4773
|
-
* concrete event producer, rather than in shell-owned
|
|
4773
|
+
* concrete event producer, rather than in shell-owned stamp data.
|
|
4774
4774
|
*/
|
|
4775
4775
|
createEvent(type, payload, options) {
|
|
4776
|
-
return
|
|
4776
|
+
return buildUnstampedDomainEvent(type, payload, {
|
|
4777
4777
|
...options,
|
|
4778
4778
|
aggregateId: this.id,
|
|
4779
4779
|
aggregateType: this.aggregateType
|
|
@@ -4785,7 +4785,7 @@ var BaseAggregate = class extends Entity {
|
|
|
4785
4785
|
* `EventSourcedAggregate.replayHistory`: a target carrying unflushed
|
|
4786
4786
|
* `pendingEvents` throws {@link UnreplayableAggregateError} BEFORE anything
|
|
4787
4787
|
* moves. A restore advances the aggregate's current version, so unflushed
|
|
4788
|
-
* events recorded against the old version would later be
|
|
4788
|
+
* events recorded against the old version would later be collected claiming
|
|
4789
4789
|
* a version baseline they were never part of. When the discard is
|
|
4790
4790
|
* deliberate, discard this dirty instance and reconstitute a fresh aggregate
|
|
4791
4791
|
* instead of mutating persistence lifecycle state publicly.
|
|
@@ -4793,8 +4793,8 @@ var BaseAggregate = class extends Entity {
|
|
|
4793
4793
|
* Deliberately a module-level function, not a class method: it MUST not be
|
|
4794
4794
|
* overridable by consumer subclasses (a no-op override would silently
|
|
4795
4795
|
* 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
|
|
4796
|
+
* the private list; the commit collection reads the public `pendingEvents`
|
|
4797
|
+
* getter, so a subclass that overrides the getter changes the collection,
|
|
4798
4798
|
* not this guard.
|
|
4799
4799
|
*
|
|
4800
4800
|
* @internal Shared by the aggregate flavours in this package; not part of
|
|
@@ -4926,12 +4926,12 @@ var EventSourcedAggregate = class extends BaseAggregate {
|
|
|
4926
4926
|
* @param event - The domain event to apply
|
|
4927
4927
|
*/
|
|
4928
4928
|
apply(event) {
|
|
4929
|
-
const stamped = this.
|
|
4929
|
+
const stamped = this.attributeNewEvent(event);
|
|
4930
4930
|
this.assertEventIdsNotPending([stamped]);
|
|
4931
4931
|
this.assertPendingEventLimit(1);
|
|
4932
4932
|
this.validateEvent(stamped);
|
|
4933
4933
|
const next = freezeEntityState(this, this.fold(stamped));
|
|
4934
|
-
|
|
4934
|
+
assertStateHasNoOwnProtoKey(next, "Aggregate state");
|
|
4935
4935
|
assertStateInvariant(this, next);
|
|
4936
4936
|
this.bumpVersion();
|
|
4937
4937
|
storeTrustedState(this, next);
|
|
@@ -4959,7 +4959,7 @@ var EventSourcedAggregate = class extends BaseAggregate {
|
|
|
4959
4959
|
* stream corruption) after the all-or-nothing rollback. History
|
|
4960
4960
|
* events without the optional aggregate identity fields pass unchecked (the
|
|
4961
4961
|
* fields are optional on the event shape); NEW events are covered
|
|
4962
|
-
* by the stricter `
|
|
4962
|
+
* by the stricter `attributeNewEvent` on the apply path.
|
|
4963
4963
|
*/
|
|
4964
4964
|
assertReplayedEventBelongsHere(event) {
|
|
4965
4965
|
const idMismatch = event.aggregateId !== void 0 && event.aggregateId !== this.id;
|
|
@@ -5020,7 +5020,7 @@ var EventSourcedAggregate = class extends BaseAggregate {
|
|
|
5020
5020
|
this.assertReplayedEventBelongsHere(event);
|
|
5021
5021
|
storeTrustedState(this, this.fold(event));
|
|
5022
5022
|
}
|
|
5023
|
-
|
|
5023
|
+
assertStateHasNoOwnProtoKey(this.state, "Aggregate state");
|
|
5024
5024
|
this.markReconstituted(startVersion + history.length);
|
|
5025
5025
|
} catch (e) {
|
|
5026
5026
|
storeTrustedState(this, previousState);
|
|
@@ -5057,20 +5057,20 @@ function reconstituteAggregateFromHistory(createReplayTarget, history) {
|
|
|
5057
5057
|
* The aggregate owns identity, valid domain state, behavior, its current
|
|
5058
5058
|
* domain version, and pending domain events. It deliberately does not own a
|
|
5059
5059
|
* database baseline or dirty-key bookkeeping. A repository adapter defines
|
|
5060
|
-
* its own persistence
|
|
5060
|
+
* its own persistence capture; the application shell retains that opaque
|
|
5061
5061
|
* baseline and derives the adapter's change set at flush.
|
|
5062
5062
|
*/
|
|
5063
5063
|
var StateStoredAggregate = class extends BaseAggregate {
|
|
5064
5064
|
/**
|
|
5065
5065
|
* Replaces the state, advances the OCC version, and records the events
|
|
5066
|
-
* of the change, in that order. State validation, the
|
|
5066
|
+
* of the change, in that order. State validation, the kit-built check,
|
|
5067
5067
|
* the aggregate identity check of the event, the pending event-id check,
|
|
5068
5068
|
* and the pending event limit check run before the change becomes
|
|
5069
5069
|
* observable, so a rejected decision records nothing and moves nothing.
|
|
5070
5070
|
* Without events the call is a plain versioned state change.
|
|
5071
5071
|
*/
|
|
5072
5072
|
setState(newState, events = []) {
|
|
5073
|
-
const stamped = (Array.isArray(events) ? events : [events]).map((event) => this.
|
|
5073
|
+
const stamped = (Array.isArray(events) ? events : [events]).map((event) => this.attributeNewEvent(event));
|
|
5074
5074
|
this.assertEventIdsNotPending(stamped);
|
|
5075
5075
|
this.assertPendingEventLimit(stamped.length);
|
|
5076
5076
|
const next = this.nextVersion();
|
|
@@ -5229,684 +5229,16 @@ var NotSpecification = class extends Specification {
|
|
|
5229
5229
|
};
|
|
5230
5230
|
|
|
5231
5231
|
//#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
|
|
5232
|
+
//#region src/domain/value-object/value-object-or-issues.ts
|
|
5901
5233
|
/**
|
|
5902
5234
|
* Builds an immutable value object while collecting **all** validation
|
|
5903
5235
|
* violations into a single {@link ValidationError}, instead of failing on the
|
|
5904
5236
|
* first one. This is the Result-first, multi-error counterpart to
|
|
5905
|
-
* `
|
|
5237
|
+
* `valueObjectOrError` (which returns a single string message).
|
|
5906
5238
|
*
|
|
5907
5239
|
* The `validate` callback receives a fresh `ValidationError` to push field
|
|
5908
5240
|
* issues onto (via `addIssue` / `addIssues`) and the raw input. When no issue
|
|
5909
|
-
* was recorded the input is frozen into a `
|
|
5241
|
+
* was recorded the input is frozen into a `ValueObjectOf<T>` and returned as `Ok`;
|
|
5910
5242
|
* otherwise the populated `ValidationError` is returned as `Err`.
|
|
5911
5243
|
*
|
|
5912
5244
|
* `ValidationError` comes from `@shirudo/base-error`; import it from there to
|
|
@@ -5916,7 +5248,7 @@ var DomainStateMachine = class {
|
|
|
5916
5248
|
*
|
|
5917
5249
|
* @example
|
|
5918
5250
|
* ```ts
|
|
5919
|
-
* const result =
|
|
5251
|
+
* const result = valueObjectOrIssues(
|
|
5920
5252
|
* { email, age },
|
|
5921
5253
|
* (issues, m) => {
|
|
5922
5254
|
* if (!isEmail(m.email))
|
|
@@ -5929,10 +5261,10 @@ var DomainStateMachine = class {
|
|
|
5929
5261
|
* // result.isErr() → result.error.publicIssues() has both violations
|
|
5930
5262
|
* ```
|
|
5931
5263
|
*/
|
|
5932
|
-
function
|
|
5264
|
+
function valueObjectOrIssues(t, validate, message = "Validation failed") {
|
|
5933
5265
|
const issues = new ValidationError(message);
|
|
5934
5266
|
validate(issues, t);
|
|
5935
|
-
return issues.hasIssues() ? err(issues) : ok(
|
|
5267
|
+
return issues.hasIssues() ? err(issues) : ok(valueObject(t));
|
|
5936
5268
|
}
|
|
5937
5269
|
|
|
5938
5270
|
//#endregion
|
|
@@ -6041,7 +5373,7 @@ var PublishChainTracker = class {
|
|
|
6041
5373
|
stateBySignal = /* @__PURE__ */ new WeakMap();
|
|
6042
5374
|
syncWindowState;
|
|
6043
5375
|
constructor(store) {
|
|
6044
|
-
if (store !== void 0 && (typeof store.run !== "function" || typeof store.getStore !== "function")) throw new TypeError("
|
|
5376
|
+
if (store !== void 0 && (typeof store.run !== "function" || typeof store.getStore !== "function")) throw new TypeError("InProcessEventBus.chainStore must provide run and getStore");
|
|
6045
5377
|
this.store = store;
|
|
6046
5378
|
}
|
|
6047
5379
|
/** Depth, path and enclosing state that a publication on `signal` inherits. */
|
|
@@ -6152,7 +5484,7 @@ function toHandlerError(reason) {
|
|
|
6152
5484
|
/** Bound for one publish chain. Real nesting stays far below this. */
|
|
6153
5485
|
const DEFAULT_MAX_PUBLISH_DEPTH = 32;
|
|
6154
5486
|
/** Marks the catch-all subscriptions in the reported-once set. */
|
|
6155
|
-
const CATCH_ALL = Symbol("
|
|
5487
|
+
const CATCH_ALL = Symbol("InProcessEventBus.subscribeAll");
|
|
6156
5488
|
/**
|
|
6157
5489
|
* Subscriptions for one event type above which the bus reports a leak. A
|
|
6158
5490
|
* fan-out of projections stays far below this.
|
|
@@ -6166,7 +5498,7 @@ const DEFAULT_MAX_SUBSCRIPTIONS_PER_EVENT_TYPE = 32;
|
|
|
6166
5498
|
*
|
|
6167
5499
|
* @example
|
|
6168
5500
|
* ```typescript
|
|
6169
|
-
* const bus = new
|
|
5501
|
+
* const bus = new InProcessEventBus<OrderEvent>();
|
|
6170
5502
|
*
|
|
6171
5503
|
* bus.subscribe("OrderCreated", async (event) => {
|
|
6172
5504
|
* await sendEmail(event.payload.customerId);
|
|
@@ -6226,7 +5558,7 @@ const DEFAULT_MAX_SUBSCRIPTIONS_PER_EVENT_TYPE = 32;
|
|
|
6226
5558
|
* the signal.
|
|
6227
5559
|
*
|
|
6228
5560
|
*/
|
|
6229
|
-
var
|
|
5561
|
+
var InProcessEventBus = class {
|
|
6230
5562
|
handlers = /* @__PURE__ */ new Map();
|
|
6231
5563
|
catchAllHandlers = [];
|
|
6232
5564
|
maxPublishDepth;
|
|
@@ -6237,12 +5569,12 @@ var EventBusImpl = class {
|
|
|
6237
5569
|
observers;
|
|
6238
5570
|
reportedAt = /* @__PURE__ */ new Map();
|
|
6239
5571
|
constructor(options = {}) {
|
|
6240
|
-
if (options.maxPublishDepth !== void 0) assertPositiveInteger("
|
|
6241
|
-
if (options.maxSubscriptionsPerEventType !== void 0) assertPositiveInteger("
|
|
5572
|
+
if (options.maxPublishDepth !== void 0) assertPositiveInteger("InProcessEventBus", "maxPublishDepth", options.maxPublishDepth);
|
|
5573
|
+
if (options.maxSubscriptionsPerEventType !== void 0) assertPositiveInteger("InProcessEventBus", "maxSubscriptionsPerEventType", options.maxSubscriptionsPerEventType);
|
|
6242
5574
|
this.chain = new PublishChainTracker(options.chainStore);
|
|
6243
5575
|
this.maxPublishDepth = options.maxPublishDepth ?? DEFAULT_MAX_PUBLISH_DEPTH;
|
|
6244
5576
|
this.maxSubscriptionsPerEventType = options.maxSubscriptionsPerEventType ?? DEFAULT_MAX_SUBSCRIPTIONS_PER_EVENT_TYPE;
|
|
6245
|
-
this.observers = options.observers === void 0 ? void 0 : captureObserverFunctions("
|
|
5577
|
+
this.observers = options.observers === void 0 ? void 0 : captureObserverFunctions("InProcessEventBus", options.observers, [
|
|
6246
5578
|
"onSubscriptionThresholdExceeded",
|
|
6247
5579
|
"onHandlerError",
|
|
6248
5580
|
"onPublishAborted"
|
|
@@ -6515,7 +5847,7 @@ function decodeIntegrationMessage(serialized) {
|
|
|
6515
5847
|
}
|
|
6516
5848
|
}
|
|
6517
5849
|
/**
|
|
6518
|
-
* Composes a validated public message into a
|
|
5850
|
+
* Composes a validated public message into a kit-built local projector input.
|
|
6519
5851
|
* Relationship headers become local event metadata. The public JSON schema is
|
|
6520
5852
|
* retained; producer-private domain types are not reconstructed.
|
|
6521
5853
|
*/
|
|
@@ -6668,9 +6000,9 @@ function outboxWriterAcceptingEventLoss() {
|
|
|
6668
6000
|
* another aggregate version. Without an `InMemoryTransactionScope`, it keeps
|
|
6669
6001
|
* the first add of a rolled-back attempt. Dead-lettered and acknowledged
|
|
6670
6002
|
* retries must match the complete original candidate receipt. Reusing an `eventId` for another source or commit position
|
|
6671
|
-
* throws {@link
|
|
6003
|
+
* throws {@link EventCollectionError} while the pending, dead-letter, or bounded
|
|
6672
6004
|
* dispatched receipt still proves the collision. `add` also throws
|
|
6673
|
-
* {@link
|
|
6005
|
+
* {@link EventCollectionError} for a malformed candidate: a part that is not an
|
|
6674
6006
|
* object, an `eventId` or `type` that is not an own string data property, a
|
|
6675
6007
|
* source field that is not a string, or a position that is not a valid
|
|
6676
6008
|
* commit position. Insertion order is preserved:
|
|
@@ -6685,7 +6017,7 @@ function outboxWriterAcceptingEventLoss() {
|
|
|
6685
6017
|
* To link future eventful commits, the implementation also retains one
|
|
6686
6018
|
* source cursor per qualified aggregate after dispatch. `endEventSources`
|
|
6687
6019
|
* marks the cursor of a removed aggregate as ended, and a new event of that
|
|
6688
|
-
* source then throws {@link
|
|
6020
|
+
* source then throws {@link EventCollectionError}. Consequently a
|
|
6689
6021
|
* long-lived instance is bounded only when `maxRecords` and `maxSources` are
|
|
6690
6022
|
* configured, plus `maxRetainedDispatchedEventIds`; use a durable adapter with
|
|
6691
6023
|
* an explicit source-head lifecycle and an event-id unique key for unbounded
|
|
@@ -6717,10 +6049,10 @@ function outboxWriterAcceptingEventLoss() {
|
|
|
6717
6049
|
*
|
|
6718
6050
|
* @example
|
|
6719
6051
|
* ```ts
|
|
6720
|
-
* import { InMemoryOutbox,
|
|
6052
|
+
* import { InMemoryOutbox, InProcessEventBus } from "@shirudo/ddd-kit";
|
|
6721
6053
|
*
|
|
6722
6054
|
* const outbox = new InMemoryOutbox<OrderEvent>();
|
|
6723
|
-
* const bus = new
|
|
6055
|
+
* const bus = new InProcessEventBus<OrderEvent>();
|
|
6724
6056
|
*
|
|
6725
6057
|
* const uow = makeOrderUnitOfWork({ outbox, bus });
|
|
6726
6058
|
* await uow.run(async ({ repositories }) => {
|
|
@@ -7022,7 +6354,7 @@ var InMemoryOutbox = class {
|
|
|
7022
6354
|
if (this.pending.has(event.eventId) || this.dead.has(event.eventId) || this.dispatchedEventIds.has(event.eventId)) return;
|
|
7023
6355
|
const cursor = this.sourceCursors.get(sourceKey);
|
|
7024
6356
|
if (cursor !== void 0 && (position.aggregateVersion < cursor.aggregateVersion || position.aggregateVersion === cursor.aggregateVersion && cursor.eventIdsBySequence.has(position.commitSequence))) return;
|
|
7025
|
-
throw new
|
|
6357
|
+
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
6358
|
}
|
|
7027
6359
|
/**
|
|
7028
6360
|
* Event ids of the batch that the source head names at their position
|
|
@@ -7091,7 +6423,7 @@ function assertSameCandidateReceipt(event, received, recorded) {
|
|
|
7091
6423
|
}
|
|
7092
6424
|
/**
|
|
7093
6425
|
* The lenient variant for PENDING records only: an outbox without an
|
|
7094
|
-
* `InMemoryTransactionScope` cannot observe a rollback, so a re-
|
|
6426
|
+
* `InMemoryTransactionScope` cannot observe a rollback, so a re-collected
|
|
7095
6427
|
* event may legitimately arrive at a new aggregateVersion. Index and commit
|
|
7096
6428
|
* cardinality stay immutable.
|
|
7097
6429
|
*/
|
|
@@ -7100,7 +6432,7 @@ function assertSameCandidateReceiptAllowingVersionRefresh(event, received, recor
|
|
|
7100
6432
|
}
|
|
7101
6433
|
function assertReceiptShape(event, received, recorded, allowAggregateVersionRefresh) {
|
|
7102
6434
|
if ((allowAggregateVersionRefresh || received.aggregateVersion === recorded.aggregateVersion) && received.commitSequence === recorded.commitSequence && received.commitSize === recorded.commitSize) return;
|
|
7103
|
-
throw new
|
|
6435
|
+
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
6436
|
}
|
|
7105
6437
|
/**
|
|
7106
6438
|
* The fields of a candidate that the outbox reads, read once and checked.
|
|
@@ -7112,7 +6444,7 @@ function assertReceiptShape(event, received, recorded, allowAggregateVersionRefr
|
|
|
7112
6444
|
function ownCandidate(candidate, index) {
|
|
7113
6445
|
let subject = `the candidate at index ${index}`;
|
|
7114
6446
|
let eventType;
|
|
7115
|
-
const rejection = (field, expected, observed) => new
|
|
6447
|
+
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
6448
|
if (typeof candidate !== "object" || candidate === null) throw rejection("the candidate", "an object", describeRejected(candidate));
|
|
7117
6449
|
const { event, source, position } = candidate;
|
|
7118
6450
|
if (typeof event !== "object" || event === null) throw rejection("event", "an object", describeRejected(event));
|
|
@@ -7162,17 +6494,17 @@ function refill(map, entries) {
|
|
|
7162
6494
|
for (const [key, value] of entries) map.set(key, value);
|
|
7163
6495
|
}
|
|
7164
6496
|
function commitSizeMismatchError(event, source, position, recordedCommitSize) {
|
|
7165
|
-
return new
|
|
6497
|
+
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
6498
|
}
|
|
7167
6499
|
function positionOwnedError(event, source, position, positionOwner) {
|
|
7168
|
-
return new
|
|
6500
|
+
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
6501
|
}
|
|
7170
6502
|
function staleHeadError(event, source, position, staleHeadVersion) {
|
|
7171
|
-
return new
|
|
6503
|
+
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
6504
|
}
|
|
7173
6505
|
function assertSameEventSource(event, received, recorded) {
|
|
7174
6506
|
if (received.aggregateType === recorded.aggregateType && received.aggregateId === recorded.aggregateId) return;
|
|
7175
|
-
throw new
|
|
6507
|
+
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
6508
|
}
|
|
7177
6509
|
|
|
7178
6510
|
//#endregion
|
|
@@ -7433,7 +6765,7 @@ var OutboxDispatcher = class extends PollLoop {
|
|
|
7433
6765
|
function copyForStorage(event, index, stream) {
|
|
7434
6766
|
const subject = `InMemoryEventStore.append (stream ${describeAggregateIdentity(stream)}): events[${index}]`;
|
|
7435
6767
|
if (event === null || typeof event !== "object") throw new TypeError(`${subject} is not an event object`);
|
|
7436
|
-
return
|
|
6768
|
+
return copyFreezableState(event, subject);
|
|
7437
6769
|
}
|
|
7438
6770
|
function assertStreamPosition(name, value) {
|
|
7439
6771
|
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 +6824,7 @@ var InMemoryEventStore = class {
|
|
|
7492
6824
|
async append(stream, events, options) {
|
|
7493
6825
|
const batch = Array.from(events);
|
|
7494
6826
|
if (batch.length === 0) return;
|
|
7495
|
-
const identity =
|
|
6827
|
+
const identity = copyAggregateIdentity(stream);
|
|
7496
6828
|
const expectedVersion = options.expectedVersion;
|
|
7497
6829
|
const key = encodeAggregateIdentity(identity);
|
|
7498
6830
|
this.existingStreamForAppend(identity, key, expectedVersion, batch.length);
|
|
@@ -7770,7 +7102,7 @@ function defaultSleep(ms, signal) {
|
|
|
7770
7102
|
*
|
|
7771
7103
|
* **Classification is by error, not by guesswork.** Only errors the
|
|
7772
7104
|
* `isRetryable` predicate accepts are retried; everything else (a
|
|
7773
|
-
* `DomainError`, `
|
|
7105
|
+
* `DomainError`, `EventCollectionError`, `UnregisteredChangesError`,
|
|
7774
7106
|
* `DuplicateAggregateError`, a non-Error throw) surfaces immediately.
|
|
7775
7107
|
* After `maxAttempts` the last error is rethrown unchanged, so a caller
|
|
7776
7108
|
* can still match `ConcurrencyConflictError` and map it to HTTP 409.
|
|
@@ -7936,12 +7268,12 @@ var InMemorySnapshotStore = class {
|
|
|
7936
7268
|
//#region src/persistence/snapshot-store/snapshot-model.ts
|
|
7937
7269
|
/** Type-inference helper for declaring an adapter-owned snapshot model. */
|
|
7938
7270
|
function defineSnapshotModel(model) {
|
|
7939
|
-
const
|
|
7940
|
-
assertSnapshotModel(
|
|
7941
|
-
return
|
|
7271
|
+
const copy = Object.freeze({ ...model });
|
|
7272
|
+
assertSnapshotModel(copy);
|
|
7273
|
+
return copy;
|
|
7942
7274
|
}
|
|
7943
7275
|
/**
|
|
7944
|
-
* Captures
|
|
7276
|
+
* Captures an independent persistence envelope at an application-supplied time.
|
|
7945
7277
|
* The application decides when snapshotting is worthwhile; this function does
|
|
7946
7278
|
* not read a clock or perform I/O. The envelope is deep-frozen, its `state`
|
|
7947
7279
|
* and its `snapshotAt` included: a write into the captured state or a
|
|
@@ -7952,7 +7284,7 @@ function defineSnapshotModel(model) {
|
|
|
7952
7284
|
function captureAggregateSnapshot(model, aggregate, snapshotAt) {
|
|
7953
7285
|
assertSnapshotModel(model);
|
|
7954
7286
|
const recordedAt = copySnapshotAt(snapshotAt);
|
|
7955
|
-
const state =
|
|
7287
|
+
const state = copyPlainState(model.capture(aggregate));
|
|
7956
7288
|
return deepFreeze({
|
|
7957
7289
|
state,
|
|
7958
7290
|
version: aggregate.version,
|
|
@@ -7991,8 +7323,8 @@ function reconstituteAggregateFromSnapshot(model, id, snapshot) {
|
|
|
7991
7323
|
let aggregate;
|
|
7992
7324
|
try {
|
|
7993
7325
|
let state;
|
|
7994
|
-
if (storedSchemaVersion === model.schemaVersion) state =
|
|
7995
|
-
else if (model.migrate) state =
|
|
7326
|
+
if (storedSchemaVersion === model.schemaVersion) state = copyPlainState(snapshot.state);
|
|
7327
|
+
else if (model.migrate) state = copyPlainState(model.migrate(copyPlainState(snapshot.state), storedSchemaVersion));
|
|
7996
7328
|
else throw new SnapshotSchemaMismatchError({
|
|
7997
7329
|
identity: {
|
|
7998
7330
|
aggregateType: model.aggregateType,
|
|
@@ -8031,5 +7363,5 @@ function copySnapshotAt(snapshotAt) {
|
|
|
8031
7363
|
}
|
|
8032
7364
|
|
|
8033
7365
|
//#endregion
|
|
8034
|
-
export { AggregateDeletedError, AggregateNotFoundError, AggregateTrackingError, CapabilityRegistryConflictError, CommandBus, CommitError, ConcurrencyConflictError, DeadlineProcessor, DirectStateMutationError, DomainError, DomainEventValidationError,
|
|
7366
|
+
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
7367
|
//# sourceMappingURL=index.js.map
|