@shirudo/ddd-kit 1.0.0-rc.7 → 1.0.0-rc.9
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/dist/index.d.ts +549 -555
- package/dist/index.js +165 -190
- package/dist/index.js.map +1 -1
- package/package.json +72 -71
package/dist/index.js
CHANGED
|
@@ -589,35 +589,83 @@ function entityIds(entities) {
|
|
|
589
589
|
}
|
|
590
590
|
__name(entityIds, "entityIds");
|
|
591
591
|
|
|
592
|
-
// src/aggregate/aggregate
|
|
593
|
-
var
|
|
592
|
+
// src/aggregate/base-aggregate.ts
|
|
593
|
+
var BaseAggregate = class extends Entity {
|
|
594
594
|
static {
|
|
595
|
-
__name(this, "
|
|
595
|
+
__name(this, "BaseAggregate");
|
|
596
596
|
}
|
|
597
597
|
_version = 0;
|
|
598
|
+
/**
|
|
599
|
+
* DB-baseline version. `undefined` until the aggregate has been
|
|
600
|
+
* persisted or restored at least once. Repository implementations
|
|
601
|
+
* route INSERT vs UPDATE on this field and use it as the OCC
|
|
602
|
+
* baseline. See `IRepository.save` JSDoc.
|
|
603
|
+
*
|
|
604
|
+
* Distinct from {@link version}, which is the in-memory
|
|
605
|
+
* post-mutation value. Mutations bump `_version` but never touch
|
|
606
|
+
* `_persistedVersion` — that field only moves on {@link markRestored}
|
|
607
|
+
* (Post-Load) and {@link markPersisted} (Post-Save).
|
|
608
|
+
*/
|
|
609
|
+
_persistedVersion = void 0;
|
|
610
|
+
_pendingEvents = [];
|
|
598
611
|
get version() {
|
|
599
612
|
return this._version;
|
|
600
613
|
}
|
|
601
|
-
|
|
602
|
-
this.
|
|
614
|
+
get persistedVersion() {
|
|
615
|
+
return this._persistedVersion;
|
|
603
616
|
}
|
|
604
|
-
_config;
|
|
605
|
-
_autoVersionBump;
|
|
606
|
-
_pendingEvents = [];
|
|
607
617
|
/**
|
|
608
|
-
* Read-only list of domain events recorded on this aggregate that
|
|
609
|
-
* not yet been flushed to the outbox / persistence layer.
|
|
618
|
+
* Read-only list of domain events recorded on this aggregate that
|
|
619
|
+
* have not yet been flushed to the outbox / persistence layer.
|
|
610
620
|
*/
|
|
611
621
|
get pendingEvents() {
|
|
612
622
|
return Object.freeze(this._pendingEvents.slice());
|
|
613
623
|
}
|
|
614
624
|
/**
|
|
615
|
-
* Clears the pending-event list.
|
|
616
|
-
*
|
|
625
|
+
* Clears the pending-event list. Called by `markPersisted` after a
|
|
626
|
+
* successful write — the events have been handed off to the outbox
|
|
627
|
+
* / event store and are no longer the aggregate's responsibility.
|
|
617
628
|
*/
|
|
618
629
|
clearPendingEvents() {
|
|
619
630
|
this._pendingEvents = [];
|
|
620
631
|
}
|
|
632
|
+
setVersion(version) {
|
|
633
|
+
this._version = version;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Manually bumps the aggregate version. Used by state-stored
|
|
637
|
+
* aggregates' `setState(_, true)` / `commit()` paths and by the
|
|
638
|
+
* event-sourced replay path after each applied event.
|
|
639
|
+
*/
|
|
640
|
+
bumpVersion() {
|
|
641
|
+
this.setVersion(this._version + 1);
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* **Lifecycle marker — Post-Load.** Syncs both `_version` and
|
|
645
|
+
* `_persistedVersion` to the DB-stored version. Used by
|
|
646
|
+
* `reconstitute(...)` factories to assemble an in-memory aggregate
|
|
647
|
+
* from a persisted row.
|
|
648
|
+
*
|
|
649
|
+
* Does NOT fire {@link onPersisted} — that hook has post-save
|
|
650
|
+
* semantics (metrics, audit, cache eviction), not post-load. The
|
|
651
|
+
* Factory-vs-Reconstitution distinction (Vernon §11) is honoured
|
|
652
|
+
* structurally: two separate markers, one for each transition.
|
|
653
|
+
*
|
|
654
|
+
* @param version - The version the row currently holds in the DB
|
|
655
|
+
*
|
|
656
|
+
* @example
|
|
657
|
+
* ```ts
|
|
658
|
+
* static reconstitute(id: OrderId, state: OrderState, version: Version): Order {
|
|
659
|
+
* const order = new Order(id, state);
|
|
660
|
+
* order.markRestored(version);
|
|
661
|
+
* return order;
|
|
662
|
+
* }
|
|
663
|
+
* ```
|
|
664
|
+
*/
|
|
665
|
+
markRestored(version) {
|
|
666
|
+
this.setVersion(version);
|
|
667
|
+
this._persistedVersion = version;
|
|
668
|
+
}
|
|
621
669
|
/**
|
|
622
670
|
* **Framework lifecycle method — `@sealed`.** Called by `withCommit`
|
|
623
671
|
* (or by your own orchestration code, after harvesting `pendingEvents`)
|
|
@@ -639,7 +687,7 @@ var AggregateRoot = class extends Entity {
|
|
|
639
687
|
* @see onPersisted — the safe extension point for subclasses
|
|
640
688
|
*/
|
|
641
689
|
markPersisted(version) {
|
|
642
|
-
this.
|
|
690
|
+
this.markRestored(version);
|
|
643
691
|
this._pendingEvents = [];
|
|
644
692
|
this.onPersisted(version);
|
|
645
693
|
}
|
|
@@ -675,6 +723,83 @@ var AggregateRoot = class extends Entity {
|
|
|
675
723
|
*/
|
|
676
724
|
onPersisted(_version) {
|
|
677
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* Appends a domain event to the pending list. Prefer the higher-level
|
|
728
|
+
* `AggregateRoot.commit()` (state-stored) or `EventSourcedAggregate.apply()`
|
|
729
|
+
* (event-sourced) call sites — both wrap `addDomainEvent` in the
|
|
730
|
+
* canonical record-AFTER-mutation order (Vernon §8). Calling
|
|
731
|
+
* `addDomainEvent` directly is appropriate only when state and event
|
|
732
|
+
* recording have already been decoupled deliberately (e.g. a
|
|
733
|
+
* deletion event before a hard-delete; see `docs/guide/repository.md`).
|
|
734
|
+
*/
|
|
735
|
+
addDomainEvent(event) {
|
|
736
|
+
this._pendingEvents.push(event);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Creates a snapshot of the current aggregate state — the state at
|
|
740
|
+
* this moment plus the version. Useful for ES snapshot policies and
|
|
741
|
+
* for state-stored backup / restore.
|
|
742
|
+
*/
|
|
743
|
+
createSnapshot() {
|
|
744
|
+
return {
|
|
745
|
+
state: structuredClone(this._state),
|
|
746
|
+
version: this.version,
|
|
747
|
+
snapshotAt: /* @__PURE__ */ new Date()
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Sugar for `createDomainEvent` that auto-injects `aggregateId`
|
|
752
|
+
* (from `this.id`) and `aggregateType` (from {@link aggregateType})
|
|
753
|
+
* into the event's metadata fields. This is the canonical path for
|
|
754
|
+
* recording events from inside aggregate domain methods.
|
|
755
|
+
*
|
|
756
|
+
* Downstream consumers — outbox dispatchers, projection handlers,
|
|
757
|
+
* audit logs — route by these two fields. Calling
|
|
758
|
+
* `createDomainEvent(...)` directly inside an aggregate method
|
|
759
|
+
* leaves them unset and is caught at the `withCommit` harvest
|
|
760
|
+
* boundary, but `this.recordEvent(...)` makes the right thing
|
|
761
|
+
* impossible to forget.
|
|
762
|
+
*
|
|
763
|
+
* @example
|
|
764
|
+
* ```ts
|
|
765
|
+
* class Order extends AggregateRoot<OrderState, OrderId, OrderEvent> {
|
|
766
|
+
* protected readonly aggregateType = "Order";
|
|
767
|
+
*
|
|
768
|
+
* confirm(): void {
|
|
769
|
+
* this.commit(
|
|
770
|
+
* { ...this.state, status: "confirmed" },
|
|
771
|
+
* this.recordEvent("OrderConfirmed", { orderId: this.id }),
|
|
772
|
+
* );
|
|
773
|
+
* }
|
|
774
|
+
* }
|
|
775
|
+
* ```
|
|
776
|
+
*
|
|
777
|
+
* @param type - event type discriminator (must be one of `TEvent`'s tags)
|
|
778
|
+
* @param payload - payload for that event subtype
|
|
779
|
+
* @param options - any remaining `createDomainEvent` options
|
|
780
|
+
* (`eventId`, `occurredAt`, `metadata`, `version`); `aggregateId`
|
|
781
|
+
* and `aggregateType` are deliberately omitted — the helper sets
|
|
782
|
+
* them.
|
|
783
|
+
*/
|
|
784
|
+
recordEvent(type, payload, options) {
|
|
785
|
+
return createDomainEvent(type, payload, {
|
|
786
|
+
...options,
|
|
787
|
+
aggregateId: this.id,
|
|
788
|
+
aggregateType: this.aggregateType
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// src/aggregate/aggregate-root.ts
|
|
794
|
+
var AggregateRoot = class extends BaseAggregate {
|
|
795
|
+
static {
|
|
796
|
+
__name(this, "AggregateRoot");
|
|
797
|
+
}
|
|
798
|
+
_autoVersionBump;
|
|
799
|
+
constructor(id, initialState, config) {
|
|
800
|
+
super(id, initialState);
|
|
801
|
+
this._autoVersionBump = config?.autoVersionBump ?? false;
|
|
802
|
+
}
|
|
678
803
|
/**
|
|
679
804
|
* Mutates state and records the resulting domain events in the
|
|
680
805
|
* **canonical record-after-mutation order**. Use this instead of calling
|
|
@@ -706,7 +831,7 @@ var AggregateRoot = class extends Entity {
|
|
|
706
831
|
* }
|
|
707
832
|
* this.commit(
|
|
708
833
|
* { ...this.state, status: "confirmed" },
|
|
709
|
-
*
|
|
834
|
+
* this.recordEvent("OrderConfirmed", { orderId: this.id }),
|
|
710
835
|
* );
|
|
711
836
|
* }
|
|
712
837
|
* ```
|
|
@@ -726,59 +851,9 @@ var AggregateRoot = class extends Entity {
|
|
|
726
851
|
this.addDomainEvent(ev);
|
|
727
852
|
}
|
|
728
853
|
}
|
|
729
|
-
constructor(id, initialState, config) {
|
|
730
|
-
super(id, initialState);
|
|
731
|
-
this._config = config ?? {};
|
|
732
|
-
this._autoVersionBump = this._config.autoVersionBump ?? false;
|
|
733
|
-
}
|
|
734
|
-
/**
|
|
735
|
-
* Records a domain event for later publication.
|
|
736
|
-
*
|
|
737
|
-
* **Ordering: record AFTER state mutation.** Vernon (IDDD §8) is
|
|
738
|
-
* explicit: a domain event describes something that has just happened
|
|
739
|
-
* to the aggregate — its existence implies the state change already
|
|
740
|
-
* occurred. Concretely:
|
|
741
|
-
*
|
|
742
|
-
* ```ts
|
|
743
|
-
* confirm(): void {
|
|
744
|
-
* if (this.state.status === "confirmed") {
|
|
745
|
-
* throw new OrderAlreadyConfirmedError(this.id);
|
|
746
|
-
* }
|
|
747
|
-
* this.setState({ ...this.state, status: "confirmed" }, true);
|
|
748
|
-
* this.addDomainEvent({ type: "OrderConfirmed", orderId: this.id });
|
|
749
|
-
* // ↑ post-mutation. The event represents the committed fact.
|
|
750
|
-
* }
|
|
751
|
-
* ```
|
|
752
|
-
*
|
|
753
|
-
* Recording before mutation is a footgun: if a subsequent invariant
|
|
754
|
-
* check throws, the event has already been queued but the state never
|
|
755
|
-
* actually changed — consumers see an event for a fact that did not
|
|
756
|
-
* happen.
|
|
757
|
-
*
|
|
758
|
-
* `EventSourcedAggregate.apply()` enforces this ordering structurally;
|
|
759
|
-
* `AggregateRoot` leaves it as a convention because the state-mutation
|
|
760
|
-
* path (`setState`) is decoupled from event recording.
|
|
761
|
-
*
|
|
762
|
-
* @param event - The domain event to record
|
|
763
|
-
*/
|
|
764
|
-
addDomainEvent(event) {
|
|
765
|
-
this._pendingEvents.push(event);
|
|
766
|
-
}
|
|
767
|
-
/**
|
|
768
|
-
* Manually bumps the aggregate version.
|
|
769
|
-
* Call this after state changes for Optimistic Concurrency Control.
|
|
770
|
-
*
|
|
771
|
-
* If `autoVersionBump` is enabled, this is called automatically
|
|
772
|
-
* when using `setState()`.
|
|
773
|
-
*/
|
|
774
|
-
bumpVersion() {
|
|
775
|
-
this.setVersion(this._version + 1);
|
|
776
|
-
}
|
|
777
854
|
/**
|
|
778
855
|
* Sets the state and optionally bumps the version automatically.
|
|
779
|
-
*
|
|
780
|
-
* Automatically validates the newState using `validateState()`.
|
|
781
|
-
* Overrides Entity.setState to add version bumping.
|
|
856
|
+
* Validates `newState` via `validateState()`.
|
|
782
857
|
*
|
|
783
858
|
* @param newState - The new state
|
|
784
859
|
* @param bumpVersion - Whether to bump the version (defaults to autoVersionBump config)
|
|
@@ -791,43 +866,17 @@ var AggregateRoot = class extends Entity {
|
|
|
791
866
|
}
|
|
792
867
|
}
|
|
793
868
|
/**
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
* @returns A snapshot containing the current state and version
|
|
798
|
-
*
|
|
799
|
-
* @example
|
|
800
|
-
* ```typescript
|
|
801
|
-
* const snapshot = aggregate.createSnapshot();
|
|
802
|
-
* await snapshotRepository.save(aggregate.id, snapshot);
|
|
803
|
-
* ```
|
|
804
|
-
*/
|
|
805
|
-
createSnapshot() {
|
|
806
|
-
return {
|
|
807
|
-
state: structuredClone(this._state),
|
|
808
|
-
version: this.version,
|
|
809
|
-
snapshotAt: /* @__PURE__ */ new Date()
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
/**
|
|
813
|
-
* Restores the aggregate from a snapshot.
|
|
814
|
-
* This is useful for loading aggregates from snapshots instead of
|
|
815
|
-
* rebuilding them from scratch.
|
|
816
|
-
* Validates the restored state.
|
|
869
|
+
* Restores the aggregate from a snapshot — loads state and aligns
|
|
870
|
+
* `version` + `persistedVersion` to the snapshot version. Validates
|
|
871
|
+
* the restored state.
|
|
817
872
|
*
|
|
818
873
|
* @param snapshot - The snapshot to restore from
|
|
819
|
-
*
|
|
820
|
-
* @example
|
|
821
|
-
* ```typescript
|
|
822
|
-
* const snapshot = await snapshotRepository.getLatest(aggregateId);
|
|
823
|
-
* aggregate.restoreFromSnapshot(snapshot);
|
|
824
|
-
* ```
|
|
825
874
|
*/
|
|
826
875
|
restoreFromSnapshot(snapshot) {
|
|
827
876
|
const cloned = structuredClone(snapshot.state);
|
|
828
877
|
this.validateState(cloned);
|
|
829
878
|
this._state = freezeShallow(cloned);
|
|
830
|
-
this.
|
|
879
|
+
this.markRestored(snapshot.version);
|
|
831
880
|
}
|
|
832
881
|
};
|
|
833
882
|
var DomainError = class extends BaseError {
|
|
@@ -888,87 +937,10 @@ var ConcurrencyConflictError = class extends InfrastructureError {
|
|
|
888
937
|
};
|
|
889
938
|
|
|
890
939
|
// src/aggregate/event-sourced-aggregate.ts
|
|
891
|
-
var EventSourcedAggregate = class extends
|
|
940
|
+
var EventSourcedAggregate = class extends BaseAggregate {
|
|
892
941
|
static {
|
|
893
942
|
__name(this, "EventSourcedAggregate");
|
|
894
943
|
}
|
|
895
|
-
// --- Version management (own, not inherited from AggregateRoot) ---
|
|
896
|
-
_version = 0;
|
|
897
|
-
get version() {
|
|
898
|
-
return this._version;
|
|
899
|
-
}
|
|
900
|
-
setVersion(version) {
|
|
901
|
-
this._version = version;
|
|
902
|
-
}
|
|
903
|
-
// --- Event tracking ---
|
|
904
|
-
_pendingEvents = [];
|
|
905
|
-
get pendingEvents() {
|
|
906
|
-
return Object.freeze(this._pendingEvents.slice());
|
|
907
|
-
}
|
|
908
|
-
clearPendingEvents() {
|
|
909
|
-
this._pendingEvents = [];
|
|
910
|
-
}
|
|
911
|
-
/**
|
|
912
|
-
* **Framework lifecycle method — `@sealed`.** Called by `withCommit`
|
|
913
|
-
* (or by your own orchestration code, after harvesting `pendingEvents`)
|
|
914
|
-
* to push the persisted version back into the in-memory aggregate and
|
|
915
|
-
* clear `pendingEvents`. TypeScript has no `final` keyword, but
|
|
916
|
-
* subclasses **should not** override this method directly.
|
|
917
|
-
*
|
|
918
|
-
* Overriding without calling `super.markPersisted(version)` silently
|
|
919
|
-
* leaks `pendingEvents` — the next `withCommit` will re-dispatch them
|
|
920
|
-
* through the outbox, double-emitting events. This bug has been hit
|
|
921
|
-
* in production by consumers; the {@link onPersisted} hook below is
|
|
922
|
-
* the safer extension point.
|
|
923
|
-
*
|
|
924
|
-
* If you must override (legitimate cases are very rare), call
|
|
925
|
-
* `super.markPersisted(version)` FIRST so the framework's cleanup
|
|
926
|
-
* runs, then add your logic afterwards.
|
|
927
|
-
*
|
|
928
|
-
* @param version - The version assigned by the persistence layer
|
|
929
|
-
* @see onPersisted — the safe extension point for subclasses
|
|
930
|
-
*/
|
|
931
|
-
markPersisted(version) {
|
|
932
|
-
this.setVersion(version);
|
|
933
|
-
this._pendingEvents = [];
|
|
934
|
-
this.onPersisted(version);
|
|
935
|
-
}
|
|
936
|
-
/**
|
|
937
|
-
* Subclass extension point — fires AFTER {@link markPersisted} has
|
|
938
|
-
* updated the version and cleared `pendingEvents`. Override this for
|
|
939
|
-
* post-persist logging, metrics, or cache-eviction without risk of
|
|
940
|
-
* breaking the framework's pendingEvents cleanup.
|
|
941
|
-
*
|
|
942
|
-
* The default implementation is a no-op. Subclasses do NOT need to
|
|
943
|
-
* call `super.onPersisted(version)` — there is nothing in the parent
|
|
944
|
-
* implementation to preserve.
|
|
945
|
-
*
|
|
946
|
-
* **`onPersisted` deliberately receives only the version, not the
|
|
947
|
-
* drained events.** Event-driven post-persist logic (aggregate-level
|
|
948
|
-
* audit logging, per-event-type side effects) belongs in `EventBus`
|
|
949
|
-
* subscribers or the outbox dispatcher — that is the proper
|
|
950
|
-
* Aggregate-Boundary separation. Building event-aware logic into
|
|
951
|
-
* `onPersisted` couples aggregate lifecycle to event processing and
|
|
952
|
-
* recreates the boundary problems Vernon's aggregate discipline is
|
|
953
|
-
* meant to prevent.
|
|
954
|
-
*
|
|
955
|
-
* **The hook must return synchronously.** `markPersisted` is `void`-
|
|
956
|
-
* typed and calls `onPersisted` without `await`. TypeScript's
|
|
957
|
-
* permissive `void` will accept an `async`-override returning
|
|
958
|
-
* `Promise<void>`, but the returned promise is fire-and-forget —
|
|
959
|
-
* any rejection becomes an unhandled rejection and `withCommit`
|
|
960
|
-
* proceeds without waiting. For asynchronous work, subscribe to the
|
|
961
|
-
* relevant domain event on the `EventBus` instead; that is the
|
|
962
|
-
* properly awaited extension point.
|
|
963
|
-
*
|
|
964
|
-
* @param version - The version that was just persisted
|
|
965
|
-
*/
|
|
966
|
-
onPersisted(_version) {
|
|
967
|
-
}
|
|
968
|
-
constructor(id, initialState) {
|
|
969
|
-
super(id, initialState);
|
|
970
|
-
}
|
|
971
|
-
// --- Event application ---
|
|
972
944
|
/**
|
|
973
945
|
* Validates an event before it is applied. Default is no-op.
|
|
974
946
|
* Subclasses override to throw a concrete `DomainError` subclass when
|
|
@@ -1013,11 +985,10 @@ var EventSourcedAggregate = class extends Entity {
|
|
|
1013
985
|
const nextState = handler(this._state, event);
|
|
1014
986
|
this._state = freezeShallow(nextState);
|
|
1015
987
|
if (isNew) {
|
|
1016
|
-
this.
|
|
1017
|
-
this.
|
|
988
|
+
this.addDomainEvent(event);
|
|
989
|
+
this.bumpVersion();
|
|
1018
990
|
}
|
|
1019
991
|
}
|
|
1020
|
-
// --- History & Snapshots ---
|
|
1021
992
|
/**
|
|
1022
993
|
* Reconstitutes the aggregate from an event history. Catches `DomainError`
|
|
1023
994
|
* thrown during replay and returns it as an `Err` — this is the
|
|
@@ -1030,7 +1001,7 @@ var EventSourcedAggregate = class extends Entity {
|
|
|
1030
1001
|
* 2 events ends at v=3, not v=2.
|
|
1031
1002
|
*/
|
|
1032
1003
|
loadFromHistory(history) {
|
|
1033
|
-
const startVersion = this.
|
|
1004
|
+
const startVersion = this.version;
|
|
1034
1005
|
for (const event of history) {
|
|
1035
1006
|
try {
|
|
1036
1007
|
this.dispatchAndCommit(event, false);
|
|
@@ -1039,19 +1010,9 @@ var EventSourcedAggregate = class extends Entity {
|
|
|
1039
1010
|
throw e;
|
|
1040
1011
|
}
|
|
1041
1012
|
}
|
|
1042
|
-
this.
|
|
1013
|
+
this.markRestored(startVersion + history.length);
|
|
1043
1014
|
return ok();
|
|
1044
1015
|
}
|
|
1045
|
-
/**
|
|
1046
|
-
* Creates a snapshot of the current aggregate state.
|
|
1047
|
-
*/
|
|
1048
|
-
createSnapshot() {
|
|
1049
|
-
return {
|
|
1050
|
-
state: structuredClone(this._state),
|
|
1051
|
-
version: this._version,
|
|
1052
|
-
snapshotAt: /* @__PURE__ */ new Date()
|
|
1053
|
-
};
|
|
1054
|
-
}
|
|
1055
1016
|
/**
|
|
1056
1017
|
* Restores the aggregate from a snapshot and applies events that occurred
|
|
1057
1018
|
* after. Same infrastructure-boundary semantics as `loadFromHistory`:
|
|
@@ -1064,7 +1025,7 @@ var EventSourcedAggregate = class extends Entity {
|
|
|
1064
1025
|
*/
|
|
1065
1026
|
restoreFromSnapshotWithEvents(snapshot, eventsAfterSnapshot) {
|
|
1066
1027
|
const previousState = this._state;
|
|
1067
|
-
const previousVersion = this.
|
|
1028
|
+
const previousVersion = this.version;
|
|
1068
1029
|
this._state = freezeShallow(structuredClone(snapshot.state));
|
|
1069
1030
|
this.setVersion(snapshot.version);
|
|
1070
1031
|
for (const event of eventsAfterSnapshot) {
|
|
@@ -1077,7 +1038,9 @@ var EventSourcedAggregate = class extends Entity {
|
|
|
1077
1038
|
throw e;
|
|
1078
1039
|
}
|
|
1079
1040
|
}
|
|
1080
|
-
this.
|
|
1041
|
+
this.markRestored(
|
|
1042
|
+
snapshot.version + eventsAfterSnapshot.length
|
|
1043
|
+
);
|
|
1081
1044
|
return ok();
|
|
1082
1045
|
}
|
|
1083
1046
|
};
|
|
@@ -1113,6 +1076,18 @@ async function withCommit(deps, fn) {
|
|
|
1113
1076
|
const harvested = uniqueAggregates.flatMap(
|
|
1114
1077
|
(agg) => agg.pendingEvents
|
|
1115
1078
|
);
|
|
1079
|
+
for (const event of harvested) {
|
|
1080
|
+
const missing = [];
|
|
1081
|
+
if (!event.aggregateId) missing.push("aggregateId");
|
|
1082
|
+
if (!event.aggregateType) missing.push("aggregateType");
|
|
1083
|
+
if (missing.length > 0) {
|
|
1084
|
+
throw new Error(
|
|
1085
|
+
`withCommit: event "${event.type}" is missing ${missing.join(
|
|
1086
|
+
" and "
|
|
1087
|
+
)}. Use this.recordEvent(type, payload) inside aggregate methods instead of createDomainEvent(...) \u2014 recordEvent auto-injects aggregateId and aggregateType. Outbox dispatchers and projection handlers rely on these fields for routing.`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1116
1091
|
if (harvested.length > 0) {
|
|
1117
1092
|
await deps.outbox.add(harvested);
|
|
1118
1093
|
}
|