@shirudo/ddd-kit 1.0.1 → 1.1.0

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 CHANGED
@@ -709,6 +709,14 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
709
709
  * Subclasses can mutate this directly or use helper methods.
710
710
  */
711
711
  protected _state: TState;
712
+ /**
713
+ * **State ownership.** Plain-object and array states are shallow-copied
714
+ * before the freeze, so the caller's own object stays mutable. A CLASS
715
+ * INSTANCE passed as state is an ownership transfer: it is frozen
716
+ * in place (a copy would strip its prototype) — do not keep mutating
717
+ * the instance after handing it to the entity. The same contract
718
+ * applies to {@link setState}.
719
+ */
712
720
  protected constructor(id: TId, initialState: TState);
713
721
  /**
714
722
  * Optional validation hook to ensure state invariants. Called during
@@ -737,6 +745,10 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
737
745
  * This is a convenience method for state mutations.
738
746
  * Automatically validates the newState using `validateState()`.
739
747
  *
748
+ * Plain-object and array states are shallow-copied before the freeze
749
+ * (the caller's object stays mutable); a class-instance state is an
750
+ * ownership transfer and is frozen in place — see the constructor.
751
+ *
740
752
  * @param newState - The new state
741
753
  */
742
754
  protected setState(newState: TState): void;
