@shirudo/ddd-kit 2.0.0 → 3.0.0-rc.10
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 +191 -53
- package/dist/chunks/kit-errors.d.ts +1081 -0
- package/dist/chunks/kit-errors.js +1165 -0
- package/dist/chunks/kit-errors.js.map +1 -0
- package/dist/chunks/ports.js +1815 -0
- package/dist/chunks/ports.js.map +1 -0
- package/dist/chunks/snapshot-store.d.ts +2397 -0
- package/dist/http.d.ts +64 -52
- package/dist/http.js +54 -20
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +4011 -2367
- package/dist/index.js +7091 -2370
- 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/public-errors.d.ts +102 -0
- package/dist/public-errors.js +215 -0
- package/dist/public-errors.js.map +1 -0
- package/dist/testing.d.ts +631 -252
- package/dist/testing.js +2983 -782
- package/dist/testing.js.map +1 -1
- package/package.json +46 -29
- package/dist/aggregate-CePTINEt.d.ts +0 -722
- package/dist/presentation.d.ts +0 -54
- package/dist/presentation.js +0 -45
- package/dist/presentation.js.map +0 -1
- package/dist/utils.d.ts +0 -105
- package/dist/utils.js +0 -389
- package/dist/utils.js.map +0 -1
|
@@ -0,0 +1,1815 @@
|
|
|
1
|
+
import { J as assertNoHostileOwnProtoKey } from "./kit-errors.js";
|
|
2
|
+
import { err, ok } from "@shirudo/result";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/structural/is-built-in.ts
|
|
5
|
+
/**
|
|
6
|
+
* Set of `Object.prototype.toString.call(x)` tags that the library treats
|
|
7
|
+
* as built-in atomic types. Members of this set are compared/cloned by
|
|
8
|
+
* reference (or with type-specific logic) rather than walked structurally.
|
|
9
|
+
*
|
|
10
|
+
* Detection is tag-based, since `Object.prototype.toString` gives the same
|
|
11
|
+
* answer across realms (an iframe's `Date` has the same tag as the main
|
|
12
|
+
* window's `Date`), and then brand-verified via internal-slot probes,
|
|
13
|
+
* because `Symbol.toStringTag` lets any plain object claim a built-in tag.
|
|
14
|
+
* The previous strategy also checked `globalThis[name] === constructor`
|
|
15
|
+
* and a `proto !== Object.prototype` heuristic; both broke for cross-realm
|
|
16
|
+
* objects and the latter additionally misclassified ordinary user classes
|
|
17
|
+
* as built-ins.
|
|
18
|
+
*/
|
|
19
|
+
const BUILT_IN_TAGS = /* @__PURE__ */ new Set([
|
|
20
|
+
"[object Date]",
|
|
21
|
+
"[object RegExp]",
|
|
22
|
+
"[object Map]",
|
|
23
|
+
"[object Set]",
|
|
24
|
+
"[object WeakMap]",
|
|
25
|
+
"[object WeakSet]",
|
|
26
|
+
"[object Promise]",
|
|
27
|
+
"[object Error]",
|
|
28
|
+
"[object Boolean]",
|
|
29
|
+
"[object Number]",
|
|
30
|
+
"[object String]",
|
|
31
|
+
"[object BigInt]",
|
|
32
|
+
"[object ArrayBuffer]",
|
|
33
|
+
"[object SharedArrayBuffer]",
|
|
34
|
+
"[object DataView]"
|
|
35
|
+
]);
|
|
36
|
+
function intrinsicGetter(proto, prop) {
|
|
37
|
+
const get = Object.getOwnPropertyDescriptor(proto, prop)?.get;
|
|
38
|
+
if (!get) throw new Error(`missing intrinsic getter for ${prop}`);
|
|
39
|
+
return get;
|
|
40
|
+
}
|
|
41
|
+
const dateGetTime = Date.prototype.getTime;
|
|
42
|
+
const mapSizeGet = intrinsicGetter(Map.prototype, "size");
|
|
43
|
+
const setSizeGet = intrinsicGetter(Set.prototype, "size");
|
|
44
|
+
const weakMapHas = WeakMap.prototype.has;
|
|
45
|
+
const weakSetHas = WeakSet.prototype.has;
|
|
46
|
+
const dataViewByteLengthGet = intrinsicGetter(DataView.prototype, "byteLength");
|
|
47
|
+
const arrayBufferByteLengthGet = intrinsicGetter(ArrayBuffer.prototype, "byteLength");
|
|
48
|
+
const sharedArrayBufferByteLengthGet = typeof SharedArrayBuffer === "undefined" ? void 0 : intrinsicGetter(SharedArrayBuffer.prototype, "byteLength");
|
|
49
|
+
const regExpSourceGet = intrinsicGetter(RegExp.prototype, "source");
|
|
50
|
+
const booleanValueOf = Boolean.prototype.valueOf;
|
|
51
|
+
const numberValueOf = Number.prototype.valueOf;
|
|
52
|
+
const stringValueOf = String.prototype.valueOf;
|
|
53
|
+
const bigIntValueOf = BigInt.prototype.valueOf;
|
|
54
|
+
const functionToString = Function.prototype.toString;
|
|
55
|
+
const PROBE_KEY = {};
|
|
56
|
+
const intrinsicConstructorSources = new Map([
|
|
57
|
+
"Object",
|
|
58
|
+
"Array",
|
|
59
|
+
"Date",
|
|
60
|
+
"RegExp",
|
|
61
|
+
"Map",
|
|
62
|
+
"Set",
|
|
63
|
+
"WeakMap",
|
|
64
|
+
"WeakSet",
|
|
65
|
+
"Promise",
|
|
66
|
+
"Error",
|
|
67
|
+
"EvalError",
|
|
68
|
+
"RangeError",
|
|
69
|
+
"ReferenceError",
|
|
70
|
+
"SyntaxError",
|
|
71
|
+
"TypeError",
|
|
72
|
+
"URIError",
|
|
73
|
+
"AggregateError",
|
|
74
|
+
"Boolean",
|
|
75
|
+
"Number",
|
|
76
|
+
"String",
|
|
77
|
+
"BigInt",
|
|
78
|
+
"ArrayBuffer",
|
|
79
|
+
"SharedArrayBuffer",
|
|
80
|
+
"DataView",
|
|
81
|
+
"Int8Array",
|
|
82
|
+
"Uint8Array",
|
|
83
|
+
"Uint8ClampedArray",
|
|
84
|
+
"Int16Array",
|
|
85
|
+
"Uint16Array",
|
|
86
|
+
"Int32Array",
|
|
87
|
+
"Uint32Array",
|
|
88
|
+
"Float32Array",
|
|
89
|
+
"Float64Array",
|
|
90
|
+
"BigInt64Array",
|
|
91
|
+
"BigUint64Array"
|
|
92
|
+
].flatMap((name) => {
|
|
93
|
+
const intrinsic = Object.getOwnPropertyDescriptor(globalThis, name)?.value;
|
|
94
|
+
return typeof intrinsic === "function" ? [[name, functionToString.call(intrinsic)]] : [];
|
|
95
|
+
}));
|
|
96
|
+
const intrinsicConstructorSourceSet = new Set(intrinsicConstructorSources.values());
|
|
97
|
+
const ERROR_INTRINSIC_NAMES = [
|
|
98
|
+
"Error",
|
|
99
|
+
"EvalError",
|
|
100
|
+
"RangeError",
|
|
101
|
+
"ReferenceError",
|
|
102
|
+
"SyntaxError",
|
|
103
|
+
"TypeError",
|
|
104
|
+
"URIError",
|
|
105
|
+
"AggregateError"
|
|
106
|
+
];
|
|
107
|
+
function isIntrinsicConstructorPrototype(prototype, expectedName) {
|
|
108
|
+
const constructorDescriptor = Object.getOwnPropertyDescriptor(prototype, "constructor");
|
|
109
|
+
const candidateConstructor = constructorDescriptor?.value;
|
|
110
|
+
if (constructorDescriptor === void 0 || !("value" in constructorDescriptor) || typeof candidateConstructor !== "function") return false;
|
|
111
|
+
let candidateSource;
|
|
112
|
+
try {
|
|
113
|
+
candidateSource = functionToString.call(candidateConstructor);
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const expectedSource = expectedName === void 0 ? void 0 : intrinsicConstructorSources.get(expectedName);
|
|
118
|
+
if (expectedSource !== void 0 && candidateSource !== expectedSource || expectedSource === void 0 && !intrinsicConstructorSourceSet.has(candidateSource)) return false;
|
|
119
|
+
const nameDescriptor = Object.getOwnPropertyDescriptor(candidateConstructor, "name");
|
|
120
|
+
const candidateName = nameDescriptor !== void 0 && "value" in nameDescriptor && typeof nameDescriptor.value === "string" ? nameDescriptor.value : void 0;
|
|
121
|
+
const intrinsicName = expectedName ?? candidateName;
|
|
122
|
+
const intrinsicSource = intrinsicName === void 0 ? void 0 : intrinsicConstructorSources.get(intrinsicName);
|
|
123
|
+
return candidateName === intrinsicName && intrinsicSource !== void 0 && candidateSource === intrinsicSource && Object.getOwnPropertyDescriptor(candidateConstructor, "prototype")?.value === prototype;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Accepts an intrinsic prototype, optionally behind transparent
|
|
127
|
+
* `Symbol.toStringTag` override layers. A user-defined subclass has its own
|
|
128
|
+
* non-native constructor and is therefore rejected before reaching the
|
|
129
|
+
* intrinsic prototype.
|
|
130
|
+
*/
|
|
131
|
+
function hasIntrinsicPrototypeChain(value, expectedName) {
|
|
132
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
133
|
+
let prototype = Object.getPrototypeOf(value);
|
|
134
|
+
while (prototype !== null && !visited.has(prototype)) {
|
|
135
|
+
visited.add(prototype);
|
|
136
|
+
if (Object.hasOwn(prototype, "constructor")) return isIntrinsicConstructorPrototype(prototype, expectedName);
|
|
137
|
+
const ownKeys = Reflect.ownKeys(prototype);
|
|
138
|
+
if (ownKeys.length !== 1 || ownKeys[0] !== Symbol.toStringTag) return false;
|
|
139
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Tags that `deepEqual` compares BY REFERENCE (its unhandled-built-in
|
|
145
|
+
* fallback) and that `deepOmit` must therefore ALIAS rather than clone:
|
|
146
|
+
* a clone would break `deepEqualExcept(x, x)` reflexivity. Single source
|
|
147
|
+
* of truth so the two modules cannot drift: if `deepEqual` ever learns a
|
|
148
|
+
* by-value comparison for one of these, remove it here and add a clone
|
|
149
|
+
* case in `deepOmit`'s `cloneBuiltIn` in the same change.
|
|
150
|
+
*/
|
|
151
|
+
const REFERENCE_COMPARED_TAGS = /* @__PURE__ */ new Set([
|
|
152
|
+
"[object Error]",
|
|
153
|
+
"[object ArrayBuffer]",
|
|
154
|
+
"[object SharedArrayBuffer]",
|
|
155
|
+
"[object Promise]",
|
|
156
|
+
"[object WeakMap]",
|
|
157
|
+
"[object WeakSet]"
|
|
158
|
+
]);
|
|
159
|
+
/**
|
|
160
|
+
* Intrinsic tags of OPAQUE exotics: objects whose internal state no
|
|
161
|
+
* structural walk can observe (boxed Symbols, generator objects,
|
|
162
|
+
* WeakRefs, FinalizationRegistry handles). `deepEqual` compares them by
|
|
163
|
+
* identity and `deepOmit` passes them through by reference; treating
|
|
164
|
+
* them as (empty) plain objects would make ALL such exotics equal to
|
|
165
|
+
* each other. Deliberately a curated INTRINSIC list, not "every unknown
|
|
166
|
+
* tag": a user class exposing its own `Symbol.toStringTag` (e.g.
|
|
167
|
+
* "Money") keeps structural comparison, and a plain object spoofing one
|
|
168
|
+
* of these intrinsic tags gets the identity semantics of the thing it
|
|
169
|
+
* claims to be.
|
|
170
|
+
*/
|
|
171
|
+
const OPAQUE_EXOTIC_TAGS = /* @__PURE__ */ new Set([
|
|
172
|
+
"[object Symbol]",
|
|
173
|
+
"[object Generator]",
|
|
174
|
+
"[object AsyncGenerator]",
|
|
175
|
+
"[object WeakRef]",
|
|
176
|
+
"[object FinalizationRegistry]"
|
|
177
|
+
]);
|
|
178
|
+
/** True when `tag` names an opaque intrinsic; see {@link OPAQUE_EXOTIC_TAGS}. */
|
|
179
|
+
function isOpaqueExoticTag(tag) {
|
|
180
|
+
return OPAQUE_EXOTIC_TAGS.has(tag);
|
|
181
|
+
}
|
|
182
|
+
function findPropertyDescriptor(value, key) {
|
|
183
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
184
|
+
let current = value;
|
|
185
|
+
while (current !== null && !visited.has(current)) {
|
|
186
|
+
visited.add(current);
|
|
187
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
188
|
+
if (descriptor !== void 0) return descriptor;
|
|
189
|
+
current = Object.getPrototypeOf(current);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function builtInTagWithoutInvokingAccessors(value) {
|
|
193
|
+
if (ArrayBuffer.isView(value)) return hasBrand(value, "[object DataView]") ? "[object DataView]" : "[object TypedArray]";
|
|
194
|
+
const descriptor = findPropertyDescriptor(value, Symbol.toStringTag);
|
|
195
|
+
if (descriptor !== void 0 && !("value" in descriptor)) return builtInTagFromBrand(value);
|
|
196
|
+
const tag = Object.prototype.toString.call(value);
|
|
197
|
+
if (BUILT_IN_TAGS.has(tag) && hasBrand(value, tag)) return tag;
|
|
198
|
+
return descriptor === void 0 ? void 0 : builtInTagFromBrand(value);
|
|
199
|
+
}
|
|
200
|
+
function builtInTagFromBrand(value) {
|
|
201
|
+
if (hasBrand(value, "[object Date]")) return "[object Date]";
|
|
202
|
+
if (hasBrand(value, "[object RegExp]")) return "[object RegExp]";
|
|
203
|
+
if (hasBrand(value, "[object Map]")) return "[object Map]";
|
|
204
|
+
if (hasBrand(value, "[object Set]")) return "[object Set]";
|
|
205
|
+
if (hasBrand(value, "[object WeakMap]")) return "[object WeakMap]";
|
|
206
|
+
if (hasBrand(value, "[object WeakSet]")) return "[object WeakSet]";
|
|
207
|
+
if (hasBrand(value, "[object DataView]")) return "[object DataView]";
|
|
208
|
+
if (hasBrand(value, "[object ArrayBuffer]")) return "[object ArrayBuffer]";
|
|
209
|
+
if (hasBrand(value, "[object SharedArrayBuffer]")) return "[object SharedArrayBuffer]";
|
|
210
|
+
if (hasBrand(value, "[object Boolean]")) return "[object Boolean]";
|
|
211
|
+
if (hasBrand(value, "[object Number]")) return "[object Number]";
|
|
212
|
+
if (hasBrand(value, "[object String]")) return "[object String]";
|
|
213
|
+
if (hasBrand(value, "[object BigInt]")) return "[object BigInt]";
|
|
214
|
+
if (hasNativePrototype(value, "Promise")) return "[object Promise]";
|
|
215
|
+
if (hasNativePrototype(value, "Error")) return "[object Error]";
|
|
216
|
+
}
|
|
217
|
+
function hasNativePrototype(value, expectedName) {
|
|
218
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
219
|
+
let prototype = Object.getPrototypeOf(value);
|
|
220
|
+
while (prototype !== null && !visited.has(prototype)) {
|
|
221
|
+
visited.add(prototype);
|
|
222
|
+
if (isIntrinsicConstructorPrototype(prototype, expectedName)) return true;
|
|
223
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Verifies that `obj` genuinely is the type its tag claims, via an
|
|
229
|
+
* internal-slot probe. Promise and Error have no side-effect-free standard
|
|
230
|
+
* probe, so their visible tags remain conservative; masked instances are
|
|
231
|
+
* identified separately through their native prototype chain.
|
|
232
|
+
*/
|
|
233
|
+
function hasBrand(obj, tag) {
|
|
234
|
+
try {
|
|
235
|
+
switch (tag) {
|
|
236
|
+
case "[object Date]":
|
|
237
|
+
dateGetTime.call(obj);
|
|
238
|
+
return true;
|
|
239
|
+
case "[object RegExp]":
|
|
240
|
+
regExpSourceGet.call(obj);
|
|
241
|
+
return true;
|
|
242
|
+
case "[object Map]":
|
|
243
|
+
mapSizeGet.call(obj);
|
|
244
|
+
return true;
|
|
245
|
+
case "[object Set]":
|
|
246
|
+
setSizeGet.call(obj);
|
|
247
|
+
return true;
|
|
248
|
+
case "[object WeakMap]":
|
|
249
|
+
weakMapHas.call(obj, PROBE_KEY);
|
|
250
|
+
return true;
|
|
251
|
+
case "[object WeakSet]":
|
|
252
|
+
weakSetHas.call(obj, PROBE_KEY);
|
|
253
|
+
return true;
|
|
254
|
+
case "[object DataView]":
|
|
255
|
+
dataViewByteLengthGet.call(obj);
|
|
256
|
+
return true;
|
|
257
|
+
case "[object ArrayBuffer]":
|
|
258
|
+
arrayBufferByteLengthGet.call(obj);
|
|
259
|
+
return true;
|
|
260
|
+
case "[object SharedArrayBuffer]":
|
|
261
|
+
if (!sharedArrayBufferByteLengthGet) return false;
|
|
262
|
+
sharedArrayBufferByteLengthGet.call(obj);
|
|
263
|
+
return true;
|
|
264
|
+
case "[object Boolean]":
|
|
265
|
+
booleanValueOf.call(obj);
|
|
266
|
+
return true;
|
|
267
|
+
case "[object Number]":
|
|
268
|
+
numberValueOf.call(obj);
|
|
269
|
+
return true;
|
|
270
|
+
case "[object String]":
|
|
271
|
+
stringValueOf.call(obj);
|
|
272
|
+
return true;
|
|
273
|
+
case "[object BigInt]":
|
|
274
|
+
bigIntValueOf.call(obj);
|
|
275
|
+
return true;
|
|
276
|
+
case "[object Promise]": return hasNativePrototype(obj, "Promise");
|
|
277
|
+
case "[object Error]": return ERROR_INTRINSIC_NAMES.some((name) => hasNativePrototype(obj, name));
|
|
278
|
+
default: return false;
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Returns `true` when `obj` is a built-in JavaScript type that should be
|
|
286
|
+
* treated atomically (compared/cloned as a unit, not walked structurally).
|
|
287
|
+
* Cross-realm safe, and brand-verified: a plain object spoofing a built-in
|
|
288
|
+
* tag via `Symbol.toStringTag` returns `false` and is walked structurally
|
|
289
|
+
* like any other plain object instead of crashing type-specific code.
|
|
290
|
+
*
|
|
291
|
+
* @param obj - The object to classify
|
|
292
|
+
* @param tag - The result of `Object.prototype.toString.call(obj)`, passed
|
|
293
|
+
* in so callers that already computed it don't pay twice
|
|
294
|
+
*/
|
|
295
|
+
function isBuiltInObject(obj, tag) {
|
|
296
|
+
if (ArrayBuffer.isView(obj)) return true;
|
|
297
|
+
if (tag.endsWith("Array]")) return false;
|
|
298
|
+
return BUILT_IN_TAGS.has(tag) && hasBrand(obj, tag);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Brand-verified `WeakMap` check that holds across realms. `instanceof
|
|
302
|
+
* WeakMap` binds to the constructor of one realm, so a WeakMap from a `vm`
|
|
303
|
+
* context or an iframe reads as a foreign value. The internal-slot probe
|
|
304
|
+
* answers the same in every realm, and a plain object cannot spoof it
|
|
305
|
+
* through `Symbol.toStringTag`.
|
|
306
|
+
*/
|
|
307
|
+
function isWeakMap(value) {
|
|
308
|
+
return typeof value === "object" && value !== null && hasBrand(value, "[object WeakMap]");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/internal/structural/deep-equal.ts
|
|
313
|
+
const objProto = Object.prototype;
|
|
314
|
+
const objToString = objProto.toString;
|
|
315
|
+
const objHasOwn = objProto.hasOwnProperty;
|
|
316
|
+
/**
|
|
317
|
+
* SameValueZero: `===` plus NaN-equals-NaN (and `+0 === -0`, unlike
|
|
318
|
+
* `Object.is`). The numeric semantics `deepEqual` documents for primitives,
|
|
319
|
+
* applied consistently inside TypedArrays, Dates and Number wrappers.
|
|
320
|
+
*/
|
|
321
|
+
function sameValueZero(a, b) {
|
|
322
|
+
return a === b || Number.isNaN(a) && Number.isNaN(b);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Performs a deep equality check between two values.
|
|
326
|
+
*
|
|
327
|
+
* This function compares values recursively, handling:
|
|
328
|
+
* - Primitives (with special handling for NaN)
|
|
329
|
+
* - Arrays (nested arrays supported)
|
|
330
|
+
* - Objects (plain objects and class instances)
|
|
331
|
+
* - TypedArrays (Uint8Array, Int32Array, etc.)
|
|
332
|
+
* - DataView
|
|
333
|
+
* - Maps and Sets
|
|
334
|
+
* - Dates and RegExp
|
|
335
|
+
* - Wrapper objects (Boolean, Number, String)
|
|
336
|
+
* - Circular references (detected and handled)
|
|
337
|
+
*
|
|
338
|
+
* @param a - The first value to compare
|
|
339
|
+
* @param b - The second value to compare
|
|
340
|
+
* @returns `true` if the values are deeply equal, `false` otherwise
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* ```ts
|
|
344
|
+
* deepEqual([1, 2, 3], [1, 2, 3]); // true
|
|
345
|
+
* deepEqual({ a: 1, b: [2, 3] }, { a: 1, b: [2, 3] }); // true
|
|
346
|
+
* deepEqual(NaN, NaN); // true
|
|
347
|
+
* deepEqual([1, 2], [1, 2, 3]); // false
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
function deepEqual(a, b) {
|
|
351
|
+
return deepEqualInner(a, b, /* @__PURE__ */ new WeakMap());
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Internal recursive function for deep equality comparison.
|
|
355
|
+
*
|
|
356
|
+
* @internal
|
|
357
|
+
*/
|
|
358
|
+
function deepEqualInner(a, b, visited) {
|
|
359
|
+
if (a === b) return true;
|
|
360
|
+
const typeA = typeof a;
|
|
361
|
+
const typeB = typeof b;
|
|
362
|
+
if (typeA !== "object" || a === null || typeB !== "object" || b === null) {
|
|
363
|
+
if (typeA === "number" && typeB === "number") return Number.isNaN(a) && Number.isNaN(b);
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
const objA = a;
|
|
367
|
+
const objB = b;
|
|
368
|
+
let cachedBs = visited.get(objA);
|
|
369
|
+
if (cachedBs?.has(objB)) return true;
|
|
370
|
+
if (!cachedBs) {
|
|
371
|
+
cachedBs = /* @__PURE__ */ new WeakSet();
|
|
372
|
+
visited.set(objA, cachedBs);
|
|
373
|
+
}
|
|
374
|
+
cachedBs.add(objB);
|
|
375
|
+
if (ArrayBuffer.isView(objA) || ArrayBuffer.isView(objB)) {
|
|
376
|
+
if (!ArrayBuffer.isView(objA) || !ArrayBuffer.isView(objB)) return false;
|
|
377
|
+
const tagA = objToString.call(objA);
|
|
378
|
+
if (tagA !== objToString.call(objB)) return false;
|
|
379
|
+
if (tagA === "[object DataView]") {
|
|
380
|
+
const viewA = objA;
|
|
381
|
+
const viewB = objB;
|
|
382
|
+
if (viewA.byteLength !== viewB.byteLength) return false;
|
|
383
|
+
const len = viewA.byteLength;
|
|
384
|
+
for (let i = 0; i < len; i++) if (viewA.getUint8(i) !== viewB.getUint8(i)) return false;
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
const arrA = objA;
|
|
388
|
+
const arrB = objB;
|
|
389
|
+
const len = arrA.length;
|
|
390
|
+
if (len !== arrB.length) return false;
|
|
391
|
+
for (let i = 0; i < len; i++) if (!sameValueZero(arrA[i], arrB[i])) return false;
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
if (Array.isArray(objA) || Array.isArray(objB)) {
|
|
395
|
+
if (!Array.isArray(objA) || !Array.isArray(objB)) return false;
|
|
396
|
+
if (objA.length !== objB.length) return false;
|
|
397
|
+
const keysA = Reflect.ownKeys(objA).filter((key) => key !== "length");
|
|
398
|
+
const keysB = Reflect.ownKeys(objB).filter((key) => key !== "length");
|
|
399
|
+
if (keysA.length !== keysB.length) return false;
|
|
400
|
+
const arrA = objA;
|
|
401
|
+
const arrB = objB;
|
|
402
|
+
for (const key of keysA) {
|
|
403
|
+
if (!objHasOwn.call(objB, key)) return false;
|
|
404
|
+
if (!deepEqualInner(arrA[key], arrB[key], visited)) return false;
|
|
405
|
+
}
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
const tagA = objToString.call(objA);
|
|
409
|
+
const tagB = objToString.call(objB);
|
|
410
|
+
if (tagA !== tagB) return false;
|
|
411
|
+
const builtInA = isBuiltInObject(objA, tagA);
|
|
412
|
+
if (builtInA !== isBuiltInObject(objB, tagB)) return false;
|
|
413
|
+
if (!builtInA) {
|
|
414
|
+
if (isOpaqueExoticTag(tagA)) return objA === objB;
|
|
415
|
+
return comparePlainObjects(objA, objB, visited);
|
|
416
|
+
}
|
|
417
|
+
switch (tagA) {
|
|
418
|
+
case "[object Map]": {
|
|
419
|
+
const mapA = objA;
|
|
420
|
+
const mapB = objB;
|
|
421
|
+
if (mapA.size !== mapB.size) return false;
|
|
422
|
+
for (const [key, valA] of mapA) {
|
|
423
|
+
if (!mapB.has(key)) return false;
|
|
424
|
+
if (!deepEqualInner(valA, mapB.get(key), visited)) return false;
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
case "[object Set]": {
|
|
429
|
+
const setA = objA;
|
|
430
|
+
const setB = objB;
|
|
431
|
+
if (setA.size !== setB.size) return false;
|
|
432
|
+
for (const value of setA) if (!setB.has(value)) return false;
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
case "[object Date]": return sameValueZero(objA.getTime(), objB.getTime());
|
|
436
|
+
case "[object RegExp]": {
|
|
437
|
+
const regA = objA;
|
|
438
|
+
const regB = objB;
|
|
439
|
+
return regA.source === regB.source && regA.flags === regB.flags;
|
|
440
|
+
}
|
|
441
|
+
case "[object Boolean]":
|
|
442
|
+
case "[object Number]":
|
|
443
|
+
case "[object String]":
|
|
444
|
+
case "[object BigInt]": return sameValueZero(objA.valueOf(), objB.valueOf());
|
|
445
|
+
default: return objA === objB;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Plain / custom objects: compare own enumerable string keys + own symbol
|
|
450
|
+
* keys and their values. Used both as the final fallback and for objects
|
|
451
|
+
* whose built-in-looking tag failed brand verification.
|
|
452
|
+
*/
|
|
453
|
+
function comparePlainObjects(objA, objB, visited) {
|
|
454
|
+
const recA = objA;
|
|
455
|
+
const recB = objB;
|
|
456
|
+
const stringKeysA = Object.getOwnPropertyNames(objA);
|
|
457
|
+
const stringKeysB = Object.getOwnPropertyNames(objB);
|
|
458
|
+
if (stringKeysA.length !== stringKeysB.length) return false;
|
|
459
|
+
const symbolKeysA = Object.getOwnPropertySymbols(objA);
|
|
460
|
+
const symbolKeysB = Object.getOwnPropertySymbols(objB);
|
|
461
|
+
if (symbolKeysA.length !== symbolKeysB.length) return false;
|
|
462
|
+
const symbolKeysBSet = new Set(symbolKeysB);
|
|
463
|
+
for (const key of stringKeysA) if (!objHasOwn.call(objB, key)) return false;
|
|
464
|
+
for (const key of symbolKeysA) if (!symbolKeysBSet.has(key)) return false;
|
|
465
|
+
for (const key of stringKeysA) if (!deepEqualInner(recA[key], recB[key], visited)) return false;
|
|
466
|
+
for (const key of symbolKeysA) if (!deepEqualInner(recA[key], recB[key], visited)) return false;
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/internal/structural/deep-omit.ts
|
|
472
|
+
/**
|
|
473
|
+
* Creates a deep copy of `value` with certain keys removed according to the
|
|
474
|
+
* provided rules.
|
|
475
|
+
*
|
|
476
|
+
* Walks the object tree and skips keys that match `ignoreKeys` /
|
|
477
|
+
* `ignoreKeyPredicate`. Built-in atomic types that `deepEqual` compares by
|
|
478
|
+
* value (Date, RegExp, Map, Set, TypedArrays, DataView) are cloned by type
|
|
479
|
+
* rather than walked, since their internal structure has no key filtering to
|
|
480
|
+
* apply. Types that `deepEqual` compares by reference (Error, ArrayBuffer,
|
|
481
|
+
* SharedArrayBuffer, Promise, WeakMap, WeakSet) are passed through by
|
|
482
|
+
* reference, so `deepEqualExcept(x, x)` stays reflexive. Cycles are
|
|
483
|
+
* preserved: a cycle `a → a` clones to `a' → a'`. Arrays retain sparse
|
|
484
|
+
* holes and all non-ignored own properties, including symbol keys.
|
|
485
|
+
*
|
|
486
|
+
* **Shared references.** Without `ignoreKeyPredicate`, an object reached
|
|
487
|
+
* via several paths dedupes to a single clone. With a predicate, each
|
|
488
|
+
* path gets its own clone, because the predicate may decide differently per
|
|
489
|
+
* path, so memoising the first path's result would be wrong. This is
|
|
490
|
+
* inherently exponential for diamond-shaped sharing (a node reachable
|
|
491
|
+
* via 2^n paths is cloned 2^n times); the walk aborts with a descriptive
|
|
492
|
+
* error after {@link PATH_SENSITIVE_VISIT_BUDGET} node visits instead of
|
|
493
|
+
* hanging the process.
|
|
494
|
+
*
|
|
495
|
+
* **Prototype-pollution safety.** `__proto__` and `constructor` keys
|
|
496
|
+
* encountered as *own* properties of the input (typical of `JSON.parse`
|
|
497
|
+
* output) are copied as inert data properties via `Object.defineProperty`
|
|
498
|
+
* so the clone graph cannot bleed into `Object.prototype`.
|
|
499
|
+
*
|
|
500
|
+
* **Class instances.** When the input is a class instance, the clone is
|
|
501
|
+
* built via `Object.create(proto)` so the prototype is preserved, but the
|
|
502
|
+
* constructor is NOT re-invoked, so class invariants enforced by the
|
|
503
|
+
* constructor are not re-checked. `deepOmit` is therefore best used for
|
|
504
|
+
* comparison/serialisation (`voEqualsExcept`, `deepEqualExcept`), not as
|
|
505
|
+
* a general-purpose clone for behaviour-carrying objects.
|
|
506
|
+
*
|
|
507
|
+
* @param value - The value to create a deep copy from
|
|
508
|
+
* @param options - Options specifying which keys to ignore
|
|
509
|
+
* @returns A deep copy of `value` with specified keys removed
|
|
510
|
+
*/
|
|
511
|
+
function deepOmit(value, options) {
|
|
512
|
+
const visited = /* @__PURE__ */ new WeakMap();
|
|
513
|
+
return omitInternal(value, options, options.ignoreKeys ? new Set(options.ignoreKeys) : void 0, [], visited, options.ignoreKeyPredicate ? { visits: 0 } : void 0);
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Maximum object-node visits for a single path-sensitive `deepOmit` walk.
|
|
517
|
+
* Per-path cloning expands exponentially on diamond-shaped sharing; past
|
|
518
|
+
* this bound the walk throws instead of hanging the process. One million
|
|
519
|
+
* visits covers any realistically tree-shaped input.
|
|
520
|
+
*/
|
|
521
|
+
const PATH_SENSITIVE_VISIT_BUDGET = 1e6;
|
|
522
|
+
function omitInternal(value, options, ignoreKeys, path, visited, budget) {
|
|
523
|
+
if (value === null) return value;
|
|
524
|
+
if (typeof value !== "object") return value;
|
|
525
|
+
const obj = value;
|
|
526
|
+
if (visited.has(obj)) return visited.get(obj);
|
|
527
|
+
if (budget && ++budget.visits > PATH_SENSITIVE_VISIT_BUDGET) throw new Error(`deepOmit: exceeded ${PATH_SENSITIVE_VISIT_BUDGET} node visits. With ignoreKeyPredicate, objects reached via shared references are cloned once per path (the predicate may decide differently per path), which expands exponentially on diamond-shaped sharing. Restructure the input to a tree, or use ignoreKeys for path-independent filtering.`);
|
|
528
|
+
if (Array.isArray(obj)) {
|
|
529
|
+
const arr = obj;
|
|
530
|
+
const clone = new Array(arr.length);
|
|
531
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(arr, "length");
|
|
532
|
+
visited.set(obj, clone);
|
|
533
|
+
for (const key of Reflect.ownKeys(arr)) {
|
|
534
|
+
if (key === "length") continue;
|
|
535
|
+
const segment = arrayPathSegment(key);
|
|
536
|
+
if (typeof segment !== "number" && shouldIgnoreKey(key, path, ignoreKeys, options)) continue;
|
|
537
|
+
const descriptor = Object.getOwnPropertyDescriptor(arr, key);
|
|
538
|
+
if (descriptor === void 0) continue;
|
|
539
|
+
path.push(segment);
|
|
540
|
+
if ("value" in descriptor) descriptor.value = omitInternal(descriptor.value, options, ignoreKeys, path, visited, budget);
|
|
541
|
+
Object.defineProperty(clone, key, descriptor);
|
|
542
|
+
path.pop();
|
|
543
|
+
}
|
|
544
|
+
if (lengthDescriptor !== void 0) Object.defineProperty(clone, "length", lengthDescriptor);
|
|
545
|
+
if (budget) visited.delete(obj);
|
|
546
|
+
return clone;
|
|
547
|
+
}
|
|
548
|
+
const tag = Object.prototype.toString.call(obj);
|
|
549
|
+
if (isBuiltInObject(obj, tag)) {
|
|
550
|
+
const builtInClone = cloneBuiltIn(obj, tag);
|
|
551
|
+
visited.set(obj, builtInClone);
|
|
552
|
+
return builtInClone;
|
|
553
|
+
}
|
|
554
|
+
if (isOpaqueExoticTag(tag)) {
|
|
555
|
+
visited.set(obj, obj);
|
|
556
|
+
return obj;
|
|
557
|
+
}
|
|
558
|
+
const clone = Object.create(Object.getPrototypeOf(obj));
|
|
559
|
+
visited.set(obj, clone);
|
|
560
|
+
const stringKeys = Object.getOwnPropertyNames(obj);
|
|
561
|
+
const symbolKeys = Object.getOwnPropertySymbols(obj);
|
|
562
|
+
for (const key of [...stringKeys, ...symbolKeys]) {
|
|
563
|
+
if (shouldIgnoreKey(key, path, ignoreKeys, options)) continue;
|
|
564
|
+
path.push(key);
|
|
565
|
+
assignOwn(clone, key, omitInternal(obj[key], options, ignoreKeys, path, visited, budget), Object.getOwnPropertyDescriptor(obj, key)?.enumerable ?? true);
|
|
566
|
+
path.pop();
|
|
567
|
+
}
|
|
568
|
+
if (budget) visited.delete(obj);
|
|
569
|
+
return clone;
|
|
570
|
+
}
|
|
571
|
+
function arrayPathSegment(key) {
|
|
572
|
+
if (typeof key === "symbol") return key;
|
|
573
|
+
const index = Number(key);
|
|
574
|
+
return Number.isInteger(index) && index >= 0 && index < 4294967295 && String(index) === key ? index : key;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Assigns `value` as an OWN data property on `target` without going through
|
|
578
|
+
* any inherited setter; critically, it never invokes the `__proto__` setter
|
|
579
|
+
* even when `key === "__proto__"`. Required to defeat prototype-pollution
|
|
580
|
+
* payloads that ship `__proto__` as a parsed-JSON own key.
|
|
581
|
+
*/
|
|
582
|
+
function assignOwn(target, key, value, enumerable = true) {
|
|
583
|
+
Object.defineProperty(target, key, {
|
|
584
|
+
value,
|
|
585
|
+
writable: true,
|
|
586
|
+
enumerable,
|
|
587
|
+
configurable: true
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Clones a built-in atomic type by case. Falls back to `structuredClone`
|
|
592
|
+
* for anything not explicitly enumerated (e.g. DataView, TypedArrays,
|
|
593
|
+
* Boolean/Number/String wrappers, all of which `deepEqual` compares by
|
|
594
|
+
* value). Types that `deepEqual` compares BY REFERENCE (the shared
|
|
595
|
+
* {@link REFERENCE_COMPARED_TAGS} set) are passed through by reference
|
|
596
|
+
* instead; cloning them would make `deepEqualExcept(x, x)` false.
|
|
597
|
+
* Promise/WeakMap/WeakSet additionally cannot be cloned at all
|
|
598
|
+
* (`structuredClone` rejects them).
|
|
599
|
+
*/
|
|
600
|
+
function cloneBuiltIn(obj, tag) {
|
|
601
|
+
if (REFERENCE_COMPARED_TAGS.has(tag)) return obj;
|
|
602
|
+
switch (tag) {
|
|
603
|
+
case "[object Date]": return new Date(obj.getTime());
|
|
604
|
+
case "[object RegExp]": {
|
|
605
|
+
const re = obj;
|
|
606
|
+
const copy = new RegExp(re.source, re.flags);
|
|
607
|
+
copy.lastIndex = re.lastIndex;
|
|
608
|
+
return copy;
|
|
609
|
+
}
|
|
610
|
+
case "[object Map]": return new Map(obj);
|
|
611
|
+
case "[object Set]": return new Set(obj);
|
|
612
|
+
default: return structuredClone(obj);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function shouldIgnoreKey(key, path, ignoreKeys, options) {
|
|
616
|
+
if (ignoreKeys?.has(key)) return true;
|
|
617
|
+
if (options.ignoreKeyPredicate?.(key, path.slice())) return true;
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
//#endregion
|
|
622
|
+
//#region src/internal/structural/deep-equal-except.ts
|
|
623
|
+
/**
|
|
624
|
+
* Performs a deep equality comparison between two values after omitting specified keys.
|
|
625
|
+
*
|
|
626
|
+
* This function first removes the specified keys from both values using `deepOmit`,
|
|
627
|
+
* then performs a deep equality check using `deepEqual`.
|
|
628
|
+
*
|
|
629
|
+
* @param a - The first value to compare
|
|
630
|
+
* @param b - The second value to compare
|
|
631
|
+
* @param options - Options specifying which keys to omit before comparison
|
|
632
|
+
* @returns `true` if the values are deeply equal after omitting specified keys, `false` otherwise
|
|
633
|
+
*
|
|
634
|
+
* @example
|
|
635
|
+
* ```ts
|
|
636
|
+
* const obj1 = { id: 1, name: "Alice", updatedAt: "2024-01-01" };
|
|
637
|
+
* const obj2 = { id: 2, name: "Alice", updatedAt: "2024-01-02" };
|
|
638
|
+
*
|
|
639
|
+
* deepEqualExcept(obj1, obj2, { ignoreKeys: ["id", "updatedAt"] }); // true
|
|
640
|
+
* ```
|
|
641
|
+
*/
|
|
642
|
+
function deepEqualExcept(a, b, options) {
|
|
643
|
+
const prunedA = deepOmit(a, options);
|
|
644
|
+
const prunedB = deepOmit(b, options);
|
|
645
|
+
return deepEqual(prunedA, prunedB);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region src/domain/value-object/value-object.ts
|
|
650
|
+
/**
|
|
651
|
+
* `Object.freeze` does not protect internal slots: a frozen Date still
|
|
652
|
+
* accepts `setTime`, a frozen Map still accepts `set`. To make the
|
|
653
|
+
* "deeply immutable" guarantee real, the mutator methods are shadowed
|
|
654
|
+
* with own throwing functions BEFORE the freeze. The shadows are
|
|
655
|
+
* non-enumerable, so they are invisible to `Object.keys`/spread (deep
|
|
656
|
+
* equality is unaffected) and `structuredClone` drops them (a `vo()`
|
|
657
|
+
* round-trip never sees them).
|
|
658
|
+
*/
|
|
659
|
+
const DATE_MUTATORS = [
|
|
660
|
+
"setTime",
|
|
661
|
+
"setMilliseconds",
|
|
662
|
+
"setUTCMilliseconds",
|
|
663
|
+
"setSeconds",
|
|
664
|
+
"setUTCSeconds",
|
|
665
|
+
"setMinutes",
|
|
666
|
+
"setUTCMinutes",
|
|
667
|
+
"setHours",
|
|
668
|
+
"setUTCHours",
|
|
669
|
+
"setDate",
|
|
670
|
+
"setUTCDate",
|
|
671
|
+
"setMonth",
|
|
672
|
+
"setUTCMonth",
|
|
673
|
+
"setFullYear",
|
|
674
|
+
"setUTCFullYear",
|
|
675
|
+
"setYear"
|
|
676
|
+
];
|
|
677
|
+
const mutationThrowers = /* @__PURE__ */ new Map();
|
|
678
|
+
const mapEntries = Map.prototype.entries;
|
|
679
|
+
const setValues = Set.prototype.values;
|
|
680
|
+
function mutationThrower(typeName, method) {
|
|
681
|
+
const key = `${typeName}.${method}`;
|
|
682
|
+
let thrower = mutationThrowers.get(key);
|
|
683
|
+
if (!thrower) {
|
|
684
|
+
thrower = function throwFrozenMutation() {
|
|
685
|
+
throw new TypeError(`Cannot call ${method}() on a ${typeName} inside a deeply frozen value`);
|
|
686
|
+
};
|
|
687
|
+
mutationThrowers.set(key, thrower);
|
|
688
|
+
}
|
|
689
|
+
return thrower;
|
|
690
|
+
}
|
|
691
|
+
const shadowDescriptor = {
|
|
692
|
+
value: void 0,
|
|
693
|
+
writable: false,
|
|
694
|
+
enumerable: false,
|
|
695
|
+
configurable: false
|
|
696
|
+
};
|
|
697
|
+
/** Returns whether the shadows were installed. */
|
|
698
|
+
function shadowMutators(obj, typeName, methods) {
|
|
699
|
+
if (!Object.isExtensible(obj)) return false;
|
|
700
|
+
for (const method of methods) {
|
|
701
|
+
shadowDescriptor.value = mutationThrower(typeName, method);
|
|
702
|
+
Object.defineProperty(obj, method, shadowDescriptor);
|
|
703
|
+
}
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
const VALUE_OBJECT_CLASS = Symbol.for("@shirudo/ddd-kit/value-object-class/v1");
|
|
707
|
+
function recordValueObjectClass(instance, valueObjectClass) {
|
|
708
|
+
Object.defineProperty(instance, VALUE_OBJECT_CLASS, {
|
|
709
|
+
value: valueObjectClass,
|
|
710
|
+
enumerable: false,
|
|
711
|
+
writable: false,
|
|
712
|
+
configurable: false
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
function isValueObjectInstance(value) {
|
|
716
|
+
if (value === null || typeof value !== "object") return false;
|
|
717
|
+
const record = Reflect.getOwnPropertyDescriptor(value, VALUE_OBJECT_CLASS);
|
|
718
|
+
if (record === void 0 || typeof record.value !== "function" || record.enumerable !== false || record.writable !== false || record.configurable !== false) return false;
|
|
719
|
+
const props = Reflect.getOwnPropertyDescriptor(value, "props");
|
|
720
|
+
return props !== void 0 && isSealedProps(props.value);
|
|
721
|
+
}
|
|
722
|
+
function isSealedProps(props) {
|
|
723
|
+
if (typeof props !== "object" || props === null) return false;
|
|
724
|
+
return Object.isFrozen(props) || builtInTagWithoutInvokingAccessors(props) === "[object RegExp]";
|
|
725
|
+
}
|
|
726
|
+
function looksLikeValueObject(instance) {
|
|
727
|
+
if (Object.hasOwn(instance, VALUE_OBJECT_CLASS)) return true;
|
|
728
|
+
const props = Reflect.getOwnPropertyDescriptor(instance, "props");
|
|
729
|
+
return props !== void 0 && isSealedProps(props.value);
|
|
730
|
+
}
|
|
731
|
+
function openValueObjectKeys(instance) {
|
|
732
|
+
return Reflect.ownKeys(instance).filter((key) => key !== "props" && key !== VALUE_OBJECT_CLASS);
|
|
733
|
+
}
|
|
734
|
+
function rejectValueObjectAsInput(input, entry) {
|
|
735
|
+
if (isValueObjectInstance(input)) throw new TypeError(`${entry} does not accept a value object as its input: nest the value object under a key, or pass its props`);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Deep freezes an object and all its nested properties recursively, then
|
|
739
|
+
* returns it. Iterates both string-keyed and symbol-keyed own properties
|
|
740
|
+
* so the freeze symmetry matches `deepEqual` (which also considers symbol
|
|
741
|
+
* keys). Handles circular references by tracking visited objects.
|
|
742
|
+
*
|
|
743
|
+
* Note: `deepFreeze` mutates its argument in place; it sets `[[Frozen]]`
|
|
744
|
+
* on the object you pass in. Callers that need to avoid touching the
|
|
745
|
+
* input (e.g. `vo()`) should deep-clone first.
|
|
746
|
+
*
|
|
747
|
+
* Date/Map/Set keep internal-slot mutability under `Object.freeze`
|
|
748
|
+
* (`setTime`, `set`, `add`, … still work on frozen instances), so their
|
|
749
|
+
* mutator methods are shadowed with throwing own properties and Map/Set
|
|
750
|
+
* contents are frozen recursively. The shadows are non-enumerable:
|
|
751
|
+
* invisible to `Object.keys`, spread, `deepEqual`, and `structuredClone`.
|
|
752
|
+
*
|
|
753
|
+
* The shadowing is deny-by-enumeration: only the mutators known at
|
|
754
|
+
* release time are blocked. If the runtime grows a NEW mutator (e.g. the
|
|
755
|
+
* stage-3 `Map.prototype.getOrInsert` upsert proposal), it is not blocked
|
|
756
|
+
* until the list is updated. Treat the mutator blocking as a guard rail,
|
|
757
|
+
* not a security boundary.
|
|
758
|
+
*
|
|
759
|
+
* Two kinds of built-in pass through unfrozen. ArrayBuffer views
|
|
760
|
+
* (TypedArrays, DataView): the spec forbids freezing a view with elements,
|
|
761
|
+
* and a freeze cannot protect the underlying buffer, so their contents stay
|
|
762
|
+
* mutable. RegExp: pattern and flags live in immutable internal slots, and
|
|
763
|
+
* the only own data property, `lastIndex`, is scan state that every global
|
|
764
|
+
* or sticky match writes. A frozen RegExp protects nothing and throws on
|
|
765
|
+
* the first such match, so the RegExp keeps matching instead. A view or a
|
|
766
|
+
* RegExp passes through whole: an expando property on it stays open, and
|
|
767
|
+
* the walk does not enter its subtree.
|
|
768
|
+
*/
|
|
769
|
+
const DEEP_FROZEN = /* @__PURE__ */ new WeakSet();
|
|
770
|
+
const KIT_SHADOWED = /* @__PURE__ */ new WeakSet();
|
|
771
|
+
function deepFreeze(obj) {
|
|
772
|
+
const walk = {
|
|
773
|
+
cyclic: false,
|
|
774
|
+
sealed: [],
|
|
775
|
+
done: /* @__PURE__ */ new Map(),
|
|
776
|
+
inProgress: /* @__PURE__ */ new Set()
|
|
777
|
+
};
|
|
778
|
+
freezeDeep(obj, walk);
|
|
779
|
+
if (!walk.cyclic) for (const object of walk.sealed) DEEP_FROZEN.add(object);
|
|
780
|
+
return obj;
|
|
781
|
+
}
|
|
782
|
+
/** Freezes `obj` and its subtree; returns whether the subtree is sealed. */
|
|
783
|
+
function freezeDeep(obj, walk) {
|
|
784
|
+
if (obj === null || typeof obj !== "object") return true;
|
|
785
|
+
if (DEEP_FROZEN.has(obj)) return true;
|
|
786
|
+
if (ArrayBuffer.isView(obj)) return true;
|
|
787
|
+
const finished = walk.done.get(obj);
|
|
788
|
+
if (finished !== void 0) return finished;
|
|
789
|
+
if (walk.inProgress.has(obj)) {
|
|
790
|
+
walk.cyclic = true;
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
const builtInTag = builtInTagWithoutInvokingAccessors(obj);
|
|
794
|
+
if (builtInTag === "[object RegExp]") return true;
|
|
795
|
+
walk.inProgress.add(obj);
|
|
796
|
+
let sealed = true;
|
|
797
|
+
if (builtInTag === "[object Date]" || builtInTag === "[object Map]" || builtInTag === "[object Set]") {
|
|
798
|
+
let shadowed = KIT_SHADOWED.has(obj);
|
|
799
|
+
if (builtInTag === "[object Date]") shadowed = shadowMutators(obj, "Date", DATE_MUTATORS) || shadowed;
|
|
800
|
+
else if (builtInTag === "[object Map]") {
|
|
801
|
+
for (const [key, value] of obj) {
|
|
802
|
+
if (!freezeDeep(key, walk)) sealed = false;
|
|
803
|
+
if (!freezeDeep(value, walk)) sealed = false;
|
|
804
|
+
}
|
|
805
|
+
shadowed = shadowMutators(obj, "Map", [
|
|
806
|
+
"set",
|
|
807
|
+
"delete",
|
|
808
|
+
"clear"
|
|
809
|
+
]) || shadowed;
|
|
810
|
+
} else if (builtInTag === "[object Set]") {
|
|
811
|
+
for (const member of obj) if (!freezeDeep(member, walk)) sealed = false;
|
|
812
|
+
shadowed = shadowMutators(obj, "Set", [
|
|
813
|
+
"add",
|
|
814
|
+
"delete",
|
|
815
|
+
"clear"
|
|
816
|
+
]) || shadowed;
|
|
817
|
+
}
|
|
818
|
+
if (shadowed) KIT_SHADOWED.add(obj);
|
|
819
|
+
else sealed = false;
|
|
820
|
+
}
|
|
821
|
+
const keys = Reflect.ownKeys(obj);
|
|
822
|
+
for (const key of keys) {
|
|
823
|
+
const value = obj[key];
|
|
824
|
+
if (value !== null && typeof value === "object") {
|
|
825
|
+
if (!freezeDeep(value, walk)) sealed = false;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
Object.freeze(obj);
|
|
829
|
+
walk.inProgress.delete(obj);
|
|
830
|
+
walk.done.set(obj, sealed);
|
|
831
|
+
if (sealed) walk.sealed.push(obj);
|
|
832
|
+
return sealed;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Deep clone used by `vo()` and the `ValueObject` constructor.
|
|
836
|
+
*
|
|
837
|
+
* Plain objects, arrays, and Map values are walked manually so
|
|
838
|
+
* that symbol-keyed properties survive (which `structuredClone` silently
|
|
839
|
+
* drops; they would otherwise be invisible to `voEquals`, whose
|
|
840
|
+
* `deepEqual` DOES consider symbol keys) and shared references / cycles
|
|
841
|
+
* keep their identity across Map boundaries. Function values throw,
|
|
842
|
+
* preserving `vo()`'s documented data-not-behaviour gate. Built-ins without
|
|
843
|
+
* immutable value semantics throw a descriptive `TypeError`. A kit
|
|
844
|
+
* `ValueObject` instance is admitted by reference (see
|
|
845
|
+
* `VALUE_OBJECT_CLASS`). Every other custom class instance and every
|
|
846
|
+
* subclass of a built-in is rejected because cloning it without invoking
|
|
847
|
+
* its constructor can silently lose private or non-enumerable state. Map
|
|
848
|
+
* keys and Set members must be primitive because their equality is
|
|
849
|
+
* identity-based and object identity cannot survive defensive cloning.
|
|
850
|
+
* Accessor properties are rejected without invoking them. Admitted atomic
|
|
851
|
+
* built-ins (Date, RegExp and primitive wrappers) delegate to
|
|
852
|
+
* `structuredClone`, brand-verified so a `Symbol.toStringTag` spoofer is
|
|
853
|
+
* walked as the plain object it is. `__proto__` own keys are copied as
|
|
854
|
+
* inert data properties.
|
|
855
|
+
*/
|
|
856
|
+
function cloneForVo(value, visited) {
|
|
857
|
+
if (typeof value === "function") throw new TypeError("vo() does not accept function values: Value Objects are data, not behaviour");
|
|
858
|
+
if (value === null || typeof value !== "object") return value;
|
|
859
|
+
const obj = value;
|
|
860
|
+
if (ArrayBuffer.isView(obj)) throwUnsupportedValueSemantics(builtInTagWithoutInvokingAccessors(obj) ?? "[object ArrayBuffer view]");
|
|
861
|
+
if (visited.has(obj)) return visited.get(obj);
|
|
862
|
+
if (Array.isArray(obj)) {
|
|
863
|
+
if (!hasIntrinsicPrototypeChain(obj, "Array")) throwUnsupportedClassInstance(obj);
|
|
864
|
+
const clone = new Array(obj.length);
|
|
865
|
+
visited.set(obj, clone);
|
|
866
|
+
for (const key of Reflect.ownKeys(obj)) {
|
|
867
|
+
if (key === "length") continue;
|
|
868
|
+
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
|
|
869
|
+
if (descriptor === void 0) continue;
|
|
870
|
+
if (!("value" in descriptor)) throwUnsupportedAccessorProperty();
|
|
871
|
+
if (typeof key === "string" && !descriptor.enumerable) continue;
|
|
872
|
+
descriptor.value = cloneForVo(descriptor.value, visited);
|
|
873
|
+
Object.defineProperty(clone, key, descriptor);
|
|
874
|
+
}
|
|
875
|
+
return clone;
|
|
876
|
+
}
|
|
877
|
+
const tag = builtInTagWithoutInvokingAccessors(obj);
|
|
878
|
+
if (tag !== void 0) {
|
|
879
|
+
if (!hasIntrinsicPrototypeChain(obj)) throwUnsupportedClassInstance(obj);
|
|
880
|
+
if (tag === "[object Map]") {
|
|
881
|
+
const clone = /* @__PURE__ */ new Map();
|
|
882
|
+
visited.set(obj, clone);
|
|
883
|
+
for (const [key, entry] of mapEntries.call(obj)) {
|
|
884
|
+
if (!isPrimitiveValue(key)) throw new TypeError("vo() Map keys must be primitive values to preserve value equality");
|
|
885
|
+
clone.set(key, cloneForVo(entry, visited));
|
|
886
|
+
}
|
|
887
|
+
return clone;
|
|
888
|
+
}
|
|
889
|
+
if (tag === "[object Set]") {
|
|
890
|
+
const clone = /* @__PURE__ */ new Set();
|
|
891
|
+
visited.set(obj, clone);
|
|
892
|
+
for (const member of setValues.call(obj)) {
|
|
893
|
+
if (!isPrimitiveValue(member)) throw new TypeError("vo() Set members must be primitive values to preserve value equality");
|
|
894
|
+
clone.add(member);
|
|
895
|
+
}
|
|
896
|
+
return clone;
|
|
897
|
+
}
|
|
898
|
+
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`);
|
|
899
|
+
if (tag === "[object Error]" || tag === "[object ArrayBuffer]" || tag === "[object SharedArrayBuffer]") throwUnsupportedValueSemantics(tag);
|
|
900
|
+
if (tag === "[object RegExp]") {
|
|
901
|
+
const regExp = obj;
|
|
902
|
+
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");
|
|
903
|
+
}
|
|
904
|
+
const builtInClone = structuredClone(obj);
|
|
905
|
+
visited.set(obj, builtInClone);
|
|
906
|
+
return builtInClone;
|
|
907
|
+
}
|
|
908
|
+
const prototype = Object.getPrototypeOf(obj);
|
|
909
|
+
if (prototype !== null && (!isIntrinsicConstructorPrototype(prototype, "Object") || Object.getPrototypeOf(prototype) !== null)) {
|
|
910
|
+
if (isValueObjectInstance(obj)) {
|
|
911
|
+
const openKeys = openValueObjectKeys(obj);
|
|
912
|
+
if (openKeys.length > 0) throwOpenValueObjectFields(openKeys);
|
|
913
|
+
return obj;
|
|
914
|
+
}
|
|
915
|
+
throwUnsupportedClassInstance(obj);
|
|
916
|
+
}
|
|
917
|
+
const clone = Object.create(prototype === null ? null : Object.prototype);
|
|
918
|
+
visited.set(obj, clone);
|
|
919
|
+
for (const key of Reflect.ownKeys(obj)) {
|
|
920
|
+
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
|
|
921
|
+
if (descriptor === void 0) continue;
|
|
922
|
+
if (!("value" in descriptor)) throwUnsupportedAccessorProperty();
|
|
923
|
+
if (typeof key === "string" && !descriptor.enumerable) continue;
|
|
924
|
+
Object.defineProperty(clone, key, {
|
|
925
|
+
value: cloneForVo(descriptor.value, visited),
|
|
926
|
+
writable: true,
|
|
927
|
+
enumerable: descriptor.enumerable,
|
|
928
|
+
configurable: true
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
return clone;
|
|
932
|
+
}
|
|
933
|
+
function throwUnsupportedClassInstance(instance) {
|
|
934
|
+
const valueObjectHint = looksLikeValueObject(instance) ? ". A value object is recognized only when a copy of this kit version built it and its props are frozen" : "";
|
|
935
|
+
throw new TypeError(`vo() cannot clone custom class instances: Value Objects are plain data${valueObjectHint}`);
|
|
936
|
+
}
|
|
937
|
+
function throwOpenValueObjectFields(keys) {
|
|
938
|
+
throw new TypeError(`vo() cannot nest a value object with own fields outside props (${keys.map(String).join(", ")}): keep the state of a value object in props`);
|
|
939
|
+
}
|
|
940
|
+
function throwUnsupportedAccessorProperty() {
|
|
941
|
+
throw new TypeError("vo() cannot clone accessor properties: Value Objects are plain data");
|
|
942
|
+
}
|
|
943
|
+
function throwUnsupportedValueSemantics(tag) {
|
|
944
|
+
const name = tag.startsWith("[object ") ? tag.slice(8, -1) : tag;
|
|
945
|
+
throw new TypeError(`vo() cannot accept ${name} values: Value Objects require immutable value semantics`);
|
|
946
|
+
}
|
|
947
|
+
function isPrimitiveValue(value) {
|
|
948
|
+
return value === null || typeof value !== "object" && typeof value !== "function";
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Creates a deeply immutable value object from the given data.
|
|
952
|
+
*
|
|
953
|
+
* The input is first deep-cloned, then the clone is frozen, so calling
|
|
954
|
+
* `vo(input)` never freezes the caller's own object graph as a
|
|
955
|
+
* side-effect. Mutating the input afterwards does not bleed into the VO.
|
|
956
|
+
* Symbol-keyed properties are preserved (matching `voEquals`). A kit
|
|
957
|
+
* `ValueObject` instance nested in the input is kept by reference and
|
|
958
|
+
* frozen in place; it must keep all of its state in `props`. A value
|
|
959
|
+
* object as the input itself is rejected.
|
|
960
|
+
* Function values and every other custom class instance are rejected
|
|
961
|
+
* (Value Objects are plain data, not behaviour-bearing object graphs). Inputs must be trusted and
|
|
962
|
+
* Proxy-free: ECMAScript provides no portable way to identify a transparent
|
|
963
|
+
* Proxy without potentially executing its traps, so `vo()` is not a sandbox
|
|
964
|
+
* for hostile in-process objects. Built-ins that cannot provide immutable,
|
|
965
|
+
* value-based semantics are rejected instead of weakening the VO contract.
|
|
966
|
+
*
|
|
967
|
+
* @example
|
|
968
|
+
* ```typescript
|
|
969
|
+
* const nested = { lat: 52.5, lng: 13.4 };
|
|
970
|
+
* const address = vo({ street: "Main St", coordinates: nested });
|
|
971
|
+
* address.coordinates.lat = 99; // ❌ Cannot assign to read-only property
|
|
972
|
+
* nested.lat = 0; // ✅ caller's input still mutable
|
|
973
|
+
* ```
|
|
974
|
+
*/
|
|
975
|
+
function vo(t) {
|
|
976
|
+
rejectValueObjectAsInput(t, "vo()");
|
|
977
|
+
return deepFreeze(cloneForVo(t, /* @__PURE__ */ new WeakMap()));
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Compares two value objects for equality based on their values.
|
|
981
|
+
* Uses deep equality comparison that handles:
|
|
982
|
+
* - Nested objects and arrays
|
|
983
|
+
* - Primitives (including NaN)
|
|
984
|
+
* - Dates, Maps, Sets, RegExp
|
|
985
|
+
* - Symbol keys
|
|
986
|
+
* - Circular references
|
|
987
|
+
*
|
|
988
|
+
* @param a - First value object
|
|
989
|
+
* @param b - Second value object
|
|
990
|
+
* @returns true if both objects have the same values, false otherwise
|
|
991
|
+
*
|
|
992
|
+
* @example
|
|
993
|
+
* ```typescript
|
|
994
|
+
* const money1 = vo({ amount: 100, currency: "USD" });
|
|
995
|
+
* const money2 = vo({ amount: 100, currency: "USD" });
|
|
996
|
+
* voEquals(money1, money2); // true
|
|
997
|
+
*
|
|
998
|
+
* const address1 = vo({
|
|
999
|
+
* street: "Main St",
|
|
1000
|
+
* coordinates: { lat: 52.5, lng: 13.4 }
|
|
1001
|
+
* });
|
|
1002
|
+
* const address2 = vo({
|
|
1003
|
+
* street: "Main St",
|
|
1004
|
+
* coordinates: { lat: 52.5, lng: 13.4 }
|
|
1005
|
+
* });
|
|
1006
|
+
* voEquals(address1, address2); // true
|
|
1007
|
+
* ```
|
|
1008
|
+
*/
|
|
1009
|
+
function voEquals(a, b) {
|
|
1010
|
+
return deepEqual(a, b);
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Compares two value objects for equality while ignoring specified keys.
|
|
1014
|
+
* Useful for comparing value objects that contain metadata or optional fields
|
|
1015
|
+
* that should not affect equality comparison.
|
|
1016
|
+
*
|
|
1017
|
+
* The walk enters a nested `ValueObject` instance like any other object,
|
|
1018
|
+
* so inside it the path continues with `props`; `ignoreKeys: ["props"]`
|
|
1019
|
+
* empties every nested value object. The key under which the kit records
|
|
1020
|
+
* the class of the instance is never ignored.
|
|
1021
|
+
*
|
|
1022
|
+
* @param a - First value object
|
|
1023
|
+
* @param b - Second value object
|
|
1024
|
+
* @param options - Options specifying which keys to ignore during comparison
|
|
1025
|
+
* @returns true if both objects have the same values (after ignoring specified keys), false otherwise
|
|
1026
|
+
*
|
|
1027
|
+
* @example
|
|
1028
|
+
* ```typescript
|
|
1029
|
+
* // Value object with metadata
|
|
1030
|
+
* const address1 = vo({
|
|
1031
|
+
* street: "Main St",
|
|
1032
|
+
* city: "Berlin",
|
|
1033
|
+
* metadata: { createdAt: "2024-01-01", updatedAt: "2024-01-02" }
|
|
1034
|
+
* });
|
|
1035
|
+
*
|
|
1036
|
+
* const address2 = vo({
|
|
1037
|
+
* street: "Main St",
|
|
1038
|
+
* city: "Berlin",
|
|
1039
|
+
* metadata: { createdAt: "2024-01-01", updatedAt: "2024-01-03" }
|
|
1040
|
+
* });
|
|
1041
|
+
*
|
|
1042
|
+
* // Compare ignoring metadata timestamps
|
|
1043
|
+
* voEqualsExcept(address1, address2, {
|
|
1044
|
+
* ignoreKeys: ["updatedAt"],
|
|
1045
|
+
* ignoreKeyPredicate: (key, path) => path.includes("metadata")
|
|
1046
|
+
* }); // true
|
|
1047
|
+
*
|
|
1048
|
+
* // Compare ignoring all metadata
|
|
1049
|
+
* voEqualsExcept(address1, address2, {
|
|
1050
|
+
* ignoreKeyPredicate: (key, path) => path.includes("metadata")
|
|
1051
|
+
* }); // true
|
|
1052
|
+
* ```
|
|
1053
|
+
*/
|
|
1054
|
+
function voEqualsExcept(a, b, options) {
|
|
1055
|
+
return deepEqualExcept(a, b, keepValueObjectClass(options));
|
|
1056
|
+
}
|
|
1057
|
+
function keepValueObjectClass(options) {
|
|
1058
|
+
const { ignoreKeys, ignoreKeyPredicate } = options;
|
|
1059
|
+
return {
|
|
1060
|
+
...options,
|
|
1061
|
+
ignoreKeys: ignoreKeys?.filter((key) => key !== VALUE_OBJECT_CLASS),
|
|
1062
|
+
ignoreKeyPredicate: ignoreKeyPredicate && ((key, path) => key !== VALUE_OBJECT_CLASS && ignoreKeyPredicate(key, path))
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Creates a value object with optional validation.
|
|
1067
|
+
* Returns a Result type instead of throwing an error.
|
|
1068
|
+
*
|
|
1069
|
+
* Note: the Result covers VALIDATION failures only. Non-data values and
|
|
1070
|
+
* built-ins without immutable value semantics still throw a
|
|
1071
|
+
* `TypeError` from `vo()`; they cannot occur in parsed JSON and signal
|
|
1072
|
+
* a programming error, not a validation failure.
|
|
1073
|
+
*
|
|
1074
|
+
* @param t - The data to convert into a value object
|
|
1075
|
+
* @param validate - Validation function that returns true if valid
|
|
1076
|
+
* @param errorMessage - Optional custom error message if validation fails
|
|
1077
|
+
* @returns Result containing the value object if valid, or an error message if validation fails
|
|
1078
|
+
*
|
|
1079
|
+
* @example
|
|
1080
|
+
* ```typescript
|
|
1081
|
+
* const result = voWithValidation(
|
|
1082
|
+
* { amount: 100, currency: "USD" },
|
|
1083
|
+
* (m) => m.amount >= 0 && m.currency.length === 3,
|
|
1084
|
+
* "Invalid money: amount must be non-negative and currency must be 3 characters"
|
|
1085
|
+
* );
|
|
1086
|
+
*
|
|
1087
|
+
* if (result.ok) {
|
|
1088
|
+
* console.log(result.value); // Use the value object
|
|
1089
|
+
* } else {
|
|
1090
|
+
* console.error(result.error); // Handle validation error
|
|
1091
|
+
* }
|
|
1092
|
+
* ```
|
|
1093
|
+
*/
|
|
1094
|
+
function voWithValidation(t, validate, errorMessage) {
|
|
1095
|
+
if (!validate(t)) return err(errorMessage ?? `Validation failed for value object: ${describeValue(t)}`);
|
|
1096
|
+
return ok(vo(t));
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Best-effort rendering of a value for the default validation-failure
|
|
1100
|
+
* message. `JSON.stringify` throws for cyclic and BigInt-bearing values, and
|
|
1101
|
+
* the error path of a Result-returning function must never throw itself.
|
|
1102
|
+
*/
|
|
1103
|
+
function describeValue(value) {
|
|
1104
|
+
try {
|
|
1105
|
+
const json = JSON.stringify(value);
|
|
1106
|
+
if (json !== void 0) return json;
|
|
1107
|
+
} catch {}
|
|
1108
|
+
return String(value);
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Abstract base class for creating Value Objects.
|
|
1112
|
+
* Value Objects are immutable and defined by their properties. A value
|
|
1113
|
+
* object can hold other value objects in its props. Every instance
|
|
1114
|
+
* records its class under an own symbol key, so `equals` compares a
|
|
1115
|
+
* nested value object by class and props and does not call its `equals`
|
|
1116
|
+
* method.
|
|
1117
|
+
*
|
|
1118
|
+
* @template T - The shape of the value object's properties
|
|
1119
|
+
*/
|
|
1120
|
+
var ValueObject = class {
|
|
1121
|
+
props;
|
|
1122
|
+
/**
|
|
1123
|
+
* Creates a new ValueObject.
|
|
1124
|
+
* The plain-data properties are deep-cloned and then deeply
|
|
1125
|
+
* frozen, so the caller's own object graph is never frozen or mutated,
|
|
1126
|
+
* and later mutation of the input does not bleed into the value object.
|
|
1127
|
+
*
|
|
1128
|
+
* @param props - The properties of the value object
|
|
1129
|
+
* @example
|
|
1130
|
+
* ```ts
|
|
1131
|
+
* class Money extends ValueObject<{ amount: number; currency: string }> {
|
|
1132
|
+
* constructor(props: { amount: number; currency: string }) {
|
|
1133
|
+
* super(props);
|
|
1134
|
+
* }
|
|
1135
|
+
*
|
|
1136
|
+
* protected validate(props: { amount: number; currency: string }): void {
|
|
1137
|
+
* if (props.amount < 0) throw new Error("Amount cannot be negative");
|
|
1138
|
+
* }
|
|
1139
|
+
* }
|
|
1140
|
+
* ```
|
|
1141
|
+
*/
|
|
1142
|
+
constructor(props) {
|
|
1143
|
+
rejectValueObjectAsInput(props, "new ValueObject()");
|
|
1144
|
+
this.validate(props);
|
|
1145
|
+
this.props = deepFreeze(cloneForVo(props, /* @__PURE__ */ new WeakMap()));
|
|
1146
|
+
recordValueObjectClass(this, this.constructor);
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Optional validation hook that can be overridden by subclasses.
|
|
1150
|
+
* Should throw an error if validation fails.
|
|
1151
|
+
*
|
|
1152
|
+
* @param props - The properties to validate
|
|
1153
|
+
* @throws Error if validation fails
|
|
1154
|
+
*/
|
|
1155
|
+
validate(props) {}
|
|
1156
|
+
/**
|
|
1157
|
+
* Checks if this value object is equal to another.
|
|
1158
|
+
* Uses deep equality comparison on the properties and checks for constructor equality.
|
|
1159
|
+
*
|
|
1160
|
+
* @param other - The other value object to compare
|
|
1161
|
+
* @returns true if the properties are deeply equal and constructors match
|
|
1162
|
+
*/
|
|
1163
|
+
equals(other) {
|
|
1164
|
+
if (other === null || other === void 0) return false;
|
|
1165
|
+
if (this.constructor !== other.constructor) return false;
|
|
1166
|
+
return deepEqual(this.props, other.props);
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Creates a clone of the value object with optional property overrides.
|
|
1170
|
+
*
|
|
1171
|
+
* @param props - Optional properties to override
|
|
1172
|
+
* @returns A new instance of the value object
|
|
1173
|
+
*/
|
|
1174
|
+
clone(props) {
|
|
1175
|
+
const Constructor = this.constructor;
|
|
1176
|
+
const merged = {
|
|
1177
|
+
...this.props,
|
|
1178
|
+
...props || {}
|
|
1179
|
+
};
|
|
1180
|
+
for (const key of Reflect.ownKeys(this.props)) {
|
|
1181
|
+
const descriptor = Object.getOwnPropertyDescriptor(this.props, key);
|
|
1182
|
+
if (descriptor === void 0 || descriptor.enumerable) continue;
|
|
1183
|
+
if (props && Object.hasOwn(props, key)) continue;
|
|
1184
|
+
Object.defineProperty(merged, key, {
|
|
1185
|
+
value: this.props[key],
|
|
1186
|
+
writable: true,
|
|
1187
|
+
enumerable: false,
|
|
1188
|
+
configurable: true
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
return new Constructor(merged);
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Serializes the value object to its raw properties for JSON operations.
|
|
1195
|
+
*
|
|
1196
|
+
* @returns The raw properties object
|
|
1197
|
+
*/
|
|
1198
|
+
toJSON() {
|
|
1199
|
+
return this.props;
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
//#endregion
|
|
1204
|
+
//#region src/internal/cooperative-brand.ts
|
|
1205
|
+
/**
|
|
1206
|
+
* A cooperative brand marks an object that a kit constructor produced, so
|
|
1207
|
+
* a second loaded copy of the kit (duplicate npm dependency, dual CJS/ESM
|
|
1208
|
+
* load, plugin bundle) recognizes it. A module-private WeakSet cannot do
|
|
1209
|
+
* that: it is bound to one loaded copy. The brand is a `Symbol.for` key
|
|
1210
|
+
* with the value `true`. The property is non-enumerable, so it never
|
|
1211
|
+
* leaks into spreads, JSON, or equality, and non-writable and
|
|
1212
|
+
* non-configurable, so nothing edits it after the stamp.
|
|
1213
|
+
*
|
|
1214
|
+
* The probe reads the brand as an OWN property with exactly these
|
|
1215
|
+
* attributes and requires the carrier to be frozen. An object that
|
|
1216
|
+
* inherits a branded object through its prototype can carry mutable own
|
|
1217
|
+
* overrides; a branded object that is still open can change after the
|
|
1218
|
+
* stamp. Neither is what the constructor produced. Every stamp site
|
|
1219
|
+
* therefore freezes the carrier right after {@link stampCooperativeBrand}.
|
|
1220
|
+
*
|
|
1221
|
+
* The brand is forgeable BY DESIGN. It catches accidental hand-rolled
|
|
1222
|
+
* literals; it is not a security boundary against code in the same
|
|
1223
|
+
* process that fakes it on purpose.
|
|
1224
|
+
*
|
|
1225
|
+
* Internal utility (not exported from the package barrels).
|
|
1226
|
+
*/
|
|
1227
|
+
function stampCooperativeBrand(target, brand) {
|
|
1228
|
+
Object.defineProperty(target, brand, {
|
|
1229
|
+
value: true,
|
|
1230
|
+
enumerable: false,
|
|
1231
|
+
writable: false,
|
|
1232
|
+
configurable: false
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Whether `value` carries `brand` as an own, non-enumerable, non-writable,
|
|
1237
|
+
* non-configurable `true` and is frozen. A Proxy trap that throws reads
|
|
1238
|
+
* as unbranded.
|
|
1239
|
+
*/
|
|
1240
|
+
function hasCooperativeBrand(value, brand) {
|
|
1241
|
+
if (value === null || typeof value !== "object" && typeof value !== "function") return false;
|
|
1242
|
+
try {
|
|
1243
|
+
const marker = Reflect.getOwnPropertyDescriptor(value, brand);
|
|
1244
|
+
return marker?.value === true && marker.enumerable === false && marker.writable === false && marker.configurable === false && Object.isFrozen(value);
|
|
1245
|
+
} catch {
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
//#endregion
|
|
1251
|
+
//#region src/domain/event/clock.ts
|
|
1252
|
+
/** Immutable library default captured by the default domain-event factory. */
|
|
1253
|
+
const defaultClockFactory = () => /* @__PURE__ */ new Date();
|
|
1254
|
+
/** Internal defensive event-clock read. */
|
|
1255
|
+
function readClock(factory) {
|
|
1256
|
+
const reading = factory();
|
|
1257
|
+
const value = reading instanceof Date ? reading.getTime() : NaN;
|
|
1258
|
+
if (!Number.isFinite(value)) throw new TypeError("domain-event clock must return a valid Date");
|
|
1259
|
+
return new Date(value);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
//#endregion
|
|
1263
|
+
//#region src/domain/event/domain-event-errors.ts
|
|
1264
|
+
/**
|
|
1265
|
+
* Stable contract error for malformed domain-event data.
|
|
1266
|
+
*
|
|
1267
|
+
* It remains a `TypeError` for JavaScript callers while exposing a code and
|
|
1268
|
+
* field that do not depend on the wording of the human-readable message.
|
|
1269
|
+
*/
|
|
1270
|
+
var DomainEventValidationError = class extends TypeError {
|
|
1271
|
+
code;
|
|
1272
|
+
field;
|
|
1273
|
+
name;
|
|
1274
|
+
constructor(code, field, message) {
|
|
1275
|
+
super(message);
|
|
1276
|
+
this.code = code;
|
|
1277
|
+
this.field = field;
|
|
1278
|
+
this.name = code;
|
|
1279
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
var SnapshotTimeValidationError = class extends TypeError {
|
|
1283
|
+
name = "SNAPSHOT_TIME_INVALID";
|
|
1284
|
+
code = "SNAPSHOT_TIME_INVALID";
|
|
1285
|
+
field = "snapshotAt";
|
|
1286
|
+
constructor() {
|
|
1287
|
+
super("snapshotAt must be a valid Date");
|
|
1288
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
//#endregion
|
|
1293
|
+
//#region src/domain/event/domain-event.ts
|
|
1294
|
+
const defaultEventIdFactory = () => crypto.randomUUID();
|
|
1295
|
+
/**
|
|
1296
|
+
* Creates an immutable, instance-bound domain-event factory.
|
|
1297
|
+
*
|
|
1298
|
+
* The supplied functions are read once and captured by value. The returned
|
|
1299
|
+
* object is frozen, so another request, test, or library cannot replace its
|
|
1300
|
+
* policy. Its {@link DomainEventFactory.createStamp} method is the
|
|
1301
|
+
* application-shell bridge that records an accepted aggregate decision.
|
|
1302
|
+
* Passing the factory through `AggregateConfig`
|
|
1303
|
+
* enables the explicitly named convenience methods, whose defaults read time
|
|
1304
|
+
* and randomness.
|
|
1305
|
+
*
|
|
1306
|
+
* @example
|
|
1307
|
+
* ```ts
|
|
1308
|
+
* const domainEvents = createDomainEventFactory({
|
|
1309
|
+
* eventIdFactory: () => uuidv7(),
|
|
1310
|
+
* clock: () => new Date(),
|
|
1311
|
+
* });
|
|
1312
|
+
* order.confirm();
|
|
1313
|
+
* recordPendingEvents(order, domainEvents);
|
|
1314
|
+
* ```
|
|
1315
|
+
*/
|
|
1316
|
+
/**
|
|
1317
|
+
* Fills the origin of a factory into options that name none.
|
|
1318
|
+
*
|
|
1319
|
+
* A call site that states its own source keeps it: an explicit fact about one
|
|
1320
|
+
* event outranks the default of the factory that mints it.
|
|
1321
|
+
*/
|
|
1322
|
+
function withFactorySource(options, source) {
|
|
1323
|
+
const given = options ?? {};
|
|
1324
|
+
if (source === void 0 || given.metadata?.source !== void 0) return given;
|
|
1325
|
+
return {
|
|
1326
|
+
...given,
|
|
1327
|
+
metadata: {
|
|
1328
|
+
...given.metadata,
|
|
1329
|
+
source
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
function createDomainEventFactory(options = {}) {
|
|
1334
|
+
const eventIdFactory = options.eventIdFactory ?? defaultEventIdFactory;
|
|
1335
|
+
const clock = options.clock ?? defaultClockFactory;
|
|
1336
|
+
const { source } = options;
|
|
1337
|
+
const create = ((type, payload, createOptions) => mintDomainEvent(type, payload, withFactorySource(createOptions, source), eventIdFactory, clock));
|
|
1338
|
+
const createStamp = (stampOptions = {}) => {
|
|
1339
|
+
const explicitOccurredAt = stampOptions.occurredAt === void 0 ? void 0 : copyValidEventDate(stampOptions.occurredAt);
|
|
1340
|
+
if (stampOptions.eventId !== void 0) assertNonBlankEventField(stampOptions.eventId, "eventId", "EVENT_ID_INVALID");
|
|
1341
|
+
const eventId = stampOptions.eventId ?? eventIdFactory();
|
|
1342
|
+
assertNonBlankEventField(eventId, "eventId", "EVENT_ID_INVALID");
|
|
1343
|
+
const stamp = {
|
|
1344
|
+
eventId,
|
|
1345
|
+
occurredAt: explicitOccurredAt ?? readEventClock(clock),
|
|
1346
|
+
metadata: cloneOwnedEventData(withFactorySource(stampOptions, source).metadata, "metadata")
|
|
1347
|
+
};
|
|
1348
|
+
const owned = deepFreeze(stamp);
|
|
1349
|
+
FACTORY_OWNED_EVENT_STAMPS.add(owned);
|
|
1350
|
+
return owned;
|
|
1351
|
+
};
|
|
1352
|
+
return Object.freeze({
|
|
1353
|
+
createStamp,
|
|
1354
|
+
create,
|
|
1355
|
+
now: () => readClock(clock)
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Immutable UUID-v4/platform-clock factory used by the top-level
|
|
1360
|
+
* {@link createDomainEvent}. It cannot be reconfigured; construct an instance
|
|
1361
|
+
* with {@link createDomainEventFactory} for custom policy.
|
|
1362
|
+
*/
|
|
1363
|
+
const defaultDomainEventFactory = createDomainEventFactory();
|
|
1364
|
+
/**
|
|
1365
|
+
* Creates a domain event with default values.
|
|
1366
|
+
* Sets occurredAt to current date and schemaVersion to 1 if not provided.
|
|
1367
|
+
*
|
|
1368
|
+
* **Input ownership.** The event is deeply frozen, and `payload` and
|
|
1369
|
+
* `metadata` are deep-cloned first, so the caller's own objects are never
|
|
1370
|
+
* frozen in place and later mutation of them does not bleed into the
|
|
1371
|
+
* event (same contract as `vo()`). The clone follows the plain-data event
|
|
1372
|
+
* contract via `structuredClone`: functions, Promise, and WeakMap/WeakSet
|
|
1373
|
+
* values throw a `TypeError`; symbol-keyed properties are not carried
|
|
1374
|
+
* over.
|
|
1375
|
+
*
|
|
1376
|
+
* **For aggregate-internal events, prefer `this.createEvent(...)` on
|
|
1377
|
+
* `StateStoredAggregate` / `EventSourcedAggregate`.** That helper auto-injects
|
|
1378
|
+
* `aggregateId` (from `this.id`) and `aggregateType` (from the
|
|
1379
|
+
* aggregate's declared `aggregateType` property), which downstream
|
|
1380
|
+
* consumers (outbox dispatchers, projection handlers, audit logs)
|
|
1381
|
+
* route by. The commit boundary validates that both fields are present
|
|
1382
|
+
* and throws if they are missing, so a direct `createDomainEvent(...)`
|
|
1383
|
+
* call inside an aggregate that forgets the options is caught at
|
|
1384
|
+
* runtime. Record pending decisions in the application
|
|
1385
|
+
* shell before repository persistence or outbox harvest.
|
|
1386
|
+
*
|
|
1387
|
+
* Use `createDomainEvent(...)` directly for events that don't belong to
|
|
1388
|
+
* an aggregate: system events, integration events, configuration events,
|
|
1389
|
+
* test fixtures. For those, set `aggregateId` / `aggregateType` in
|
|
1390
|
+
* `options` if downstream consumers expect routing metadata.
|
|
1391
|
+
*
|
|
1392
|
+
* @param type - The event type
|
|
1393
|
+
* @param payload - The event payload
|
|
1394
|
+
* @param options - Optional event configuration (including `aggregateId`
|
|
1395
|
+
* and `aggregateType` for routing)
|
|
1396
|
+
* @returns A domain event
|
|
1397
|
+
*
|
|
1398
|
+
* @example
|
|
1399
|
+
* ```typescript
|
|
1400
|
+
* const event = createDomainEvent("OrderCreated", { orderId: "123" });
|
|
1401
|
+
* ```
|
|
1402
|
+
*/
|
|
1403
|
+
const RECORDED_EVENTS = /* @__PURE__ */ new WeakSet();
|
|
1404
|
+
const UNCOMMITTED_EVENTS = /* @__PURE__ */ new WeakSet();
|
|
1405
|
+
const FACTORY_OWNED_EVENT_STAMPS = /* @__PURE__ */ new WeakSet();
|
|
1406
|
+
const RECORDED_BRAND = Symbol.for("@shirudo/ddd-kit.mintedEvent");
|
|
1407
|
+
const UNCOMMITTED_BRAND = Symbol.for("@shirudo/ddd-kit.uncommittedEvent");
|
|
1408
|
+
function stampRecordedBrand(event) {
|
|
1409
|
+
stampCooperativeBrand(event, RECORDED_BRAND);
|
|
1410
|
+
}
|
|
1411
|
+
function stampUncommittedBrand(event) {
|
|
1412
|
+
stampCooperativeBrand(event, UNCOMMITTED_BRAND);
|
|
1413
|
+
}
|
|
1414
|
+
function isFactoryOwnedDomainEventStamp(stamp) {
|
|
1415
|
+
return FACTORY_OWNED_EVENT_STAMPS.has(stamp);
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Whether `event` is a recorded domain event: it came out of
|
|
1419
|
+
* {@link createDomainEvent}, {@link createDomainEventFromFacts}, or
|
|
1420
|
+
* {@link recordDomainEvent}, so it is deeply frozen with defensively copied
|
|
1421
|
+
* payload and metadata. An uncommitted decision carries the other brand; see
|
|
1422
|
+
* {@link isUncommittedDomainEvent}. Two tiers: events of THIS
|
|
1423
|
+
* loaded copy of the kit are verified through the module-private
|
|
1424
|
+
* WeakSet; events minted by ANOTHER copy (duplicate dependency, dual
|
|
1425
|
+
* CJS/ESM load) are recognized through a cooperative `Symbol.for`
|
|
1426
|
+
* brand that code in the same process can fake. The gate catches
|
|
1427
|
+
* accidents, not adversaries. Module-internal export for the aggregate
|
|
1428
|
+
* recording paths; not part of the package entries.
|
|
1429
|
+
*/
|
|
1430
|
+
function isRecordedDomainEvent(event) {
|
|
1431
|
+
return RECORDED_EVENTS.has(event) || hasCooperativeBrand(event, RECORDED_BRAND);
|
|
1432
|
+
}
|
|
1433
|
+
/** Whether a value was created by {@link createUncommittedDomainEvent}. */
|
|
1434
|
+
function isUncommittedDomainEvent(event) {
|
|
1435
|
+
return UNCOMMITTED_EVENTS.has(event) || hasCooperativeBrand(event, UNCOMMITTED_BRAND);
|
|
1436
|
+
}
|
|
1437
|
+
function createUncommittedDomainEvent(type, payload, options) {
|
|
1438
|
+
assertProducerOwnedEventFields(type, options);
|
|
1439
|
+
const event = {
|
|
1440
|
+
type,
|
|
1441
|
+
aggregateId: options?.aggregateId,
|
|
1442
|
+
aggregateType: options?.aggregateType,
|
|
1443
|
+
payload: cloneOwnedEventData(payload, "payload"),
|
|
1444
|
+
schemaVersion: options?.schemaVersion ?? 1
|
|
1445
|
+
};
|
|
1446
|
+
stampUncommittedBrand(event);
|
|
1447
|
+
const uncommitted = deepFreeze(event);
|
|
1448
|
+
UNCOMMITTED_EVENTS.add(uncommitted);
|
|
1449
|
+
return uncommitted;
|
|
1450
|
+
}
|
|
1451
|
+
/** Brands and freezes a kit-derived copy of an uncommitted event. */
|
|
1452
|
+
function adoptUncommittedDomainEvent(copy) {
|
|
1453
|
+
stampUncommittedBrand(copy);
|
|
1454
|
+
Object.freeze(copy);
|
|
1455
|
+
UNCOMMITTED_EVENTS.add(copy);
|
|
1456
|
+
return copy;
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Attaches shell-owned recording data to an accepted aggregate decision.
|
|
1460
|
+
*
|
|
1461
|
+
* The decision supplies the domain type, payload, source address, and payload
|
|
1462
|
+
* schema version. The stamp supplies only event identity, recording time, and
|
|
1463
|
+
* trace metadata.
|
|
1464
|
+
*/
|
|
1465
|
+
function recordDomainEvent(event, stamp) {
|
|
1466
|
+
if (!isUncommittedDomainEvent(event)) throw new TypeError("recordDomainEvent requires an event created by createUncommittedDomainEvent");
|
|
1467
|
+
if (isFactoryOwnedDomainEventStamp(stamp)) return mintRecordedEvent(event, stamp.eventId, stamp.occurredAt, stamp.metadata);
|
|
1468
|
+
assertNonBlankEventField(stamp.eventId, "eventId", "EVENT_ID_INVALID");
|
|
1469
|
+
const occurredAt = deepFreeze(copyValidEventDate(stamp.occurredAt));
|
|
1470
|
+
const metadata = cloneOwnedEventData(stamp.metadata, "metadata");
|
|
1471
|
+
return mintRecordedEvent(event, stamp.eventId, occurredAt, metadata === void 0 ? void 0 : deepFreeze(metadata));
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Single mint tail for both stamp provenances. The stamp fields arrive
|
|
1475
|
+
* pre-validated, copied, and frozen (by `createStamp` for factory-owned
|
|
1476
|
+
* stamps, by `recordDomainEvent` for caller-built stamps); the uncommitted
|
|
1477
|
+
* event's payload is already defensively cloned and deeply frozen by its
|
|
1478
|
+
* constructor and is shared instead of paying a second deep copy per event.
|
|
1479
|
+
*/
|
|
1480
|
+
function mintRecordedEvent(event, eventId, occurredAt, metadata) {
|
|
1481
|
+
assertProducerOwnedEventFields(event.type, event);
|
|
1482
|
+
const recorded = {
|
|
1483
|
+
eventId,
|
|
1484
|
+
type: event.type,
|
|
1485
|
+
aggregateId: event.aggregateId,
|
|
1486
|
+
aggregateType: event.aggregateType,
|
|
1487
|
+
payload: event.payload,
|
|
1488
|
+
occurredAt,
|
|
1489
|
+
schemaVersion: event.schemaVersion,
|
|
1490
|
+
metadata
|
|
1491
|
+
};
|
|
1492
|
+
stampRecordedBrand(recorded);
|
|
1493
|
+
Object.freeze(recorded);
|
|
1494
|
+
RECORDED_EVENTS.add(recorded);
|
|
1495
|
+
return recorded;
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Brands, freezes, and registers a kit-derived copy of a recorded event
|
|
1499
|
+
* (e.g. the address-stamped copy `apply()` creates) as recorded itself.
|
|
1500
|
+
* The copy shares the already-frozen payload/metadata of its source,
|
|
1501
|
+
* so the mint guarantee carries over. Stamping the cooperative brand
|
|
1502
|
+
* before freezing keeps the copy recognizable by another loaded kit
|
|
1503
|
+
* instance as well as by this instance's WeakSet. Module-internal
|
|
1504
|
+
* export; not part of the package entries.
|
|
1505
|
+
*/
|
|
1506
|
+
function adoptRecordedDomainEvent(copy) {
|
|
1507
|
+
stampRecordedBrand(copy);
|
|
1508
|
+
Object.freeze(copy);
|
|
1509
|
+
RECORDED_EVENTS.add(copy);
|
|
1510
|
+
return copy;
|
|
1511
|
+
}
|
|
1512
|
+
function createDomainEvent(type, payload, options) {
|
|
1513
|
+
return defaultDomainEventFactory.create(type, payload, options);
|
|
1514
|
+
}
|
|
1515
|
+
function createDomainEventFromFacts(type, payload, options) {
|
|
1516
|
+
if (options?.eventId === void 0) missingExplicitEventId();
|
|
1517
|
+
if (options.occurredAt === void 0) missingExplicitOccurredAt();
|
|
1518
|
+
return mintDomainEvent(type, payload, options, missingExplicitEventId, missingExplicitOccurredAt);
|
|
1519
|
+
}
|
|
1520
|
+
function missingExplicitEventId() {
|
|
1521
|
+
throw new DomainEventValidationError("EVENT_ID_REQUIRED", "eventId", "createDomainEventFromFacts requires an explicit eventId");
|
|
1522
|
+
}
|
|
1523
|
+
function missingExplicitOccurredAt() {
|
|
1524
|
+
throw new DomainEventValidationError("EVENT_OCCURRED_AT_REQUIRED", "occurredAt", "createDomainEventFromFacts requires an explicit occurredAt");
|
|
1525
|
+
}
|
|
1526
|
+
function mintDomainEvent(type, payload, options, eventIdFactory, clock) {
|
|
1527
|
+
assertProducerOwnedEventFields(type, options);
|
|
1528
|
+
const eventId = options?.eventId ?? eventIdFactory();
|
|
1529
|
+
assertNonBlankEventField(eventId, "eventId", "EVENT_ID_INVALID");
|
|
1530
|
+
const occurredAt = options?.occurredAt === void 0 ? readEventClock(clock) : copyValidEventDate(options.occurredAt);
|
|
1531
|
+
const schemaVersion = options?.schemaVersion ?? 1;
|
|
1532
|
+
const event = {
|
|
1533
|
+
eventId,
|
|
1534
|
+
type,
|
|
1535
|
+
aggregateId: options?.aggregateId,
|
|
1536
|
+
aggregateType: options?.aggregateType,
|
|
1537
|
+
payload: cloneOwnedEventData(payload, "payload"),
|
|
1538
|
+
occurredAt,
|
|
1539
|
+
schemaVersion,
|
|
1540
|
+
metadata: cloneOwnedEventData(options?.metadata, "metadata")
|
|
1541
|
+
};
|
|
1542
|
+
stampRecordedBrand(event);
|
|
1543
|
+
const minted = deepFreeze(event);
|
|
1544
|
+
RECORDED_EVENTS.add(minted);
|
|
1545
|
+
return minted;
|
|
1546
|
+
}
|
|
1547
|
+
function assertProducerOwnedEventFields(type, options) {
|
|
1548
|
+
assertNonBlankEventField(type, "type", "EVENT_TYPE_INVALID");
|
|
1549
|
+
const schemaVersion = options?.schemaVersion ?? 1;
|
|
1550
|
+
if (!Number.isSafeInteger(schemaVersion) || typeof schemaVersion !== "number" || schemaVersion < 1) throw new DomainEventValidationError("EVENT_SCHEMA_VERSION_INVALID", "schemaVersion", "domain-event schemaVersion must be a safe integer greater than or equal to 1");
|
|
1551
|
+
if (options?.aggregateId !== void 0) assertNonBlankEventField(options.aggregateId, "aggregateId", "EVENT_ADDRESS_INVALID");
|
|
1552
|
+
if (options?.aggregateType !== void 0) assertNonBlankEventField(options.aggregateType, "aggregateType", "EVENT_ADDRESS_INVALID");
|
|
1553
|
+
}
|
|
1554
|
+
function assertNonBlankEventField(value, field, code) {
|
|
1555
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new DomainEventValidationError(code, field, `domain-event ${field} must be a non-blank string`);
|
|
1556
|
+
}
|
|
1557
|
+
function copyValidEventDate(value) {
|
|
1558
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) throw new DomainEventValidationError("EVENT_OCCURRED_AT_INVALID", "occurredAt", "domain-event occurredAt must be a valid Date");
|
|
1559
|
+
return new Date(value.getTime());
|
|
1560
|
+
}
|
|
1561
|
+
function readEventClock(clock) {
|
|
1562
|
+
return copyValidEventDate(clock());
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Deep-clones caller-supplied event data (payload, metadata) before the
|
|
1566
|
+
* event is frozen, so `createDomainEvent` never freezes or aliases the
|
|
1567
|
+
* caller's own object graph. Primitives pass through unchanged.
|
|
1568
|
+
*
|
|
1569
|
+
* Uses `structuredClone`, which matches the documented plain-data event
|
|
1570
|
+
* contract: functions, Promise, and WeakMap/WeakSet values throw a
|
|
1571
|
+
* descriptive `TypeError` (they are not data); symbol-keyed properties
|
|
1572
|
+
* are not carried over; a class instance would silently lose its
|
|
1573
|
+
* prototype, which the plain-data contract already rules out.
|
|
1574
|
+
*/
|
|
1575
|
+
function cloneOwnedEventData(value, field) {
|
|
1576
|
+
if (typeof value === "function") throw new TypeError(`createDomainEvent: ${field} must not be a function: domain events are plain data`);
|
|
1577
|
+
if (value === null && field === "metadata") throw new TypeError("createDomainEvent: metadata must be an object or undefined; received null");
|
|
1578
|
+
if (value === null || typeof value !== "object") return value;
|
|
1579
|
+
assertNoHostileOwnProtoKey(value, field === "payload" ? "Event payload" : "Event metadata");
|
|
1580
|
+
assertNoBinaryData(value, field);
|
|
1581
|
+
try {
|
|
1582
|
+
return structuredClone(value);
|
|
1583
|
+
} catch (cause) {
|
|
1584
|
+
throw new TypeError(`createDomainEvent: ${field} must be plain, structured-cloneable data (no functions, Promises, or WeakMap/WeakSet values): domain events are plain data`, { cause });
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
function isBinaryData(value) {
|
|
1588
|
+
return ArrayBuffer.isView(value) || value instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer;
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Walks caller-supplied event data and rejects binary buffers anywhere
|
|
1592
|
+
* in the graph (TypedArray, DataView, ArrayBuffer, SharedArrayBuffer):
|
|
1593
|
+
* they are mutable by construction, so the deep-freeze that backs the
|
|
1594
|
+
* mint guarantee cannot cover them. Runs before the structured clone,
|
|
1595
|
+
* on the small plain-data graphs events are documented to carry.
|
|
1596
|
+
*/
|
|
1597
|
+
function assertNoBinaryData(value, field, visited = /* @__PURE__ */ new WeakSet()) {
|
|
1598
|
+
if (value === null || typeof value !== "object") return;
|
|
1599
|
+
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.`);
|
|
1600
|
+
if (visited.has(value)) return;
|
|
1601
|
+
visited.add(value);
|
|
1602
|
+
if (value instanceof Map) {
|
|
1603
|
+
for (const [k, v] of value) {
|
|
1604
|
+
assertNoBinaryData(k, field, visited);
|
|
1605
|
+
assertNoBinaryData(v, field, visited);
|
|
1606
|
+
}
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
if (value instanceof Set) {
|
|
1610
|
+
for (const v of value) assertNoBinaryData(v, field, visited);
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (Array.isArray(value)) {
|
|
1614
|
+
for (const v of value) assertNoBinaryData(v, field, visited);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
for (const key of Object.keys(value)) assertNoBinaryData(value[key], field, visited);
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Copies metadata from a source event to a new event.
|
|
1621
|
+
* Useful for maintaining correlation chains in event-driven architectures.
|
|
1622
|
+
*
|
|
1623
|
+
* @example
|
|
1624
|
+
* ```typescript
|
|
1625
|
+
* const newEvent = createDomainEvent(
|
|
1626
|
+
* "OrderShipped",
|
|
1627
|
+
* { orderId: "123" },
|
|
1628
|
+
* { metadata: copyMetadata(previousEvent, { causationId: previousEvent.eventId }) }
|
|
1629
|
+
* );
|
|
1630
|
+
* ```
|
|
1631
|
+
*/
|
|
1632
|
+
function copyMetadata(sourceEvent, additionalMetadata) {
|
|
1633
|
+
if (sourceEvent.metadata !== void 0) assertNoHostileOwnProtoKey(sourceEvent.metadata, "Event metadata");
|
|
1634
|
+
if (additionalMetadata !== void 0) assertNoHostileOwnProtoKey(additionalMetadata, "Event metadata");
|
|
1635
|
+
return {
|
|
1636
|
+
...sourceEvent.metadata ?? {},
|
|
1637
|
+
...additionalMetadata ?? {}
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Merges multiple metadata objects into one.
|
|
1642
|
+
* Later metadata objects override earlier ones for the same keys.
|
|
1643
|
+
*
|
|
1644
|
+
* @example
|
|
1645
|
+
* ```typescript
|
|
1646
|
+
* const metadata = mergeMetadata(
|
|
1647
|
+
* { correlationId: "corr-123" },
|
|
1648
|
+
* { userId: "user-456" },
|
|
1649
|
+
* { source: "order-service" }
|
|
1650
|
+
* );
|
|
1651
|
+
* ```
|
|
1652
|
+
*/
|
|
1653
|
+
function mergeMetadata(...metadataObjects) {
|
|
1654
|
+
const merged = {};
|
|
1655
|
+
for (const metadata of metadataObjects) {
|
|
1656
|
+
if (!metadata) continue;
|
|
1657
|
+
assertNoHostileOwnProtoKey(metadata, "Event metadata");
|
|
1658
|
+
for (const key of Reflect.ownKeys(metadata)) {
|
|
1659
|
+
if (!Object.getOwnPropertyDescriptor(metadata, key)?.enumerable) continue;
|
|
1660
|
+
Object.defineProperty(merged, key, {
|
|
1661
|
+
value: metadata[key],
|
|
1662
|
+
writable: true,
|
|
1663
|
+
enumerable: true,
|
|
1664
|
+
configurable: true
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
return merged;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
//#endregion
|
|
1672
|
+
//#region src/internal/async/abort.ts
|
|
1673
|
+
/**
|
|
1674
|
+
* The value to reject with when an `AbortSignal` has fired.
|
|
1675
|
+
*
|
|
1676
|
+
* Returns the signal's `reason` (a `DOMException` `AbortError` for
|
|
1677
|
+
* `controller.abort()`, `TimeoutError` for `AbortSignal.timeout`), falling
|
|
1678
|
+
* back to a plain `Error` with `fallbackMessage` when `reason` is nullish.
|
|
1679
|
+
* A spec-compliant signal always populates `reason` when aborted, so the
|
|
1680
|
+
* fallback only fires for a non-spec polyfill; without it, a bare
|
|
1681
|
+
* `throw undefined` would surface, breaking `instanceof Error` handling.
|
|
1682
|
+
*
|
|
1683
|
+
* Centralizes the `signal.reason ?? new Error(...)` idiom used at every
|
|
1684
|
+
* abort site (event bus, `withCommit`, `UnitOfWork.run`, the retrying
|
|
1685
|
+
* scope) so a single fix covers all of them.
|
|
1686
|
+
*/
|
|
1687
|
+
function abortReason(signal, fallbackMessage) {
|
|
1688
|
+
return signal.reason ?? new Error(fallbackMessage);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
//#endregion
|
|
1692
|
+
//#region src/internal/validate.ts
|
|
1693
|
+
/**
|
|
1694
|
+
* Shared construction-time guards for numeric options. `context` names
|
|
1695
|
+
* the throwing component so the error reads like the component's own
|
|
1696
|
+
* validation ("OutboxDispatcher: pollIntervalMs must be...").
|
|
1697
|
+
*/
|
|
1698
|
+
/** Guard for numeric options that must be a non-negative finite number. */
|
|
1699
|
+
function assertNonNegativeFinite(context, field, value) {
|
|
1700
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`${context}: ${field} must be a non-negative finite number, got ${value}`);
|
|
1701
|
+
}
|
|
1702
|
+
/** Guard for count options that must be a whole number of at least 1. */
|
|
1703
|
+
function assertPositiveInteger(context, field, value) {
|
|
1704
|
+
if (!Number.isInteger(value) || value < 1) throw new Error(`${context}: ${field} must be an integer >= 1, got ${value}`);
|
|
1705
|
+
}
|
|
1706
|
+
/** Guard for retained-record capacities that must fit exact JS integers. */
|
|
1707
|
+
function assertPositiveSafeInteger(context, field, value) {
|
|
1708
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new RangeError(`${context}: ${field} must be a positive safe integer, got ${value}`);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
//#endregion
|
|
1712
|
+
//#region src/internal/async/execution.ts
|
|
1713
|
+
/** Default bound for delivery and post-commit operations. */
|
|
1714
|
+
const DEFAULT_EXECUTION_TIMEOUT_MS = 3e4;
|
|
1715
|
+
/**
|
|
1716
|
+
* Owner signal of each child signal that {@link runBoundedExecution} minted.
|
|
1717
|
+
*
|
|
1718
|
+
* One bounded operation often wraps another, and every hop derives a fresh
|
|
1719
|
+
* signal. A consumer that follows a call chain by signal identity alone loses
|
|
1720
|
+
* the link at the first hop. Key and value are both weak: a long chain of
|
|
1721
|
+
* nested operations must not hold its whole ancestry alive.
|
|
1722
|
+
*/
|
|
1723
|
+
const executionOwners = /* @__PURE__ */ new WeakMap();
|
|
1724
|
+
/**
|
|
1725
|
+
* The signal that a bounded execution derived this one from, or `undefined`
|
|
1726
|
+
* when the signal did not come from {@link runBoundedExecution} or had no
|
|
1727
|
+
* owner. Walk it to follow a chain across nested bounded executions.
|
|
1728
|
+
*/
|
|
1729
|
+
function ownerSignalOf(signal) {
|
|
1730
|
+
return executionOwners.get(signal)?.deref();
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Runs one operation with a child signal that combines owner cancellation and a
|
|
1734
|
+
* shell-owned timeout. The returned promise settles on abort even when an
|
|
1735
|
+
* adapter ignores the signal; the adapter promise remains observed so a later
|
|
1736
|
+
* rejection cannot become an unhandled rejection.
|
|
1737
|
+
*
|
|
1738
|
+
* This bounds how long the shell waits; JavaScript cannot forcibly terminate
|
|
1739
|
+
* an arbitrary promise. An I/O adapter that must prevent zombie work and
|
|
1740
|
+
* overlapping retries has to pass `context.signal` to its native operation or
|
|
1741
|
+
* enforce a native timeout no later than `context.deadlineAt`.
|
|
1742
|
+
*/
|
|
1743
|
+
function runBoundedExecution(label, options, operation) {
|
|
1744
|
+
if (options.deadlineAt === void 0) assertNonNegativeFinite(label, "timeoutMs", options.timeoutMs);
|
|
1745
|
+
else assertNonNegativeFinite(label, "deadlineAt", options.deadlineAt);
|
|
1746
|
+
const startedAt = Date.now();
|
|
1747
|
+
const deadlineAt = options.deadlineAt ?? startedAt + options.timeoutMs;
|
|
1748
|
+
const timeoutMs = Math.max(0, deadlineAt - startedAt);
|
|
1749
|
+
const timeoutError = () => new DOMException(`${label} timed out after ${timeoutMs}ms`, "TimeoutError");
|
|
1750
|
+
const controller = new AbortController();
|
|
1751
|
+
const context = Object.freeze({
|
|
1752
|
+
signal: controller.signal,
|
|
1753
|
+
deadlineAt
|
|
1754
|
+
});
|
|
1755
|
+
const ownerSignal = options.signal;
|
|
1756
|
+
if (ownerSignal !== void 0) executionOwners.set(controller.signal, new WeakRef(ownerSignal));
|
|
1757
|
+
const abortFromOwner = () => {
|
|
1758
|
+
controller.abort(ownerSignal === void 0 ? /* @__PURE__ */ new Error(`${label} aborted`) : abortReason(ownerSignal, `${label} aborted`));
|
|
1759
|
+
};
|
|
1760
|
+
if (ownerSignal?.aborted) abortFromOwner();
|
|
1761
|
+
else ownerSignal?.addEventListener("abort", abortFromOwner, { once: true });
|
|
1762
|
+
if (!controller.signal.aborted && options.deadlineAt !== void 0 && deadlineAt <= startedAt) controller.abort(timeoutError());
|
|
1763
|
+
const timer = setTimeout(() => {
|
|
1764
|
+
controller.abort(timeoutError());
|
|
1765
|
+
}, timeoutMs);
|
|
1766
|
+
return new Promise((resolve, reject) => {
|
|
1767
|
+
let settled = false;
|
|
1768
|
+
const finish = (complete) => {
|
|
1769
|
+
if (settled) return;
|
|
1770
|
+
settled = true;
|
|
1771
|
+
clearTimeout(timer);
|
|
1772
|
+
ownerSignal?.removeEventListener("abort", abortFromOwner);
|
|
1773
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
1774
|
+
complete();
|
|
1775
|
+
};
|
|
1776
|
+
const onAbort = () => {
|
|
1777
|
+
queueMicrotask(() => finish(() => reject(abortReason(controller.signal, `${label} aborted`))));
|
|
1778
|
+
};
|
|
1779
|
+
if (controller.signal.aborted) {
|
|
1780
|
+
onAbort();
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
1784
|
+
let outcome;
|
|
1785
|
+
try {
|
|
1786
|
+
outcome = Promise.resolve(operation(context));
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
finish(() => reject(error));
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
outcome.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
//#endregion
|
|
1796
|
+
//#region src/messaging/outbox/ports.ts
|
|
1797
|
+
/**
|
|
1798
|
+
* Discriminates a {@link DispatchTrackingOutbox} from a plain
|
|
1799
|
+
* {@link Outbox} at runtime. The single source of truth for the check;
|
|
1800
|
+
* the dispatcher and the contract suite both use it, so what counts as
|
|
1801
|
+
* a tracking outbox cannot drift between them. Both tracking methods
|
|
1802
|
+
* must be present: a plain adapter that happens to expose an unrelated
|
|
1803
|
+
* `markFailed` helper must not be mistaken for one that implements the
|
|
1804
|
+
* tracking protocol and then be fed `(dispatchId, error)` arguments it
|
|
1805
|
+
* never asked for. Internal plumbing, not exported from the package
|
|
1806
|
+
* entries.
|
|
1807
|
+
*/
|
|
1808
|
+
function isDispatchTrackingOutbox(outbox) {
|
|
1809
|
+
const candidate = outbox;
|
|
1810
|
+
return typeof candidate.markFailed === "function" && typeof candidate.deadLetters === "function";
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
//#endregion
|
|
1814
|
+
export { voWithValidation as A, hasCooperativeBrand as C, vo as D, deepFreeze as E, findPropertyDescriptor as F, hasIntrinsicPrototypeChain as I, isBuiltInObject as L, deepOmit as M, deepEqual as N, voEquals as O, builtInTagWithoutInvokingAccessors as P, isIntrinsicConstructorPrototype as R, SnapshotTimeValidationError as S, ValueObject as T, isRecordedDomainEvent as _, assertNonNegativeFinite as a, recordDomainEvent as b, abortReason as c, copyMetadata as d, createDomainEvent as f, defaultDomainEventFactory as g, createUncommittedDomainEvent as h, runBoundedExecution as i, deepEqualExcept as j, voEqualsExcept as k, adoptRecordedDomainEvent as l, createDomainEventFromFacts as m, DEFAULT_EXECUTION_TIMEOUT_MS as n, assertPositiveInteger as o, createDomainEventFactory as p, ownerSignalOf as r, assertPositiveSafeInteger as s, isDispatchTrackingOutbox as t, adoptUncommittedDomainEvent as u, isUncommittedDomainEvent as v, stampCooperativeBrand as w, DomainEventValidationError as x, mergeMetadata as y, isWeakMap as z };
|
|
1815
|
+
//# sourceMappingURL=ports.js.map
|