@shirudo/ddd-kit 1.0.1 → 1.2.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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { err, ok } from '@shirudo/result';
1
+ import { ok, err } from '@shirudo/result';
2
2
  import { BaseError, ValidationError } from '@shirudo/base-error';
3
3
 
4
4
  var __defProp = Object.defineProperty;
@@ -21,10 +21,83 @@ var BUILT_IN_TAGS = /* @__PURE__ */ new Set([
21
21
  "[object SharedArrayBuffer]",
22
22
  "[object DataView]"
23
23
  ]);
24
+ function intrinsicGetter(proto, prop) {
25
+ const get = Object.getOwnPropertyDescriptor(proto, prop)?.get;
26
+ if (!get) throw new Error(`missing intrinsic getter for ${prop}`);
27
+ return get;
28
+ }
29
+ __name(intrinsicGetter, "intrinsicGetter");
30
+ var dateGetTime = Date.prototype.getTime;
31
+ var mapSizeGet = intrinsicGetter(Map.prototype, "size");
32
+ var setSizeGet = intrinsicGetter(Set.prototype, "size");
33
+ var weakMapHas = WeakMap.prototype.has;
34
+ var weakSetHas = WeakSet.prototype.has;
35
+ var dataViewByteLengthGet = intrinsicGetter(DataView.prototype, "byteLength");
36
+ var arrayBufferByteLengthGet = intrinsicGetter(
37
+ ArrayBuffer.prototype,
38
+ "byteLength"
39
+ );
40
+ var regExpSourceGet = intrinsicGetter(RegExp.prototype, "source");
41
+ var booleanValueOf = Boolean.prototype.valueOf;
42
+ var numberValueOf = Number.prototype.valueOf;
43
+ var stringValueOf = String.prototype.valueOf;
44
+ var PROBE_KEY = {};
45
+ var REFERENCE_COMPARED_TAGS = /* @__PURE__ */ new Set([
46
+ "[object Error]",
47
+ "[object ArrayBuffer]",
48
+ "[object SharedArrayBuffer]",
49
+ "[object Promise]",
50
+ "[object WeakMap]",
51
+ "[object WeakSet]"
52
+ ]);
53
+ function hasBrand(obj, tag) {
54
+ try {
55
+ switch (tag) {
56
+ case "[object Date]":
57
+ dateGetTime.call(obj);
58
+ return true;
59
+ case "[object RegExp]":
60
+ regExpSourceGet.call(obj);
61
+ return true;
62
+ case "[object Map]":
63
+ mapSizeGet.call(obj);
64
+ return true;
65
+ case "[object Set]":
66
+ setSizeGet.call(obj);
67
+ return true;
68
+ case "[object WeakMap]":
69
+ weakMapHas.call(obj, PROBE_KEY);
70
+ return true;
71
+ case "[object WeakSet]":
72
+ weakSetHas.call(obj, PROBE_KEY);
73
+ return true;
74
+ case "[object DataView]":
75
+ dataViewByteLengthGet.call(obj);
76
+ return true;
77
+ case "[object ArrayBuffer]":
78
+ arrayBufferByteLengthGet.call(obj);
79
+ return true;
80
+ case "[object Boolean]":
81
+ booleanValueOf.call(obj);
82
+ return true;
83
+ case "[object Number]":
84
+ numberValueOf.call(obj);
85
+ return true;
86
+ case "[object String]":
87
+ stringValueOf.call(obj);
88
+ return true;
89
+ default:
90
+ return true;
91
+ }
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+ __name(hasBrand, "hasBrand");
24
97
  function isBuiltInObject(obj, tag) {
25
- if (tag.endsWith("Array]")) return true;
26
98
  if (ArrayBuffer.isView(obj)) return true;
27
- return BUILT_IN_TAGS.has(tag);
99
+ if (tag.endsWith("Array]")) return false;
100
+ return BUILT_IN_TAGS.has(tag) && hasBrand(obj, tag);
28
101
  }
29
102
  __name(isBuiltInObject, "isBuiltInObject");
30
103
 
@@ -32,6 +105,10 @@ __name(isBuiltInObject, "isBuiltInObject");
32
105
  var objProto = Object.prototype;
33
106
  var objToString = objProto.toString;
34
107
  var objHasOwn = objProto.hasOwnProperty;
108
+ function sameValueZero(a, b) {
109
+ return a === b || Number.isNaN(a) && Number.isNaN(b);
110
+ }
111
+ __name(sameValueZero, "sameValueZero");
35
112
  function deepEqual(a, b) {
36
113
  return deepEqualInner(a, b, /* @__PURE__ */ new WeakMap());
37
114
  }
@@ -77,24 +154,31 @@ function deepEqualInner(a, b, visited) {
77
154
  const len = arrA.length;
78
155
  if (len !== arrB.length) return false;
79
156
  for (let i = 0; i < len; i++) {
80
- if (arrA[i] !== arrB[i]) return false;
157
+ if (!sameValueZero(arrA[i], arrB[i])) return false;
158
+ }
159
+ return true;
160
+ }
161
+ if (Array.isArray(objA) || Array.isArray(objB)) {
162
+ if (!Array.isArray(objA) || !Array.isArray(objB)) return false;
163
+ const arrA = objA;
164
+ const arrB = objB;
165
+ const len = arrA.length;
166
+ if (len !== arrB.length) return false;
167
+ for (let i = 0; i < len; i++) {
168
+ if (!deepEqualInner(arrA[i], arrB[i], visited)) return false;
81
169
  }
82
170
  return true;
83
171
  }
84
172
  const tagA = objToString.call(objA);
85
173
  const tagB = objToString.call(objB);
86
174
  if (tagA !== tagB) return false;
175
+ const builtInA = isBuiltInObject(objA, tagA);
176
+ const builtInB = isBuiltInObject(objB, tagB);
177
+ if (builtInA !== builtInB) return false;
178
+ if (!builtInA) {
179
+ return comparePlainObjects(objA, objB, visited);
180
+ }
87
181
  switch (tagA) {
88
- case "[object Array]": {
89
- const arrA = objA;
90
- const arrB = objB;
91
- const len = arrA.length;
92
- if (len !== arrB.length) return false;
93
- for (let i = 0; i < len; i++) {
94
- if (!deepEqualInner(arrA[i], arrB[i], visited)) return false;
95
- }
96
- return true;
97
- }
98
182
  case "[object Map]": {
99
183
  const mapA = objA;
100
184
  const mapB = objB;
@@ -116,9 +200,7 @@ function deepEqualInner(a, b, visited) {
116
200
  return true;
117
201
  }
118
202
  case "[object Date]": {
119
- const timeA = objA.getTime();
120
- const timeB = objB.getTime();
121
- return timeA === timeB;
203
+ return sameValueZero(objA.getTime(), objB.getTime());
122
204
  }
123
205
  case "[object RegExp]": {
124
206
  const regA = objA;
@@ -128,69 +210,88 @@ function deepEqualInner(a, b, visited) {
128
210
  case "[object Boolean]":
129
211
  case "[object Number]":
130
212
  case "[object String]": {
131
- return objA.valueOf() === objB.valueOf();
213
+ return sameValueZero(
214
+ objA.valueOf(),
215
+ objB.valueOf()
216
+ );
132
217
  }
133
218
  default: {
134
- if (isBuiltInObject(objA, tagA) && isBuiltInObject(objB, tagB)) {
135
- return objA === objB;
136
- }
137
- const recA = objA;
138
- const recB = objB;
139
- const stringKeysA = Object.keys(objA);
140
- const stringKeysB = Object.keys(objB);
141
- if (stringKeysA.length !== stringKeysB.length) return false;
142
- const symbolKeysA = Object.getOwnPropertySymbols(objA);
143
- const symbolKeysB = Object.getOwnPropertySymbols(objB);
144
- if (symbolKeysA.length !== symbolKeysB.length) return false;
145
- const symbolKeysBSet = new Set(symbolKeysB);
146
- for (const key of stringKeysA) {
147
- if (!objHasOwn.call(objB, key)) return false;
148
- }
149
- for (const key of symbolKeysA) {
150
- if (!symbolKeysBSet.has(key)) return false;
151
- }
152
- for (const key of stringKeysA) {
153
- if (!deepEqualInner(recA[key], recB[key], visited)) {
154
- return false;
155
- }
156
- }
157
- for (const key of symbolKeysA) {
158
- if (!deepEqualInner(recA[key], recB[key], visited)) {
159
- return false;
160
- }
161
- }
162
- return true;
219
+ return objA === objB;
163
220
  }
164
221
  }
165
222
  }
166
223
  __name(deepEqualInner, "deepEqualInner");
224
+ function comparePlainObjects(objA, objB, visited) {
225
+ const recA = objA;
226
+ const recB = objB;
227
+ const stringKeysA = Object.keys(objA);
228
+ const stringKeysB = Object.keys(objB);
229
+ if (stringKeysA.length !== stringKeysB.length) return false;
230
+ const symbolKeysA = Object.getOwnPropertySymbols(objA);
231
+ const symbolKeysB = Object.getOwnPropertySymbols(objB);
232
+ if (symbolKeysA.length !== symbolKeysB.length) return false;
233
+ const symbolKeysBSet = new Set(symbolKeysB);
234
+ for (const key of stringKeysA) {
235
+ if (!objHasOwn.call(objB, key)) return false;
236
+ }
237
+ for (const key of symbolKeysA) {
238
+ if (!symbolKeysBSet.has(key)) return false;
239
+ }
240
+ for (const key of stringKeysA) {
241
+ if (!deepEqualInner(recA[key], recB[key], visited)) {
242
+ return false;
243
+ }
244
+ }
245
+ for (const key of symbolKeysA) {
246
+ if (!deepEqualInner(recA[key], recB[key], visited)) {
247
+ return false;
248
+ }
249
+ }
250
+ return true;
251
+ }
252
+ __name(comparePlainObjects, "comparePlainObjects");
167
253
 
168
254
  // src/utils/array/deep-omit.ts
169
255
  function deepOmit(value, options) {
170
256
  const visited = /* @__PURE__ */ new WeakMap();
171
257
  const ignoreKeys = options.ignoreKeys ? new Set(options.ignoreKeys) : void 0;
172
- return omitInternal(value, options, ignoreKeys, [], visited);
258
+ const budget = options.ignoreKeyPredicate ? { visits: 0 } : void 0;
259
+ return omitInternal(value, options, ignoreKeys, [], visited, budget);
173
260
  }
174
261
  __name(deepOmit, "deepOmit");
175
- function omitInternal(value, options, ignoreKeys, path, visited) {
262
+ var PATH_SENSITIVE_VISIT_BUDGET = 1e6;
263
+ function omitInternal(value, options, ignoreKeys, path, visited, budget) {
176
264
  if (value === null) return value;
177
265
  if (typeof value !== "object") return value;
178
266
  const obj = value;
179
267
  if (visited.has(obj)) {
180
268
  return visited.get(obj);
181
269
  }
182
- const tag = Object.prototype.toString.call(obj);
183
- if (tag === "[object Array]") {
270
+ if (budget && ++budget.visits > PATH_SENSITIVE_VISIT_BUDGET) {
271
+ throw new Error(
272
+ `deepOmit: exceeded ${PATH_SENSITIVE_VISIT_BUDGET} node visits. With ignoreKeyPredicate, objects reached via shared references are cloned once per path (the predicate may decide differently per path), which expands exponentially on diamond-shaped sharing. Restructure the input to a tree, or use ignoreKeys for path-independent filtering.`
273
+ );
274
+ }
275
+ if (Array.isArray(obj)) {
184
276
  const arr = obj;
185
277
  const clone2 = new Array(arr.length);
186
278
  visited.set(obj, clone2);
187
279
  for (let i = 0; i < arr.length; i++) {
188
280
  path.push(i);
189
- clone2[i] = omitInternal(arr[i], options, ignoreKeys, path, visited);
281
+ clone2[i] = omitInternal(
282
+ arr[i],
283
+ options,
284
+ ignoreKeys,
285
+ path,
286
+ visited,
287
+ budget
288
+ );
190
289
  path.pop();
191
290
  }
291
+ if (budget) visited.delete(obj);
192
292
  return clone2;
193
293
  }
294
+ const tag = Object.prototype.toString.call(obj);
194
295
  if (isBuiltInObject(obj, tag)) {
195
296
  const builtInClone = cloneBuiltIn(obj, tag);
196
297
  visited.set(obj, builtInClone);
@@ -211,7 +312,8 @@ function omitInternal(value, options, ignoreKeys, path, visited) {
211
312
  options,
212
313
  ignoreKeys,
213
314
  path,
214
- visited
315
+ visited,
316
+ budget
215
317
  )
216
318
  );
217
319
  path.pop();
@@ -227,11 +329,13 @@ function omitInternal(value, options, ignoreKeys, path, visited) {
227
329
  options,
228
330
  ignoreKeys,
229
331
  path,
230
- visited
332
+ visited,
333
+ budget
231
334
  )
232
335
  );
233
336
  path.pop();
234
337
  }
338
+ if (budget) visited.delete(obj);
235
339
  return clone;
236
340
  }
237
341
  __name(omitInternal, "omitInternal");
@@ -245,6 +349,7 @@ function assignOwn(target, key, value) {
245
349
  }
246
350
  __name(assignOwn, "assignOwn");
247
351
  function cloneBuiltIn(obj, tag) {
352
+ if (REFERENCE_COMPARED_TAGS.has(tag)) return obj;
248
353
  switch (tag) {
249
354
  case "[object Date]":
250
355
  return new Date(obj.getTime());
@@ -281,14 +386,81 @@ function deepEqualExcept(a, b, options) {
281
386
  return deepEqual(prunedA, prunedB);
282
387
  }
283
388
  __name(deepEqualExcept, "deepEqualExcept");
389
+ var DATE_MUTATORS = [
390
+ "setTime",
391
+ "setMilliseconds",
392
+ "setUTCMilliseconds",
393
+ "setSeconds",
394
+ "setUTCSeconds",
395
+ "setMinutes",
396
+ "setUTCMinutes",
397
+ "setHours",
398
+ "setUTCHours",
399
+ "setDate",
400
+ "setUTCDate",
401
+ "setMonth",
402
+ "setUTCMonth",
403
+ "setFullYear",
404
+ "setUTCFullYear",
405
+ "setYear"
406
+ ];
407
+ var mutationThrowers = /* @__PURE__ */ new Map();
408
+ function mutationThrower(typeName, method) {
409
+ const key = `${typeName}.${method}`;
410
+ let thrower = mutationThrowers.get(key);
411
+ if (!thrower) {
412
+ thrower = /* @__PURE__ */ __name(function throwFrozenMutation() {
413
+ throw new TypeError(
414
+ `Cannot call ${method}() on a ${typeName} inside a deeply frozen value`
415
+ );
416
+ }, "throwFrozenMutation");
417
+ mutationThrowers.set(key, thrower);
418
+ }
419
+ return thrower;
420
+ }
421
+ __name(mutationThrower, "mutationThrower");
422
+ var shadowDescriptor = {
423
+ value: void 0,
424
+ writable: false,
425
+ enumerable: false,
426
+ configurable: false
427
+ };
428
+ function shadowMutators(obj, typeName, methods) {
429
+ if (!Object.isExtensible(obj)) return;
430
+ for (const method of methods) {
431
+ shadowDescriptor.value = mutationThrower(typeName, method);
432
+ Object.defineProperty(obj, method, shadowDescriptor);
433
+ }
434
+ }
435
+ __name(shadowMutators, "shadowMutators");
284
436
  function deepFreeze(obj, visited = /* @__PURE__ */ new WeakSet()) {
285
437
  if (obj === null || typeof obj !== "object") {
286
438
  return obj;
287
439
  }
440
+ if (ArrayBuffer.isView(obj)) {
441
+ return obj;
442
+ }
288
443
  if (visited.has(obj)) {
289
444
  return obj;
290
445
  }
291
446
  visited.add(obj);
447
+ const tag = Object.prototype.toString.call(obj);
448
+ if (isBuiltInObject(obj, tag)) {
449
+ if (tag === "[object Date]") {
450
+ shadowMutators(obj, "Date", DATE_MUTATORS);
451
+ } else if (tag === "[object Map]") {
452
+ for (const [key, value] of obj) {
453
+ deepFreeze(key, visited);
454
+ deepFreeze(value, visited);
455
+ }
456
+ shadowMutators(obj, "Map", ["set", "delete", "clear"]);
457
+ } else if (tag === "[object Set]") {
458
+ for (const member of obj) {
459
+ deepFreeze(member, visited);
460
+ }
461
+ shadowMutators(obj, "Set", ["add", "delete", "clear"]);
462
+ }
463
+ }
292
464
  const keys = Reflect.ownKeys(obj);
293
465
  for (const key of keys) {
294
466
  const value = obj[key];
@@ -299,8 +471,74 @@ function deepFreeze(obj, visited = /* @__PURE__ */ new WeakSet()) {
299
471
  return Object.freeze(obj);
300
472
  }
301
473
  __name(deepFreeze, "deepFreeze");
474
+ function cloneForVo(value, visited) {
475
+ if (typeof value === "function") {
476
+ throw new TypeError(
477
+ "vo() does not accept function values: Value Objects are data, not behaviour"
478
+ );
479
+ }
480
+ if (value === null || typeof value !== "object") {
481
+ return value;
482
+ }
483
+ const obj = value;
484
+ if (visited.has(obj)) {
485
+ return visited.get(obj);
486
+ }
487
+ if (Array.isArray(obj)) {
488
+ const clone2 = new Array(obj.length);
489
+ visited.set(obj, clone2);
490
+ for (let i = 0; i < obj.length; i++) {
491
+ clone2[i] = cloneForVo(obj[i], visited);
492
+ }
493
+ return clone2;
494
+ }
495
+ const tag = Object.prototype.toString.call(obj);
496
+ if (isBuiltInObject(obj, tag)) {
497
+ if (tag === "[object Map]") {
498
+ const clone2 = /* @__PURE__ */ new Map();
499
+ visited.set(obj, clone2);
500
+ for (const [key, entry] of obj) {
501
+ clone2.set(cloneForVo(key, visited), cloneForVo(entry, visited));
502
+ }
503
+ return clone2;
504
+ }
505
+ if (tag === "[object Set]") {
506
+ const clone2 = /* @__PURE__ */ new Set();
507
+ visited.set(obj, clone2);
508
+ for (const member of obj) {
509
+ clone2.add(cloneForVo(member, visited));
510
+ }
511
+ return clone2;
512
+ }
513
+ if (tag === "[object Promise]" || tag === "[object WeakMap]" || tag === "[object WeakSet]") {
514
+ throw new TypeError(
515
+ `vo() cannot clone a ${tag.slice(8, -1)}: Value Objects are plain data`
516
+ );
517
+ }
518
+ const builtInClone = structuredClone(obj);
519
+ visited.set(obj, builtInClone);
520
+ return builtInClone;
521
+ }
522
+ const clone = Object.create(Object.getPrototypeOf(obj));
523
+ visited.set(obj, clone);
524
+ for (const key of Reflect.ownKeys(obj)) {
525
+ const descriptor = Object.getOwnPropertyDescriptor(obj, key);
526
+ if (!descriptor?.enumerable) continue;
527
+ Object.defineProperty(clone, key, {
528
+ value: cloneForVo(
529
+ obj[key],
530
+ visited
531
+ ),
532
+ writable: true,
533
+ enumerable: true,
534
+ configurable: true
535
+ });
536
+ }
537
+ return clone;
538
+ }
539
+ __name(cloneForVo, "cloneForVo");
302
540
  function vo(t) {
303
- return deepFreeze(structuredClone(t));
541
+ return deepFreeze(cloneForVo(t, /* @__PURE__ */ new WeakMap()));
304
542
  }
305
543
  __name(vo, "vo");
306
544
  function voEquals(a, b) {
@@ -314,12 +552,21 @@ __name(voEqualsExcept, "voEqualsExcept");
314
552
  function voWithValidation(t, validate, errorMessage) {
315
553
  if (!validate(t)) {
316
554
  return err(
317
- errorMessage ?? `Validation failed for value object: ${JSON.stringify(t)}`
555
+ errorMessage ?? `Validation failed for value object: ${describeValue(t)}`
318
556
  );
319
557
  }
320
558
  return ok(vo(t));
321
559
  }
322
560
  __name(voWithValidation, "voWithValidation");
561
+ function describeValue(value) {
562
+ try {
563
+ const json = JSON.stringify(value);
564
+ if (json !== void 0) return json;
565
+ } catch {
566
+ }
567
+ return String(value);
568
+ }
569
+ __name(describeValue, "describeValue");
323
570
  var ValueObject = class {
324
571
  static {
325
572
  __name(this, "ValueObject");
@@ -327,7 +574,9 @@ var ValueObject = class {
327
574
  props;
328
575
  /**
329
576
  * Creates a new ValueObject.
330
- * The properties are deeply frozen to ensure immutability.
577
+ * The properties are deep-cloned (prototype-preserving) and then deeply
578
+ * frozen, so the caller's own object graph is never frozen or mutated,
579
+ * and later mutation of the input does not bleed into the value object.
331
580
  *
332
581
  * @param props - The properties of the value object
333
582
  * @example
@@ -345,7 +594,7 @@ var ValueObject = class {
345
594
  */
346
595
  constructor(props) {
347
596
  this.validate(props);
348
- this.props = deepFreeze({ ...props });
597
+ this.props = deepFreeze(cloneForVo(props, /* @__PURE__ */ new WeakMap()));
349
598
  }
350
599
  /**
351
600
  * Optional validation hook that can be overridden by subclasses.
@@ -452,8 +701,11 @@ function createDomainEvent(type, payload, options) {
452
701
  aggregateId: options?.aggregateId,
453
702
  aggregateType: options?.aggregateType,
454
703
  payload,
455
- occurredAt: options?.occurredAt ?? currentClockFactory(),
704
+ // Defensive copy: the event must not share the caller's live Date
705
+ // instance, or a later mutation of it would bleed into the event.
706
+ occurredAt: options?.occurredAt ? new Date(options.occurredAt.getTime()) : currentClockFactory(),
456
707
  version: options?.version ?? 1,
708
+ aggregateVersion: options?.aggregateVersion,
457
709
  metadata: options?.metadata
458
710
  };
459
711
  return deepFreeze(event);
@@ -474,7 +726,21 @@ function copyMetadata(sourceEvent, additionalMetadata) {
474
726
  }
475
727
  __name(copyMetadata, "copyMetadata");
476
728
  function mergeMetadata(...metadataObjects) {
477
- return Object.assign({}, ...metadataObjects.filter(Boolean));
729
+ const merged = {};
730
+ for (const metadata of metadataObjects) {
731
+ if (!metadata) continue;
732
+ for (const key of Reflect.ownKeys(metadata)) {
733
+ const descriptor = Object.getOwnPropertyDescriptor(metadata, key);
734
+ if (!descriptor?.enumerable) continue;
735
+ Object.defineProperty(merged, key, {
736
+ value: metadata[key],
737
+ writable: true,
738
+ enumerable: true,
739
+ configurable: true
740
+ });
741
+ }
742
+ }
743
+ return merged;
478
744
  }
479
745
  __name(mergeMetadata, "mergeMetadata");
480
746
 
@@ -493,7 +759,7 @@ var Entity = class {
493
759
  /**
494
760
  * Returns the current state of the entity.
495
761
  *
496
- * The state object is **shallowly frozen** direct property writes
762
+ * The state object is **shallowly frozen**: direct property writes
497
763
  * (`entity.state.foo = …`) throw in strict mode, but writes to nested
498
764
  * objects (`entity.state.address.zip = …`) bypass the freeze. For deep
499
765
  * immutability either model nested data with `vo()` (which freezes
@@ -510,12 +776,20 @@ var Entity = class {
510
776
  * Subclasses can mutate this directly or use helper methods.
511
777
  */
512
778
  _state;
779
+ /**
780
+ * **State ownership.** Plain-object and array states are shallow-copied
781
+ * before the freeze, so the caller's own object stays mutable. A CLASS
782
+ * INSTANCE passed as state is an ownership transfer: it is frozen
783
+ * in place (a copy would strip its prototype). Do not keep mutating
784
+ * the instance after handing it to the entity. The same contract
785
+ * applies to {@link setState}.
786
+ */
513
787
  constructor(id, initialState) {
514
788
  if (id === null || id === void 0) {
515
789
  throw new Error("Entity ID cannot be null or undefined");
516
790
  }
517
791
  this.id = id;
518
- this._state = freezeShallow(initialState);
792
+ this._state = freezeShallow(shallowCopyOwned(initialState));
519
793
  this.validateState(this._state);
520
794
  }
521
795
  /**
@@ -526,7 +800,7 @@ var Entity = class {
526
800
  * **⚠️ Must not read subclass instance fields via `this`.** The
527
801
  * constructor calls `validateState(initialState)` BEFORE the subclass's
528
802
  * field initializers run, so `this.someField` is `undefined` at that
529
- * point a classic TypeScript/JavaScript constructor-ordering footgun.
803
+ * point, a classic TypeScript/JavaScript constructor-ordering footgun.
530
804
  * The `state` argument is the single source of truth; treat the method
531
805
  * as pure with respect to `this`.
532
806
  *
@@ -546,11 +820,15 @@ var Entity = class {
546
820
  * This is a convenience method for state mutations.
547
821
  * Automatically validates the newState using `validateState()`.
548
822
  *
823
+ * Plain-object and array states are shallow-copied before the freeze
824
+ * (the caller's object stays mutable); a class-instance state is an
825
+ * ownership transfer and is frozen in place; see the constructor.
826
+ *
549
827
  * @param newState - The new state
550
828
  */
551
829
  setState(newState) {
552
830
  this.validateState(newState);
553
- this._state = freezeShallow(newState);
831
+ this._state = freezeShallow(shallowCopyOwned(newState));
554
832
  }
555
833
  };
556
834
  function freezeShallow(value) {
@@ -560,6 +838,14 @@ function freezeShallow(value) {
560
838
  return value;
561
839
  }
562
840
  __name(freezeShallow, "freezeShallow");
841
+ function shallowCopyOwned(value) {
842
+ if (value === null || typeof value !== "object") return value;
843
+ if (Array.isArray(value)) return [...value];
844
+ const proto = Object.getPrototypeOf(value);
845
+ if (proto !== Object.prototype && proto !== null) return value;
846
+ return Object.assign(Object.create(proto), value);
847
+ }
848
+ __name(shallowCopyOwned, "shallowCopyOwned");
563
849
  function sameEntity(a, b) {
564
850
  return a.id === b.id;
565
851
  }
@@ -603,7 +889,7 @@ var BaseAggregate = class extends Entity {
603
889
  *
604
890
  * Distinct from {@link version}, which is the in-memory
605
891
  * post-mutation value. Mutations bump `_version` but never touch
606
- * `_persistedVersion` that field only moves on {@link markRestored}
892
+ * `_persistedVersion`; that field only moves on {@link markRestored}
607
893
  * (Post-Load) and {@link markPersisted} (Post-Save).
608
894
  */
609
895
  _persistedVersion = void 0;
@@ -623,7 +909,7 @@ var BaseAggregate = class extends Entity {
623
909
  }
624
910
  /**
625
911
  * Clears the pending-event list. Called by `markPersisted` after a
626
- * successful write the events have been handed off to the outbox
912
+ * successful write: the events have been handed off to the outbox
627
913
  * / event store and are no longer the aggregate's responsibility.
628
914
  */
629
915
  clearPendingEvents() {
@@ -641,16 +927,26 @@ var BaseAggregate = class extends Entity {
641
927
  this.setVersion(this._version + 1);
642
928
  }
643
929
  /**
644
- * **Lifecycle marker Post-Load.** Syncs both `_version` and
930
+ * **Lifecycle marker, Post-Load.** Syncs both `_version` and
645
931
  * `_persistedVersion` to the DB-stored version. Used by
646
932
  * `reconstitute(...)` factories to assemble an in-memory aggregate
647
933
  * from a persisted row.
648
934
  *
649
- * Does NOT fire {@link onPersisted} that hook has post-save
935
+ * Does NOT fire {@link onPersisted}; that hook has post-save
650
936
  * semantics (metrics, audit, cache eviction), not post-load. The
651
937
  * Factory-vs-Reconstitution distinction (Vernon §11) is honoured
652
938
  * structurally: two separate markers, one for each transition.
653
939
  *
940
+ * **If you override this, call `super.markRestored(version)` FIRST**,
941
+ * same discipline as {@link markPersisted}. The marker is load-bearing
942
+ * twice over: it syncs `version`/`persistedVersion`, and on
943
+ * `AggregateRoot` it also captures the dirty-tracking baseline for
944
+ * `changedKeys`/`hasChanges`. An override that skips `super` leaves
945
+ * that baseline uncaptured: `changedKeys` permanently reports ALL
946
+ * keys and `hasChanges` never returns `false`, so a partial-write
947
+ * repository silently degrades to full writes on every save — on top
948
+ * of the broken version sync.
949
+ *
654
950
  * @param version - The version the row currently holds in the DB
655
951
  *
656
952
  * @example
@@ -667,14 +963,14 @@ var BaseAggregate = class extends Entity {
667
963
  this._persistedVersion = version;
668
964
  }
669
965
  /**
670
- * **Framework lifecycle method `@sealed`.** Called by `withCommit`
966
+ * **Framework lifecycle method (`@sealed`).** Called by `withCommit`
671
967
  * (or by your own orchestration code, after harvesting `pendingEvents`)
672
968
  * to push the persisted version back into the in-memory aggregate and
673
969
  * clear `pendingEvents`. TypeScript has no `final` keyword, but
674
970
  * subclasses **should not** override this method directly.
675
971
  *
676
972
  * Overriding without calling `super.markPersisted(version)` silently
677
- * leaks `pendingEvents` the next `withCommit` will re-dispatch them
973
+ * leaks `pendingEvents`: the next `withCommit` will re-dispatch them
678
974
  * through the outbox, double-emitting events. This bug has been hit
679
975
  * in production by consumers; the {@link onPersisted} hook below is
680
976
  * the safer extension point.
@@ -684,7 +980,7 @@ var BaseAggregate = class extends Entity {
684
980
  * runs, then add your logic afterwards.
685
981
  *
686
982
  * @param version - The version assigned by the persistence layer
687
- * @see onPersisted the safe extension point for subclasses
983
+ * @see onPersisted, the safe extension point for subclasses
688
984
  */
689
985
  markPersisted(version) {
690
986
  this.markRestored(version);
@@ -692,19 +988,25 @@ var BaseAggregate = class extends Entity {
692
988
  this.onPersisted(version);
693
989
  }
694
990
  /**
695
- * Subclass extension point fires AFTER {@link markPersisted} has
991
+ * Subclass extension point: fires AFTER {@link markPersisted} has
696
992
  * updated the version and cleared `pendingEvents`. Override this for
697
993
  * post-persist logging, metrics, or cache-eviction without risk of
698
994
  * breaking the framework's pendingEvents cleanup.
699
995
  *
700
996
  * The default implementation is a no-op. Subclasses do NOT need to
701
- * call `super.onPersisted(version)` there is nothing in the parent
997
+ * call `super.onPersisted(version)`: there is nothing in the parent
702
998
  * implementation to preserve.
703
999
  *
1000
+ * **Observer contract: errors are swallowed.** `withCommit` invokes
1001
+ * `markPersisted` after the transaction has committed; a throwing hook
1002
+ * must neither abort the loop for peer aggregates nor make the
1003
+ * committed write look failed, so `withCommit` catches and discards
1004
+ * hook errors. Handle failures inside the hook if you need them.
1005
+ *
704
1006
  * **`onPersisted` deliberately receives only the version, not the
705
1007
  * drained events.** Event-driven post-persist logic (aggregate-level
706
1008
  * audit logging, per-event-type side effects) belongs in `EventBus`
707
- * subscribers or the outbox dispatcher that is the proper
1009
+ * subscribers or the outbox dispatcher; that is the proper
708
1010
  * Aggregate-Boundary separation. Building event-aware logic into
709
1011
  * `onPersisted` couples aggregate lifecycle to event processing and
710
1012
  * recreates the boundary problems Vernon's aggregate discipline is
@@ -713,7 +1015,7 @@ var BaseAggregate = class extends Entity {
713
1015
  * **The hook must return synchronously.** `markPersisted` is `void`-
714
1016
  * typed and calls `onPersisted` without `await`. TypeScript's
715
1017
  * permissive `void` will accept an `async`-override returning
716
- * `Promise<void>`, but the returned promise is fire-and-forget
1018
+ * `Promise<void>`, but the returned promise is fire-and-forget:
717
1019
  * any rejection becomes an unhandled rejection and `withCommit`
718
1020
  * proceeds without waiting. For asynchronous work, subscribe to the
719
1021
  * relevant domain event on the `EventBus` instead; that is the
@@ -726,7 +1028,7 @@ var BaseAggregate = class extends Entity {
726
1028
  /**
727
1029
  * Appends a domain event to the pending list. Prefer the higher-level
728
1030
  * `AggregateRoot.commit()` (state-stored) or `EventSourcedAggregate.apply()`
729
- * (event-sourced) call sites both wrap `addDomainEvent` in the
1031
+ * (event-sourced) call sites, both of which wrap `addDomainEvent` in the
730
1032
  * canonical record-AFTER-mutation order (Vernon §8). Calling
731
1033
  * `addDomainEvent` directly is appropriate only when state and event
732
1034
  * recording have already been decoupled deliberately (e.g. a
@@ -736,25 +1038,57 @@ var BaseAggregate = class extends Entity {
736
1038
  this._pendingEvents.push(event);
737
1039
  }
738
1040
  /**
739
- * Creates a snapshot of the current aggregate state the state at
1041
+ * Creates a snapshot of the current aggregate state: the state at
740
1042
  * this moment plus the version. Useful for ES snapshot policies and
741
1043
  * for state-stored backup / restore.
1044
+ *
1045
+ * The state is converted via {@link toSnapshotState}; the default
1046
+ * requires plain, serialisable data and fails fast otherwise.
742
1047
  */
743
1048
  createSnapshot() {
744
1049
  return {
745
- state: structuredClone(this._state),
1050
+ state: this.toSnapshotState(this._state),
746
1051
  version: this.version,
747
1052
  snapshotAt: /* @__PURE__ */ new Date()
748
1053
  };
749
1054
  }
1055
+ /**
1056
+ * Converts live aggregate state into the plain-data shape stored in a
1057
+ * snapshot. The default validates that the state graph is plain,
1058
+ * serialisable data (no class instances, functions, Promise/WeakMap/
1059
+ * WeakSet) and then `structuredClone`s it: class instances would
1060
+ * silently lose their prototype here AND on every snapshot-store
1061
+ * round-trip, so the default fails fast with the offending path
1062
+ * instead of producing a snapshot that breaks on first method call
1063
+ * after restore.
1064
+ *
1065
+ * Override this together with {@link fromSnapshotState} (and the
1066
+ * `TSnapshotState` generic) when the state carries class-based child
1067
+ * entities. The override owns isolation: return fresh objects, not
1068
+ * references into live state.
1069
+ */
1070
+ toSnapshotState(state) {
1071
+ assertSnapshotSafe(state, "", /* @__PURE__ */ new WeakSet());
1072
+ return structuredClone(state);
1073
+ }
1074
+ /**
1075
+ * Converts the plain-data snapshot shape back into live aggregate
1076
+ * state. The default `structuredClone`s the stored state so the
1077
+ * restored aggregate never aliases the snapshot object. Override
1078
+ * together with {@link toSnapshotState} to reconstruct class-based
1079
+ * child entities.
1080
+ */
1081
+ fromSnapshotState(stored) {
1082
+ return structuredClone(stored);
1083
+ }
750
1084
  /**
751
1085
  * Sugar for `createDomainEvent` that auto-injects `aggregateId`
752
1086
  * (from `this.id`) and `aggregateType` (from {@link aggregateType})
753
1087
  * into the event's metadata fields. This is the canonical path for
754
1088
  * recording events from inside aggregate domain methods.
755
1089
  *
756
- * Downstream consumers outbox dispatchers, projection handlers,
757
- * audit logs route by these two fields. Calling
1090
+ * Downstream consumers (outbox dispatchers, projection handlers,
1091
+ * audit logs) route by these two fields. Calling
758
1092
  * `createDomainEvent(...)` directly inside an aggregate method
759
1093
  * leaves them unset and is caught at the `withCommit` harvest
760
1094
  * boundary, but `this.recordEvent(...)` makes the right thing
@@ -778,8 +1112,8 @@ var BaseAggregate = class extends Entity {
778
1112
  * @param payload - payload for that event subtype
779
1113
  * @param options - any remaining `createDomainEvent` options
780
1114
  * (`eventId`, `occurredAt`, `metadata`, `version`); `aggregateId`
781
- * and `aggregateType` are deliberately omitted the helper sets
782
- * them.
1115
+ * and `aggregateType` are deliberately omitted, because the helper
1116
+ * sets them.
783
1117
  */
784
1118
  recordEvent(type, payload, options) {
785
1119
  return createDomainEvent(type, payload, {
@@ -789,6 +1123,77 @@ var BaseAggregate = class extends Entity {
789
1123
  });
790
1124
  }
791
1125
  };
1126
+ function assertSnapshotSafe(value, path, seen) {
1127
+ if (typeof value === "function") {
1128
+ throw new Error(
1129
+ `createSnapshot: state${path} is a function: snapshot state must be plain, serialisable data. Override toSnapshotState()/fromSnapshotState() to map it.`
1130
+ );
1131
+ }
1132
+ if (value === null || typeof value !== "object") return;
1133
+ const obj = value;
1134
+ if (seen.has(obj)) return;
1135
+ seen.add(obj);
1136
+ if (Array.isArray(obj)) {
1137
+ for (let i = 0; i < obj.length; i++) {
1138
+ assertSnapshotSafe(obj[i], `${path}[${i}]`, seen);
1139
+ }
1140
+ return;
1141
+ }
1142
+ const tag = Object.prototype.toString.call(obj);
1143
+ if (isBuiltInObject(obj, tag)) {
1144
+ if (tag === "[object Map]") {
1145
+ let i = 0;
1146
+ for (const [key, entryValue] of obj) {
1147
+ assertSnapshotSafe(key, `${path}<map key #${i}>`, seen);
1148
+ assertSnapshotSafe(entryValue, `${path}<map value #${i}>`, seen);
1149
+ i++;
1150
+ }
1151
+ return;
1152
+ }
1153
+ if (tag === "[object Set]") {
1154
+ let i = 0;
1155
+ for (const member of obj) {
1156
+ assertSnapshotSafe(member, `${path}<set member #${i}>`, seen);
1157
+ i++;
1158
+ }
1159
+ return;
1160
+ }
1161
+ if (tag === "[object Promise]" || tag === "[object WeakMap]" || tag === "[object WeakSet]") {
1162
+ throw new Error(
1163
+ `createSnapshot: state${path} is a ${tag.slice(8, -1)}: it cannot be cloned or persisted. Override toSnapshotState()/fromSnapshotState() to map it.`
1164
+ );
1165
+ }
1166
+ if (tag === "[object Error]") {
1167
+ throw new Error(
1168
+ `createSnapshot: state${path} is an Error: structuredClone downgrades Error subclasses to plain Error and silently drops custom fields, so the restored value would not round-trip. Override toSnapshotState()/fromSnapshotState() to map it to plain data.`
1169
+ );
1170
+ }
1171
+ return;
1172
+ }
1173
+ const proto = Object.getPrototypeOf(obj);
1174
+ if (proto === Object.prototype || proto === null) {
1175
+ for (const key of Reflect.ownKeys(obj)) {
1176
+ const descriptor = Object.getOwnPropertyDescriptor(obj, key);
1177
+ if (!descriptor?.enumerable) continue;
1178
+ if (typeof key === "symbol") {
1179
+ throw new Error(
1180
+ `createSnapshot: state${path} has a symbol-keyed property (${String(key)}): structuredClone silently drops symbol keys, so the snapshot would lose state. Override toSnapshotState()/fromSnapshotState() to map it.`
1181
+ );
1182
+ }
1183
+ assertSnapshotSafe(
1184
+ obj[key],
1185
+ `${path}.${key}`,
1186
+ seen
1187
+ );
1188
+ }
1189
+ return;
1190
+ }
1191
+ const name = proto.constructor?.name || "anonymous class";
1192
+ throw new Error(
1193
+ `createSnapshot: state${path} is a class instance (${name}): structuredClone would strip its prototype and methods, producing a snapshot that breaks on the first method call after restore. Override toSnapshotState()/fromSnapshotState() to map child entities to plain data.`
1194
+ );
1195
+ }
1196
+ __name(assertSnapshotSafe, "assertSnapshotSafe");
792
1197
 
793
1198
  // src/aggregate/aggregate-root.ts
794
1199
  var AggregateRoot = class extends BaseAggregate {
@@ -796,10 +1201,125 @@ var AggregateRoot = class extends BaseAggregate {
796
1201
  __name(this, "AggregateRoot");
797
1202
  }
798
1203
  _autoVersionBump;
1204
+ /**
1205
+ * The state reference as of the last {@link markRestored} /
1206
+ * `markPersisted` (the persistence-lifecycle markers). Only
1207
+ * meaningful while {@link _hasBaseline} is `true`; tracked by a
1208
+ * separate flag rather than an `undefined` sentinel so a `TState`
1209
+ * that itself admits `undefined` cannot be confused with the
1210
+ * never-persisted insert path.
1211
+ *
1212
+ * Held by reference, never copied: `_state` is shallow-frozen and only
1213
+ * ever *replaced* (via `setState` / restore), so the captured reference
1214
+ * stays an exact image of the state at baseline time.
1215
+ */
1216
+ _baselineState = void 0;
1217
+ /**
1218
+ * `false` until the aggregate has been persisted or restored at least
1219
+ * once: the insert path, where every key counts as changed.
1220
+ */
1221
+ _hasBaseline = false;
799
1222
  constructor(id, initialState, config) {
800
1223
  super(id, initialState);
801
1224
  this._autoVersionBump = config?.autoVersionBump ?? false;
802
1225
  }
1226
+ /**
1227
+ * **Lifecycle marker, Post-Load (see `BaseAggregate.markRestored`).**
1228
+ * Additionally captures the current state reference as the dirty-
1229
+ * tracking baseline for {@link changedKeys} / {@link hasChanges}.
1230
+ *
1231
+ * Covers all three baseline-capture paths through a single override:
1232
+ * `reconstitute(...)` factories, {@link restoreFromSnapshot} (which
1233
+ * assigns the restored state *before* calling this), and
1234
+ * `markPersisted` (which delegates here, so a successful save
1235
+ * re-baselines the diff).
1236
+ *
1237
+ * If you override this, call `super.markRestored(version)` FIRST:
1238
+ * skipping it leaves the baseline uncaptured, so `changedKeys`
1239
+ * permanently reports ALL keys and `hasChanges` never returns `false`
1240
+ * — partial-write repositories silently degrade to full writes — on
1241
+ * top of breaking version sync.
1242
+ */
1243
+ markRestored(version) {
1244
+ super.markRestored(version);
1245
+ this._baselineState = this._state;
1246
+ this._hasBaseline = true;
1247
+ }
1248
+ /**
1249
+ * Top-level state keys whose value (or presence) changed since the
1250
+ * last {@link markRestored} / `markPersisted`. Never-persisted
1251
+ * aggregates report ALL current keys (the insert path).
1252
+ *
1253
+ * This is the write-scoping signal for **partial writes in multi-table
1254
+ * repositories**: a `save()` for an aggregate whose state spans a root
1255
+ * row plus N child-collection tables can write only the collections
1256
+ * whose key is dirty, while the root-row OCC version write rides every
1257
+ * save. See `docs/guide/repository.md` → "Partial writes for
1258
+ * multi-table aggregates".
1259
+ *
1260
+ * **How it works.** `setState()` replaces state immutably and the
1261
+ * state object is shallow-frozen, so unchanged top-level sub-objects
1262
+ * keep reference identity across mutations. The diff is therefore a
1263
+ * shallow per-key `!==` against the baseline reference — O(top-level
1264
+ * keys), no proxies, no deep diff. A key also counts as dirty when its
1265
+ * *presence* differs (added or removed, even with an `undefined`
1266
+ * value). Computed fresh on every access (a new `Set` each time), so
1267
+ * callers cannot poison later reads.
1268
+ *
1269
+ * **Soundness contract (same one `freezeShallow` already makes):**
1270
+ * the per-key diff is exact only for plain-record `TState` mutated via
1271
+ * `setState` / `commit` (whole-state replacement). In-place mutation
1272
+ * of NESTED objects bypasses the shallow freeze AND this diff; a
1273
+ * class-instance `TState` mutated through its own methods defeats
1274
+ * tracking entirely (the reference never changes). A keyless `TState`
1275
+ * (primitive, bare `Date`) has no keys to report, so `changedKeys`
1276
+ * stays empty for it — use {@link hasChanges}, whose reference
1277
+ * fallback covers keyless states. A deep-equal but newly-referenced
1278
+ * value reports a false POSITIVE (harmless extra write); under the
1279
+ * contract above there are no false negatives.
1280
+ *
1281
+ * Granularity is per top-level key — table-granular, not row-granular:
1282
+ * a dirty collection key means "this child table changed", not which
1283
+ * rows. `EventSourcedAggregate` deliberately has no `changedKeys`;
1284
+ * its `pendingEvents` are the change record.
1285
+ */
1286
+ get changedKeys() {
1287
+ if (!this._hasBaseline) {
1288
+ return new Set(ownKeys(this._state));
1289
+ }
1290
+ return computeChangedKeys(this._baselineState, this._state);
1291
+ }
1292
+ /**
1293
+ * Safe skip signal: `false` only when there is genuinely nothing to
1294
+ * persist or flush. `true` when the aggregate has never been
1295
+ * persisted, the version moved past `persistedVersion`, there are
1296
+ * unflushed {@link pendingEvents}, any state key is dirty, or — for
1297
+ * keyless states the per-key diff cannot see (primitive `TState`,
1298
+ * zero-own-key objects like a bare `Date`) — the state reference
1299
+ * changed since the baseline.
1300
+ *
1301
+ * The version clause is deliberate: `setState({...state}, true)` with
1302
+ * identical per-key values yields empty {@link changedKeys} but a
1303
+ * bumped version. If a repository skipped `save()` on a state-only
1304
+ * check, `withCommit` would still call `markPersisted(version)` after
1305
+ * commit, desyncing `persistedVersion` from the DB row — and the next
1306
+ * uncontended save would throw a false `ConcurrencyConflictError`.
1307
+ *
1308
+ * The pending-events clause covers the sanctioned decoupled
1309
+ * `addDomainEvent` path (an event recorded without a state change,
1310
+ * e.g. a deletion event before a hard delete): the aggregate still
1311
+ * needs its trip through `withCommit` so the event reaches the
1312
+ * outbox. With all clauses included, `hasChanges === false` genuinely
1313
+ * means "skipping save is safe".
1314
+ */
1315
+ get hasChanges() {
1316
+ if (!this._hasBaseline) return true;
1317
+ if (this.version !== this.persistedVersion) return true;
1318
+ if (this.pendingEvents.length > 0) return true;
1319
+ if (this.changedKeys.size > 0) return true;
1320
+ const baseline = this._baselineState;
1321
+ return baseline !== this._state && ownKeys(baseline).length === 0 && ownKeys(this._state).length === 0;
1322
+ }
803
1323
  /**
804
1324
  * Mutates state and records the resulting domain events in the
805
1325
  * **canonical record-after-mutation order**. Use this instead of calling
@@ -807,7 +1327,7 @@ var AggregateRoot = class extends BaseAggregate {
807
1327
  * "event for a fact that never happened" footgun.
808
1328
  *
809
1329
  * Order of operations:
810
- * 1. `setState(newState, true)` runs `validateState` first.
1330
+ * 1. `setState(newState, true)`: runs `validateState` first.
811
1331
  * If it throws, the method propagates and **no event is recorded
812
1332
  * and no version is bumped**.
813
1333
  * 2. Each event in `events` is appended via `addDomainEvent`.
@@ -866,19 +1386,46 @@ var AggregateRoot = class extends BaseAggregate {
866
1386
  }
867
1387
  }
868
1388
  /**
869
- * Restores the aggregate from a snapshot loads state and aligns
1389
+ * Restores the aggregate from a snapshot: loads state and aligns
870
1390
  * `version` + `persistedVersion` to the snapshot version. Validates
871
1391
  * the restored state.
872
1392
  *
873
1393
  * @param snapshot - The snapshot to restore from
874
1394
  */
875
1395
  restoreFromSnapshot(snapshot) {
876
- const cloned = structuredClone(snapshot.state);
877
- this.validateState(cloned);
878
- this._state = freezeShallow(cloned);
1396
+ const restored = this.fromSnapshotState(snapshot.state);
1397
+ this.validateState(restored);
1398
+ this._state = freezeShallow(restored);
879
1399
  this.markRestored(snapshot.version);
880
1400
  }
881
1401
  };
1402
+ function ownKeys(value) {
1403
+ return value !== null && typeof value === "object" ? Object.keys(value) : [];
1404
+ }
1405
+ __name(ownKeys, "ownKeys");
1406
+ function computeChangedKeys(baseline, current) {
1407
+ const baselineKeys = new Set(ownKeys(baseline));
1408
+ const currentKeys = new Set(ownKeys(current));
1409
+ const dirty = /* @__PURE__ */ new Set();
1410
+ for (const key of currentKeys) {
1411
+ if (!baselineKeys.has(key)) {
1412
+ dirty.add(key);
1413
+ continue;
1414
+ }
1415
+ const before = baseline[key];
1416
+ const after = current[key];
1417
+ if (before !== after) {
1418
+ dirty.add(key);
1419
+ }
1420
+ }
1421
+ for (const key of baselineKeys) {
1422
+ if (!currentKeys.has(key)) {
1423
+ dirty.add(key);
1424
+ }
1425
+ }
1426
+ return dirty;
1427
+ }
1428
+ __name(computeChangedKeys, "computeChangedKeys");
882
1429
  var DomainError = class extends BaseError {
883
1430
  static {
884
1431
  __name(this, "DomainError");
@@ -891,16 +1438,33 @@ var InfrastructureError = class extends BaseError {
891
1438
  };
892
1439
  var MissingHandlerError = class extends BaseError {
893
1440
  constructor(eventType, cause) {
894
- super(`Missing handler for event type: ${eventType}`, cause);
1441
+ super(`Missing handler for event type: ${eventType}`, cause, {
1442
+ name: "MissingHandlerError"
1443
+ });
895
1444
  this.eventType = eventType;
896
1445
  }
897
1446
  static {
898
1447
  __name(this, "MissingHandlerError");
899
1448
  }
900
1449
  };
1450
+ var AggregateDeletedError = class extends BaseError {
1451
+ constructor(aggregateId) {
1452
+ super(
1453
+ `Aggregate ${aggregateId} was deleted in this unit of work and cannot be saved or registered again. Deletion is final within an operation; if the aggregate must live, do not delete it.`,
1454
+ void 0,
1455
+ { name: "AggregateDeletedError" }
1456
+ );
1457
+ this.aggregateId = aggregateId;
1458
+ }
1459
+ static {
1460
+ __name(this, "AggregateDeletedError");
1461
+ }
1462
+ };
901
1463
  var AggregateNotFoundError = class extends InfrastructureError {
902
1464
  constructor(aggregateType, id, cause) {
903
- super(`Aggregate not found: ${aggregateType}(${id})`, cause);
1465
+ super(`Aggregate not found: ${aggregateType}(${id})`, cause, {
1466
+ name: "AggregateNotFoundError"
1467
+ });
904
1468
  this.aggregateType = aggregateType;
905
1469
  this.id = id;
906
1470
  this.withUserMessage(
@@ -911,11 +1475,29 @@ var AggregateNotFoundError = class extends InfrastructureError {
911
1475
  __name(this, "AggregateNotFoundError");
912
1476
  }
913
1477
  };
1478
+ var DuplicateAggregateError = class extends InfrastructureError {
1479
+ constructor(aggregateType, aggregateId, cause) {
1480
+ super(
1481
+ `Duplicate aggregate: ${aggregateType}(${aggregateId}) already exists`,
1482
+ cause,
1483
+ { name: "DuplicateAggregateError" }
1484
+ );
1485
+ this.aggregateType = aggregateType;
1486
+ this.aggregateId = aggregateId;
1487
+ this.withUserMessage(
1488
+ `This ${aggregateType} already exists. It may have been created by a concurrent request.`
1489
+ );
1490
+ }
1491
+ static {
1492
+ __name(this, "DuplicateAggregateError");
1493
+ }
1494
+ };
914
1495
  var ConcurrencyConflictError = class extends InfrastructureError {
915
1496
  constructor(aggregateType, aggregateId, expectedVersion, actualVersion, cause) {
916
1497
  super(
917
1498
  `Concurrency conflict on ${aggregateType}(${aggregateId}): expected version ${expectedVersion}, actual ${actualVersion}`,
918
- cause
1499
+ cause,
1500
+ { name: "ConcurrencyConflictError" }
919
1501
  );
920
1502
  this.aggregateType = aggregateType;
921
1503
  this.aggregateId = aggregateId;
@@ -955,13 +1537,13 @@ var EventSourcedAggregate = class extends BaseAggregate {
955
1537
  * Throws `DomainError` (or a subclass) on validation failure.
956
1538
  * Throws `MissingHandlerError` if no handler is registered for `event.type`.
957
1539
  *
958
- * State is not mutated if any step throws the handler is invoked into
1540
+ * State is not mutated if any step throws: the handler is invoked into
959
1541
  * a local and only assigned to `_state` once all checks pass.
960
1542
  *
961
1543
  * The method is generic in the event tag `K`, so concrete callers
962
1544
  * (`this.apply(orderCreated)`) narrow to the literal tag and the
963
- * dispatched handler is typed as `Handler<TState, Extract<TEvent, { type: K }>>`
964
- * no `as` cast required at the call site.
1545
+ * dispatched handler is typed as `Handler<TState, Extract<TEvent, { type: K }>>`,
1546
+ * with no `as` cast required at the call site.
965
1547
  *
966
1548
  * @param event - The domain event to apply
967
1549
  * @param isNew - Whether the event is new (needs persisting) or replayed from history
@@ -978,7 +1560,7 @@ var EventSourcedAggregate = class extends BaseAggregate {
978
1560
  */
979
1561
  dispatchAndCommit(event, isNew) {
980
1562
  this.validateEvent(event);
981
- const handler = this.handlers[event.type];
1563
+ const handler = Object.hasOwn(this.handlers, event.type) ? this.handlers[event.type] : void 0;
982
1564
  if (!handler) {
983
1565
  throw new MissingHandlerError(event.type);
984
1566
  }
@@ -991,21 +1573,30 @@ var EventSourcedAggregate = class extends BaseAggregate {
991
1573
  }
992
1574
  /**
993
1575
  * Reconstitutes the aggregate from an event history. Catches `DomainError`
994
- * thrown during replay and returns it as an `Err` this is the
1576
+ * thrown during replay and returns it as an `Err`: this is the
995
1577
  * infrastructure boundary, where event-stream corruption is an expected
996
1578
  * recoverable failure. Unexpected (non-DomainError) throws propagate.
997
1579
  *
1580
+ * All-or-nothing: if any event mid-stream throws, the aggregate's state
1581
+ * is rolled back to its pre-call value, the same contract as
1582
+ * `restoreFromSnapshotWithEvents`. Partial replay is never observable.
1583
+ * (Version needs no rollback: replay dispatches with `isNew = false`,
1584
+ * which never bumps it; only the final `markRestored` advances it.)
1585
+ *
998
1586
  * Version advances additively: the aggregate's pre-existing version plus
999
1587
  * `history.length`. A fresh aggregate (v=0) loading 3 events ends at v=3;
1000
1588
  * an aggregate already at v=1 (e.g. after a creation event) loading
1001
1589
  * 2 events ends at v=3, not v=2.
1002
1590
  */
1003
1591
  loadFromHistory(history) {
1592
+ if (history.length === 0) return ok();
1593
+ const previousState = this._state;
1004
1594
  const startVersion = this.version;
1005
1595
  for (const event of history) {
1006
1596
  try {
1007
1597
  this.dispatchAndCommit(event, false);
1008
1598
  } catch (e) {
1599
+ this._state = previousState;
1009
1600
  if (e instanceof DomainError) return err(e);
1010
1601
  throw e;
1011
1602
  }
@@ -1026,7 +1617,7 @@ var EventSourcedAggregate = class extends BaseAggregate {
1026
1617
  restoreFromSnapshotWithEvents(snapshot, eventsAfterSnapshot) {
1027
1618
  const previousState = this._state;
1028
1619
  const previousVersion = this.version;
1029
- this._state = freezeShallow(structuredClone(snapshot.state));
1620
+ this._state = freezeShallow(this.fromSnapshotState(snapshot.state));
1030
1621
  this.setVersion(snapshot.version);
1031
1622
  for (const event of eventsAfterSnapshot) {
1032
1623
  try {
@@ -1044,12 +1635,31 @@ var EventSourcedAggregate = class extends BaseAggregate {
1044
1635
  return ok();
1045
1636
  }
1046
1637
  };
1638
+
1639
+ // src/app/describe-thrown.ts
1640
+ function describeThrown(error) {
1641
+ if (error instanceof Error) return error.message;
1642
+ try {
1643
+ const json = JSON.stringify(error);
1644
+ if (json !== void 0) return json;
1645
+ } catch {
1646
+ }
1647
+ return String(error);
1648
+ }
1649
+ __name(describeThrown, "describeThrown");
1650
+
1651
+ // src/app/command-bus.ts
1047
1652
  var CommandBus = class {
1048
1653
  static {
1049
1654
  __name(this, "CommandBus");
1050
1655
  }
1051
1656
  handlers = /* @__PURE__ */ new Map();
1052
1657
  register(commandType, handler) {
1658
+ if (this.handlers.has(commandType)) {
1659
+ throw new Error(
1660
+ `CommandBus: a handler for command type "${commandType}" is already registered`
1661
+ );
1662
+ }
1053
1663
  this.handlers.set(commandType, (cmd) => handler(cmd));
1054
1664
  }
1055
1665
  async execute(command) {
@@ -1060,21 +1670,32 @@ var CommandBus = class {
1060
1670
  try {
1061
1671
  return await handler(command);
1062
1672
  } catch (error) {
1063
- return err(
1064
- error instanceof Error ? error.message : String(error)
1065
- );
1673
+ return err(describeThrown(error));
1066
1674
  }
1067
1675
  }
1068
1676
  };
1069
1677
 
1070
1678
  // src/app/handler.ts
1071
1679
  async function withCommit(deps, fn) {
1072
- const { result, aggregates, events } = await deps.scope.transactional(
1680
+ const { result, aggregates, deleted, events } = await deps.scope.transactional(
1073
1681
  async (ctx) => {
1074
1682
  const fnResult = await fn(ctx);
1075
1683
  const uniqueAggregates = Array.from(new Set(fnResult.aggregates));
1076
1684
  const harvested = uniqueAggregates.flatMap(
1077
- (agg) => agg.pendingEvents
1685
+ (agg) => agg.pendingEvents.map((event) => {
1686
+ if (event.aggregateVersion === void 0) {
1687
+ return Object.freeze({
1688
+ ...event,
1689
+ aggregateVersion: agg.version
1690
+ });
1691
+ }
1692
+ if (event.aggregateVersion > agg.version) {
1693
+ throw new Error(
1694
+ `withCommit: event "${event.type}" carries a pre-set aggregateVersion (${event.aggregateVersion}) AHEAD of its aggregate's commit version (${agg.version}). A stale-or-copied pre-set would advance consumer idempotency watermarks past real history; remove the manual aggregateVersion or correct it.`
1695
+ );
1696
+ }
1697
+ return event;
1698
+ })
1078
1699
  );
1079
1700
  for (const event of harvested) {
1080
1701
  const missing = [];
@@ -1084,31 +1705,351 @@ async function withCommit(deps, fn) {
1084
1705
  throw new Error(
1085
1706
  `withCommit: event "${event.type}" is missing ${missing.join(
1086
1707
  " 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.`
1708
+ )}. Use this.recordEvent(type, payload) inside aggregate methods instead of createDomainEvent(...); recordEvent auto-injects aggregateId and aggregateType. Outbox dispatchers and projection handlers rely on these fields for routing.`
1088
1709
  );
1089
1710
  }
1090
1711
  }
1091
1712
  if (harvested.length > 0) {
1092
1713
  await deps.outbox.add(harvested);
1093
1714
  }
1094
- return { ...fnResult, aggregates: uniqueAggregates, events: harvested };
1715
+ return {
1716
+ ...fnResult,
1717
+ aggregates: uniqueAggregates,
1718
+ deleted: new Set(fnResult.deleted ?? []),
1719
+ events: harvested
1720
+ };
1095
1721
  }
1096
1722
  );
1097
1723
  for (const agg of aggregates) {
1098
- agg.markPersisted(agg.version);
1724
+ try {
1725
+ if (deleted.has(agg)) {
1726
+ agg.clearPendingEvents();
1727
+ } else {
1728
+ agg.markPersisted(agg.version);
1729
+ }
1730
+ } catch {
1731
+ }
1099
1732
  }
1100
1733
  if (deps.bus && events.length > 0) {
1101
- await deps.bus.publish(events);
1734
+ try {
1735
+ await deps.bus.publish(events);
1736
+ } catch (error) {
1737
+ try {
1738
+ deps.onPublishError?.(error, events);
1739
+ } catch {
1740
+ }
1741
+ }
1102
1742
  }
1103
1743
  return result;
1104
1744
  }
1105
1745
  __name(withCommit, "withCommit");
1746
+
1747
+ // src/repo/identity-map.ts
1748
+ var IdentityMap = class {
1749
+ static {
1750
+ __name(this, "IdentityMap");
1751
+ }
1752
+ _stores = /* @__PURE__ */ new Map();
1753
+ _deleted = /* @__PURE__ */ new Map();
1754
+ /** The cached instance for type+id, or `undefined` (also after {@link delete}). */
1755
+ get(type, id) {
1756
+ return this._stores.get(type)?.get(id);
1757
+ }
1758
+ /** Whether an instance is registered for type+id (false after {@link delete}). */
1759
+ has(type, id) {
1760
+ return this._stores.get(type)?.has(id) ?? false;
1761
+ }
1762
+ /**
1763
+ * Whether type+id was {@link delete}d in this unit of work. The
1764
+ * read path checks this BEFORE hydrating and returns `null`, so
1765
+ * "deleted in this operation" reads uniformly as not-found —
1766
+ * regardless of whether the repository's physical delete already
1767
+ * removed the row or is deferred within the transaction. Without
1768
+ * the check, a read-only probe of a deleted aggregate would crash
1769
+ * in {@link set} for deferred-write repositories and return `null`
1770
+ * for immediate-write ones.
1771
+ */
1772
+ isDeleted(type, id) {
1773
+ return this._deleted.get(type)?.has(id) ?? false;
1774
+ }
1775
+ /**
1776
+ * Registers the hydrated instance for type+id.
1777
+ *
1778
+ * - Re-registering the SAME instance is a no-op (idempotent).
1779
+ * - Registering a DIFFERENT instance for an occupied type+id throws:
1780
+ * that is precisely the identity-map violation this class exists
1781
+ * to prevent (the repository hydrated twice instead of checking
1782
+ * {@link get} first), and letting it pass would double-harvest
1783
+ * events downstream.
1784
+ * - Registering a type+id that was {@link delete}d in this unit of
1785
+ * work throws `AggregateDeletedError`: deletion is final within
1786
+ * the operation.
1787
+ */
1788
+ set(type, id, aggregate) {
1789
+ if (this._deleted.get(type)?.has(id)) {
1790
+ throw new AggregateDeletedError(String(id));
1791
+ }
1792
+ let store = this._stores.get(type);
1793
+ if (store === void 0) {
1794
+ store = /* @__PURE__ */ new Map();
1795
+ this._stores.set(type, store);
1796
+ }
1797
+ const existing = store.get(id);
1798
+ if (existing !== void 0 && existing !== aggregate) {
1799
+ throw new Error(
1800
+ `IdentityMap: a different instance is already registered for ${type.name}(${String(id)}). Check get() before hydrating - two live instances of one aggregate break the one-instance-per-unit-of-work contract that exactly-once event harvest relies on.`
1801
+ );
1802
+ }
1803
+ store.set(id, aggregate);
1804
+ }
1805
+ /**
1806
+ * Removes the entry for type+id and records a tombstone: subsequent
1807
+ * {@link get} / {@link has} report absence, and a subsequent
1808
+ * {@link set} of the same type+id throws `AggregateDeletedError`.
1809
+ * Called by a repository's `delete(aggregate)` alongside
1810
+ * `session.enrollDeleted(aggregate)`.
1811
+ */
1812
+ delete(type, id) {
1813
+ this._stores.get(type)?.delete(id);
1814
+ let tombstones = this._deleted.get(type);
1815
+ if (tombstones === void 0) {
1816
+ tombstones = /* @__PURE__ */ new Set();
1817
+ this._deleted.set(type, tombstones);
1818
+ }
1819
+ tombstones.add(id);
1820
+ }
1821
+ /** Empties all stores and tombstones. Called by the unit of work on close. */
1822
+ clear() {
1823
+ this._stores.clear();
1824
+ this._deleted.clear();
1825
+ }
1826
+ };
1827
+
1828
+ // src/app/unit-of-work.ts
1829
+ var NestedUnitOfWorkError = class extends BaseError {
1830
+ static {
1831
+ __name(this, "NestedUnitOfWorkError");
1832
+ }
1833
+ constructor() {
1834
+ super(
1835
+ "UnitOfWork.run() was called while this instance is already running. A nested run() would open an independent transaction, not join the outer one - merge the work into a single run() callback. For concurrent operations, construct one UnitOfWork per operation.",
1836
+ void 0,
1837
+ { name: "NestedUnitOfWorkError" }
1838
+ );
1839
+ }
1840
+ };
1841
+ var TransactionClosedError = class extends BaseError {
1842
+ constructor(operation) {
1843
+ super(
1844
+ `Unit of work is closed: ${operation} was called after the transaction committed or rolled back. Do not use the context or session outside the run() callback.`,
1845
+ void 0,
1846
+ { name: "TransactionClosedError" }
1847
+ );
1848
+ this.operation = operation;
1849
+ }
1850
+ static {
1851
+ __name(this, "TransactionClosedError");
1852
+ }
1853
+ };
1854
+ var CommitError = class extends InfrastructureError {
1855
+ static {
1856
+ __name(this, "CommitError");
1857
+ }
1858
+ constructor(cause) {
1859
+ super(
1860
+ "Unit of work failed after the work callback completed: the event harvest, outbox write, or transaction commit rejected. The transaction did not commit; see cause.",
1861
+ cause,
1862
+ { name: "CommitError" }
1863
+ );
1864
+ }
1865
+ };
1866
+ var RollbackError = class extends InfrastructureError {
1867
+ constructor(cause, rollbackCause) {
1868
+ super(
1869
+ "The work callback failed and the transaction scope rejected with a different error (possible rollback failure). The callback's error is the cause; the scope's error is in rollbackCause.",
1870
+ cause,
1871
+ { name: "RollbackError" }
1872
+ );
1873
+ this.rollbackCause = rollbackCause;
1874
+ }
1875
+ static {
1876
+ __name(this, "RollbackError");
1877
+ }
1878
+ };
1879
+ var UnitOfWork = class {
1880
+ constructor(deps) {
1881
+ this.deps = deps;
1882
+ }
1883
+ static {
1884
+ __name(this, "UnitOfWork");
1885
+ }
1886
+ _active = false;
1887
+ /**
1888
+ * Execute one unit of work: open the transaction, hand the callback
1889
+ * tx-bound repositories, commit on resolve, roll back on throw,
1890
+ * run the post-commit lifecycle (markPersisted, publish) for every
1891
+ * enrolled aggregate. Returns the callback's result.
1892
+ */
1893
+ async run(work) {
1894
+ if (this._active) {
1895
+ throw new NestedUnitOfWorkError();
1896
+ }
1897
+ this._active = true;
1898
+ let session;
1899
+ let workCompleted = false;
1900
+ let workThrew = false;
1901
+ let workError;
1902
+ try {
1903
+ return await withCommit(
1904
+ {
1905
+ outbox: this.deps.outbox,
1906
+ bus: this.deps.bus,
1907
+ scope: this.deps.scope,
1908
+ onPublishError: this.deps.onPublishError
1909
+ },
1910
+ async (tx) => {
1911
+ session?.close();
1912
+ const s = new Session();
1913
+ session = s;
1914
+ workCompleted = false;
1915
+ workThrew = false;
1916
+ workError = void 0;
1917
+ const repositories = this.buildRepositories(tx, s);
1918
+ const context = makeContext(repositories, tx, s);
1919
+ try {
1920
+ const result = await work(context);
1921
+ workCompleted = true;
1922
+ const aggregates = s.enrolledAggregates;
1923
+ const deleted = s.deletedAggregates;
1924
+ s.close();
1925
+ return { result, aggregates, deleted };
1926
+ } catch (error) {
1927
+ workThrew = true;
1928
+ workError = error;
1929
+ throw error;
1930
+ }
1931
+ }
1932
+ );
1933
+ } catch (error) {
1934
+ if (workThrew) {
1935
+ if (error === workError || causeChainContains(error, workError)) {
1936
+ throw error;
1937
+ }
1938
+ throw new RollbackError(workError, error);
1939
+ }
1940
+ if (workCompleted) {
1941
+ throw new CommitError(error);
1942
+ }
1943
+ throw error;
1944
+ } finally {
1945
+ session?.close();
1946
+ this._active = false;
1947
+ }
1948
+ }
1949
+ buildRepositories(tx, session) {
1950
+ const repositories = {};
1951
+ for (const key of Object.keys(this.deps.repositories)) {
1952
+ repositories[key] = this.deps.repositories[key](tx, session);
1953
+ }
1954
+ return repositories;
1955
+ }
1956
+ };
1957
+ var Session = class {
1958
+ static {
1959
+ __name(this, "Session");
1960
+ }
1961
+ // Insertion-ordered: harvest order = enrollment order (withCommit
1962
+ // then preserves per-aggregate emission order).
1963
+ _enrolled = /* @__PURE__ */ new Set();
1964
+ _deleted = /* @__PURE__ */ new Set();
1965
+ _identityMap = new IdentityMap();
1966
+ _closed = false;
1967
+ get identityMap() {
1968
+ this.assertOpen("session.identityMap");
1969
+ return this._identityMap;
1970
+ }
1971
+ enrollSaved(aggregate) {
1972
+ this.assertOpen("session.enrollSaved");
1973
+ if (this._deleted.has(aggregate) || this._identityMap.isDeleted(
1974
+ aggregate.constructor,
1975
+ aggregate.id
1976
+ )) {
1977
+ throw new AggregateDeletedError(String(aggregate.id));
1978
+ }
1979
+ this._enrolled.add(aggregate);
1980
+ }
1981
+ enrollDeleted(aggregate) {
1982
+ this.assertOpen("session.enrollDeleted");
1983
+ this._deleted.add(aggregate);
1984
+ this._identityMap.delete(
1985
+ aggregate.constructor,
1986
+ aggregate.id
1987
+ );
1988
+ this._enrolled.add(aggregate);
1989
+ }
1990
+ get enrolledAggregates() {
1991
+ return [...this._enrolled];
1992
+ }
1993
+ get deletedAggregates() {
1994
+ return [...this._deleted];
1995
+ }
1996
+ close() {
1997
+ this._closed = true;
1998
+ this._identityMap.clear();
1999
+ }
2000
+ assertOpen(operation) {
2001
+ if (this._closed) {
2002
+ throw new TransactionClosedError(operation);
2003
+ }
2004
+ }
2005
+ };
2006
+ function makeContext(repositories, transaction, session) {
2007
+ return {
2008
+ get repositories() {
2009
+ session.assertOpen("context.repositories");
2010
+ return repositories;
2011
+ },
2012
+ get rawTransaction() {
2013
+ session.assertOpen("context.rawTransaction");
2014
+ return transaction;
2015
+ },
2016
+ session
2017
+ };
2018
+ }
2019
+ __name(makeContext, "makeContext");
2020
+ function causeChainContains(error, target) {
2021
+ if (target === void 0 || target === null) {
2022
+ return false;
2023
+ }
2024
+ const seen = /* @__PURE__ */ new Set();
2025
+ let current = error;
2026
+ while (current !== null && typeof current === "object" && !seen.has(current)) {
2027
+ seen.add(current);
2028
+ let next;
2029
+ try {
2030
+ next = current.cause;
2031
+ } catch {
2032
+ return false;
2033
+ }
2034
+ if (next === target) {
2035
+ return true;
2036
+ }
2037
+ current = next;
2038
+ }
2039
+ return false;
2040
+ }
2041
+ __name(causeChainContains, "causeChainContains");
1106
2042
  var QueryBus = class {
1107
2043
  static {
1108
2044
  __name(this, "QueryBus");
1109
2045
  }
1110
2046
  handlers = /* @__PURE__ */ new Map();
1111
2047
  register(queryType, handler) {
2048
+ if (this.handlers.has(queryType)) {
2049
+ throw new Error(
2050
+ `QueryBus: a handler for query type "${queryType}" is already registered`
2051
+ );
2052
+ }
1112
2053
  this.handlers.set(queryType, (query) => handler(query));
1113
2054
  }
1114
2055
  async execute(query) {
@@ -1120,9 +2061,7 @@ var QueryBus = class {
1120
2061
  const result = await handler(query);
1121
2062
  return ok(result);
1122
2063
  } catch (error) {
1123
- return err(
1124
- error instanceof Error ? error.message : String(error)
1125
- );
2064
+ return err(describeThrown(error));
1126
2065
  }
1127
2066
  }
1128
2067
  async executeUnsafe(query) {
@@ -1218,12 +2157,19 @@ var EventBusImpl = class {
1218
2157
  const handlersForType = this.handlers.get(event.type);
1219
2158
  if (handlersForType) {
1220
2159
  const results = await Promise.allSettled(
1221
- handlersForType.slice().map((handler) => handler(event))
2160
+ handlersForType.slice().map(async (handler) => handler(event))
1222
2161
  );
1223
2162
  for (const result of results) {
1224
2163
  if (result.status === "rejected") {
1225
2164
  errors.push(
1226
- result.reason instanceof Error ? result.reason : new Error(String(result.reason))
2165
+ result.reason instanceof Error ? result.reason : (
2166
+ // Attach the raw reason as cause: a handler
2167
+ // rejecting with a structured payload must stay
2168
+ // diagnosable, not collapse to '[object Object]'.
2169
+ new Error(String(result.reason), {
2170
+ cause: result.reason
2171
+ })
2172
+ )
1227
2173
  );
1228
2174
  }
1229
2175
  }
@@ -1254,7 +2200,8 @@ var InMemoryOutbox = class {
1254
2200
  }
1255
2201
  async getPending(limit) {
1256
2202
  const all = [...this.pending.values()];
1257
- return typeof limit === "number" ? all.slice(0, limit) : all;
2203
+ if (typeof limit !== "number") return all;
2204
+ return all.slice(0, Math.max(0, limit));
1258
2205
  }
1259
2206
  async markDispatched(dispatchIds) {
1260
2207
  for (const id of dispatchIds) this.pending.delete(id);
@@ -1267,6 +2214,6 @@ function voValidated(t, validate, message = "Validation failed") {
1267
2214
  }
1268
2215
  __name(voValidated, "voValidated");
1269
2216
 
1270
- 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, voValidated, voWithValidation, withClockFactory, withCommit, withEventIdFactory };
2217
+ export { AggregateDeletedError, AggregateNotFoundError, AggregateRoot, CommandBus, CommitError, ConcurrencyConflictError, DomainError, DuplicateAggregateError, Entity, EventBusImpl, EventSourcedAggregate, IdentityMap, InMemoryOutbox, InfrastructureError, MissingHandlerError, NestedUnitOfWorkError, QueryBus, RollbackError, TransactionClosedError, UnitOfWork, 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, voValidated, voWithValidation, withClockFactory, withCommit, withEventIdFactory };
1271
2218
  //# sourceMappingURL=index.js.map
1272
2219
  //# sourceMappingURL=index.js.map