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