@shirudo/ddd-kit 2.2.0 → 3.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 +206 -55
- package/dist/chunks/deep-equal-except.js +639 -0
- package/dist/chunks/deep-equal-except.js.map +1 -0
- package/dist/chunks/errors.d.ts +785 -0
- package/dist/chunks/errors.js +822 -0
- package/dist/chunks/errors.js.map +1 -0
- package/dist/chunks/ports.js +891 -0
- package/dist/chunks/ports.js.map +1 -0
- package/dist/chunks/snapshot-store.d.ts +2808 -0
- package/dist/chunks/utils.d.ts +110 -0
- package/dist/http.d.ts +64 -51
- package/dist/http.js +54 -20
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +2351 -2650
- package/dist/index.js +6140 -3915
- package/dist/index.js.map +1 -1
- package/dist/money.d.ts +376 -0
- package/dist/money.js +578 -0
- package/dist/money.js.map +1 -0
- package/dist/presentation.d.ts +86 -37
- package/dist/presentation.js +208 -39
- package/dist/presentation.js.map +1 -1
- package/dist/testing.d.ts +517 -335
- package/dist/testing.js +2396 -1184
- package/dist/testing.js.map +1 -1
- package/dist/utils.d.ts +2 -106
- package/dist/utils.js +2 -530
- package/package.json +35 -18
- package/dist/aggregate-DFi6HlEh.d.ts +0 -771
- package/dist/utils.js.map +0 -1
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
import { L as assertNoHostileOwnProtoKey } from "./errors.js";
|
|
2
|
+
import { c as isIntrinsicConstructorPrototype, i as builtInTagWithoutInvokingAccessors, l as mutableBuiltInTagWithoutInvokingAccessors, o as hasIntrinsicPrototypeChain, r as deepEqual, t as deepEqualExcept } from "./deep-equal-except.js";
|
|
3
|
+
import { err, ok } from "@shirudo/result";
|
|
4
|
+
|
|
5
|
+
//#region src/value-object/value-object.ts
|
|
6
|
+
/**
|
|
7
|
+
* `Object.freeze` does not protect internal slots: a frozen Date still
|
|
8
|
+
* accepts `setTime`, a frozen Map still accepts `set`. To make the
|
|
9
|
+
* "deeply immutable" guarantee real, the mutator methods are shadowed
|
|
10
|
+
* with own throwing functions BEFORE the freeze. The shadows are
|
|
11
|
+
* non-enumerable, so they are invisible to `Object.keys`/spread (deep
|
|
12
|
+
* equality is unaffected) and `structuredClone` drops them (a `vo()`
|
|
13
|
+
* round-trip never sees them).
|
|
14
|
+
*/
|
|
15
|
+
const DATE_MUTATORS = [
|
|
16
|
+
"setTime",
|
|
17
|
+
"setMilliseconds",
|
|
18
|
+
"setUTCMilliseconds",
|
|
19
|
+
"setSeconds",
|
|
20
|
+
"setUTCSeconds",
|
|
21
|
+
"setMinutes",
|
|
22
|
+
"setUTCMinutes",
|
|
23
|
+
"setHours",
|
|
24
|
+
"setUTCHours",
|
|
25
|
+
"setDate",
|
|
26
|
+
"setUTCDate",
|
|
27
|
+
"setMonth",
|
|
28
|
+
"setUTCMonth",
|
|
29
|
+
"setFullYear",
|
|
30
|
+
"setUTCFullYear",
|
|
31
|
+
"setYear"
|
|
32
|
+
];
|
|
33
|
+
const mutationThrowers = /* @__PURE__ */ new Map();
|
|
34
|
+
const mapEntries = Map.prototype.entries;
|
|
35
|
+
const setValues = Set.prototype.values;
|
|
36
|
+
function mutationThrower(typeName, method) {
|
|
37
|
+
const key = `${typeName}.${method}`;
|
|
38
|
+
let thrower = mutationThrowers.get(key);
|
|
39
|
+
if (!thrower) {
|
|
40
|
+
thrower = function throwFrozenMutation() {
|
|
41
|
+
throw new TypeError(`Cannot call ${method}() on a ${typeName} inside a deeply frozen value`);
|
|
42
|
+
};
|
|
43
|
+
mutationThrowers.set(key, thrower);
|
|
44
|
+
}
|
|
45
|
+
return thrower;
|
|
46
|
+
}
|
|
47
|
+
const shadowDescriptor = {
|
|
48
|
+
value: void 0,
|
|
49
|
+
writable: false,
|
|
50
|
+
enumerable: false,
|
|
51
|
+
configurable: false
|
|
52
|
+
};
|
|
53
|
+
function shadowMutators(obj, typeName, methods) {
|
|
54
|
+
if (!Object.isExtensible(obj)) return;
|
|
55
|
+
for (const method of methods) {
|
|
56
|
+
shadowDescriptor.value = mutationThrower(typeName, method);
|
|
57
|
+
Object.defineProperty(obj, method, shadowDescriptor);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Deep freezes an object and all its nested properties recursively, then
|
|
62
|
+
* returns it. Iterates both string-keyed and symbol-keyed own properties
|
|
63
|
+
* so the freeze symmetry matches `deepEqual` (which also considers symbol
|
|
64
|
+
* keys). Handles circular references by tracking visited objects.
|
|
65
|
+
*
|
|
66
|
+
* Note: `deepFreeze` mutates its argument in place; it sets `[[Frozen]]`
|
|
67
|
+
* on the object you pass in. Callers that need to avoid touching the
|
|
68
|
+
* input (e.g. `vo()`) should deep-clone first.
|
|
69
|
+
*
|
|
70
|
+
* Date/Map/Set keep internal-slot mutability under `Object.freeze`
|
|
71
|
+
* (`setTime`, `set`, `add`, … still work on frozen instances), so their
|
|
72
|
+
* mutator methods are shadowed with throwing own properties and Map/Set
|
|
73
|
+
* contents are frozen recursively. The shadows are non-enumerable:
|
|
74
|
+
* invisible to `Object.keys`, spread, `deepEqual`, and `structuredClone`.
|
|
75
|
+
*
|
|
76
|
+
* The shadowing is deny-by-enumeration: only the mutators known at
|
|
77
|
+
* release time are blocked. If the runtime grows a NEW mutator (e.g. the
|
|
78
|
+
* stage-3 `Map.prototype.getOrInsert` upsert proposal), it is not blocked
|
|
79
|
+
* until the list is updated. Treat the mutator blocking as a guard rail,
|
|
80
|
+
* not a security boundary.
|
|
81
|
+
*
|
|
82
|
+
* Limitation: ArrayBuffer views (TypedArrays, DataView) are passed through
|
|
83
|
+
* unfrozen, because the spec forbids freezing a view with elements, and
|
|
84
|
+
* freezing cannot protect the underlying buffer. Their contents remain mutable.
|
|
85
|
+
*/
|
|
86
|
+
function deepFreeze(obj, visited = /* @__PURE__ */ new WeakSet()) {
|
|
87
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
88
|
+
if (ArrayBuffer.isView(obj)) return obj;
|
|
89
|
+
if (visited.has(obj)) return obj;
|
|
90
|
+
visited.add(obj);
|
|
91
|
+
const mutableBuiltInTag = mutableBuiltInTagWithoutInvokingAccessors(obj);
|
|
92
|
+
if (mutableBuiltInTag !== void 0) {
|
|
93
|
+
if (mutableBuiltInTag === "[object Date]") shadowMutators(obj, "Date", DATE_MUTATORS);
|
|
94
|
+
else if (mutableBuiltInTag === "[object Map]") {
|
|
95
|
+
for (const [key, value] of obj) {
|
|
96
|
+
deepFreeze(key, visited);
|
|
97
|
+
deepFreeze(value, visited);
|
|
98
|
+
}
|
|
99
|
+
shadowMutators(obj, "Map", [
|
|
100
|
+
"set",
|
|
101
|
+
"delete",
|
|
102
|
+
"clear"
|
|
103
|
+
]);
|
|
104
|
+
} else if (mutableBuiltInTag === "[object Set]") {
|
|
105
|
+
for (const member of obj) deepFreeze(member, visited);
|
|
106
|
+
shadowMutators(obj, "Set", [
|
|
107
|
+
"add",
|
|
108
|
+
"delete",
|
|
109
|
+
"clear"
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const keys = Reflect.ownKeys(obj);
|
|
114
|
+
for (const key of keys) {
|
|
115
|
+
const value = obj[key];
|
|
116
|
+
if (value !== null && typeof value === "object") deepFreeze(value, visited);
|
|
117
|
+
}
|
|
118
|
+
return Object.freeze(obj);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Deep clone used by `vo()` and the `ValueObject` constructor.
|
|
122
|
+
*
|
|
123
|
+
* Plain objects, arrays, and Map values are walked manually so
|
|
124
|
+
* that symbol-keyed properties survive (which `structuredClone` silently
|
|
125
|
+
* drops; they would otherwise be invisible to `voEquals`, whose
|
|
126
|
+
* `deepEqual` DOES consider symbol keys) and shared references / cycles
|
|
127
|
+
* keep their identity across Map boundaries. Function values throw,
|
|
128
|
+
* preserving `vo()`'s documented data-not-behaviour gate. Built-ins without
|
|
129
|
+
* immutable value semantics throw a descriptive `TypeError`. Custom class
|
|
130
|
+
* instances and subclasses of built-ins are rejected because cloning them
|
|
131
|
+
* without invoking their
|
|
132
|
+
* constructor can silently lose private or non-enumerable state. Map keys
|
|
133
|
+
* and Set members must be primitive because their equality is
|
|
134
|
+
* identity-based and object identity cannot survive defensive cloning.
|
|
135
|
+
* Accessor properties are rejected without invoking them. Admitted atomic
|
|
136
|
+
* built-ins (Date, RegExp and primitive wrappers) delegate to
|
|
137
|
+
* `structuredClone`, brand-verified so a `Symbol.toStringTag` spoofer is
|
|
138
|
+
* walked as the plain object it is. `__proto__` own keys are copied as
|
|
139
|
+
* inert data properties.
|
|
140
|
+
*/
|
|
141
|
+
function cloneForVo(value, visited) {
|
|
142
|
+
if (typeof value === "function") throw new TypeError("vo() does not accept function values: Value Objects are data, not behaviour");
|
|
143
|
+
if (value === null || typeof value !== "object") return value;
|
|
144
|
+
const obj = value;
|
|
145
|
+
if (ArrayBuffer.isView(obj)) throwUnsupportedValueSemantics(builtInTagWithoutInvokingAccessors(obj) ?? "[object ArrayBuffer view]");
|
|
146
|
+
if (visited.has(obj)) return visited.get(obj);
|
|
147
|
+
if (Array.isArray(obj)) {
|
|
148
|
+
if (!hasIntrinsicPrototypeChain(obj, "Array")) throwUnsupportedClassInstance();
|
|
149
|
+
const clone = new Array(obj.length);
|
|
150
|
+
visited.set(obj, clone);
|
|
151
|
+
for (const key of Reflect.ownKeys(obj)) {
|
|
152
|
+
if (key === "length") continue;
|
|
153
|
+
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
|
|
154
|
+
if (descriptor === void 0) continue;
|
|
155
|
+
if (!("value" in descriptor)) throwUnsupportedAccessorProperty();
|
|
156
|
+
if (typeof key === "string" && !descriptor.enumerable) continue;
|
|
157
|
+
descriptor.value = cloneForVo(descriptor.value, visited);
|
|
158
|
+
Object.defineProperty(clone, key, descriptor);
|
|
159
|
+
}
|
|
160
|
+
return clone;
|
|
161
|
+
}
|
|
162
|
+
const tag = builtInTagWithoutInvokingAccessors(obj);
|
|
163
|
+
if (tag !== void 0) {
|
|
164
|
+
if (!hasIntrinsicPrototypeChain(obj)) throwUnsupportedClassInstance();
|
|
165
|
+
if (tag === "[object Map]") {
|
|
166
|
+
const clone = /* @__PURE__ */ new Map();
|
|
167
|
+
visited.set(obj, clone);
|
|
168
|
+
for (const [key, entry] of mapEntries.call(obj)) {
|
|
169
|
+
if (!isPrimitiveValue(key)) throw new TypeError("vo() Map keys must be primitive values to preserve value equality");
|
|
170
|
+
clone.set(key, cloneForVo(entry, visited));
|
|
171
|
+
}
|
|
172
|
+
return clone;
|
|
173
|
+
}
|
|
174
|
+
if (tag === "[object Set]") {
|
|
175
|
+
const clone = /* @__PURE__ */ new Set();
|
|
176
|
+
visited.set(obj, clone);
|
|
177
|
+
for (const member of setValues.call(obj)) {
|
|
178
|
+
if (!isPrimitiveValue(member)) throw new TypeError("vo() Set members must be primitive values to preserve value equality");
|
|
179
|
+
clone.add(member);
|
|
180
|
+
}
|
|
181
|
+
return clone;
|
|
182
|
+
}
|
|
183
|
+
if (tag === "[object Promise]" || tag === "[object WeakMap]" || tag === "[object WeakSet]") throw new TypeError(`vo() cannot clone a ${tag.slice(8, -1)}: Value Objects are plain data`);
|
|
184
|
+
if (tag === "[object Error]" || tag === "[object ArrayBuffer]" || tag === "[object SharedArrayBuffer]") throwUnsupportedValueSemantics(tag);
|
|
185
|
+
if (tag === "[object RegExp]") {
|
|
186
|
+
const regExp = obj;
|
|
187
|
+
if (regExp.global || regExp.sticky) throw new TypeError("vo() cannot accept a global or sticky RegExp: its lastIndex is mutable scan state, not an immutable value");
|
|
188
|
+
}
|
|
189
|
+
const builtInClone = structuredClone(obj);
|
|
190
|
+
visited.set(obj, builtInClone);
|
|
191
|
+
return builtInClone;
|
|
192
|
+
}
|
|
193
|
+
const prototype = Object.getPrototypeOf(obj);
|
|
194
|
+
if (prototype !== null && (!isIntrinsicConstructorPrototype(prototype, "Object") || Object.getPrototypeOf(prototype) !== null)) throwUnsupportedClassInstance();
|
|
195
|
+
const clone = Object.create(prototype === null ? null : Object.prototype);
|
|
196
|
+
visited.set(obj, clone);
|
|
197
|
+
for (const key of Reflect.ownKeys(obj)) {
|
|
198
|
+
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
|
|
199
|
+
if (descriptor === void 0) continue;
|
|
200
|
+
if (!("value" in descriptor)) throwUnsupportedAccessorProperty();
|
|
201
|
+
if (typeof key === "string" && !descriptor.enumerable) continue;
|
|
202
|
+
Object.defineProperty(clone, key, {
|
|
203
|
+
value: cloneForVo(descriptor.value, visited),
|
|
204
|
+
writable: true,
|
|
205
|
+
enumerable: descriptor.enumerable,
|
|
206
|
+
configurable: true
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return clone;
|
|
210
|
+
}
|
|
211
|
+
function throwUnsupportedClassInstance() {
|
|
212
|
+
throw new TypeError("vo() cannot clone custom class instances: Value Objects are plain data");
|
|
213
|
+
}
|
|
214
|
+
function throwUnsupportedAccessorProperty() {
|
|
215
|
+
throw new TypeError("vo() cannot clone accessor properties: Value Objects are plain data");
|
|
216
|
+
}
|
|
217
|
+
function throwUnsupportedValueSemantics(tag) {
|
|
218
|
+
const name = tag.startsWith("[object ") ? tag.slice(8, -1) : tag;
|
|
219
|
+
throw new TypeError(`vo() cannot accept ${name} values: Value Objects require immutable value semantics`);
|
|
220
|
+
}
|
|
221
|
+
function isPrimitiveValue(value) {
|
|
222
|
+
return value === null || typeof value !== "object" && typeof value !== "function";
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Creates a deeply immutable value object from the given data.
|
|
226
|
+
*
|
|
227
|
+
* The input is first deep-cloned, then the clone is frozen, so calling
|
|
228
|
+
* `vo(input)` never freezes the caller's own object graph as a
|
|
229
|
+
* side-effect. Mutating the input afterwards does not bleed into the VO.
|
|
230
|
+
* Symbol-keyed properties are preserved (matching `voEquals`); function
|
|
231
|
+
* values and custom class instances are rejected (Value Objects are plain
|
|
232
|
+
* data, not behaviour-bearing object graphs). Inputs must be trusted and
|
|
233
|
+
* Proxy-free: ECMAScript provides no portable way to identify a transparent
|
|
234
|
+
* Proxy without potentially executing its traps, so `vo()` is not a sandbox
|
|
235
|
+
* for hostile in-process objects. Built-ins that cannot provide immutable,
|
|
236
|
+
* value-based semantics are rejected instead of weakening the VO contract.
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```typescript
|
|
240
|
+
* const nested = { lat: 52.5, lng: 13.4 };
|
|
241
|
+
* const address = vo({ street: "Main St", coordinates: nested });
|
|
242
|
+
* address.coordinates.lat = 99; // ❌ Cannot assign to read-only property
|
|
243
|
+
* nested.lat = 0; // ✅ caller's input still mutable
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
function vo(t) {
|
|
247
|
+
return deepFreeze(cloneForVo(t, /* @__PURE__ */ new WeakMap()));
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Compares two value objects for equality based on their values.
|
|
251
|
+
* Uses deep equality comparison that handles:
|
|
252
|
+
* - Nested objects and arrays
|
|
253
|
+
* - Primitives (including NaN)
|
|
254
|
+
* - Dates, Maps, Sets, RegExp
|
|
255
|
+
* - Symbol keys
|
|
256
|
+
* - Circular references
|
|
257
|
+
*
|
|
258
|
+
* @param a - First value object
|
|
259
|
+
* @param b - Second value object
|
|
260
|
+
* @returns true if both objects have the same values, false otherwise
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```typescript
|
|
264
|
+
* const money1 = vo({ amount: 100, currency: "USD" });
|
|
265
|
+
* const money2 = vo({ amount: 100, currency: "USD" });
|
|
266
|
+
* voEquals(money1, money2); // true
|
|
267
|
+
*
|
|
268
|
+
* const address1 = vo({
|
|
269
|
+
* street: "Main St",
|
|
270
|
+
* coordinates: { lat: 52.5, lng: 13.4 }
|
|
271
|
+
* });
|
|
272
|
+
* const address2 = vo({
|
|
273
|
+
* street: "Main St",
|
|
274
|
+
* coordinates: { lat: 52.5, lng: 13.4 }
|
|
275
|
+
* });
|
|
276
|
+
* voEquals(address1, address2); // true
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
function voEquals(a, b) {
|
|
280
|
+
return deepEqual(a, b);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Compares two value objects for equality while ignoring specified keys.
|
|
284
|
+
* Useful for comparing value objects that contain metadata or optional fields
|
|
285
|
+
* that should not affect equality comparison.
|
|
286
|
+
*
|
|
287
|
+
* @param a - First value object
|
|
288
|
+
* @param b - Second value object
|
|
289
|
+
* @param options - Options specifying which keys to ignore during comparison
|
|
290
|
+
* @returns true if both objects have the same values (after ignoring specified keys), false otherwise
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```typescript
|
|
294
|
+
* // Value object with metadata
|
|
295
|
+
* const address1 = vo({
|
|
296
|
+
* street: "Main St",
|
|
297
|
+
* city: "Berlin",
|
|
298
|
+
* metadata: { createdAt: "2024-01-01", updatedAt: "2024-01-02" }
|
|
299
|
+
* });
|
|
300
|
+
*
|
|
301
|
+
* const address2 = vo({
|
|
302
|
+
* street: "Main St",
|
|
303
|
+
* city: "Berlin",
|
|
304
|
+
* metadata: { createdAt: "2024-01-01", updatedAt: "2024-01-03" }
|
|
305
|
+
* });
|
|
306
|
+
*
|
|
307
|
+
* // Compare ignoring metadata timestamps
|
|
308
|
+
* voEqualsExcept(address1, address2, {
|
|
309
|
+
* ignoreKeys: ["updatedAt"],
|
|
310
|
+
* ignoreKeyPredicate: (key, path) => path.includes("metadata")
|
|
311
|
+
* }); // true
|
|
312
|
+
*
|
|
313
|
+
* // Compare ignoring all metadata
|
|
314
|
+
* voEqualsExcept(address1, address2, {
|
|
315
|
+
* ignoreKeyPredicate: (key, path) => path.includes("metadata")
|
|
316
|
+
* }); // true
|
|
317
|
+
* ```
|
|
318
|
+
*/
|
|
319
|
+
function voEqualsExcept(a, b, options) {
|
|
320
|
+
return deepEqualExcept(a, b, options);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Creates a value object with optional validation.
|
|
324
|
+
* Returns a Result type instead of throwing an error.
|
|
325
|
+
*
|
|
326
|
+
* Note: the Result covers VALIDATION failures only. Non-data values and
|
|
327
|
+
* built-ins without immutable value semantics still throw a
|
|
328
|
+
* `TypeError` from `vo()`; they cannot occur in parsed JSON and signal
|
|
329
|
+
* a programming error, not a validation failure.
|
|
330
|
+
*
|
|
331
|
+
* @param t - The data to convert into a value object
|
|
332
|
+
* @param validate - Validation function that returns true if valid
|
|
333
|
+
* @param errorMessage - Optional custom error message if validation fails
|
|
334
|
+
* @returns Result containing the value object if valid, or an error message if validation fails
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```typescript
|
|
338
|
+
* const result = voWithValidation(
|
|
339
|
+
* { amount: 100, currency: "USD" },
|
|
340
|
+
* (m) => m.amount >= 0 && m.currency.length === 3,
|
|
341
|
+
* "Invalid money: amount must be non-negative and currency must be 3 characters"
|
|
342
|
+
* );
|
|
343
|
+
*
|
|
344
|
+
* if (result.ok) {
|
|
345
|
+
* console.log(result.value); // Use the value object
|
|
346
|
+
* } else {
|
|
347
|
+
* console.error(result.error); // Handle validation error
|
|
348
|
+
* }
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
function voWithValidation(t, validate, errorMessage) {
|
|
352
|
+
if (!validate(t)) return err(errorMessage ?? `Validation failed for value object: ${describeValue(t)}`);
|
|
353
|
+
return ok(vo(t));
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Best-effort rendering of a value for the default validation-failure
|
|
357
|
+
* message. `JSON.stringify` throws for cyclic and BigInt-bearing values, and
|
|
358
|
+
* the error path of a Result-returning function must never throw itself.
|
|
359
|
+
*/
|
|
360
|
+
function describeValue(value) {
|
|
361
|
+
try {
|
|
362
|
+
const json = JSON.stringify(value);
|
|
363
|
+
if (json !== void 0) return json;
|
|
364
|
+
} catch {}
|
|
365
|
+
return String(value);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Abstract base class for creating Value Objects.
|
|
369
|
+
* Value Objects are immutable and defined by their properties.
|
|
370
|
+
*
|
|
371
|
+
* @template T - The shape of the value object's properties
|
|
372
|
+
*/
|
|
373
|
+
var ValueObject = class {
|
|
374
|
+
props;
|
|
375
|
+
/**
|
|
376
|
+
* Creates a new ValueObject.
|
|
377
|
+
* The plain-data properties are deep-cloned and then deeply
|
|
378
|
+
* frozen, so the caller's own object graph is never frozen or mutated,
|
|
379
|
+
* and later mutation of the input does not bleed into the value object.
|
|
380
|
+
*
|
|
381
|
+
* @param props - The properties of the value object
|
|
382
|
+
* @example
|
|
383
|
+
* ```ts
|
|
384
|
+
* class Money extends ValueObject<{ amount: number; currency: string }> {
|
|
385
|
+
* constructor(props: { amount: number; currency: string }) {
|
|
386
|
+
* super(props);
|
|
387
|
+
* }
|
|
388
|
+
*
|
|
389
|
+
* protected validate(props: { amount: number; currency: string }): void {
|
|
390
|
+
* if (props.amount < 0) throw new Error("Amount cannot be negative");
|
|
391
|
+
* }
|
|
392
|
+
* }
|
|
393
|
+
* ```
|
|
394
|
+
*/
|
|
395
|
+
constructor(props) {
|
|
396
|
+
this.validate(props);
|
|
397
|
+
this.props = deepFreeze(cloneForVo(props, /* @__PURE__ */ new WeakMap()));
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Optional validation hook that can be overridden by subclasses.
|
|
401
|
+
* Should throw an error if validation fails.
|
|
402
|
+
*
|
|
403
|
+
* @param props - The properties to validate
|
|
404
|
+
* @throws Error if validation fails
|
|
405
|
+
*/
|
|
406
|
+
validate(props) {}
|
|
407
|
+
/**
|
|
408
|
+
* Checks if this value object is equal to another.
|
|
409
|
+
* Uses deep equality comparison on the properties and checks for constructor equality.
|
|
410
|
+
*
|
|
411
|
+
* @param other - The other value object to compare
|
|
412
|
+
* @returns true if the properties are deeply equal and constructors match
|
|
413
|
+
*/
|
|
414
|
+
equals(other) {
|
|
415
|
+
if (other === null || other === void 0) return false;
|
|
416
|
+
if (this.constructor !== other.constructor) return false;
|
|
417
|
+
return deepEqual(this.props, other.props);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Creates a clone of the value object with optional property overrides.
|
|
421
|
+
*
|
|
422
|
+
* @param props - Optional properties to override
|
|
423
|
+
* @returns A new instance of the value object
|
|
424
|
+
*/
|
|
425
|
+
clone(props) {
|
|
426
|
+
const Constructor = this.constructor;
|
|
427
|
+
const merged = {
|
|
428
|
+
...this.props,
|
|
429
|
+
...props || {}
|
|
430
|
+
};
|
|
431
|
+
for (const key of Reflect.ownKeys(this.props)) {
|
|
432
|
+
const descriptor = Object.getOwnPropertyDescriptor(this.props, key);
|
|
433
|
+
if (descriptor === void 0 || descriptor.enumerable) continue;
|
|
434
|
+
if (props && Object.hasOwn(props, key)) continue;
|
|
435
|
+
Object.defineProperty(merged, key, {
|
|
436
|
+
value: this.props[key],
|
|
437
|
+
writable: true,
|
|
438
|
+
enumerable: false,
|
|
439
|
+
configurable: true
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return new Constructor(merged);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Serializes the value object to its raw properties for JSON operations.
|
|
446
|
+
*
|
|
447
|
+
* @returns The raw properties object
|
|
448
|
+
*/
|
|
449
|
+
toJSON() {
|
|
450
|
+
return this.props;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region src/aggregate/clock.ts
|
|
456
|
+
/** Immutable library default captured by the default domain-event factory. */
|
|
457
|
+
const defaultClockFactory = () => /* @__PURE__ */ new Date();
|
|
458
|
+
/** Internal defensive event-clock read. */
|
|
459
|
+
function readClock(factory) {
|
|
460
|
+
const reading = factory();
|
|
461
|
+
const value = reading instanceof Date ? reading.getTime() : NaN;
|
|
462
|
+
if (!Number.isFinite(value)) throw new TypeError("domain-event clock must return a valid Date");
|
|
463
|
+
return new Date(value);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/aggregate/domain-event-errors.ts
|
|
468
|
+
/**
|
|
469
|
+
* Stable contract error for malformed domain-event data.
|
|
470
|
+
*
|
|
471
|
+
* It remains a `TypeError` for JavaScript callers while exposing a code and
|
|
472
|
+
* field that do not depend on the wording of the human-readable message.
|
|
473
|
+
*/
|
|
474
|
+
var DomainEventValidationError = class extends TypeError {
|
|
475
|
+
code;
|
|
476
|
+
field;
|
|
477
|
+
name;
|
|
478
|
+
constructor(code, field, message) {
|
|
479
|
+
super(message);
|
|
480
|
+
this.code = code;
|
|
481
|
+
this.field = field;
|
|
482
|
+
this.name = code;
|
|
483
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
var SnapshotTimeValidationError = class extends TypeError {
|
|
487
|
+
name = "SNAPSHOT_TIME_INVALID";
|
|
488
|
+
code = "SNAPSHOT_TIME_INVALID";
|
|
489
|
+
field = "snapshotAt";
|
|
490
|
+
constructor() {
|
|
491
|
+
super("snapshotAt must be a valid Date");
|
|
492
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
//#endregion
|
|
497
|
+
//#region src/aggregate/domain-event.ts
|
|
498
|
+
const defaultEventIdFactory = () => crypto.randomUUID();
|
|
499
|
+
/**
|
|
500
|
+
* Creates an immutable, instance-bound domain-event factory.
|
|
501
|
+
*
|
|
502
|
+
* The supplied functions are read once and captured by value. The returned
|
|
503
|
+
* object is frozen, so another request, test, or library cannot replace its
|
|
504
|
+
* policy. Its {@link DomainEventFactory.createStamp} method is the
|
|
505
|
+
* application-shell bridge that records an accepted aggregate decision.
|
|
506
|
+
* Passing the factory through `AggregateConfig`
|
|
507
|
+
* enables the explicitly named convenience methods, whose defaults read time
|
|
508
|
+
* and randomness.
|
|
509
|
+
*
|
|
510
|
+
* @example
|
|
511
|
+
* ```ts
|
|
512
|
+
* const domainEvents = createDomainEventFactory({
|
|
513
|
+
* eventIdFactory: () => uuidv7(),
|
|
514
|
+
* clock: () => new Date(),
|
|
515
|
+
* });
|
|
516
|
+
* order.confirm();
|
|
517
|
+
* recordPendingEvents(order, domainEvents);
|
|
518
|
+
* ```
|
|
519
|
+
*/
|
|
520
|
+
function createDomainEventFactory(options = {}) {
|
|
521
|
+
const eventIdFactory = options.eventIdFactory ?? defaultEventIdFactory;
|
|
522
|
+
const clock = options.clock ?? defaultClockFactory;
|
|
523
|
+
const create = ((type, payload, createOptions) => mintDomainEvent(type, payload, createOptions, eventIdFactory, clock));
|
|
524
|
+
const createStamp = (stampOptions = {}) => {
|
|
525
|
+
const explicitOccurredAt = stampOptions.occurredAt === void 0 ? void 0 : copyValidEventDate(stampOptions.occurredAt);
|
|
526
|
+
if (stampOptions.eventId !== void 0) assertNonBlankEventField(stampOptions.eventId, "eventId", "EVENT_ID_INVALID");
|
|
527
|
+
const eventId = stampOptions.eventId ?? eventIdFactory();
|
|
528
|
+
assertNonBlankEventField(eventId, "eventId", "EVENT_ID_INVALID");
|
|
529
|
+
const owned = deepFreeze({
|
|
530
|
+
eventId,
|
|
531
|
+
occurredAt: explicitOccurredAt ?? readEventClock(clock),
|
|
532
|
+
metadata: guardedMetadataClone(stampOptions.metadata)
|
|
533
|
+
});
|
|
534
|
+
FACTORY_OWNED_EVENT_STAMPS.add(owned);
|
|
535
|
+
return owned;
|
|
536
|
+
};
|
|
537
|
+
return Object.freeze({
|
|
538
|
+
createStamp,
|
|
539
|
+
create,
|
|
540
|
+
now: () => readClock(clock)
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Immutable UUID-v4/platform-clock factory used by the top-level
|
|
545
|
+
* {@link createDomainEvent}. It cannot be reconfigured; construct an instance
|
|
546
|
+
* with {@link createDomainEventFactory} for custom policy.
|
|
547
|
+
*/
|
|
548
|
+
const defaultDomainEventFactory = createDomainEventFactory();
|
|
549
|
+
/**
|
|
550
|
+
* Creates a domain event with default values.
|
|
551
|
+
* Sets occurredAt to current date and version to 1 if not provided.
|
|
552
|
+
*
|
|
553
|
+
* **Input ownership.** The event is deeply frozen, and `payload` and
|
|
554
|
+
* `metadata` are deep-cloned first, so the caller's own objects are never
|
|
555
|
+
* frozen in place and later mutation of them does not bleed into the
|
|
556
|
+
* event (same contract as `vo()`). The clone follows the plain-data event
|
|
557
|
+
* contract via `structuredClone`: functions, Promise, and WeakMap/WeakSet
|
|
558
|
+
* values throw a `TypeError`; symbol-keyed properties are not carried
|
|
559
|
+
* over.
|
|
560
|
+
*
|
|
561
|
+
* **For aggregate-internal events, prefer `this.createEvent(...)` on
|
|
562
|
+
* `AggregateRoot` / `EventSourcedAggregate`.** That helper auto-injects
|
|
563
|
+
* `aggregateId` (from `this.id`) and `aggregateType` (from the
|
|
564
|
+
* aggregate's declared `aggregateType` property), which downstream
|
|
565
|
+
* consumers (outbox dispatchers, projection handlers, audit logs)
|
|
566
|
+
* route by. The `withCommit` harvest boundary now validates both fields
|
|
567
|
+
* are present and throws if they're missing, so a direct
|
|
568
|
+
* `createDomainEvent(...)` call inside an aggregate that forgets the
|
|
569
|
+
* options is caught at runtime. Record pending decisions in the application
|
|
570
|
+
* shell before repository persistence or outbox harvest.
|
|
571
|
+
*
|
|
572
|
+
* Use `createDomainEvent(...)` directly for events that don't belong to
|
|
573
|
+
* an aggregate: system events, integration events, configuration events,
|
|
574
|
+
* test fixtures. For those, set `aggregateId` / `aggregateType` in
|
|
575
|
+
* `options` if downstream consumers expect routing metadata.
|
|
576
|
+
*
|
|
577
|
+
* @param type - The event type
|
|
578
|
+
* @param payload - The event payload
|
|
579
|
+
* @param options - Optional event configuration (including `aggregateId`
|
|
580
|
+
* and `aggregateType` for routing)
|
|
581
|
+
* @returns A domain event
|
|
582
|
+
*
|
|
583
|
+
* @example
|
|
584
|
+
* ```typescript
|
|
585
|
+
* const event = createDomainEvent("OrderCreated", { orderId: "123" });
|
|
586
|
+
* ```
|
|
587
|
+
*/
|
|
588
|
+
const MINTED_EVENTS = /* @__PURE__ */ new WeakSet();
|
|
589
|
+
const UNCOMMITTED_EVENTS = /* @__PURE__ */ new WeakSet();
|
|
590
|
+
const FACTORY_OWNED_EVENT_STAMPS = /* @__PURE__ */ new WeakSet();
|
|
591
|
+
const MINT_BRAND = Symbol.for("@shirudo/ddd-kit.mintedEvent");
|
|
592
|
+
const UNCOMMITTED_BRAND = Symbol.for("@shirudo/ddd-kit.uncommittedEvent");
|
|
593
|
+
function stampMintBrand(event) {
|
|
594
|
+
Object.defineProperty(event, MINT_BRAND, {
|
|
595
|
+
value: true,
|
|
596
|
+
enumerable: false,
|
|
597
|
+
writable: false,
|
|
598
|
+
configurable: false
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
function stampUncommittedBrand(event) {
|
|
602
|
+
Object.defineProperty(event, UNCOMMITTED_BRAND, {
|
|
603
|
+
value: true,
|
|
604
|
+
enumerable: false,
|
|
605
|
+
writable: false,
|
|
606
|
+
configurable: false
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
function isFactoryOwnedDomainEventStamp(stamp) {
|
|
610
|
+
return FACTORY_OWNED_EVENT_STAMPS.has(stamp);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Whether `event` came out of {@link createDomainEvent} (or a helper
|
|
614
|
+
* built on it, such as the aggregate `createEvent` helper), i.e. is deeply frozen with
|
|
615
|
+
* defensively copied payload and metadata. Two tiers: events of THIS
|
|
616
|
+
* loaded copy of the kit are verified unforgeably via the module's
|
|
617
|
+
* WeakSet; events minted by ANOTHER copy (duplicate dependency, dual
|
|
618
|
+
* CJS/ESM load) are recognized cooperatively via a global-registry
|
|
619
|
+
* brand. Module-internal export for the aggregate recording paths;
|
|
620
|
+
* not part of the package entries.
|
|
621
|
+
*/
|
|
622
|
+
function isMintedEvent(event) {
|
|
623
|
+
return MINTED_EVENTS.has(event) || event[MINT_BRAND] === true;
|
|
624
|
+
}
|
|
625
|
+
/** Whether a value was created by {@link createUncommittedDomainEvent}. */
|
|
626
|
+
function isUncommittedDomainEvent(event) {
|
|
627
|
+
return UNCOMMITTED_EVENTS.has(event) || event[UNCOMMITTED_BRAND] === true;
|
|
628
|
+
}
|
|
629
|
+
function createUncommittedDomainEvent(type, payload, options) {
|
|
630
|
+
assertProducerOwnedEventFields(type, options);
|
|
631
|
+
const event = {
|
|
632
|
+
type,
|
|
633
|
+
aggregateId: options?.aggregateId,
|
|
634
|
+
aggregateType: options?.aggregateType,
|
|
635
|
+
payload: cloneOwnedEventData(payload, "payload"),
|
|
636
|
+
version: options?.version ?? 1
|
|
637
|
+
};
|
|
638
|
+
stampUncommittedBrand(event);
|
|
639
|
+
const uncommitted = deepFreeze(event);
|
|
640
|
+
UNCOMMITTED_EVENTS.add(uncommitted);
|
|
641
|
+
return uncommitted;
|
|
642
|
+
}
|
|
643
|
+
/** Brands and freezes a kit-derived copy of an uncommitted event. */
|
|
644
|
+
function adoptUncommittedDomainEvent(copy) {
|
|
645
|
+
stampUncommittedBrand(copy);
|
|
646
|
+
Object.freeze(copy);
|
|
647
|
+
UNCOMMITTED_EVENTS.add(copy);
|
|
648
|
+
return copy;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Attaches shell-owned recording data to an accepted aggregate decision.
|
|
652
|
+
*
|
|
653
|
+
* The decision supplies the domain type, payload, source address, and payload
|
|
654
|
+
* schema version. The stamp supplies only event identity, recording time, and
|
|
655
|
+
* trace metadata.
|
|
656
|
+
*/
|
|
657
|
+
function recordDomainEvent(event, stamp) {
|
|
658
|
+
if (!isUncommittedDomainEvent(event)) throw new TypeError("recordDomainEvent requires an event created by createUncommittedDomainEvent");
|
|
659
|
+
if (isFactoryOwnedDomainEventStamp(stamp)) return mintRecordedEvent(event, stamp.eventId, stamp.occurredAt, stamp.metadata);
|
|
660
|
+
assertNonBlankEventField(stamp.eventId, "eventId", "EVENT_ID_INVALID");
|
|
661
|
+
const occurredAt = deepFreeze(copyValidEventDate(stamp.occurredAt));
|
|
662
|
+
const metadata = guardedMetadataClone(stamp.metadata);
|
|
663
|
+
return mintRecordedEvent(event, stamp.eventId, occurredAt, metadata === void 0 ? void 0 : deepFreeze(metadata));
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Single mint tail for both stamp provenances. The stamp fields arrive
|
|
667
|
+
* pre-validated, copied, and frozen (by `createStamp` for factory-owned
|
|
668
|
+
* stamps, by `recordDomainEvent` for caller-built stamps); the uncommitted
|
|
669
|
+
* event's payload is already defensively cloned and deeply frozen by its
|
|
670
|
+
* constructor and is shared instead of paying a second deep copy per event.
|
|
671
|
+
*/
|
|
672
|
+
function mintRecordedEvent(event, eventId, occurredAt, metadata) {
|
|
673
|
+
assertProducerOwnedEventFields(event.type, event);
|
|
674
|
+
const recorded = {
|
|
675
|
+
eventId,
|
|
676
|
+
type: event.type,
|
|
677
|
+
aggregateId: event.aggregateId,
|
|
678
|
+
aggregateType: event.aggregateType,
|
|
679
|
+
payload: event.payload,
|
|
680
|
+
occurredAt,
|
|
681
|
+
version: event.version,
|
|
682
|
+
metadata
|
|
683
|
+
};
|
|
684
|
+
stampMintBrand(recorded);
|
|
685
|
+
Object.freeze(recorded);
|
|
686
|
+
MINTED_EVENTS.add(recorded);
|
|
687
|
+
return recorded;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Brands, freezes, and registers a kit-derived copy of a minted event
|
|
691
|
+
* (e.g. the address-stamped copy `apply()` creates) as minted itself.
|
|
692
|
+
* The copy shares the already-frozen payload/metadata of its source,
|
|
693
|
+
* so the mint guarantee carries over. Stamping the cooperative brand
|
|
694
|
+
* before freezing keeps the copy recognizable by another loaded kit
|
|
695
|
+
* instance as well as by this instance's WeakSet. Module-internal
|
|
696
|
+
* export; not part of the package entries.
|
|
697
|
+
*/
|
|
698
|
+
function adoptMintedEvent(copy) {
|
|
699
|
+
stampMintBrand(copy);
|
|
700
|
+
Object.freeze(copy);
|
|
701
|
+
MINTED_EVENTS.add(copy);
|
|
702
|
+
return copy;
|
|
703
|
+
}
|
|
704
|
+
function createDomainEvent(type, payload, options) {
|
|
705
|
+
return defaultDomainEventFactory.create(type, payload, options);
|
|
706
|
+
}
|
|
707
|
+
function createDomainEventFromFacts(type, payload, options) {
|
|
708
|
+
if (options?.eventId === void 0) missingExplicitEventId();
|
|
709
|
+
if (options.occurredAt === void 0) missingExplicitOccurredAt();
|
|
710
|
+
return mintDomainEvent(type, payload, options, missingExplicitEventId, missingExplicitOccurredAt);
|
|
711
|
+
}
|
|
712
|
+
function missingExplicitEventId() {
|
|
713
|
+
throw new DomainEventValidationError("EVENT_ID_REQUIRED", "eventId", "createDomainEventFromFacts requires an explicit eventId");
|
|
714
|
+
}
|
|
715
|
+
function missingExplicitOccurredAt() {
|
|
716
|
+
throw new DomainEventValidationError("EVENT_OCCURRED_AT_REQUIRED", "occurredAt", "createDomainEventFromFacts requires an explicit occurredAt");
|
|
717
|
+
}
|
|
718
|
+
function mintDomainEvent(type, payload, options, eventIdFactory, clock) {
|
|
719
|
+
assertProducerOwnedEventFields(type, options);
|
|
720
|
+
const eventId = options?.eventId ?? eventIdFactory();
|
|
721
|
+
assertNonBlankEventField(eventId, "eventId", "EVENT_ID_INVALID");
|
|
722
|
+
const occurredAt = options?.occurredAt === void 0 ? readEventClock(clock) : copyValidEventDate(options.occurredAt);
|
|
723
|
+
const version = options?.version ?? 1;
|
|
724
|
+
const event = {
|
|
725
|
+
eventId,
|
|
726
|
+
type,
|
|
727
|
+
aggregateId: options?.aggregateId,
|
|
728
|
+
aggregateType: options?.aggregateType,
|
|
729
|
+
payload: cloneOwnedEventData(payload, "payload"),
|
|
730
|
+
occurredAt,
|
|
731
|
+
version,
|
|
732
|
+
metadata: guardedMetadataClone(options?.metadata)
|
|
733
|
+
};
|
|
734
|
+
stampMintBrand(event);
|
|
735
|
+
const minted = deepFreeze(event);
|
|
736
|
+
MINTED_EVENTS.add(minted);
|
|
737
|
+
return minted;
|
|
738
|
+
}
|
|
739
|
+
function assertProducerOwnedEventFields(type, options) {
|
|
740
|
+
assertNonBlankEventField(type, "type", "EVENT_TYPE_INVALID");
|
|
741
|
+
const version = options?.version ?? 1;
|
|
742
|
+
if (!Number.isSafeInteger(version) || typeof version !== "number" || version < 1) throw new DomainEventValidationError("EVENT_SCHEMA_VERSION_INVALID", "version", "domain-event version must be a safe integer greater than or equal to 1");
|
|
743
|
+
if (options?.aggregateId !== void 0) assertNonBlankEventField(options.aggregateId, "aggregateId", "EVENT_ADDRESS_INVALID");
|
|
744
|
+
if (options?.aggregateType !== void 0) assertNonBlankEventField(options.aggregateType, "aggregateType", "EVENT_ADDRESS_INVALID");
|
|
745
|
+
}
|
|
746
|
+
function assertNonBlankEventField(value, field, code) {
|
|
747
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new DomainEventValidationError(code, field, `domain-event ${field} must be a non-blank string`);
|
|
748
|
+
}
|
|
749
|
+
function copyValidEventDate(value) {
|
|
750
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) throw new DomainEventValidationError("EVENT_OCCURRED_AT_INVALID", "occurredAt", "domain-event occurredAt must be a valid Date");
|
|
751
|
+
return new Date(value.getTime());
|
|
752
|
+
}
|
|
753
|
+
function readEventClock(clock) {
|
|
754
|
+
return copyValidEventDate(clock());
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Deep-clones caller-supplied event data (payload, metadata) before the
|
|
758
|
+
* event is frozen, so `createDomainEvent` never freezes or aliases the
|
|
759
|
+
* caller's own object graph. Primitives pass through unchanged.
|
|
760
|
+
*
|
|
761
|
+
* Uses `structuredClone`, which matches the documented plain-data event
|
|
762
|
+
* contract: functions, Promise, and WeakMap/WeakSet values throw a
|
|
763
|
+
* descriptive `TypeError` (they are not data); symbol-keyed properties
|
|
764
|
+
* are not carried over; a class instance would silently lose its
|
|
765
|
+
* prototype, which the plain-data contract already rules out.
|
|
766
|
+
*/
|
|
767
|
+
function cloneOwnedEventData(value, field) {
|
|
768
|
+
if (typeof value === "function") throw new TypeError(`createDomainEvent: ${field} must not be a function: domain events are plain data`);
|
|
769
|
+
if (value === null || typeof value !== "object") return value;
|
|
770
|
+
assertNoBinaryData(value, field);
|
|
771
|
+
try {
|
|
772
|
+
return structuredClone(value);
|
|
773
|
+
} catch (cause) {
|
|
774
|
+
throw new TypeError(`createDomainEvent: ${field} must be plain, structured-cloneable data (no functions, Promises, or WeakMap/WeakSet values): domain events are plain data`, { cause });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function isBinaryData(value) {
|
|
778
|
+
return ArrayBuffer.isView(value) || value instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Walks caller-supplied event data and rejects binary buffers anywhere
|
|
782
|
+
* in the graph (TypedArray, DataView, ArrayBuffer, SharedArrayBuffer):
|
|
783
|
+
* they are mutable by construction, so the deep-freeze that backs the
|
|
784
|
+
* mint guarantee cannot cover them. Runs before the structured clone,
|
|
785
|
+
* on the small plain-data graphs events are documented to carry.
|
|
786
|
+
*/
|
|
787
|
+
function assertNoBinaryData(value, field, visited = /* @__PURE__ */ new WeakSet()) {
|
|
788
|
+
if (value === null || typeof value !== "object") return;
|
|
789
|
+
if (isBinaryData(value)) throw new TypeError(`createDomainEvent: ${field} must not contain binary buffers (TypedArray, DataView, ArrayBuffer, SharedArrayBuffer): they stay mutable under freezing and do not survive JSON. Encode binary as a string (base64/hex) or store it outside the event.`);
|
|
790
|
+
if (visited.has(value)) return;
|
|
791
|
+
visited.add(value);
|
|
792
|
+
if (value instanceof Map) {
|
|
793
|
+
for (const [k, v] of value) {
|
|
794
|
+
assertNoBinaryData(k, field, visited);
|
|
795
|
+
assertNoBinaryData(v, field, visited);
|
|
796
|
+
}
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (value instanceof Set) {
|
|
800
|
+
for (const v of value) assertNoBinaryData(v, field, visited);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (Array.isArray(value)) {
|
|
804
|
+
for (const v of value) assertNoBinaryData(v, field, visited);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
for (const key of Object.keys(value)) assertNoBinaryData(value[key], field, visited);
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Copies metadata from a source event to a new event.
|
|
811
|
+
* Useful for maintaining correlation chains in event-driven architectures.
|
|
812
|
+
*
|
|
813
|
+
* @example
|
|
814
|
+
* ```typescript
|
|
815
|
+
* const newEvent = createDomainEvent(
|
|
816
|
+
* "OrderShipped",
|
|
817
|
+
* { orderId: "123" },
|
|
818
|
+
* { metadata: copyMetadata(previousEvent, { causationId: previousEvent.type }) }
|
|
819
|
+
* );
|
|
820
|
+
* ```
|
|
821
|
+
*/
|
|
822
|
+
function copyMetadata(sourceEvent, additionalMetadata) {
|
|
823
|
+
if (sourceEvent.metadata !== void 0) assertNoHostileOwnProtoKey(sourceEvent.metadata, "Event metadata");
|
|
824
|
+
if (additionalMetadata !== void 0) assertNoHostileOwnProtoKey(additionalMetadata, "Event metadata");
|
|
825
|
+
return {
|
|
826
|
+
...sourceEvent.metadata ?? {},
|
|
827
|
+
...additionalMetadata ?? {}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Merges multiple metadata objects into one.
|
|
832
|
+
* Later metadata objects override earlier ones for the same keys.
|
|
833
|
+
*
|
|
834
|
+
* @example
|
|
835
|
+
* ```typescript
|
|
836
|
+
* const metadata = mergeMetadata(
|
|
837
|
+
* { correlationId: "corr-123" },
|
|
838
|
+
* { userId: "user-456" },
|
|
839
|
+
* { source: "order-service" }
|
|
840
|
+
* );
|
|
841
|
+
* ```
|
|
842
|
+
*/
|
|
843
|
+
function mergeMetadata(...metadataObjects) {
|
|
844
|
+
const merged = {};
|
|
845
|
+
for (const metadata of metadataObjects) {
|
|
846
|
+
if (!metadata) continue;
|
|
847
|
+
assertNoHostileOwnProtoKey(metadata, "Event metadata");
|
|
848
|
+
for (const key of Reflect.ownKeys(metadata)) {
|
|
849
|
+
if (!Object.getOwnPropertyDescriptor(metadata, key)?.enumerable) continue;
|
|
850
|
+
Object.defineProperty(merged, key, {
|
|
851
|
+
value: metadata[key],
|
|
852
|
+
writable: true,
|
|
853
|
+
enumerable: true,
|
|
854
|
+
configurable: true
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return merged;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Clones event metadata with the loud `__proto__` rejection applied at
|
|
862
|
+
* the SOURCE: structuredClone preserves an own `__proto__` data key, so
|
|
863
|
+
* without this guard a hostile envelope would ride into the frozen
|
|
864
|
+
* event and re-arm downstream.
|
|
865
|
+
*/
|
|
866
|
+
function guardedMetadataClone(metadata) {
|
|
867
|
+
if (metadata !== void 0) assertNoHostileOwnProtoKey(metadata, "Event metadata");
|
|
868
|
+
return cloneOwnedEventData(metadata, "metadata");
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
//#endregion
|
|
872
|
+
//#region src/events/ports.ts
|
|
873
|
+
/**
|
|
874
|
+
* Discriminates a {@link DispatchTrackingOutbox} from a plain
|
|
875
|
+
* {@link Outbox} at runtime. The single source of truth for the check;
|
|
876
|
+
* the dispatcher and the contract suite both use it, so what counts as
|
|
877
|
+
* a tracking outbox cannot drift between them. Both tracking methods
|
|
878
|
+
* must be present: a plain adapter that happens to expose an unrelated
|
|
879
|
+
* `markFailed` helper must not be mistaken for one that implements the
|
|
880
|
+
* tracking protocol and then be fed `(dispatchId, error)` arguments it
|
|
881
|
+
* never asked for. Internal plumbing, not exported from the package
|
|
882
|
+
* entries.
|
|
883
|
+
*/
|
|
884
|
+
function isDispatchTrackingOutbox(outbox) {
|
|
885
|
+
const candidate = outbox;
|
|
886
|
+
return typeof candidate.markFailed === "function" && typeof candidate.deadLetters === "function";
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
//#endregion
|
|
890
|
+
export { deepFreeze as _, createDomainEvent as a, voEqualsExcept as b, createUncommittedDomainEvent as c, isUncommittedDomainEvent as d, mergeMetadata as f, ValueObject as g, SnapshotTimeValidationError as h, copyMetadata as i, defaultDomainEventFactory as l, DomainEventValidationError as m, adoptMintedEvent as n, createDomainEventFactory as o, recordDomainEvent as p, adoptUncommittedDomainEvent as r, createDomainEventFromFacts as s, isDispatchTrackingOutbox as t, isMintedEvent as u, vo as v, voWithValidation as x, voEquals as y };
|
|
891
|
+
//# sourceMappingURL=ports.js.map
|