@shirudo/ddd-kit 1.0.0-rc.1 → 1.0.0-rc.4
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 +94 -143
- package/dist/index.d.ts +858 -186
- package/dist/index.js +655 -381
- package/dist/index.js.map +1 -1
- package/dist/utils.d.ts +92 -1
- package/dist/utils.js +281 -0
- package/dist/utils.js.map +1 -1
- package/package.json +71 -65
- package/dist/deep-equal-except-C8yoSk4L.d.ts +0 -57
- package/dist/result-jCwPSjFa.d.ts +0 -352
- package/dist/result.d.ts +0 -204
- package/dist/result.js +0 -298
- package/dist/result.js.map +0 -1
- package/dist/utils-array.d.ts +0 -47
- package/dist/utils-array.js +0 -242
- package/dist/utils-array.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,85 +1,30 @@
|
|
|
1
|
+
import { err, ok } from '@shirudo/result';
|
|
2
|
+
import { BaseError } from '@shirudo/base-error';
|
|
3
|
+
|
|
1
4
|
var __defProp = Object.defineProperty;
|
|
2
5
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
6
|
|
|
4
|
-
// src/aggregate/domain-event.ts
|
|
5
|
-
function createDomainEvent(type, payload, options) {
|
|
6
|
-
return {
|
|
7
|
-
type,
|
|
8
|
-
payload,
|
|
9
|
-
occurredAt: options?.occurredAt ?? /* @__PURE__ */ new Date(),
|
|
10
|
-
version: options?.version ?? 1,
|
|
11
|
-
metadata: options?.metadata
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
__name(createDomainEvent, "createDomainEvent");
|
|
15
|
-
function createDomainEventWithMetadata(type, payload, metadata, options) {
|
|
16
|
-
return createDomainEvent(type, payload, {
|
|
17
|
-
...options,
|
|
18
|
-
metadata
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
__name(createDomainEventWithMetadata, "createDomainEventWithMetadata");
|
|
22
|
-
function copyMetadata(sourceEvent, additionalMetadata) {
|
|
23
|
-
return {
|
|
24
|
-
...sourceEvent.metadata ?? {},
|
|
25
|
-
...additionalMetadata ?? {}
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
__name(copyMetadata, "copyMetadata");
|
|
29
|
-
function mergeMetadata(...metadataObjects) {
|
|
30
|
-
return Object.assign({}, ...metadataObjects.filter(Boolean));
|
|
31
|
-
}
|
|
32
|
-
__name(mergeMetadata, "mergeMetadata");
|
|
33
|
-
|
|
34
|
-
// src/aggregate/aggregate.ts
|
|
35
|
-
function aggregate(state, version = 0) {
|
|
36
|
-
return { state, version };
|
|
37
|
-
}
|
|
38
|
-
__name(aggregate, "aggregate");
|
|
39
|
-
function bump(agg) {
|
|
40
|
-
return { ...agg, version: agg.version + 1 };
|
|
41
|
-
}
|
|
42
|
-
__name(bump, "bump");
|
|
43
|
-
function sameVersion(a, b) {
|
|
44
|
-
return a.id === b.id && a.version === b.version;
|
|
45
|
-
}
|
|
46
|
-
__name(sameVersion, "sameVersion");
|
|
47
|
-
|
|
48
7
|
// src/utils/array/is-built-in.ts
|
|
8
|
+
var BUILT_IN_TAGS = /* @__PURE__ */ new Set([
|
|
9
|
+
"[object Date]",
|
|
10
|
+
"[object RegExp]",
|
|
11
|
+
"[object Map]",
|
|
12
|
+
"[object Set]",
|
|
13
|
+
"[object WeakMap]",
|
|
14
|
+
"[object WeakSet]",
|
|
15
|
+
"[object Promise]",
|
|
16
|
+
"[object Error]",
|
|
17
|
+
"[object Boolean]",
|
|
18
|
+
"[object Number]",
|
|
19
|
+
"[object String]",
|
|
20
|
+
"[object ArrayBuffer]",
|
|
21
|
+
"[object SharedArrayBuffer]",
|
|
22
|
+
"[object DataView]"
|
|
23
|
+
]);
|
|
49
24
|
function isBuiltInObject(obj, tag) {
|
|
50
|
-
if (tag.endsWith("Array]"))
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (ArrayBuffer.isView(obj)) {
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
if (tag === "[object ArrayBuffer]" || tag === "[object SharedArrayBuffer]") {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
const objConstructor = obj.constructor;
|
|
60
|
-
if (objConstructor && typeof objConstructor === "function") {
|
|
61
|
-
const constructorName = objConstructor.name;
|
|
62
|
-
if (constructorName && typeof globalThis !== "undefined" && constructorName in globalThis && globalThis[constructorName] === objConstructor) {
|
|
63
|
-
const proto = Object.getPrototypeOf(obj);
|
|
64
|
-
if (proto !== Object.prototype && proto !== null) {
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const knownBuiltInTags = /* @__PURE__ */ new Set([
|
|
70
|
-
"[object Date]",
|
|
71
|
-
"[object RegExp]",
|
|
72
|
-
"[object Map]",
|
|
73
|
-
"[object Set]",
|
|
74
|
-
"[object WeakMap]",
|
|
75
|
-
"[object WeakSet]",
|
|
76
|
-
"[object Promise]",
|
|
77
|
-
"[object Error]",
|
|
78
|
-
"[object Boolean]",
|
|
79
|
-
"[object Number]",
|
|
80
|
-
"[object String]"
|
|
81
|
-
]);
|
|
82
|
-
return knownBuiltInTags.has(tag);
|
|
25
|
+
if (tag.endsWith("Array]")) return true;
|
|
26
|
+
if (ArrayBuffer.isView(obj)) return true;
|
|
27
|
+
return BUILT_IN_TAGS.has(tag);
|
|
83
28
|
}
|
|
84
29
|
__name(isBuiltInObject, "isBuiltInObject");
|
|
85
30
|
|
|
@@ -103,11 +48,15 @@ function deepEqualInner(a, b, visited) {
|
|
|
103
48
|
}
|
|
104
49
|
const objA = a;
|
|
105
50
|
const objB = b;
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
return
|
|
51
|
+
let cachedBs = visited.get(objA);
|
|
52
|
+
if (cachedBs?.has(objB)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (!cachedBs) {
|
|
56
|
+
cachedBs = /* @__PURE__ */ new WeakSet();
|
|
57
|
+
visited.set(objA, cachedBs);
|
|
109
58
|
}
|
|
110
|
-
|
|
59
|
+
cachedBs.add(objB);
|
|
111
60
|
if (ArrayBuffer.isView(objA) || ArrayBuffer.isView(objB)) {
|
|
112
61
|
if (!ArrayBuffer.isView(objA) || !ArrayBuffer.isView(objB)) return false;
|
|
113
62
|
const tagA2 = objToString.call(objA);
|
|
@@ -185,25 +134,28 @@ function deepEqualInner(a, b, visited) {
|
|
|
185
134
|
if (isBuiltInObject(objA, tagA) && isBuiltInObject(objB, tagB)) {
|
|
186
135
|
return objA === objB;
|
|
187
136
|
}
|
|
137
|
+
const recA = objA;
|
|
138
|
+
const recB = objB;
|
|
188
139
|
const stringKeysA = Object.keys(objA);
|
|
189
140
|
const stringKeysB = Object.keys(objB);
|
|
141
|
+
if (stringKeysA.length !== stringKeysB.length) return false;
|
|
190
142
|
const symbolKeysA = Object.getOwnPropertySymbols(objA);
|
|
191
143
|
const symbolKeysB = Object.getOwnPropertySymbols(objB);
|
|
192
|
-
if (stringKeysA.length !== stringKeysB.length) return false;
|
|
193
144
|
if (symbolKeysA.length !== symbolKeysB.length) return false;
|
|
145
|
+
const symbolKeysBSet = new Set(symbolKeysB);
|
|
194
146
|
for (const key of stringKeysA) {
|
|
195
147
|
if (!objHasOwn.call(objB, key)) return false;
|
|
196
148
|
}
|
|
197
149
|
for (const key of symbolKeysA) {
|
|
198
|
-
if (!
|
|
150
|
+
if (!symbolKeysBSet.has(key)) return false;
|
|
199
151
|
}
|
|
200
152
|
for (const key of stringKeysA) {
|
|
201
|
-
if (!deepEqualInner(
|
|
153
|
+
if (!deepEqualInner(recA[key], recB[key], visited)) {
|
|
202
154
|
return false;
|
|
203
155
|
}
|
|
204
156
|
}
|
|
205
157
|
for (const key of symbolKeysA) {
|
|
206
|
-
if (!deepEqualInner(
|
|
158
|
+
if (!deepEqualInner(recA[key], recB[key], visited)) {
|
|
207
159
|
return false;
|
|
208
160
|
}
|
|
209
161
|
}
|
|
@@ -213,6 +165,293 @@ function deepEqualInner(a, b, visited) {
|
|
|
213
165
|
}
|
|
214
166
|
__name(deepEqualInner, "deepEqualInner");
|
|
215
167
|
|
|
168
|
+
// src/utils/array/deep-omit.ts
|
|
169
|
+
function deepOmit(value, options) {
|
|
170
|
+
const visited = /* @__PURE__ */ new WeakMap();
|
|
171
|
+
const ignoreKeys = options.ignoreKeys ? new Set(options.ignoreKeys) : void 0;
|
|
172
|
+
return omitInternal(value, options, ignoreKeys, [], visited);
|
|
173
|
+
}
|
|
174
|
+
__name(deepOmit, "deepOmit");
|
|
175
|
+
function omitInternal(value, options, ignoreKeys, path, visited) {
|
|
176
|
+
if (value === null) return value;
|
|
177
|
+
if (typeof value !== "object") return value;
|
|
178
|
+
const obj = value;
|
|
179
|
+
if (visited.has(obj)) {
|
|
180
|
+
return visited.get(obj);
|
|
181
|
+
}
|
|
182
|
+
const tag = Object.prototype.toString.call(obj);
|
|
183
|
+
if (tag === "[object Array]") {
|
|
184
|
+
const arr = obj;
|
|
185
|
+
const clone2 = new Array(arr.length);
|
|
186
|
+
visited.set(obj, clone2);
|
|
187
|
+
for (let i = 0; i < arr.length; i++) {
|
|
188
|
+
path.push(i);
|
|
189
|
+
clone2[i] = omitInternal(arr[i], options, ignoreKeys, path, visited);
|
|
190
|
+
path.pop();
|
|
191
|
+
}
|
|
192
|
+
return clone2;
|
|
193
|
+
}
|
|
194
|
+
if (isBuiltInObject(obj, tag)) {
|
|
195
|
+
const builtInClone = cloneBuiltIn(obj, tag);
|
|
196
|
+
visited.set(obj, builtInClone);
|
|
197
|
+
return builtInClone;
|
|
198
|
+
}
|
|
199
|
+
const clone = Object.create(Object.getPrototypeOf(obj));
|
|
200
|
+
visited.set(obj, clone);
|
|
201
|
+
const stringKeys = Object.keys(obj);
|
|
202
|
+
const symbolKeys = Object.getOwnPropertySymbols(obj);
|
|
203
|
+
for (const key of stringKeys) {
|
|
204
|
+
if (shouldIgnoreKey(key, path, ignoreKeys, options)) continue;
|
|
205
|
+
path.push(key);
|
|
206
|
+
assignOwn(
|
|
207
|
+
clone,
|
|
208
|
+
key,
|
|
209
|
+
omitInternal(
|
|
210
|
+
obj[key],
|
|
211
|
+
options,
|
|
212
|
+
ignoreKeys,
|
|
213
|
+
path,
|
|
214
|
+
visited
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
path.pop();
|
|
218
|
+
}
|
|
219
|
+
for (const key of symbolKeys) {
|
|
220
|
+
if (shouldIgnoreKey(key, path, ignoreKeys, options)) continue;
|
|
221
|
+
path.push(key);
|
|
222
|
+
assignOwn(
|
|
223
|
+
clone,
|
|
224
|
+
key,
|
|
225
|
+
omitInternal(
|
|
226
|
+
obj[key],
|
|
227
|
+
options,
|
|
228
|
+
ignoreKeys,
|
|
229
|
+
path,
|
|
230
|
+
visited
|
|
231
|
+
)
|
|
232
|
+
);
|
|
233
|
+
path.pop();
|
|
234
|
+
}
|
|
235
|
+
return clone;
|
|
236
|
+
}
|
|
237
|
+
__name(omitInternal, "omitInternal");
|
|
238
|
+
function assignOwn(target, key, value) {
|
|
239
|
+
Object.defineProperty(target, key, {
|
|
240
|
+
value,
|
|
241
|
+
writable: true,
|
|
242
|
+
enumerable: true,
|
|
243
|
+
configurable: true
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
__name(assignOwn, "assignOwn");
|
|
247
|
+
function cloneBuiltIn(obj, tag) {
|
|
248
|
+
switch (tag) {
|
|
249
|
+
case "[object Date]":
|
|
250
|
+
return new Date(obj.getTime());
|
|
251
|
+
case "[object RegExp]": {
|
|
252
|
+
const re = obj;
|
|
253
|
+
const copy = new RegExp(re.source, re.flags);
|
|
254
|
+
copy.lastIndex = re.lastIndex;
|
|
255
|
+
return copy;
|
|
256
|
+
}
|
|
257
|
+
case "[object Map]": {
|
|
258
|
+
const m = obj;
|
|
259
|
+
return new Map(m);
|
|
260
|
+
}
|
|
261
|
+
case "[object Set]": {
|
|
262
|
+
const s = obj;
|
|
263
|
+
return new Set(s);
|
|
264
|
+
}
|
|
265
|
+
default:
|
|
266
|
+
return structuredClone(obj);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
__name(cloneBuiltIn, "cloneBuiltIn");
|
|
270
|
+
function shouldIgnoreKey(key, path, ignoreKeys, options) {
|
|
271
|
+
if (ignoreKeys?.has(key)) return true;
|
|
272
|
+
if (options.ignoreKeyPredicate?.(key, path)) return true;
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
__name(shouldIgnoreKey, "shouldIgnoreKey");
|
|
276
|
+
|
|
277
|
+
// src/utils/array/deep-equal-except.ts
|
|
278
|
+
function deepEqualExcept(a, b, options) {
|
|
279
|
+
const prunedA = deepOmit(a, options);
|
|
280
|
+
const prunedB = deepOmit(b, options);
|
|
281
|
+
return deepEqual(prunedA, prunedB);
|
|
282
|
+
}
|
|
283
|
+
__name(deepEqualExcept, "deepEqualExcept");
|
|
284
|
+
function deepFreeze(obj, visited = /* @__PURE__ */ new WeakSet()) {
|
|
285
|
+
if (obj === null || typeof obj !== "object") {
|
|
286
|
+
return obj;
|
|
287
|
+
}
|
|
288
|
+
if (visited.has(obj)) {
|
|
289
|
+
return obj;
|
|
290
|
+
}
|
|
291
|
+
visited.add(obj);
|
|
292
|
+
const keys = Reflect.ownKeys(obj);
|
|
293
|
+
for (const key of keys) {
|
|
294
|
+
const value = obj[key];
|
|
295
|
+
if (value !== null && typeof value === "object") {
|
|
296
|
+
deepFreeze(value, visited);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return Object.freeze(obj);
|
|
300
|
+
}
|
|
301
|
+
__name(deepFreeze, "deepFreeze");
|
|
302
|
+
function vo(t) {
|
|
303
|
+
return deepFreeze(structuredClone(t));
|
|
304
|
+
}
|
|
305
|
+
__name(vo, "vo");
|
|
306
|
+
function voEquals(a, b) {
|
|
307
|
+
return deepEqual(a, b);
|
|
308
|
+
}
|
|
309
|
+
__name(voEquals, "voEquals");
|
|
310
|
+
function voEqualsExcept(a, b, options) {
|
|
311
|
+
return deepEqualExcept(a, b, options);
|
|
312
|
+
}
|
|
313
|
+
__name(voEqualsExcept, "voEqualsExcept");
|
|
314
|
+
function voWithValidation(t, validate, errorMessage) {
|
|
315
|
+
if (!validate(t)) {
|
|
316
|
+
return err(
|
|
317
|
+
errorMessage ?? `Validation failed for value object: ${JSON.stringify(t)}`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return ok(vo(t));
|
|
321
|
+
}
|
|
322
|
+
__name(voWithValidation, "voWithValidation");
|
|
323
|
+
var ValueObject = class {
|
|
324
|
+
static {
|
|
325
|
+
__name(this, "ValueObject");
|
|
326
|
+
}
|
|
327
|
+
props;
|
|
328
|
+
/**
|
|
329
|
+
* Creates a new ValueObject.
|
|
330
|
+
* The properties are deeply frozen to ensure immutability.
|
|
331
|
+
*
|
|
332
|
+
* @param props - The properties of the value object
|
|
333
|
+
* @example
|
|
334
|
+
* ```ts
|
|
335
|
+
* class Money extends ValueObject<{ amount: number; currency: string }> {
|
|
336
|
+
* constructor(props: { amount: number; currency: string }) {
|
|
337
|
+
* super(props);
|
|
338
|
+
* }
|
|
339
|
+
*
|
|
340
|
+
* protected validate(props: { amount: number; currency: string }): void {
|
|
341
|
+
* if (props.amount < 0) throw new Error("Amount cannot be negative");
|
|
342
|
+
* }
|
|
343
|
+
* }
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
constructor(props) {
|
|
347
|
+
this.validate(props);
|
|
348
|
+
this.props = deepFreeze({ ...props });
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Optional validation hook that can be overridden by subclasses.
|
|
352
|
+
* Should throw an error if validation fails.
|
|
353
|
+
*
|
|
354
|
+
* @param props - The properties to validate
|
|
355
|
+
* @throws Error if validation fails
|
|
356
|
+
*/
|
|
357
|
+
validate(props) {
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Checks if this value object is equal to another.
|
|
361
|
+
* Uses deep equality comparison on the properties and checks for constructor equality.
|
|
362
|
+
*
|
|
363
|
+
* @param other - The other value object to compare
|
|
364
|
+
* @returns true if the properties are deeply equal and constructors match
|
|
365
|
+
*/
|
|
366
|
+
equals(other) {
|
|
367
|
+
if (other === null || other === void 0) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
if (this.constructor !== other.constructor) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
return deepEqual(this.props, other.props);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Creates a clone of the value object with optional property overrides.
|
|
377
|
+
*
|
|
378
|
+
* @param props - Optional properties to override
|
|
379
|
+
* @returns A new instance of the value object
|
|
380
|
+
*/
|
|
381
|
+
clone(props) {
|
|
382
|
+
const Constructor = this.constructor;
|
|
383
|
+
return new Constructor({ ...this.props, ...props || {} });
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Serializes the value object to its raw properties for JSON operations.
|
|
387
|
+
*
|
|
388
|
+
* @returns The raw properties object
|
|
389
|
+
*/
|
|
390
|
+
toJSON() {
|
|
391
|
+
return this.props;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/aggregate/domain-event.ts
|
|
396
|
+
var defaultEventIdFactory = /* @__PURE__ */ __name(() => crypto.randomUUID(), "defaultEventIdFactory");
|
|
397
|
+
var currentEventIdFactory = defaultEventIdFactory;
|
|
398
|
+
function setEventIdFactory(factory) {
|
|
399
|
+
currentEventIdFactory = factory;
|
|
400
|
+
}
|
|
401
|
+
__name(setEventIdFactory, "setEventIdFactory");
|
|
402
|
+
function resetEventIdFactory() {
|
|
403
|
+
currentEventIdFactory = defaultEventIdFactory;
|
|
404
|
+
}
|
|
405
|
+
__name(resetEventIdFactory, "resetEventIdFactory");
|
|
406
|
+
var defaultClockFactory = /* @__PURE__ */ __name(() => /* @__PURE__ */ new Date(), "defaultClockFactory");
|
|
407
|
+
var currentClockFactory = defaultClockFactory;
|
|
408
|
+
function setClockFactory(factory) {
|
|
409
|
+
currentClockFactory = factory;
|
|
410
|
+
}
|
|
411
|
+
__name(setClockFactory, "setClockFactory");
|
|
412
|
+
function resetClockFactory() {
|
|
413
|
+
currentClockFactory = defaultClockFactory;
|
|
414
|
+
}
|
|
415
|
+
__name(resetClockFactory, "resetClockFactory");
|
|
416
|
+
function createDomainEvent(type, payload, options) {
|
|
417
|
+
const event = {
|
|
418
|
+
eventId: options?.eventId ?? currentEventIdFactory(),
|
|
419
|
+
type,
|
|
420
|
+
aggregateId: options?.aggregateId,
|
|
421
|
+
aggregateType: options?.aggregateType,
|
|
422
|
+
payload,
|
|
423
|
+
occurredAt: options?.occurredAt ?? currentClockFactory(),
|
|
424
|
+
version: options?.version ?? 1,
|
|
425
|
+
metadata: options?.metadata
|
|
426
|
+
};
|
|
427
|
+
return deepFreeze(event);
|
|
428
|
+
}
|
|
429
|
+
__name(createDomainEvent, "createDomainEvent");
|
|
430
|
+
function createDomainEventWithMetadata(type, payload, metadata, options) {
|
|
431
|
+
return createDomainEvent(type, payload, {
|
|
432
|
+
...options,
|
|
433
|
+
metadata
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
__name(createDomainEventWithMetadata, "createDomainEventWithMetadata");
|
|
437
|
+
function copyMetadata(sourceEvent, additionalMetadata) {
|
|
438
|
+
return {
|
|
439
|
+
...sourceEvent.metadata ?? {},
|
|
440
|
+
...additionalMetadata ?? {}
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
__name(copyMetadata, "copyMetadata");
|
|
444
|
+
function mergeMetadata(...metadataObjects) {
|
|
445
|
+
return Object.assign({}, ...metadataObjects.filter(Boolean));
|
|
446
|
+
}
|
|
447
|
+
__name(mergeMetadata, "mergeMetadata");
|
|
448
|
+
|
|
449
|
+
// src/aggregate/aggregate.ts
|
|
450
|
+
function sameVersion(a, b) {
|
|
451
|
+
return a.id === b.id && a.version === b.version;
|
|
452
|
+
}
|
|
453
|
+
__name(sameVersion, "sameVersion");
|
|
454
|
+
|
|
216
455
|
// src/entity/entity.ts
|
|
217
456
|
var Entity = class {
|
|
218
457
|
static {
|
|
@@ -221,7 +460,15 @@ var Entity = class {
|
|
|
221
460
|
id;
|
|
222
461
|
/**
|
|
223
462
|
* Returns the current state of the entity.
|
|
224
|
-
*
|
|
463
|
+
*
|
|
464
|
+
* The state object is **shallowly frozen** — direct property writes
|
|
465
|
+
* (`entity.state.foo = …`) throw in strict mode, but writes to nested
|
|
466
|
+
* objects (`entity.state.address.zip = …`) bypass the freeze. For deep
|
|
467
|
+
* immutability either model nested data with `vo()` (which freezes
|
|
468
|
+
* deeply) or reach for a structural-sharing library like Immer at the
|
|
469
|
+
* App layer. The shallow contract is intentional: deep freezing on
|
|
470
|
+
* every state write is too expensive for hot paths, and DDD aggregates
|
|
471
|
+
* normally treat their own state as private (`Tell, Don't Ask`).
|
|
225
472
|
*/
|
|
226
473
|
get state() {
|
|
227
474
|
return this._state;
|
|
@@ -236,16 +483,29 @@ var Entity = class {
|
|
|
236
483
|
throw new Error("Entity ID cannot be null or undefined");
|
|
237
484
|
}
|
|
238
485
|
this.id = id;
|
|
239
|
-
this._state = initialState;
|
|
486
|
+
this._state = freezeShallow(initialState);
|
|
240
487
|
this.validateState(this._state);
|
|
241
488
|
}
|
|
242
489
|
/**
|
|
243
|
-
* Optional validation hook to ensure state invariants.
|
|
244
|
-
*
|
|
245
|
-
*
|
|
490
|
+
* Optional validation hook to ensure state invariants. Called during
|
|
491
|
+
* construction (from `Entity`'s constructor) and again on every
|
|
492
|
+
* `setState()` call. Throw to reject invalid state.
|
|
493
|
+
*
|
|
494
|
+
* **⚠️ Must not read subclass instance fields via `this`.** The
|
|
495
|
+
* constructor calls `validateState(initialState)` BEFORE the subclass's
|
|
496
|
+
* field initializers run, so `this.someField` is `undefined` at that
|
|
497
|
+
* point — a classic TypeScript/JavaScript constructor-ordering footgun.
|
|
498
|
+
* The `state` argument is the single source of truth; treat the method
|
|
499
|
+
* as pure with respect to `this`.
|
|
500
|
+
*
|
|
501
|
+
* If your invariants genuinely depend on per-instance configuration
|
|
502
|
+
* that isn't part of the state, pass that configuration into the state
|
|
503
|
+
* itself (DDD-canonical: the aggregate's state contains everything it
|
|
504
|
+
* needs) or perform the additional check after construction in a
|
|
505
|
+
* dedicated factory method.
|
|
246
506
|
*
|
|
247
507
|
* @param state - The state to validate
|
|
248
|
-
* @throws Error if validation fails
|
|
508
|
+
* @throws Error (or `DomainError` subclass) if validation fails
|
|
249
509
|
*/
|
|
250
510
|
validateState(_state) {
|
|
251
511
|
}
|
|
@@ -258,31 +518,38 @@ var Entity = class {
|
|
|
258
518
|
*/
|
|
259
519
|
setState(newState) {
|
|
260
520
|
this.validateState(newState);
|
|
261
|
-
this._state = newState;
|
|
521
|
+
this._state = freezeShallow(newState);
|
|
262
522
|
}
|
|
263
523
|
};
|
|
524
|
+
function freezeShallow(value) {
|
|
525
|
+
if (value !== null && typeof value === "object") {
|
|
526
|
+
return Object.freeze(value);
|
|
527
|
+
}
|
|
528
|
+
return value;
|
|
529
|
+
}
|
|
530
|
+
__name(freezeShallow, "freezeShallow");
|
|
264
531
|
function sameEntity(a, b) {
|
|
265
|
-
return
|
|
532
|
+
return a.id === b.id;
|
|
266
533
|
}
|
|
267
534
|
__name(sameEntity, "sameEntity");
|
|
268
535
|
function findEntityById(entities, id) {
|
|
269
|
-
return entities.find((entity) =>
|
|
536
|
+
return entities.find((entity) => entity.id === id);
|
|
270
537
|
}
|
|
271
538
|
__name(findEntityById, "findEntityById");
|
|
272
539
|
function hasEntityId(entities, id) {
|
|
273
|
-
return entities.some((entity) =>
|
|
540
|
+
return entities.some((entity) => entity.id === id);
|
|
274
541
|
}
|
|
275
542
|
__name(hasEntityId, "hasEntityId");
|
|
276
543
|
function removeEntityById(entities, id) {
|
|
277
|
-
return entities.filter((entity) =>
|
|
544
|
+
return entities.filter((entity) => entity.id !== id);
|
|
278
545
|
}
|
|
279
546
|
__name(removeEntityById, "removeEntityById");
|
|
280
547
|
function updateEntityById(entities, id, updater) {
|
|
281
|
-
return entities.map((entity) =>
|
|
548
|
+
return entities.map((entity) => entity.id === id ? updater(entity) : entity);
|
|
282
549
|
}
|
|
283
550
|
__name(updateEntityById, "updateEntityById");
|
|
284
551
|
function replaceEntityById(entities, id, replacement) {
|
|
285
|
-
return entities.map((entity) =>
|
|
552
|
+
return entities.map((entity) => entity.id === id ? replacement : entity);
|
|
286
553
|
}
|
|
287
554
|
__name(replaceEntityById, "replaceEntityById");
|
|
288
555
|
function entityIds(entities) {
|
|
@@ -310,7 +577,7 @@ var AggregateRoot = class extends Entity {
|
|
|
310
577
|
* These events are side-effects of state changes.
|
|
311
578
|
*/
|
|
312
579
|
get domainEvents() {
|
|
313
|
-
return this._domainEvents;
|
|
580
|
+
return Object.freeze(this._domainEvents.slice());
|
|
314
581
|
}
|
|
315
582
|
/**
|
|
316
583
|
* Clears the list of recorded domain events.
|
|
@@ -319,16 +586,105 @@ var AggregateRoot = class extends Entity {
|
|
|
319
586
|
clearDomainEvents() {
|
|
320
587
|
this._domainEvents = [];
|
|
321
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* Post-save hook called by a `Repository.save()` implementation to push
|
|
591
|
+
* the persisted version back into the in-memory aggregate and clear the
|
|
592
|
+
* recorded domain events (they are now safely on the write side / in
|
|
593
|
+
* the outbox).
|
|
594
|
+
*
|
|
595
|
+
* Use this so `save()` can keep its `Promise<void>` return type: the
|
|
596
|
+
* caller holds the aggregate reference, which is up to date after this
|
|
597
|
+
* call.
|
|
598
|
+
*/
|
|
599
|
+
markPersisted(version) {
|
|
600
|
+
this.setVersion(version);
|
|
601
|
+
this._domainEvents = [];
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Mutates state and records the resulting domain events in the
|
|
605
|
+
* **canonical record-after-mutation order**. Use this instead of calling
|
|
606
|
+
* `setState` + `addDomainEvent` separately and you cannot trip the
|
|
607
|
+
* "event for a fact that never happened" footgun.
|
|
608
|
+
*
|
|
609
|
+
* Order of operations:
|
|
610
|
+
* 1. `setState(newState, true)` — runs `validateState` first.
|
|
611
|
+
* If it throws, the method propagates and **no event is recorded
|
|
612
|
+
* and no version is bumped**.
|
|
613
|
+
* 2. Each event in `events` is appended via `addDomainEvent`.
|
|
614
|
+
*
|
|
615
|
+
* `commit()` **always bumps the version**, regardless of the aggregate's
|
|
616
|
+
* `autoVersionBump` config. Recording a domain event implies "something
|
|
617
|
+
* happened that the outside world cares about", and optimistic-
|
|
618
|
+
* concurrency callers must see a fresh version every time. The config
|
|
619
|
+
* still governs the un-coupled `setState` path. If you need to mutate
|
|
620
|
+
* state without bumping (e.g. cosmetic caches), call `setState(newState,
|
|
621
|
+
* false)` and skip `commit` entirely.
|
|
622
|
+
*
|
|
623
|
+
* `events` accepts a single event or an array. Omit it (or pass `[]`)
|
|
624
|
+
* for state-only mutations.
|
|
625
|
+
*
|
|
626
|
+
* @example
|
|
627
|
+
* ```ts
|
|
628
|
+
* confirm(): void {
|
|
629
|
+
* if (this.state.status === "confirmed") {
|
|
630
|
+
* throw new OrderAlreadyConfirmedError(this.id);
|
|
631
|
+
* }
|
|
632
|
+
* this.commit(
|
|
633
|
+
* { ...this.state, status: "confirmed" },
|
|
634
|
+
* { type: "OrderConfirmed", orderId: this.id },
|
|
635
|
+
* );
|
|
636
|
+
* }
|
|
637
|
+
* ```
|
|
638
|
+
*
|
|
639
|
+
* `EventSourcedAggregate.apply()` enforces the same ordering
|
|
640
|
+
* structurally; `commit()` is the opt-in equivalent on `AggregateRoot`,
|
|
641
|
+
* where `setState` and `addDomainEvent` are otherwise decoupled and the
|
|
642
|
+
* ordering is convention-only.
|
|
643
|
+
*
|
|
644
|
+
* @param newState - The new state (validated by `validateState`)
|
|
645
|
+
* @param events - One event, an array of events, or none (default)
|
|
646
|
+
*/
|
|
647
|
+
commit(newState, events = []) {
|
|
648
|
+
this.setState(newState, true);
|
|
649
|
+
const list = Array.isArray(events) ? events : [events];
|
|
650
|
+
for (const ev of list) {
|
|
651
|
+
this.addDomainEvent(ev);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
322
654
|
constructor(id, initialState, config) {
|
|
323
655
|
super(id, initialState);
|
|
324
656
|
this._config = config ?? {};
|
|
325
657
|
this._autoVersionBump = this._config.autoVersionBump ?? false;
|
|
326
658
|
}
|
|
327
659
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
660
|
+
* Records a domain event for later publication.
|
|
661
|
+
*
|
|
662
|
+
* **Ordering: record AFTER state mutation.** Vernon (IDDD §8) is
|
|
663
|
+
* explicit: a domain event describes something that has just happened
|
|
664
|
+
* to the aggregate — its existence implies the state change already
|
|
665
|
+
* occurred. Concretely:
|
|
666
|
+
*
|
|
667
|
+
* ```ts
|
|
668
|
+
* confirm(): void {
|
|
669
|
+
* if (this.state.status === "confirmed") {
|
|
670
|
+
* throw new OrderAlreadyConfirmedError(this.id);
|
|
671
|
+
* }
|
|
672
|
+
* this.setState({ ...this.state, status: "confirmed" }, true);
|
|
673
|
+
* this.addDomainEvent({ type: "OrderConfirmed", orderId: this.id });
|
|
674
|
+
* // ↑ post-mutation. The event represents the committed fact.
|
|
675
|
+
* }
|
|
676
|
+
* ```
|
|
677
|
+
*
|
|
678
|
+
* Recording before mutation is a footgun: if a subsequent invariant
|
|
679
|
+
* check throws, the event has already been queued but the state never
|
|
680
|
+
* actually changed — consumers see an event for a fact that did not
|
|
681
|
+
* happen.
|
|
330
682
|
*
|
|
331
|
-
*
|
|
683
|
+
* `EventSourcedAggregate.apply()` enforces this ordering structurally;
|
|
684
|
+
* `AggregateRoot` leaves it as a convention because the state-mutation
|
|
685
|
+
* path (`setState`) is decoupled from event recording.
|
|
686
|
+
*
|
|
687
|
+
* @param event - The domain event to record
|
|
332
688
|
*/
|
|
333
689
|
addDomainEvent(event) {
|
|
334
690
|
this._domainEvents.push(event);
|
|
@@ -394,20 +750,65 @@ var AggregateRoot = class extends Entity {
|
|
|
394
750
|
*/
|
|
395
751
|
restoreFromSnapshot(snapshot) {
|
|
396
752
|
this.validateState(snapshot.state);
|
|
397
|
-
this._state = snapshot.state;
|
|
753
|
+
this._state = freezeShallow(snapshot.state);
|
|
398
754
|
this.setVersion(snapshot.version);
|
|
399
755
|
}
|
|
400
756
|
};
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
757
|
+
var DomainError = class extends BaseError {
|
|
758
|
+
static {
|
|
759
|
+
__name(this, "DomainError");
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
var InfrastructureError = class extends BaseError {
|
|
763
|
+
static {
|
|
764
|
+
__name(this, "InfrastructureError");
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
var MissingHandlerError = class extends BaseError {
|
|
768
|
+
constructor(eventType) {
|
|
769
|
+
super(`Missing handler for event type: ${eventType}`);
|
|
770
|
+
this.eventType = eventType;
|
|
771
|
+
}
|
|
772
|
+
static {
|
|
773
|
+
__name(this, "MissingHandlerError");
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
var AggregateNotFoundError = class extends InfrastructureError {
|
|
777
|
+
constructor(aggregateType, id) {
|
|
778
|
+
super(`Aggregate not found: ${aggregateType}(${id})`);
|
|
779
|
+
this.aggregateType = aggregateType;
|
|
780
|
+
this.id = id;
|
|
781
|
+
this.withUserMessage(
|
|
782
|
+
`The requested ${aggregateType.toLowerCase()} could not be found.`
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
static {
|
|
786
|
+
__name(this, "AggregateNotFoundError");
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
var ConcurrencyConflictError = class extends InfrastructureError {
|
|
790
|
+
constructor(aggregateType, aggregateId, expectedVersion, actualVersion) {
|
|
791
|
+
super(
|
|
792
|
+
`Concurrency conflict on ${aggregateType}(${aggregateId}): expected version ${expectedVersion}, actual ${actualVersion}`
|
|
793
|
+
);
|
|
794
|
+
this.aggregateType = aggregateType;
|
|
795
|
+
this.aggregateId = aggregateId;
|
|
796
|
+
this.expectedVersion = expectedVersion;
|
|
797
|
+
this.actualVersion = actualVersion;
|
|
798
|
+
this.withUserMessage(
|
|
799
|
+
"This resource was updated by another request. Please reload and try again."
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
static {
|
|
803
|
+
__name(this, "ConcurrencyConflictError");
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Marks this error as retryable so `isRetryable(err)` returns
|
|
807
|
+
* true. The canonical OCC pattern is to reload the aggregate, re-apply
|
|
808
|
+
* the use case, and retry on this exception.
|
|
809
|
+
*/
|
|
810
|
+
retryable = true;
|
|
811
|
+
};
|
|
411
812
|
|
|
412
813
|
// src/aggregate/event-sourced-aggregate.ts
|
|
413
814
|
var EventSourcedAggregate = class extends Entity {
|
|
@@ -426,73 +827,69 @@ var EventSourcedAggregate = class extends Entity {
|
|
|
426
827
|
_pendingEvents = [];
|
|
427
828
|
_autoVersionBump;
|
|
428
829
|
get pendingEvents() {
|
|
429
|
-
return this._pendingEvents;
|
|
830
|
+
return Object.freeze(this._pendingEvents.slice());
|
|
430
831
|
}
|
|
431
832
|
clearPendingEvents() {
|
|
432
833
|
this._pendingEvents = [];
|
|
433
834
|
}
|
|
835
|
+
/**
|
|
836
|
+
* Post-save hook called by a `Repository.save()` implementation to push
|
|
837
|
+
* the persisted version back into the in-memory aggregate and clear the
|
|
838
|
+
* pending events (they are now in the event store / outbox). Lets
|
|
839
|
+
* `save()` keep its `Promise<void>` return type.
|
|
840
|
+
*/
|
|
841
|
+
markPersisted(version) {
|
|
842
|
+
this.setVersion(version);
|
|
843
|
+
this._pendingEvents = [];
|
|
844
|
+
}
|
|
434
845
|
constructor(id, initialState, config) {
|
|
435
846
|
super(id, initialState);
|
|
436
847
|
this._autoVersionBump = config?.autoVersionBump ?? true;
|
|
437
848
|
}
|
|
438
849
|
// --- Event application ---
|
|
439
850
|
/**
|
|
440
|
-
* Validates an event before it is applied.
|
|
441
|
-
*
|
|
442
|
-
*
|
|
851
|
+
* Validates an event before it is applied. Default is no-op.
|
|
852
|
+
* Subclasses override to throw a concrete `DomainError` subclass when
|
|
853
|
+
* the event violates an invariant in the current state.
|
|
443
854
|
*/
|
|
444
855
|
validateEvent(_event) {
|
|
445
|
-
return ok(true);
|
|
446
856
|
}
|
|
447
857
|
/**
|
|
448
|
-
* Applies an event
|
|
449
|
-
*
|
|
858
|
+
* Applies an event: validates, locates the handler, computes the next
|
|
859
|
+
* state, then commits state + pending event + version bump atomically.
|
|
860
|
+
*
|
|
861
|
+
* Throws `DomainError` (or a subclass) on validation failure.
|
|
862
|
+
* Throws `MissingHandlerError` if no handler is registered for `event.type`.
|
|
863
|
+
*
|
|
864
|
+
* State is not mutated if any step throws — the handler is invoked into
|
|
865
|
+
* a local and only assigned to `_state` once all checks pass.
|
|
866
|
+
*
|
|
867
|
+
* The method is generic in the event tag `K`, so concrete callers
|
|
868
|
+
* (`this.apply(orderCreated)`) narrow to the literal tag and the
|
|
869
|
+
* dispatched handler is typed as `Handler<TState, Extract<TEvent, { type: K }>>`
|
|
870
|
+
* — no `as` cast required at the call site.
|
|
450
871
|
*
|
|
451
872
|
* @param event - The domain event to apply
|
|
452
|
-
* @param isNew - Whether the event is new (needs persisting) or from history
|
|
873
|
+
* @param isNew - Whether the event is new (needs persisting) or replayed from history
|
|
453
874
|
*/
|
|
454
875
|
apply(event, isNew = true) {
|
|
455
|
-
|
|
456
|
-
if (!validation.ok) {
|
|
457
|
-
return err(
|
|
458
|
-
`Event validation failed for ${event.type}: ${validation.error}`
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
const handler = this.handlers[event.type];
|
|
462
|
-
if (!handler) {
|
|
463
|
-
return err(`Missing handler for event type: ${event.type}`);
|
|
464
|
-
}
|
|
465
|
-
this._state = handler(
|
|
466
|
-
this._state,
|
|
467
|
-
event
|
|
468
|
-
);
|
|
469
|
-
if (isNew) {
|
|
470
|
-
this._pendingEvents.push(event);
|
|
471
|
-
if (this._autoVersionBump) {
|
|
472
|
-
this.setVersion(this._version + 1);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
return ok();
|
|
876
|
+
this.dispatchAndCommit(event, isNew);
|
|
476
877
|
}
|
|
477
878
|
/**
|
|
478
|
-
*
|
|
479
|
-
*
|
|
879
|
+
* Internal dispatch path used by `apply()` and the replay methods
|
|
880
|
+
* (`loadFromHistory`, `restoreFromSnapshotWithEvents`). The replay loop
|
|
881
|
+
* iterates over `TEvent[]` and therefore cannot supply a narrowed `K`
|
|
882
|
+
* generic, so this helper accepts `TEvent` and the discriminator is
|
|
883
|
+
* resolved via the (statically-sound) `handlers` map.
|
|
480
884
|
*/
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (!validation.ok) {
|
|
484
|
-
throw new Error(
|
|
485
|
-
`Event validation failed for ${event.type}: ${validation.error}`
|
|
486
|
-
);
|
|
487
|
-
}
|
|
885
|
+
dispatchAndCommit(event, isNew) {
|
|
886
|
+
this.validateEvent(event);
|
|
488
887
|
const handler = this.handlers[event.type];
|
|
489
888
|
if (!handler) {
|
|
490
|
-
throw new
|
|
889
|
+
throw new MissingHandlerError(event.type);
|
|
491
890
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
event
|
|
495
|
-
);
|
|
891
|
+
const nextState = handler(this._state, event);
|
|
892
|
+
this._state = freezeShallow(nextState);
|
|
496
893
|
if (isNew) {
|
|
497
894
|
this._pendingEvents.push(event);
|
|
498
895
|
if (this._autoVersionBump) {
|
|
@@ -509,17 +906,27 @@ var EventSourcedAggregate = class extends Entity {
|
|
|
509
906
|
}
|
|
510
907
|
// --- History & Snapshots ---
|
|
511
908
|
/**
|
|
512
|
-
* Reconstitutes the aggregate from an event history.
|
|
513
|
-
*
|
|
909
|
+
* Reconstitutes the aggregate from an event history. Catches `DomainError`
|
|
910
|
+
* thrown during replay and returns it as an `Err` — this is the
|
|
911
|
+
* infrastructure boundary, where event-stream corruption is an expected
|
|
912
|
+
* recoverable failure. Unexpected (non-DomainError) throws propagate.
|
|
913
|
+
*
|
|
914
|
+
* Version advances additively: the aggregate's pre-existing version plus
|
|
915
|
+
* `history.length`. A fresh aggregate (v=0) loading 3 events ends at v=3;
|
|
916
|
+
* an aggregate already at v=1 (e.g. after a creation event) loading
|
|
917
|
+
* 2 events ends at v=3, not v=2.
|
|
514
918
|
*/
|
|
515
919
|
loadFromHistory(history) {
|
|
920
|
+
const startVersion = this._version;
|
|
516
921
|
for (const event of history) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
922
|
+
try {
|
|
923
|
+
this.dispatchAndCommit(event, false);
|
|
924
|
+
} catch (e) {
|
|
925
|
+
if (e instanceof DomainError) return err(e);
|
|
926
|
+
throw e;
|
|
520
927
|
}
|
|
521
928
|
}
|
|
522
|
-
this.setVersion(history.length);
|
|
929
|
+
this.setVersion(startVersion + history.length);
|
|
523
930
|
return ok();
|
|
524
931
|
}
|
|
525
932
|
hasPendingEvents() {
|
|
@@ -542,23 +949,34 @@ var EventSourcedAggregate = class extends Entity {
|
|
|
542
949
|
};
|
|
543
950
|
}
|
|
544
951
|
/**
|
|
545
|
-
* Restores the aggregate from a snapshot and applies events that occurred
|
|
952
|
+
* Restores the aggregate from a snapshot and applies events that occurred
|
|
953
|
+
* after. Same infrastructure-boundary semantics as `loadFromHistory`:
|
|
954
|
+
* catches `DomainError` and returns it as an `Err`; non-domain throws
|
|
955
|
+
* propagate.
|
|
956
|
+
*
|
|
957
|
+
* All-or-nothing: if any event mid-stream throws a `DomainError`, the
|
|
958
|
+
* aggregate is rolled back to its pre-call state + version. Partial
|
|
959
|
+
* restoration is never observable to the caller.
|
|
546
960
|
*/
|
|
547
961
|
restoreFromSnapshotWithEvents(snapshot, eventsAfterSnapshot) {
|
|
548
|
-
|
|
962
|
+
const previousState = this._state;
|
|
963
|
+
const previousVersion = this._version;
|
|
964
|
+
this._state = freezeShallow(snapshot.state);
|
|
549
965
|
this.setVersion(snapshot.version);
|
|
550
966
|
for (const event of eventsAfterSnapshot) {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
967
|
+
try {
|
|
968
|
+
this.dispatchAndCommit(event, false);
|
|
969
|
+
} catch (e) {
|
|
970
|
+
this._state = previousState;
|
|
971
|
+
this.setVersion(previousVersion);
|
|
972
|
+
if (e instanceof DomainError) return err(e);
|
|
973
|
+
throw e;
|
|
554
974
|
}
|
|
555
975
|
}
|
|
556
976
|
this.setVersion(snapshot.version + eventsAfterSnapshot.length);
|
|
557
977
|
return ok();
|
|
558
978
|
}
|
|
559
979
|
};
|
|
560
|
-
|
|
561
|
-
// src/app/command-bus.ts
|
|
562
980
|
var CommandBus = class {
|
|
563
981
|
static {
|
|
564
982
|
__name(this, "CommandBus");
|
|
@@ -584,17 +1002,18 @@ var CommandBus = class {
|
|
|
584
1002
|
};
|
|
585
1003
|
|
|
586
1004
|
// src/app/handler.ts
|
|
587
|
-
function withCommit(deps, fn) {
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
await deps.outbox.add(events);
|
|
591
|
-
|
|
592
|
-
return result;
|
|
1005
|
+
async function withCommit(deps, fn) {
|
|
1006
|
+
const { result, events } = await deps.scope.transactional(async () => {
|
|
1007
|
+
const fnResult = await fn();
|
|
1008
|
+
await deps.outbox.add(fnResult.events);
|
|
1009
|
+
return fnResult;
|
|
593
1010
|
});
|
|
1011
|
+
if (deps.bus) {
|
|
1012
|
+
await deps.bus.publish(events);
|
|
1013
|
+
}
|
|
1014
|
+
return result;
|
|
594
1015
|
}
|
|
595
1016
|
__name(withCommit, "withCommit");
|
|
596
|
-
|
|
597
|
-
// src/app/query-bus.ts
|
|
598
1017
|
var QueryBus = class {
|
|
599
1018
|
static {
|
|
600
1019
|
__name(this, "QueryBus");
|
|
@@ -627,12 +1046,6 @@ var QueryBus = class {
|
|
|
627
1046
|
}
|
|
628
1047
|
};
|
|
629
1048
|
|
|
630
|
-
// src/core/guard.ts
|
|
631
|
-
function guard(cond, error) {
|
|
632
|
-
return cond ? ok(true) : err(error);
|
|
633
|
-
}
|
|
634
|
-
__name(guard, "guard");
|
|
635
|
-
|
|
636
1049
|
// src/events/event-bus.ts
|
|
637
1050
|
var EventBusImpl = class {
|
|
638
1051
|
static {
|
|
@@ -643,32 +1056,82 @@ var EventBusImpl = class {
|
|
|
643
1056
|
subscribe(eventType, handler) {
|
|
644
1057
|
const type = eventType;
|
|
645
1058
|
if (!this.handlers.has(type)) {
|
|
646
|
-
this.handlers.set(type,
|
|
1059
|
+
this.handlers.set(type, []);
|
|
647
1060
|
}
|
|
648
1061
|
const handlersForType = this.handlers.get(type);
|
|
649
|
-
|
|
1062
|
+
const casted = handler;
|
|
1063
|
+
handlersForType.push(casted);
|
|
1064
|
+
let removed = false;
|
|
650
1065
|
return () => {
|
|
651
|
-
|
|
652
|
-
|
|
1066
|
+
if (removed) return;
|
|
1067
|
+
const idx = handlersForType.indexOf(casted);
|
|
1068
|
+
if (idx !== -1) {
|
|
1069
|
+
handlersForType.splice(idx, 1);
|
|
1070
|
+
removed = true;
|
|
1071
|
+
}
|
|
1072
|
+
if (handlersForType.length === 0) {
|
|
653
1073
|
this.handlers.delete(type);
|
|
654
1074
|
}
|
|
655
1075
|
};
|
|
656
1076
|
}
|
|
657
|
-
once(eventType) {
|
|
658
|
-
return new Promise((resolve) => {
|
|
659
|
-
|
|
1077
|
+
once(eventType, options) {
|
|
1078
|
+
return new Promise((resolve, reject) => {
|
|
1079
|
+
if (options?.signal?.aborted) {
|
|
1080
|
+
reject(options.signal.reason ?? new Error("EventBus.once aborted"));
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
let timer;
|
|
1084
|
+
let settled = false;
|
|
1085
|
+
let abortListener;
|
|
1086
|
+
const cleanup = /* @__PURE__ */ __name(() => {
|
|
1087
|
+
if (settled) return;
|
|
1088
|
+
settled = true;
|
|
660
1089
|
unsubscribe();
|
|
1090
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1091
|
+
if (abortListener && options?.signal) {
|
|
1092
|
+
options.signal.removeEventListener("abort", abortListener);
|
|
1093
|
+
}
|
|
1094
|
+
}, "cleanup");
|
|
1095
|
+
const unsubscribe = this.subscribe(eventType, (event) => {
|
|
1096
|
+
cleanup();
|
|
661
1097
|
resolve(event);
|
|
662
1098
|
});
|
|
1099
|
+
if (options?.signal) {
|
|
1100
|
+
abortListener = /* @__PURE__ */ __name(() => {
|
|
1101
|
+
cleanup();
|
|
1102
|
+
reject(
|
|
1103
|
+
options.signal.reason ?? new Error("EventBus.once aborted")
|
|
1104
|
+
);
|
|
1105
|
+
}, "abortListener");
|
|
1106
|
+
options.signal.addEventListener("abort", abortListener);
|
|
1107
|
+
}
|
|
1108
|
+
if (typeof options?.timeoutMs === "number") {
|
|
1109
|
+
timer = setTimeout(() => {
|
|
1110
|
+
cleanup();
|
|
1111
|
+
reject(
|
|
1112
|
+
new Error(
|
|
1113
|
+
`EventBus.once timed out after ${options.timeoutMs}ms waiting for "${eventType}"`
|
|
1114
|
+
)
|
|
1115
|
+
);
|
|
1116
|
+
}, options.timeoutMs);
|
|
1117
|
+
}
|
|
663
1118
|
});
|
|
664
1119
|
}
|
|
1120
|
+
/**
|
|
1121
|
+
* See {@link EventBus.publish} for the full ordering / parallelism /
|
|
1122
|
+
* error-aggregation contract this implementation realises:
|
|
1123
|
+
* - events in input order, sequentially;
|
|
1124
|
+
* - handlers within one event in parallel via `Promise.allSettled`;
|
|
1125
|
+
* - errors collected and thrown after the batch (single Error, or
|
|
1126
|
+
* `AggregateError` for multiple failures).
|
|
1127
|
+
*/
|
|
665
1128
|
async publish(events) {
|
|
666
1129
|
const errors = [];
|
|
667
1130
|
for (const event of events) {
|
|
668
1131
|
const handlersForType = this.handlers.get(event.type);
|
|
669
1132
|
if (handlersForType) {
|
|
670
1133
|
const results = await Promise.allSettled(
|
|
671
|
-
|
|
1134
|
+
handlersForType.slice().map((handler) => handler(event))
|
|
672
1135
|
);
|
|
673
1136
|
for (const result of results) {
|
|
674
1137
|
if (result.status === "rejected") {
|
|
@@ -688,195 +1151,6 @@ var EventBusImpl = class {
|
|
|
688
1151
|
}
|
|
689
1152
|
};
|
|
690
1153
|
|
|
691
|
-
|
|
692
|
-
function deepOmit(value, options) {
|
|
693
|
-
const visited = /* @__PURE__ */ new WeakMap();
|
|
694
|
-
return omitInternal(value, options, [], visited);
|
|
695
|
-
}
|
|
696
|
-
__name(deepOmit, "deepOmit");
|
|
697
|
-
function omitInternal(value, options, path, visited) {
|
|
698
|
-
if (value === null) return value;
|
|
699
|
-
const type = typeof value;
|
|
700
|
-
if (type !== "object") return value;
|
|
701
|
-
const obj = value;
|
|
702
|
-
const cached = visited.get(obj);
|
|
703
|
-
if (cached !== void 0) {
|
|
704
|
-
return cached;
|
|
705
|
-
}
|
|
706
|
-
const tag = Object.prototype.toString.call(obj);
|
|
707
|
-
if (tag === "[object Array]") {
|
|
708
|
-
const arr = obj;
|
|
709
|
-
const clone2 = new Array(arr.length);
|
|
710
|
-
visited.set(obj, clone2);
|
|
711
|
-
for (let i = 0; i < arr.length; i++) {
|
|
712
|
-
path.push(i);
|
|
713
|
-
clone2[i] = omitInternal(arr[i], options, path, visited);
|
|
714
|
-
path.pop();
|
|
715
|
-
}
|
|
716
|
-
return clone2;
|
|
717
|
-
}
|
|
718
|
-
if (isBuiltInObject(obj, tag)) {
|
|
719
|
-
return value;
|
|
720
|
-
}
|
|
721
|
-
const clone = Object.create(Object.getPrototypeOf(obj));
|
|
722
|
-
visited.set(obj, clone);
|
|
723
|
-
const stringKeys = Object.keys(obj);
|
|
724
|
-
const symbolKeys = Object.getOwnPropertySymbols(obj);
|
|
725
|
-
const keys = [...stringKeys, ...symbolKeys];
|
|
726
|
-
for (const key of keys) {
|
|
727
|
-
if (shouldIgnoreKey(key, path, options)) continue;
|
|
728
|
-
path.push(key);
|
|
729
|
-
clone[key] = omitInternal(
|
|
730
|
-
obj[key],
|
|
731
|
-
options,
|
|
732
|
-
path,
|
|
733
|
-
visited
|
|
734
|
-
);
|
|
735
|
-
path.pop();
|
|
736
|
-
}
|
|
737
|
-
return clone;
|
|
738
|
-
}
|
|
739
|
-
__name(omitInternal, "omitInternal");
|
|
740
|
-
function shouldIgnoreKey(key, path, options) {
|
|
741
|
-
if (options.ignoreKeys?.includes(key)) {
|
|
742
|
-
return true;
|
|
743
|
-
}
|
|
744
|
-
if (options.ignoreKeyPredicate?.(key, path)) {
|
|
745
|
-
return true;
|
|
746
|
-
}
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
__name(shouldIgnoreKey, "shouldIgnoreKey");
|
|
750
|
-
|
|
751
|
-
// src/utils/array/deep-equal-except.ts
|
|
752
|
-
function deepEqualExcept(a, b, options) {
|
|
753
|
-
const prunedA = deepOmit(a, options);
|
|
754
|
-
const prunedB = deepOmit(b, options);
|
|
755
|
-
return deepEqual(prunedA, prunedB);
|
|
756
|
-
}
|
|
757
|
-
__name(deepEqualExcept, "deepEqualExcept");
|
|
758
|
-
|
|
759
|
-
// src/value-object/value-object.ts
|
|
760
|
-
function deepFreeze(obj, visited = /* @__PURE__ */ new WeakSet()) {
|
|
761
|
-
if (obj === null || typeof obj !== "object") {
|
|
762
|
-
return obj;
|
|
763
|
-
}
|
|
764
|
-
if (visited.has(obj)) {
|
|
765
|
-
return obj;
|
|
766
|
-
}
|
|
767
|
-
visited.add(obj);
|
|
768
|
-
const propNames = Object.getOwnPropertyNames(obj);
|
|
769
|
-
for (const name of propNames) {
|
|
770
|
-
const value = obj[name];
|
|
771
|
-
if (value && (typeof value === "object" || Array.isArray(value))) {
|
|
772
|
-
deepFreeze(value, visited);
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
return Object.freeze(obj);
|
|
776
|
-
}
|
|
777
|
-
__name(deepFreeze, "deepFreeze");
|
|
778
|
-
function vo(t) {
|
|
779
|
-
return deepFreeze({ ...t });
|
|
780
|
-
}
|
|
781
|
-
__name(vo, "vo");
|
|
782
|
-
function voEquals(a, b) {
|
|
783
|
-
return deepEqual(a, b);
|
|
784
|
-
}
|
|
785
|
-
__name(voEquals, "voEquals");
|
|
786
|
-
function voEqualsExcept(a, b, options) {
|
|
787
|
-
return deepEqualExcept(a, b, options);
|
|
788
|
-
}
|
|
789
|
-
__name(voEqualsExcept, "voEqualsExcept");
|
|
790
|
-
function voWithValidation(t, validate, errorMessage) {
|
|
791
|
-
if (!validate(t)) {
|
|
792
|
-
return err(
|
|
793
|
-
errorMessage ?? `Validation failed for value object: ${JSON.stringify(t)}`
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
return ok(vo(t));
|
|
797
|
-
}
|
|
798
|
-
__name(voWithValidation, "voWithValidation");
|
|
799
|
-
function voWithValidationUnsafe(t, validate, errorMessage) {
|
|
800
|
-
if (!validate(t)) {
|
|
801
|
-
throw new Error(
|
|
802
|
-
errorMessage ?? `Validation failed for value object: ${JSON.stringify(t)}`
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
return vo(t);
|
|
806
|
-
}
|
|
807
|
-
__name(voWithValidationUnsafe, "voWithValidationUnsafe");
|
|
808
|
-
var ValueObject = class {
|
|
809
|
-
static {
|
|
810
|
-
__name(this, "ValueObject");
|
|
811
|
-
}
|
|
812
|
-
props;
|
|
813
|
-
/**
|
|
814
|
-
* Creates a new ValueObject.
|
|
815
|
-
* The properties are deeply frozen to ensure immutability.
|
|
816
|
-
*
|
|
817
|
-
* @param props - The properties of the value object
|
|
818
|
-
* @example
|
|
819
|
-
* ```ts
|
|
820
|
-
* class Money extends ValueObject<{ amount: number; currency: string }> {
|
|
821
|
-
* constructor(props: { amount: number; currency: string }) {
|
|
822
|
-
* super(props);
|
|
823
|
-
* }
|
|
824
|
-
*
|
|
825
|
-
* protected validate(props: { amount: number; currency: string }): void {
|
|
826
|
-
* if (props.amount < 0) throw new Error("Amount cannot be negative");
|
|
827
|
-
* }
|
|
828
|
-
* }
|
|
829
|
-
* ```
|
|
830
|
-
*/
|
|
831
|
-
constructor(props) {
|
|
832
|
-
this.validate(props);
|
|
833
|
-
this.props = deepFreeze({ ...props });
|
|
834
|
-
}
|
|
835
|
-
/**
|
|
836
|
-
* Optional validation hook that can be overridden by subclasses.
|
|
837
|
-
* Should throw an error if validation fails.
|
|
838
|
-
*
|
|
839
|
-
* @param props - The properties to validate
|
|
840
|
-
* @throws Error if validation fails
|
|
841
|
-
*/
|
|
842
|
-
validate(props) {
|
|
843
|
-
}
|
|
844
|
-
/**
|
|
845
|
-
* Checks if this value object is equal to another.
|
|
846
|
-
* Uses deep equality comparison on the properties and checks for constructor equality.
|
|
847
|
-
*
|
|
848
|
-
* @param other - The other value object to compare
|
|
849
|
-
* @returns true if the properties are deeply equal and constructors match
|
|
850
|
-
*/
|
|
851
|
-
equals(other) {
|
|
852
|
-
if (other === null || other === void 0) {
|
|
853
|
-
return false;
|
|
854
|
-
}
|
|
855
|
-
if (this.constructor !== other.constructor) {
|
|
856
|
-
return false;
|
|
857
|
-
}
|
|
858
|
-
return deepEqual(this.props, other.props);
|
|
859
|
-
}
|
|
860
|
-
/**
|
|
861
|
-
* Creates a clone of the value object with optional property overrides.
|
|
862
|
-
*
|
|
863
|
-
* @param props - Optional properties to override
|
|
864
|
-
* @returns A new instance of the value object
|
|
865
|
-
*/
|
|
866
|
-
clone(props) {
|
|
867
|
-
const Constructor = this.constructor;
|
|
868
|
-
return new Constructor({ ...this.props, ...props || {} });
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Serializes the value object to its raw properties for JSON operations.
|
|
872
|
-
*
|
|
873
|
-
* @returns The raw properties object
|
|
874
|
-
*/
|
|
875
|
-
toJSON() {
|
|
876
|
-
return this.props;
|
|
877
|
-
}
|
|
878
|
-
};
|
|
879
|
-
|
|
880
|
-
export { AggregateRoot, CommandBus, Entity, EventBusImpl, EventSourcedAggregate, QueryBus, ValueObject, aggregate, bump, copyMetadata, createDomainEvent, createDomainEventWithMetadata, deepFreeze, entityIds, findEntityById, guard, hasEntityId, mergeMetadata, removeEntityById, replaceEntityById, sameEntity, sameVersion, updateEntityById, vo, voEquals, voEqualsExcept, voWithValidation, voWithValidationUnsafe, withCommit };
|
|
1154
|
+
export { AggregateNotFoundError, AggregateRoot, CommandBus, ConcurrencyConflictError, DomainError, Entity, EventBusImpl, EventSourcedAggregate, InfrastructureError, MissingHandlerError, QueryBus, ValueObject, copyMetadata, createDomainEvent, createDomainEventWithMetadata, deepEqual, deepEqualExcept, deepFreeze, deepOmit, entityIds, findEntityById, freezeShallow, hasEntityId, mergeMetadata, removeEntityById, replaceEntityById, resetClockFactory, resetEventIdFactory, sameEntity, sameVersion, setClockFactory, setEventIdFactory, updateEntityById, vo, voEquals, voEqualsExcept, voWithValidation, withCommit };
|
|
881
1155
|
//# sourceMappingURL=index.js.map
|
|
882
1156
|
//# sourceMappingURL=index.js.map
|