@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/README.md +21 -18
- package/dist/aggregate-DclYgG_D.d.ts +662 -0
- package/dist/http.d.ts +2 -2
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +715 -652
- package/dist/index.js +1059 -112
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +251 -0
- package/dist/testing.js +793 -0
- package/dist/testing.js.map +1 -0
- package/dist/utils.d.ts +16 -4
- package/dist/utils.js +158 -53
- package/dist/utils.js.map +1 -1
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
|
213
|
+
return sameValueZero(
|
|
214
|
+
objA.valueOf(),
|
|
215
|
+
objB.valueOf()
|
|
216
|
+
);
|
|
132
217
|
}
|
|
133
218
|
default: {
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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(
|
|
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(
|
|
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: ${
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
757
|
-
* audit logs
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
877
|
-
this.validateState(
|
|
878
|
-
this._state = freezeShallow(
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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(
|
|
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(...)
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 :
|
|
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
|
-
|
|
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
|