@shirudo/ddd-kit 1.0.0 → 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.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 \u2014 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)} \u2014 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 — 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,7 +701,9 @@ 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,
457
708
  metadata: options?.metadata
458
709
  };
@@ -474,7 +725,21 @@ function copyMetadata(sourceEvent, additionalMetadata) {
474
725
  }
475
726
  __name(copyMetadata, "copyMetadata");
476
727
  function mergeMetadata(...metadataObjects) {
477
- return Object.assign({}, ...metadataObjects.filter(Boolean));
728
+ const merged = {};
729
+ for (const metadata of metadataObjects) {
730
+ if (!metadata) continue;
731
+ for (const key of Reflect.ownKeys(metadata)) {
732
+ const descriptor = Object.getOwnPropertyDescriptor(metadata, key);
733
+ if (!descriptor?.enumerable) continue;
734
+ Object.defineProperty(merged, key, {
735
+ value: metadata[key],
736
+ writable: true,
737
+ enumerable: true,
738
+ configurable: true
739
+ });
740
+ }
741
+ }
742
+ return merged;
478
743
  }
479
744
  __name(mergeMetadata, "mergeMetadata");
480
745
 
@@ -510,12 +775,20 @@ var Entity = class {
510
775
  * Subclasses can mutate this directly or use helper methods.
511
776
  */
512
777
  _state;
778
+ /**
779
+ * **State ownership.** Plain-object and array states are shallow-copied
780
+ * before the freeze, so the caller's own object stays mutable. A CLASS
781
+ * INSTANCE passed as state is an ownership transfer: it is frozen
782
+ * in place (a copy would strip its prototype) — do not keep mutating
783
+ * the instance after handing it to the entity. The same contract
784
+ * applies to {@link setState}.
785
+ */
513
786
  constructor(id, initialState) {
514
787
  if (id === null || id === void 0) {
515
788
  throw new Error("Entity ID cannot be null or undefined");
516
789
  }
517
790
  this.id = id;
518
- this._state = freezeShallow(initialState);
791
+ this._state = freezeShallow(shallowCopyOwned(initialState));
519
792
  this.validateState(this._state);
520
793
  }
521
794
  /**
@@ -546,11 +819,15 @@ var Entity = class {
546
819
  * This is a convenience method for state mutations.
547
820
  * Automatically validates the newState using `validateState()`.
548
821
  *
822
+ * Plain-object and array states are shallow-copied before the freeze
823
+ * (the caller's object stays mutable); a class-instance state is an
824
+ * ownership transfer and is frozen in place — see the constructor.
825
+ *
549
826
  * @param newState - The new state
550
827
  */
551
828
  setState(newState) {
552
829
  this.validateState(newState);
553
- this._state = freezeShallow(newState);
830
+ this._state = freezeShallow(shallowCopyOwned(newState));
554
831
  }
555
832
  };