@@ -917,8 +929,12 @@ declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(
917
929
  * @template TEvent - The domain-event union. Defaults to `never` so
918
930
  * aggregates without a declared event type cannot emit events
919
931
  * (emitting any event becomes a compile error).
932
+ * @template TSnapshotState - The plain-data shape stored in snapshots.
933
+ * Defaults to `TState` for plain-data states. Aggregates whose state
934
+ * carries class-based child entities declare a plain DTO shape here
935
+ * and override {@link toSnapshotState} / {@link fromSnapshotState}.
920
936
  */
921
- declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId, TEvent> {
937
+ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never, TSnapshotState = TState> extends Entity<TState, TId> implements IAggregateRoot<TId, TEvent> {
922
938
  /**
923
939
  * The aggregate's domain type as a string, used to populate
924
940
  * `aggregateType` on events recorded via {@link recordEvent}.
@@ -1028,6 +1044,12 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
1028
1044
  * call `super.onPersisted(version)` — there is nothing in the parent
1029
1045
  * implementation to preserve.
1030
1046
  *
1047
+ * **Observer contract: errors are swallowed.** `withCommit` invokes
1048
+ * `markPersisted` after the transaction has committed; a throwing hook
1049
+ * must neither abort the loop for peer aggregates nor make the
1050
+ * committed write look failed, so `withCommit` catches and discards
1051
+ * hook errors. Handle failures inside the hook if you need them.
1052
+ *
1031
1053
  * **`onPersisted` deliberately receives only the version, not the
1032
1054
  * drained events.** Event-driven post-persist logic (aggregate-level
1033
1055
  * audit logging, per-event-type side effects) belongs in `EventBus`
@@ -1063,8 +1085,35 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
1063
1085
  * Creates a snapshot of the current aggregate state — the state at
1064
1086
  * this moment plus the version. Useful for ES snapshot policies and
1065
1087
  * for state-stored backup / restore.
1088
+ *
1089
+ * The state is converted via {@link toSnapshotState}; the default
1090
+ * requires plain, serialisable data and fails fast otherwise.
1091
+ */
1092
+ createSnapshot(): AggregateSnapshot<TSnapshotState>;
1093
+ /**
1094
+ * Converts live aggregate state into the plain-data shape stored in a
1095
+ * snapshot. The default validates that the state graph is plain,
1096
+ * serialisable data (no class instances, functions, Promise/WeakMap/
1097
+ * WeakSet) and then `structuredClone`s it — class instances would
1098
+ * silently lose their prototype here AND on every snapshot-store
1099
+ * round-trip, so the default fails fast with the offending path
1100
+ * instead of producing a snapshot that breaks on first method call
1101
+ * after restore.
1102
+ *
1103
+ * Override this together with {@link fromSnapshotState} (and the
1104
+ * `TSnapshotState` generic) when the state carries class-based child
1105
+ * entities. The override owns isolation: return fresh objects, not
1106
+ * references into live state.
1107
+ */
1108
+ protected toSnapshotState(state: TState): TSnapshotState;
1109
+ /**
1110
+ * Converts the plain-data snapshot shape back into live aggregate
1111
+ * state. The default `structuredClone`s the stored state so the
1112
+ * restored aggregate never aliases the snapshot object. Override
1113
+ * together with {@link toSnapshotState} to reconstruct class-based
1114
+ * child entities.
1066
1115
  */
1067
- createSnapshot(): AggregateSnapshot<TState>;
1116
+ protected fromSnapshotState(stored: TSnapshotState): TState;
1068
1117
  /**
1069
1118
  * Sugar for `createDomainEvent` that auto-injects `aggregateId`
1070
1119
  * (from `this.id`) and `aggregateType` (from {@link aggregateType})
@@ -1162,7 +1211,7 @@ interface AggregateConfig {
1162
1211
  * }
1163
1212
  * ```
1164
1213
  */
1165
- declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never> extends BaseAggregate<TState, TId, TEvent> {
1214
+ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never, TSnapshotState = TState> extends BaseAggregate<TState, TId, TEvent, TSnapshotState> {
1166
1215
  private readonly _autoVersionBump;
1167
1216
  protected constructor(id: TId, initialState: TState, config?: AggregateConfig);
1168
1217
  /**
@@ -1225,7 +1274,7 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent exte
1225
1274
  *
1226
1275
  * @param snapshot - The snapshot to restore from
1227
1276
  */
1228
- restoreFromSnapshot(snapshot: AggregateSnapshot<TState>): void;
1277
+ restoreFromSnapshot(snapshot: AggregateSnapshot<TSnapshotState>): void;
1229
1278
  }
1230
1279
 
1231
1280
  type Handler<TState, TEvent extends AnyDomainEvent> = (state: TState, event: TEvent) => TState;
@@ -1282,7 +1331,7 @@ type Handler<TState, TEvent extends AnyDomainEvent> = (state: TState, event: TEv
1282
1331
  * }
1283
1332
  * ```
1284
1333
  */
1285
- declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEvent, TId extends Id<string>> extends BaseAggregate<TState, TId, TEvent> implements IEventSourcedAggregate<TId, TEvent> {
1334
+ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEvent, TId extends Id<string>, TSnapshotState = TState> extends BaseAggregate<TState, TId, TEvent, TSnapshotState> implements IEventSourcedAggregate<TId, TEvent> {
1286
1335
  /**
1287
1336
  * Validates an event before it is applied. Default is no-op.
1288
1337
  * Subclasses override to throw a concrete `DomainError` subclass when
@@ -1324,6 +1373,12 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
1324
1373
  * infrastructure boundary, where event-stream corruption is an expected
1325
1374
  * recoverable failure. Unexpected (non-DomainError) throws propagate.
1326
1375
  *
1376
+ * All-or-nothing: if any event mid-stream throws, the aggregate's state
1377
+ * is rolled back to its pre-call value — same contract as
1378
+ * `restoreFromSnapshotWithEvents`. Partial replay is never observable.
1379
+ * (Version needs no rollback: replay dispatches with `isNew = false`,
1380
+ * which never bumps it; only the final `markRestored` advances it.)
1381
+ *
1327
1382
  * Version advances additively: the aggregate's pre-existing version plus
1328
1383
  * `history.length`. A fresh aggregate (v=0) loading 3 events ends at v=3;
1329
1384
  * an aggregate already at v=1 (e.g. after a creation event) loading
@@ -1340,7 +1395,7 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
1340
1395
  * aggregate is rolled back to its pre-call state + version. Partial
1341
1396
  * restoration is never observable to the caller.
1342
1397
  */
1343
- restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: ReadonlyArray<TEvent>): Result<void, DomainError>;
1398
+ restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TSnapshotState>, eventsAfterSnapshot: ReadonlyArray<TEvent>): Result<void, DomainError>;
1344
1399
  /**
1345
1400
  * A map of event types to their corresponding handlers.
1346
1401
  * Subclasses MUST implement this property.
@@ -1838,6 +1893,16 @@ interface TransactionScope<TCtx> {
1838
1893
  * outbox still holds the events and an outbox-dispatcher will deliver
1839
1894
  * them (eventual consistency).
1840
1895
  *
1896
+ * **A `bus.publish` failure never rejects `withCommit`.** Once the
1897
+ * transaction has committed, the write succeeded — surfacing a subscriber
1898
+ * failure as a rejection would hand the caller a use-case failure for a
1899
+ * committed write (a typical caller retries, double-executing it). The
1900
+ * in-process fast path is best-effort by design; the error is reported to
1901
+ * the optional `onPublishError(error, events)` hook (wire it to your
1902
+ * logger/metrics) and otherwise dropped — delivery is still guaranteed via
1903
+ * the outbox. The hook is an observer: if it throws, its error is
1904
+ * swallowed so the post-commit invariant holds.
1905
+ *
1841
1906
  * If the transaction rolls back, `markPersisted` is **not** called — the
1842
1907
  * aggregate keeps its pending events, so the caller can retry or discard.
1843
1908
  *
@@ -1869,6 +1934,12 @@ declare function withCommit<Evt extends AnyDomainEvent, R, TCtx>(deps: {
1869
1934
  outbox: Outbox<Evt>;
1870
1935
  bus?: EventBus<Evt>;
1871
1936
  scope: TransactionScope<TCtx>;
1937
+ /**
1938
+ * Observer for post-commit `bus.publish` failures. Called with the
1939
+ * error and the events that were published. Must not be relied on
1940
+ * for delivery — the outbox dispatcher is the reliable path.
1941
+ */
1942
+ onPublishError?: (error: unknown, events: ReadonlyArray<Evt>) => void;
1872
1943
  }, fn: (ctx: TCtx) => Promise<{
1873
1944
  result: R;
1874
1945
  aggregates: ReadonlyArray<IAggregateRoot<Id<string>, Evt>>;
@@ -2348,15 +2419,32 @@ type VO<T> = Readonly<T>;
2348
2419
  * Note: `deepFreeze` mutates its argument in place — it sets `[[Frozen]]`
2349
2420
  * on the object you pass in. Callers that need to avoid touching the
2350
2421
  * input (e.g. `vo()`) should deep-clone first.
2422
+ *
2423
+ * Date/Map/Set keep internal-slot mutability under `Object.freeze`
2424
+ * (`setTime`, `set`, `add`, … still work on frozen instances), so their
2425
+ * mutator methods are shadowed with throwing own properties and Map/Set
2426
+ * contents are frozen recursively. The shadows are non-enumerable —
2427
+ * invisible to `Object.keys`, spread, `deepEqual`, and `structuredClone`.
2428
+ *
2429
+ * The shadowing is deny-by-enumeration: only the mutators known at
2430
+ * release time are blocked. If the runtime grows a NEW mutator (e.g. the
2431
+ * stage-3 `Map.prototype.getOrInsert` upsert proposal), it is not blocked
2432
+ * until the list is updated — treat the mutator blocking as a guard rail,
2433
+ * not a security boundary.
2434
+ *
2435
+ * Limitation: ArrayBuffer views (TypedArrays, DataView) are passed through
2436
+ * unfrozen — the spec forbids freezing a view with elements, and freezing
2437
+ * cannot protect the underlying buffer. Their contents remain mutable.
2351
2438
  */
2352
2439
  declare function deepFreeze<T>(obj: T, visited?: WeakSet<object>): Readonly<T>;
2353
2440
  /**
2354
2441
  * Creates a deeply immutable value object from the given data.
2355
2442
  *
2356
- * The input is first deep-cloned with `structuredClone`, then the clone
2357
- * is frozen — so calling `vo(input)` never freezes the caller's own
2358
- * object graph as a side-effect. Mutating the input afterwards does not
2359
- * bleed into the VO.
2443
+ * The input is first deep-cloned, then the clone is frozen — so calling
2444
+ * `vo(input)` never freezes the caller's own object graph as a
2445
+ * side-effect. Mutating the input afterwards does not bleed into the VO.
2446
+ * Symbol-keyed properties are preserved (matching `voEquals`); function
2447
+ * values are rejected (Value Objects are data, not behaviour).
2360
2448
  *
2361
2449
  * @example
2362
2450
  * ```typescript
@@ -2441,6 +2529,11 @@ declare function voEqualsExcept<T>(a: VO<T>, b: VO<T>, options: DeepEqualExceptO
2441
2529
  * Creates a value object with optional validation.
2442
2530
  * Returns a Result type instead of throwing an error.
2443
2531
  *
2532
+ * Note: the Result covers VALIDATION failures only. Non-data values in
2533
+ * the input (functions, Promise/WeakMap/WeakSet) still throw a
2534
+ * `TypeError` from `vo()` — they cannot occur in parsed JSON and signal
2535
+ * a programming error, not a validation failure.
2536
+ *
2444
2537
  * @param t - The data to convert into a value object
2445
2538
  * @param validate - Validation function that returns true if valid
2446
2539
  * @param errorMessage - Optional custom error message if validation fails
@@ -2505,7 +2598,9 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
2505
2598
  readonly props: Readonly<T>;
2506
2599
  /**
2507
2600
  * Creates a new ValueObject.
2508
- * The properties are deeply frozen to ensure immutability.
2601
+ * The properties are deep-cloned (prototype-preserving) and then deeply
2602
+ * frozen — the caller's own object graph is never frozen or mutated,
2603
+ * and later mutation of the input does not bleed into the value object.
2509
2604
  *
2510
2605
  * @param props - The properties of the value object
2511
2606
  * @example