@shirudo/ddd-kit 1.0.0-rc.6 → 1.0.0-rc.8
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 +399 -19
- package/dist/index.js +221 -14
- package/dist/index.js.map +1 -1
- package/package.json +72 -71
package/dist/index.js
CHANGED
|
@@ -399,6 +399,26 @@ function setEventIdFactory(factory) {
|
|
|
399
399
|
currentEventIdFactory = factory;
|
|
400
400
|
}
|
|
401
401
|
__name(setEventIdFactory, "setEventIdFactory");
|
|
402
|
+
function assertNotThenable(result, helperName) {
|
|
403
|
+
if (result !== null && (typeof result === "object" || typeof result === "function") && typeof result.then === "function") {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`${helperName}: fn returned a thenable. The factory is only installed for the synchronous portion of fn; awaited continuations would see the previous factory. For async-scoped factories use AsyncLocalStorage.`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
__name(assertNotThenable, "assertNotThenable");
|
|
410
|
+
function withEventIdFactory(factory, fn) {
|
|
411
|
+
const previous = currentEventIdFactory;
|
|
412
|
+
currentEventIdFactory = factory;
|
|
413
|
+
try {
|
|
414
|
+
const result = fn();
|
|
415
|
+
assertNotThenable(result, "withEventIdFactory");
|
|
416
|
+
return result;
|
|
417
|
+
} finally {
|
|
418
|
+
currentEventIdFactory = previous;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
__name(withEventIdFactory, "withEventIdFactory");
|
|
402
422
|
function resetEventIdFactory() {
|
|
403
423
|
currentEventIdFactory = defaultEventIdFactory;
|
|
404
424
|
}
|
|
@@ -409,6 +429,18 @@ function setClockFactory(factory) {
|
|
|
409
429
|
currentClockFactory = factory;
|
|
410
430
|
}
|
|
411
431
|
__name(setClockFactory, "setClockFactory");
|
|
432
|
+
function withClockFactory(factory, fn) {
|
|
433
|
+
const previous = currentClockFactory;
|
|
434
|
+
currentClockFactory = factory;
|
|
435
|
+
try {
|
|
436
|
+
const result = fn();
|
|
437
|
+
assertNotThenable(result, "withClockFactory");
|
|
438
|
+
return result;
|
|
439
|
+
} finally {
|
|
440
|
+
currentClockFactory = previous;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
__name(withClockFactory, "withClockFactory");
|
|
412
444
|
function resetClockFactory() {
|
|
413
445
|
currentClockFactory = defaultClockFactory;
|
|
414
446
|
}
|
|
@@ -587,18 +619,61 @@ var AggregateRoot = class extends Entity {
|
|
|
587
619
|
this._pendingEvents = [];
|
|
588
620
|
}
|
|
589
621
|
/**
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
622
|
+
* **Framework lifecycle method — `@sealed`.** Called by `withCommit`
|
|
623
|
+
* (or by your own orchestration code, after harvesting `pendingEvents`)
|
|
624
|
+
* to push the persisted version back into the in-memory aggregate and
|
|
625
|
+
* clear `pendingEvents`. TypeScript has no `final` keyword, but
|
|
626
|
+
* subclasses **should not** override this method directly.
|
|
627
|
+
*
|
|
628
|
+
* Overriding without calling `super.markPersisted(version)` silently
|
|
629
|
+
* leaks `pendingEvents` — the next `withCommit` will re-dispatch them
|
|
630
|
+
* through the outbox, double-emitting events. This bug has been hit
|
|
631
|
+
* in production by consumers; the {@link onPersisted} hook below is
|
|
632
|
+
* the safer extension point.
|
|
633
|
+
*
|
|
634
|
+
* If you must override (legitimate cases are very rare), call
|
|
635
|
+
* `super.markPersisted(version)` FIRST so the framework's cleanup
|
|
636
|
+
* runs, then add your logic afterwards.
|
|
594
637
|
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
* call.
|
|
638
|
+
* @param version - The version assigned by the persistence layer
|
|
639
|
+
* @see onPersisted — the safe extension point for subclasses
|
|
598
640
|
*/
|
|
599
641
|
markPersisted(version) {
|
|
600
642
|
this.setVersion(version);
|
|
601
643
|
this._pendingEvents = [];
|
|
644
|
+
this.onPersisted(version);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Subclass extension point — fires AFTER {@link markPersisted} has
|
|
648
|
+
* updated the version and cleared `pendingEvents`. Override this for
|
|
649
|
+
* post-persist logging, metrics, or cache-eviction without risk of
|
|
650
|
+
* breaking the framework's pendingEvents cleanup.
|
|
651
|
+
*
|
|
652
|
+
* The default implementation is a no-op. Subclasses do NOT need to
|
|
653
|
+
* call `super.onPersisted(version)` — there is nothing in the parent
|
|
654
|
+
* implementation to preserve.
|
|
655
|
+
*
|
|
656
|
+
* **`onPersisted` deliberately receives only the version, not the
|
|
657
|
+
* drained events.** Event-driven post-persist logic (aggregate-level
|
|
658
|
+
* audit logging, per-event-type side effects) belongs in `EventBus`
|
|
659
|
+
* subscribers or the outbox dispatcher — that is the proper
|
|
660
|
+
* Aggregate-Boundary separation. Building event-aware logic into
|
|
661
|
+
* `onPersisted` couples aggregate lifecycle to event processing and
|
|
662
|
+
* recreates the boundary problems Vernon's aggregate discipline is
|
|
663
|
+
* meant to prevent.
|
|
664
|
+
*
|
|
665
|
+
* **The hook must return synchronously.** `markPersisted` is `void`-
|
|
666
|
+
* typed and calls `onPersisted` without `await`. TypeScript's
|
|
667
|
+
* permissive `void` will accept an `async`-override returning
|
|
668
|
+
* `Promise<void>`, but the returned promise is fire-and-forget —
|
|
669
|
+
* any rejection becomes an unhandled rejection and `withCommit`
|
|
670
|
+
* proceeds without waiting. For asynchronous work, subscribe to the
|
|
671
|
+
* relevant domain event on the `EventBus` instead; that is the
|
|
672
|
+
* properly awaited extension point.
|
|
673
|
+
*
|
|
674
|
+
* @param version - The version that was just persisted
|
|
675
|
+
*/
|
|
676
|
+
onPersisted(_version) {
|
|
602
677
|
}
|
|
603
678
|
/**
|
|
604
679
|
* Mutates state and records the resulting domain events in the
|
|
@@ -689,6 +764,47 @@ var AggregateRoot = class extends Entity {
|
|
|
689
764
|
addDomainEvent(event) {
|
|
690
765
|
this._pendingEvents.push(event);
|
|
691
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Sugar for `createDomainEvent` that auto-injects `aggregateId`
|
|
769
|
+
* (from `this.id`) and `aggregateType` (from {@link aggregateType})
|
|
770
|
+
* into the event's metadata fields. This is the canonical path for
|
|
771
|
+
* recording events from inside aggregate domain methods.
|
|
772
|
+
*
|
|
773
|
+
* Downstream consumers — outbox dispatchers, projection handlers,
|
|
774
|
+
* audit logs — route by these two fields. Calling
|
|
775
|
+
* `createDomainEvent(...)` directly inside an aggregate method
|
|
776
|
+
* leaves them unset and is caught at the `withCommit` harvest
|
|
777
|
+
* boundary, but `this.recordEvent(...)` makes the right thing
|
|
778
|
+
* impossible to forget.
|
|
779
|
+
*
|
|
780
|
+
* @example
|
|
781
|
+
* ```ts
|
|
782
|
+
* class Order extends AggregateRoot<OrderState, OrderId, OrderEvent> {
|
|
783
|
+
* protected readonly aggregateType = "Order";
|
|
784
|
+
*
|
|
785
|
+
* confirm(): void {
|
|
786
|
+
* this.commit(
|
|
787
|
+
* { ...this.state, status: "confirmed" },
|
|
788
|
+
* this.recordEvent("OrderConfirmed", { orderId: this.id }),
|
|
789
|
+
* );
|
|
790
|
+
* }
|
|
791
|
+
* }
|
|
792
|
+
* ```
|
|
793
|
+
*
|
|
794
|
+
* @param type - event type discriminator (must be one of `TEvent`'s tags)
|
|
795
|
+
* @param payload - payload for that event subtype
|
|
796
|
+
* @param options - any remaining `createDomainEvent` options
|
|
797
|
+
* (`eventId`, `occurredAt`, `metadata`, `version`); `aggregateId`
|
|
798
|
+
* and `aggregateType` are deliberately omitted — the helper sets
|
|
799
|
+
* them.
|
|
800
|
+
*/
|
|
801
|
+
recordEvent(type, payload, options) {
|
|
802
|
+
return createDomainEvent(type, payload, {
|
|
803
|
+
...options,
|
|
804
|
+
aggregateId: this.id,
|
|
805
|
+
aggregateType: this.aggregateType
|
|
806
|
+
});
|
|
807
|
+
}
|
|
692
808
|
/**
|
|
693
809
|
* Manually bumps the aggregate version.
|
|
694
810
|
* Call this after state changes for Optimistic Concurrency Control.
|
|
@@ -834,18 +950,96 @@ var EventSourcedAggregate = class extends Entity {
|
|
|
834
950
|
this._pendingEvents = [];
|
|
835
951
|
}
|
|
836
952
|
/**
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
* `
|
|
953
|
+
* **Framework lifecycle method — `@sealed`.** Called by `withCommit`
|
|
954
|
+
* (or by your own orchestration code, after harvesting `pendingEvents`)
|
|
955
|
+
* to push the persisted version back into the in-memory aggregate and
|
|
956
|
+
* clear `pendingEvents`. TypeScript has no `final` keyword, but
|
|
957
|
+
* subclasses **should not** override this method directly.
|
|
958
|
+
*
|
|
959
|
+
* Overriding without calling `super.markPersisted(version)` silently
|
|
960
|
+
* leaks `pendingEvents` — the next `withCommit` will re-dispatch them
|
|
961
|
+
* through the outbox, double-emitting events. This bug has been hit
|
|
962
|
+
* in production by consumers; the {@link onPersisted} hook below is
|
|
963
|
+
* the safer extension point.
|
|
964
|
+
*
|
|
965
|
+
* If you must override (legitimate cases are very rare), call
|
|
966
|
+
* `super.markPersisted(version)` FIRST so the framework's cleanup
|
|
967
|
+
* runs, then add your logic afterwards.
|
|
968
|
+
*
|
|
969
|
+
* @param version - The version assigned by the persistence layer
|
|
970
|
+
* @see onPersisted — the safe extension point for subclasses
|
|
841
971
|
*/
|
|
842
972
|
markPersisted(version) {
|
|
843
973
|
this.setVersion(version);
|
|
844
974
|
this._pendingEvents = [];
|
|
975
|
+
this.onPersisted(version);
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Subclass extension point — fires AFTER {@link markPersisted} has
|
|
979
|
+
* updated the version and cleared `pendingEvents`. Override this for
|
|
980
|
+
* post-persist logging, metrics, or cache-eviction without risk of
|
|
981
|
+
* breaking the framework's pendingEvents cleanup.
|
|
982
|
+
*
|
|
983
|
+
* The default implementation is a no-op. Subclasses do NOT need to
|
|
984
|
+
* call `super.onPersisted(version)` — there is nothing in the parent
|
|
985
|
+
* implementation to preserve.
|
|
986
|
+
*
|
|
987
|
+
* **`onPersisted` deliberately receives only the version, not the
|
|
988
|
+
* drained events.** Event-driven post-persist logic (aggregate-level
|
|
989
|
+
* audit logging, per-event-type side effects) belongs in `EventBus`
|
|
990
|
+
* subscribers or the outbox dispatcher — that is the proper
|
|
991
|
+
* Aggregate-Boundary separation. Building event-aware logic into
|
|
992
|
+
* `onPersisted` couples aggregate lifecycle to event processing and
|
|
993
|
+
* recreates the boundary problems Vernon's aggregate discipline is
|
|
994
|
+
* meant to prevent.
|
|
995
|
+
*
|
|
996
|
+
* **The hook must return synchronously.** `markPersisted` is `void`-
|
|
997
|
+
* typed and calls `onPersisted` without `await`. TypeScript's
|
|
998
|
+
* permissive `void` will accept an `async`-override returning
|
|
999
|
+
* `Promise<void>`, but the returned promise is fire-and-forget —
|
|
1000
|
+
* any rejection becomes an unhandled rejection and `withCommit`
|
|
1001
|
+
* proceeds without waiting. For asynchronous work, subscribe to the
|
|
1002
|
+
* relevant domain event on the `EventBus` instead; that is the
|
|
1003
|
+
* properly awaited extension point.
|
|
1004
|
+
*
|
|
1005
|
+
* @param version - The version that was just persisted
|
|
1006
|
+
*/
|
|
1007
|
+
onPersisted(_version) {
|
|
845
1008
|
}
|
|
846
1009
|
constructor(id, initialState) {
|
|
847
1010
|
super(id, initialState);
|
|
848
1011
|
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Sugar for `createDomainEvent` that auto-injects `aggregateId`
|
|
1014
|
+
* (from `this.id`) and `aggregateType` (from {@link aggregateType})
|
|
1015
|
+
* into the event's metadata fields. The canonical path for
|
|
1016
|
+
* constructing events to feed into `apply()` from inside aggregate
|
|
1017
|
+
* domain methods.
|
|
1018
|
+
*
|
|
1019
|
+
* @example
|
|
1020
|
+
* ```ts
|
|
1021
|
+
* class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
|
|
1022
|
+
* protected readonly aggregateType = "Order";
|
|
1023
|
+
*
|
|
1024
|
+
* confirm(): void {
|
|
1025
|
+
* this.apply(this.recordEvent("OrderConfirmed", { orderId: this.id }));
|
|
1026
|
+
* }
|
|
1027
|
+
* }
|
|
1028
|
+
* ```
|
|
1029
|
+
*
|
|
1030
|
+
* Calling `createDomainEvent(...)` directly inside an aggregate
|
|
1031
|
+
* method leaves `aggregateId` and `aggregateType` unset; the
|
|
1032
|
+
* `withCommit` harvest boundary catches it at runtime, but
|
|
1033
|
+
* `this.recordEvent(...)` makes the right thing impossible to
|
|
1034
|
+
* forget.
|
|
1035
|
+
*/
|
|
1036
|
+
recordEvent(type, payload, options) {
|
|
1037
|
+
return createDomainEvent(type, payload, {
|
|
1038
|
+
...options,
|
|
1039
|
+
aggregateId: this.id,
|
|
1040
|
+
aggregateType: this.aggregateType
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
849
1043
|
// --- Event application ---
|
|
850
1044
|
/**
|
|
851
1045
|
* Validates an event before it is applied. Default is no-op.
|
|
@@ -987,13 +1181,26 @@ async function withCommit(deps, fn) {
|
|
|
987
1181
|
const { result, aggregates, events } = await deps.scope.transactional(
|
|
988
1182
|
async (ctx) => {
|
|
989
1183
|
const fnResult = await fn(ctx);
|
|
990
|
-
const
|
|
1184
|
+
const uniqueAggregates = Array.from(new Set(fnResult.aggregates));
|
|
1185
|
+
const harvested = uniqueAggregates.flatMap(
|
|
991
1186
|
(agg) => agg.pendingEvents
|
|
992
1187
|
);
|
|
1188
|
+
for (const event of harvested) {
|
|
1189
|
+
const missing = [];
|
|
1190
|
+
if (!event.aggregateId) missing.push("aggregateId");
|
|
1191
|
+
if (!event.aggregateType) missing.push("aggregateType");
|
|
1192
|
+
if (missing.length > 0) {
|
|
1193
|
+
throw new Error(
|
|
1194
|
+
`withCommit: event "${event.type}" is missing ${missing.join(
|
|
1195
|
+
" and "
|
|
1196
|
+
)}. 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.`
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
993
1200
|
if (harvested.length > 0) {
|
|
994
1201
|
await deps.outbox.add(harvested);
|
|
995
1202
|
}
|
|
996
|
-
return { ...fnResult, events: harvested };
|
|
1203
|
+
return { ...fnResult, aggregates: uniqueAggregates, events: harvested };
|
|
997
1204
|
}
|
|
998
1205
|
);
|
|
999
1206
|
for (const agg of aggregates) {
|
|
@@ -1163,6 +1370,6 @@ var InMemoryOutbox = class {
|
|
|
1163
1370
|
}
|
|
1164
1371
|
};
|
|
1165
1372
|
|
|
1166
|
-
export { AggregateNotFoundError, AggregateRoot, CommandBus, ConcurrencyConflictError, DomainError, Entity, EventBusImpl, EventSourcedAggregate, InMemoryOutbox, InfrastructureError, MissingHandlerError, QueryBus, ValueObject, copyMetadata, createDomainEvent, createDomainEventWithMetadata, deepEqual, deepEqualExcept, deepFreeze, deepOmit, entityIds, findEntityById, freezeShallow, hasEntityId, mergeMetadata, removeEntityById, replaceEntityById, resetClockFactory, resetEventIdFactory, sameEntity, sameVersion, setClockFactory, setEventIdFactory, updateEntityById, vo, voEquals, voEqualsExcept, voWithValidation, withCommit };
|
|
1373
|
+
export { AggregateNotFoundError, AggregateRoot, CommandBus, ConcurrencyConflictError, DomainError, Entity, EventBusImpl, EventSourcedAggregate, InMemoryOutbox, InfrastructureError, MissingHandlerError, QueryBus, ValueObject, copyMetadata, createDomainEvent, createDomainEventWithMetadata, deepEqual, deepEqualExcept, deepFreeze, deepOmit, entityIds, findEntityById, freezeShallow, hasEntityId, mergeMetadata, removeEntityById, replaceEntityById, resetClockFactory, resetEventIdFactory, sameEntity, sameVersion, setClockFactory, setEventIdFactory, updateEntityById, vo, voEquals, voEqualsExcept, voWithValidation, withClockFactory, withCommit, withEventIdFactory };
|
|
1167
1374
|
//# sourceMappingURL=index.js.map
|
|
1168
1375
|
//# sourceMappingURL=index.js.map
|