556
833
  function freezeShallow(value) {
@@ -560,6 +837,14 @@ function freezeShallow(value) {
560
837
  return value;
561
838
  }
562
839
  __name(freezeShallow, "freezeShallow");
840
+ function shallowCopyOwned(value) {
841
+ if (value === null || typeof value !== "object") return value;
842
+ if (Array.isArray(value)) return [...value];
843
+ const proto = Object.getPrototypeOf(value);
844
+ if (proto !== Object.prototype && proto !== null) return value;
845
+ return Object.assign(Object.create(proto), value);
846
+ }
847
+ __name(shallowCopyOwned, "shallowCopyOwned");
563
848
  function sameEntity(a, b) {
564
849
  return a.id === b.id;
565
850
  }
@@ -701,6 +986,12 @@ var BaseAggregate = class extends Entity {
701
986
  * call `super.onPersisted(version)` — there is nothing in the parent
702
987
  * implementation to preserve.
703
988
  *
989
+ * **Observer contract: errors are swallowed.** `withCommit` invokes
990
+ * `markPersisted` after the transaction has committed; a throwing hook
991
+ * must neither abort the loop for peer aggregates nor make the
992
+ * committed write look failed, so `withCommit` catches and discards
993
+ * hook errors. Handle failures inside the hook if you need them.
994
+ *
704
995
  * **`onPersisted` deliberately receives only the version, not the
705
996
  * drained events.** Event-driven post-persist logic (aggregate-level
706
997
  * audit logging, per-event-type side effects) belongs in `EventBus`
@@ -739,14 +1030,46 @@ var BaseAggregate = class extends Entity {
739
1030
  * Creates a snapshot of the current aggregate state — the state at
740
1031
  * this moment plus the version. Useful for ES snapshot policies and
741
1032
  * for state-stored backup / restore.
1033
+ *
1034
+ * The state is converted via {@link toSnapshotState}; the default
1035
+ * requires plain, serialisable data and fails fast otherwise.
742
1036
  */
743
1037
  createSnapshot() {
744
1038
  return {
745
- state: structuredClone(this._state),
1039
+ state: this.toSnapshotState(this._state),
746
1040
  version: this.version,
747
1041
  snapshotAt: /* @__PURE__ */ new Date()
748
1042
  };
749
1043
  }
1044
+ /**
1045
+ * Converts live aggregate state into the plain-data shape stored in a
1046
+ * snapshot. The default validates that the state graph is plain,
1047
+ * serialisable data (no class instances, functions, Promise/WeakMap/
1048
+ * WeakSet) and then `structuredClone`s it — class instances would
1049
+ * silently lose their prototype here AND on every snapshot-store
1050
+ * round-trip, so the default fails fast with the offending path
1051
+ * instead of producing a snapshot that breaks on first method call
1052
+ * after restore.
1053
+ *
1054
+ * Override this together with {@link fromSnapshotState} (and the
1055
+ * `TSnapshotState` generic) when the state carries class-based child
1056
+ * entities. The override owns isolation: return fresh objects, not
1057
+ * references into live state.
1058
+ */
1059
+ toSnapshotState(state) {
1060
+ assertSnapshotSafe(state, "", /* @__PURE__ */ new WeakSet());
1061
+ return structuredClone(state);
1062
+ }
1063
+ /**
1064
+ * Converts the plain-data snapshot shape back into live aggregate
1065
+ * state. The default `structuredClone`s the stored state so the
1066
+ * restored aggregate never aliases the snapshot object. Override
1067
+ * together with {@link toSnapshotState} to reconstruct class-based
1068
+ * child entities.
1069
+ */
1070
+ fromSnapshotState(stored) {
1071
+ return structuredClone(stored);
1072
+ }
750
1073
  /**
751
1074
  * Sugar for `createDomainEvent` that auto-injects `aggregateId`
752
1075
  * (from `this.id`) and `aggregateType` (from {@link aggregateType})
@@ -789,6 +1112,77 @@ var BaseAggregate = class extends Entity {
789
1112
  });
790
1113
  }
791
1114
  };
1115
+ function assertSnapshotSafe(value, path, seen) {
1116
+ if (typeof value === "function") {
1117
+ throw new Error(
1118
+ `createSnapshot: state${path} is a function \u2014 snapshot state must be plain, serialisable data. Override toSnapshotState()/fromSnapshotState() to map it.`
1119
+ );
1120
+ }
1121
+ if (value === null || typeof value !== "object") return;
1122
+ const obj = value;
1123
+ if (seen.has(obj)) return;
1124
+ seen.add(obj);
1125
+ if (Array.isArray(obj)) {
1126
+ for (let i = 0; i < obj.length; i++) {
1127
+ assertSnapshotSafe(obj[i], `${path}[${i}]`, seen);
1128
+ }
1129
+ return;
1130
+ }
1131
+ const tag = Object.prototype.toString.call(obj);
1132
+ if (isBuiltInObject(obj, tag)) {
1133
+ if (tag === "[object Map]") {
1134
+ let i = 0;
1135
+ for (const [key, entryValue] of obj) {
1136
+ assertSnapshotSafe(key, `${path}<map key #${i}>`, seen);
1137
+ assertSnapshotSafe(entryValue, `${path}<map value #${i}>`, seen);
1138
+ i++;
1139
+ }
1140
+ return;
1141
+ }
1142
+ if (tag === "[object Set]") {
1143
+ let i = 0;
1144
+ for (const member of obj) {
1145
+ assertSnapshotSafe(member, `${path}<set member #${i}>`, seen);
1146
+ i++;
1147
+ }
1148
+ return;
1149
+ }
1150
+ if (tag === "[object Promise]" || tag === "[object WeakMap]" || tag === "[object WeakSet]") {
1151
+ throw new Error(
1152
+ `createSnapshot: state${path} is a ${tag.slice(8, -1)} \u2014 it cannot be cloned or persisted. Override toSnapshotState()/fromSnapshotState() to map it.`
1153
+ );
1154
+ }
1155
+ if (tag === "[object Error]") {
1156
+ throw new Error(
1157
+ `createSnapshot: state${path} is an Error \u2014 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.`
1158
+ );
1159
+ }
1160
+ return;
1161
+ }
1162
+ const proto = Object.getPrototypeOf(obj);
1163
+ if (proto === Object.prototype || proto === null) {
1164
+ for (const key of Reflect.ownKeys(obj)) {
1165
+ const descriptor = Object.getOwnPropertyDescriptor(obj, key);
1166
+ if (!descriptor?.enumerable) continue;
1167
+ if (typeof key === "symbol") {
1168
+ throw new Error(
1169
+ `createSnapshot: state${path} has a symbol-keyed property (${String(key)}) \u2014 structuredClone silently drops symbol keys, so the snapshot would lose state. Override toSnapshotState()/fromSnapshotState() to map it.`
1170
+ );
1171
+ }
1172
+ assertSnapshotSafe(
1173
+ obj[key],
1174
+ `${path}.${key}`,
1175
+ seen
1176
+ );
1177
+ }
1178
+ return;
1179
+ }
1180
+ const name = proto.constructor?.name || "anonymous class";
1181
+ throw new Error(
1182
+ `createSnapshot: state${path} is a class instance (${name}) \u2014 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.`
1183
+ );
1184
+ }
1185
+ __name(assertSnapshotSafe, "assertSnapshotSafe");
792
1186
 
793
1187
  // src/aggregate/aggregate-root.ts
794
1188
  var AggregateRoot = class extends BaseAggregate {
@@ -873,9 +1267,9 @@ var AggregateRoot = class extends BaseAggregate {
873
1267
  * @param snapshot - The snapshot to restore from
874
1268
  */
875
1269
  restoreFromSnapshot(snapshot) {
876
- const cloned = structuredClone(snapshot.state);
877
- this.validateState(cloned);
878
- this._state = freezeShallow(cloned);
1270
+ const restored = this.fromSnapshotState(snapshot.state);
1271
+ this.validateState(restored);
1272
+ this._state = freezeShallow(restored);
879
1273
  this.markRestored(snapshot.version);
880
1274
  }
881
1275
  };
@@ -978,7 +1372,7 @@ var EventSourcedAggregate = class extends BaseAggregate {
978
1372
  */
979
1373
  dispatchAndCommit(event, isNew) {
980
1374
  this.validateEvent(event);
981
- const handler = this.handlers[event.type];
1375
+ const handler = Object.hasOwn(this.handlers, event.type) ? this.handlers[event.type] : void 0;
982
1376
  if (!handler) {
983
1377
  throw new MissingHandlerError(event.type);
984
1378
  }
@@ -995,17 +1389,26 @@ var EventSourcedAggregate = class extends BaseAggregate {
995
1389
  * infrastructure boundary, where event-stream corruption is an expected
996
1390
  * recoverable failure. Unexpected (non-DomainError) throws propagate.
997
1391
  *
1392
+ * All-or-nothing: if any event mid-stream throws, the aggregate's state
1393
+ * is rolled back to its pre-call value — same contract as
1394
+ * `restoreFromSnapshotWithEvents`. Partial replay is never observable.
1395
+ * (Version needs no rollback: replay dispatches with `isNew = false`,
1396
+ * which never bumps it; only the final `markRestored` advances it.)
1397
+ *
998
1398
  * Version advances additively: the aggregate's pre-existing version plus
999
1399
  * `history.length`. A fresh aggregate (v=0) loading 3 events ends at v=3;
1000
1400
  * an aggregate already at v=1 (e.g. after a creation event) loading
1001
1401
  * 2 events ends at v=3, not v=2.
1002
1402
  */
1003
1403
  loadFromHistory(history) {
1404
+ if (history.length === 0) return ok();
1405
+ const previousState = this._state;
1004
1406
  const startVersion = this.version;
1005
1407
  for (const event of history) {
1006
1408
  try {
1007
1409
  this.dispatchAndCommit(event, false);
1008
1410
  } catch (e) {
1411
+ this._state = previousState;
1009
1412
  if (e instanceof DomainError) return err(e);
1010
1413
  throw e;
1011
1414
  }
@@ -1026,7 +1429,7 @@ var EventSourcedAggregate = class extends BaseAggregate {
1026
1429
  restoreFromSnapshotWithEvents(snapshot, eventsAfterSnapshot) {
1027
1430
  const previousState = this._state;
1028
1431
  const previousVersion = this.version;
1029
- this._state = freezeShallow(structuredClone(snapshot.state));
1432
+ this._state = freezeShallow(this.fromSnapshotState(snapshot.state));
1030
1433
  this.setVersion(snapshot.version);
1031
1434
  for (const event of eventsAfterSnapshot) {
1032
1435
  try {
@@ -1044,12 +1447,31 @@ var EventSourcedAggregate = class extends BaseAggregate {
1044
1447
  return ok();
1045
1448
  }
1046
1449
  };
1450
+
1451
+ // src/app/describe-thrown.ts
1452
+ function describeThrown(error) {
1453
+ if (error instanceof Error) return error.message;
1454
+ try {
1455
+ const json = JSON.stringify(error);
1456
+ if (json !== void 0) return json;
1457
+ } catch {
1458
+ }
1459
+ return String(error);
1460
+ }
1461
+ __name(describeThrown, "describeThrown");
1462
+
1463
+ // src/app/command-bus.ts
1047
1464
  var CommandBus = class {
1048
1465
  static {
1049
1466
  __name(this, "CommandBus");
1050
1467
  }
1051
1468
  handlers = /* @__PURE__ */ new Map();
1052
1469
  register(commandType, handler) {
1470
+ if (this.handlers.has(commandType)) {
1471
+ throw new Error(
1472
+ `CommandBus: a handler for command type "${commandType}" is already registered`
1473
+ );
1474
+ }
1053
1475
  this.handlers.set(commandType, (cmd) => handler(cmd));
1054
1476
  }
1055
1477
  async execute(command) {
@@ -1060,9 +1482,7 @@ var CommandBus = class {
1060
1482
  try {
1061
1483
  return await handler(command);
1062
1484
  } catch (error) {
1063
- return err(
1064
- error instanceof Error ? error.message : String(error)
1065
- );
1485
+ return err(describeThrown(error));
1066
1486
  }
1067
1487
  }
1068
1488
  };
@@ -1095,10 +1515,20 @@ async function withCommit(deps, fn) {
1095
1515
  }
1096
1516
  );
1097
1517
  for (const agg of aggregates) {
1098
- agg.markPersisted(agg.version);
1518
+ try {
1519
+ agg.markPersisted(agg.version);
1520
+ } catch {
1521
+ }
1099
1522
  }
1100
1523
  if (deps.bus && events.length > 0) {
1101
- await deps.bus.publish(events);
1524
+ try {
1525
+ await deps.bus.publish(events);
1526
+ } catch (error) {
1527
+ try {
1528
+ deps.onPublishError?.(error, events);
1529
+ } catch {
1530
+ }
1531
+ }
1102
1532
  }
1103
1533
  return result;
1104
1534
  }
@@ -1109,6 +1539,11 @@ var QueryBus = class {
1109
1539
  }
1110
1540
  handlers = /* @__PURE__ */ new Map();
1111
1541
  register(queryType, handler) {
1542
+ if (this.handlers.has(queryType)) {
1543
+ throw new Error(
1544
+ `QueryBus: a handler for query type "${queryType}" is already registered`
1545
+ );
1546
+ }
1112
1547
  this.handlers.set(queryType, (query) => handler(query));
1113
1548
  }
1114
1549
  async execute(query) {
@@ -1120,9 +1555,7 @@ var QueryBus = class {
1120
1555
  const result = await handler(query);
1121
1556
  return ok(result);
1122
1557
  } catch (error) {
1123
- return err(
1124
- error instanceof Error ? error.message : String(error)
1125
- );
1558
+ return err(describeThrown(error));
1126
1559
  }
1127
1560
  }
1128
1561
  async executeUnsafe(query) {
@@ -1218,12 +1651,19 @@ var EventBusImpl = class {
1218
1651
  const handlersForType = this.handlers.get(event.type);
1219
1652
  if (handlersForType) {
1220
1653
  const results = await Promise.allSettled(
1221
- handlersForType.slice().map((handler) => handler(event))
1654
+ handlersForType.slice().map(async (handler) => handler(event))
1222
1655
  );
1223
1656
  for (const result of results) {
1224
1657
  if (result.status === "rejected") {
1225
1658
  errors.push(
1226
- result.reason instanceof Error ? result.reason : new Error(String(result.reason))
1659
+ result.reason instanceof Error ? result.reason : (
1660
+ // Attach the raw reason as cause — a handler
1661
+ // rejecting with a structured payload must stay
1662
+ // diagnosable, not collapse to '[object Object]'.
1663
+ new Error(String(result.reason), {
1664
+ cause: result.reason
1665
+ })
1666
+ )
1227
1667
  );
1228
1668
  }
1229
1669
  }
@@ -1254,7 +1694,8 @@ var InMemoryOutbox = class {
1254
1694
  }
1255
1695
  async getPending(limit) {
1256
1696
  const all = [...this.pending.values()];
1257
- return typeof limit === "number" ? all.slice(0, limit) : all;
1697
+ if (typeof limit !== "number") return all;
1698
+ return all.slice(0, Math.max(0, limit));
1258
1699
  }
1259
1700
  async markDispatched(dispatchIds) {
1260
1701
  for (const id of dispatchIds) this.pending.delete(id);