@shirudo/ddd-kit 3.0.0-rc.4 → 3.0.0-rc.6
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 +4 -20
- package/dist/chunks/{errors.d.ts → kit-errors.d.ts} +272 -48
- package/dist/chunks/{errors.js → kit-errors.js} +275 -57
- package/dist/chunks/kit-errors.js.map +1 -0
- package/dist/chunks/ports.js +910 -110
- package/dist/chunks/ports.js.map +1 -1
- package/dist/chunks/snapshot-store.d.ts +925 -1338
- package/dist/http.d.ts +2 -3
- package/dist/http.js +1 -1
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +3469 -2432
- package/dist/index.js +5663 -4869
- package/dist/index.js.map +1 -1
- package/dist/money.d.ts +11 -11
- package/dist/money.js +9 -9
- package/dist/money.js.map +1 -1
- package/dist/{presentation.d.ts → public-errors.d.ts} +5 -6
- package/dist/{presentation.js → public-errors.js} +5 -5
- package/dist/public-errors.js.map +1 -0
- package/dist/testing.d.ts +61 -11
- package/dist/testing.js +526 -56
- package/dist/testing.js.map +1 -1
- package/package.json +18 -18
- package/dist/chunks/deep-equal-except.js +0 -639
- package/dist/chunks/deep-equal-except.js.map +0 -1
- package/dist/chunks/errors.js.map +0 -1
- package/dist/chunks/utils.d.ts +0 -110
- package/dist/presentation.js.map +0 -1
- package/dist/utils.d.ts +0 -2
- package/dist/utils.js +0 -3
package/dist/chunks/ports.js
CHANGED
|
@@ -1,8 +1,652 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { c as isIntrinsicConstructorPrototype, i as builtInTagWithoutInvokingAccessors, l as mutableBuiltInTagWithoutInvokingAccessors, o as hasIntrinsicPrototypeChain, r as deepEqual, t as deepEqualExcept } from "./deep-equal-except.js";
|
|
1
|
+
import { J as assertNoHostileOwnProtoKey } from "./kit-errors.js";
|
|
3
2
|
import { err, ok } from "@shirudo/result";
|
|
4
3
|
|
|
5
|
-
//#region src/
|
|
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
|
|
6
650
|
/**
|
|
7
651
|
* `Object.freeze` does not protect internal slots: a frozen Date still
|
|
8
652
|
* accepts `setTime`, a frozen Map still accepts `set`. To make the
|
|
@@ -50,12 +694,45 @@ const shadowDescriptor = {
|
|
|
50
694
|
enumerable: false,
|
|
51
695
|
configurable: false
|
|
52
696
|
};
|
|
697
|
+
/** Returns whether the shadows were installed. */
|
|
53
698
|
function shadowMutators(obj, typeName, methods) {
|
|
54
|
-
if (!Object.isExtensible(obj)) return;
|
|
699
|
+
if (!Object.isExtensible(obj)) return false;
|
|
55
700
|
for (const method of methods) {
|
|
56
701
|
shadowDescriptor.value = mutationThrower(typeName, method);
|
|
57
702
|
Object.defineProperty(obj, method, shadowDescriptor);
|
|
58
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`);
|
|
59
736
|
}
|
|
60
737
|
/**
|
|
61
738
|
* Deep freezes an object and all its nested properties recursively, then
|
|
@@ -79,43 +756,80 @@ function shadowMutators(obj, typeName, methods) {
|
|
|
79
756
|
* until the list is updated. Treat the mutator blocking as a guard rail,
|
|
80
757
|
* not a security boundary.
|
|
81
758
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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]") {
|
|
95
801
|
for (const [key, value] of obj) {
|
|
96
|
-
|
|
97
|
-
|
|
802
|
+
if (!freezeDeep(key, walk)) sealed = false;
|
|
803
|
+
if (!freezeDeep(value, walk)) sealed = false;
|
|
98
804
|
}
|
|
99
|
-
shadowMutators(obj, "Map", [
|
|
805
|
+
shadowed = shadowMutators(obj, "Map", [
|
|
100
806
|
"set",
|
|
101
807
|
"delete",
|
|
102
808
|
"clear"
|
|
103
|
-
]);
|
|
104
|
-
} else if (
|
|
105
|
-
for (const member of obj)
|
|
106
|
-
shadowMutators(obj, "Set", [
|
|
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", [
|
|
107
813
|
"add",
|
|
108
814
|
"delete",
|
|
109
815
|
"clear"
|
|
110
|
-
]);
|
|
816
|
+
]) || shadowed;
|
|
111
817
|
}
|
|
818
|
+
if (shadowed) KIT_SHADOWED.add(obj);
|
|
819
|
+
else sealed = false;
|
|
112
820
|
}
|
|
113
821
|
const keys = Reflect.ownKeys(obj);
|
|
114
822
|
for (const key of keys) {
|
|
115
823
|
const value = obj[key];
|
|
116
|
-
if (value !== null && typeof value === "object")
|
|
824
|
+
if (value !== null && typeof value === "object") {
|
|
825
|
+
if (!freezeDeep(value, walk)) sealed = false;
|
|
826
|
+
}
|
|
117
827
|
}
|
|
118
|
-
|
|
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;
|
|
119
833
|
}
|
|
120
834
|
/**
|
|
121
835
|
* Deep clone used by `vo()` and the `ValueObject` constructor.
|
|
@@ -126,11 +840,12 @@ function deepFreeze(obj, visited = /* @__PURE__ */ new WeakSet()) {
|
|
|
126
840
|
* `deepEqual` DOES consider symbol keys) and shared references / cycles
|
|
127
841
|
* keep their identity across Map boundaries. Function values throw,
|
|
128
842
|
* preserving `vo()`'s documented data-not-behaviour gate. Built-ins without
|
|
129
|
-
* immutable value semantics throw a descriptive `TypeError`.
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
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
|
|
134
849
|
* identity-based and object identity cannot survive defensive cloning.
|
|
135
850
|
* Accessor properties are rejected without invoking them. Admitted atomic
|
|
136
851
|
* built-ins (Date, RegExp and primitive wrappers) delegate to
|
|
@@ -145,7 +860,7 @@ function cloneForVo(value, visited) {
|
|
|
145
860
|
if (ArrayBuffer.isView(obj)) throwUnsupportedValueSemantics(builtInTagWithoutInvokingAccessors(obj) ?? "[object ArrayBuffer view]");
|
|
146
861
|
if (visited.has(obj)) return visited.get(obj);
|
|
147
862
|
if (Array.isArray(obj)) {
|
|
148
|
-
if (!hasIntrinsicPrototypeChain(obj, "Array")) throwUnsupportedClassInstance();
|
|
863
|
+
if (!hasIntrinsicPrototypeChain(obj, "Array")) throwUnsupportedClassInstance(obj);
|
|
149
864
|
const clone = new Array(obj.length);
|
|
150
865
|
visited.set(obj, clone);
|
|
151
866
|
for (const key of Reflect.ownKeys(obj)) {
|
|
@@ -161,7 +876,7 @@ function cloneForVo(value, visited) {
|
|
|
161
876
|
}
|
|
162
877
|
const tag = builtInTagWithoutInvokingAccessors(obj);
|
|
163
878
|
if (tag !== void 0) {
|
|
164
|
-
if (!hasIntrinsicPrototypeChain(obj)) throwUnsupportedClassInstance();
|
|
879
|
+
if (!hasIntrinsicPrototypeChain(obj)) throwUnsupportedClassInstance(obj);
|
|
165
880
|
if (tag === "[object Map]") {
|
|
166
881
|
const clone = /* @__PURE__ */ new Map();
|
|
167
882
|
visited.set(obj, clone);
|
|
@@ -191,7 +906,14 @@ function cloneForVo(value, visited) {
|
|
|
191
906
|
return builtInClone;
|
|
192
907
|
}
|
|
193
908
|
const prototype = Object.getPrototypeOf(obj);
|
|
194
|
-
if (prototype !== null && (!isIntrinsicConstructorPrototype(prototype, "Object") || Object.getPrototypeOf(prototype) !== null))
|
|
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
|
+
}
|
|
195
917
|
const clone = Object.create(prototype === null ? null : Object.prototype);
|
|
196
918
|
visited.set(obj, clone);
|
|
197
919
|
for (const key of Reflect.ownKeys(obj)) {
|
|
@@ -208,8 +930,12 @@ function cloneForVo(value, visited) {
|
|
|
208
930
|
}
|
|
209
931
|
return clone;
|
|
210
932
|
}
|
|
211
|
-
function throwUnsupportedClassInstance() {
|
|
212
|
-
|
|
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`);
|
|
213
939
|
}
|
|
214
940
|
function throwUnsupportedAccessorProperty() {
|
|
215
941
|
throw new TypeError("vo() cannot clone accessor properties: Value Objects are plain data");
|
|
@@ -227,9 +953,12 @@ function isPrimitiveValue(value) {
|
|
|
227
953
|
* The input is first deep-cloned, then the clone is frozen, so calling
|
|
228
954
|
* `vo(input)` never freezes the caller's own object graph as a
|
|
229
955
|
* side-effect. Mutating the input afterwards does not bleed into the VO.
|
|
230
|
-
* Symbol-keyed properties are preserved (matching `voEquals`)
|
|
231
|
-
*
|
|
232
|
-
*
|
|
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
|
|
233
962
|
* Proxy-free: ECMAScript provides no portable way to identify a transparent
|
|
234
963
|
* Proxy without potentially executing its traps, so `vo()` is not a sandbox
|
|
235
964
|
* for hostile in-process objects. Built-ins that cannot provide immutable,
|
|
@@ -244,6 +973,7 @@ function isPrimitiveValue(value) {
|
|
|
244
973
|
* ```
|
|
245
974
|
*/
|
|
246
975
|
function vo(t) {
|
|
976
|
+
rejectValueObjectAsInput(t, "vo()");
|
|
247
977
|
return deepFreeze(cloneForVo(t, /* @__PURE__ */ new WeakMap()));
|
|
248
978
|
}
|
|
249
979
|
/**
|
|
@@ -284,6 +1014,11 @@ function voEquals(a, b) {
|
|
|
284
1014
|
* Useful for comparing value objects that contain metadata or optional fields
|
|
285
1015
|
* that should not affect equality comparison.
|
|
286
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
|
+
*
|
|
287
1022
|
* @param a - First value object
|
|
288
1023
|
* @param b - Second value object
|
|
289
1024
|
* @param options - Options specifying which keys to ignore during comparison
|
|
@@ -317,7 +1052,15 @@ function voEquals(a, b) {
|
|
|
317
1052
|
* ```
|
|
318
1053
|
*/
|
|
319
1054
|
function voEqualsExcept(a, b, options) {
|
|
320
|
-
return deepEqualExcept(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
|
+
};
|
|
321
1064
|
}
|
|
322
1065
|
/**
|
|
323
1066
|
* Creates a value object with optional validation.
|
|
@@ -366,7 +1109,11 @@ function describeValue(value) {
|
|
|
366
1109
|
}
|
|
367
1110
|
/**
|
|
368
1111
|
* Abstract base class for creating Value Objects.
|
|
369
|
-
* Value Objects are immutable and defined by their properties.
|
|
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.
|
|
370
1117
|
*
|
|
371
1118
|
* @template T - The shape of the value object's properties
|
|
372
1119
|
*/
|
|
@@ -393,8 +1140,10 @@ var ValueObject = class {
|
|
|
393
1140
|
* ```
|
|
394
1141
|
*/
|
|
395
1142
|
constructor(props) {
|
|
1143
|
+
rejectValueObjectAsInput(props, "new ValueObject()");
|
|
396
1144
|
this.validate(props);
|
|
397
1145
|
this.props = deepFreeze(cloneForVo(props, /* @__PURE__ */ new WeakMap()));
|
|
1146
|
+
recordValueObjectClass(this, this.constructor);
|
|
398
1147
|
}
|
|
399
1148
|
/**
|
|
400
1149
|
* Optional validation hook that can be overridden by subclasses.
|
|
@@ -452,7 +1201,54 @@ var ValueObject = class {
|
|
|
452
1201
|
};
|
|
453
1202
|
|
|
454
1203
|
//#endregion
|
|
455
|
-
//#region src/
|
|
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
|
|
456
1252
|
/** Immutable library default captured by the default domain-event factory. */
|
|
457
1253
|
const defaultClockFactory = () => /* @__PURE__ */ new Date();
|
|
458
1254
|
/** Internal defensive event-clock read. */
|
|
@@ -464,7 +1260,7 @@ function readClock(factory) {
|
|
|
464
1260
|
}
|
|
465
1261
|
|
|
466
1262
|
//#endregion
|
|
467
|
-
//#region src/
|
|
1263
|
+
//#region src/domain/event/domain-event-errors.ts
|
|
468
1264
|
/**
|
|
469
1265
|
* Stable contract error for malformed domain-event data.
|
|
470
1266
|
*
|
|
@@ -494,7 +1290,7 @@ var SnapshotTimeValidationError = class extends TypeError {
|
|
|
494
1290
|
};
|
|
495
1291
|
|
|
496
1292
|
//#endregion
|
|
497
|
-
//#region src/
|
|
1293
|
+
//#region src/domain/event/domain-event.ts
|
|
498
1294
|
const defaultEventIdFactory = () => crypto.randomUUID();
|
|
499
1295
|
/**
|
|
500
1296
|
* Creates an immutable, instance-bound domain-event factory.
|
|
@@ -517,20 +1313,39 @@ const defaultEventIdFactory = () => crypto.randomUUID();
|
|
|
517
1313
|
* recordPendingEvents(order, domainEvents);
|
|
518
1314
|
* ```
|
|
519
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
|
+
}
|
|
520
1333
|
function createDomainEventFactory(options = {}) {
|
|
521
1334
|
const eventIdFactory = options.eventIdFactory ?? defaultEventIdFactory;
|
|
522
1335
|
const clock = options.clock ?? defaultClockFactory;
|
|
523
|
-
const
|
|
1336
|
+
const { source } = options;
|
|
1337
|
+
const create = ((type, payload, createOptions) => mintDomainEvent(type, payload, withFactorySource(createOptions, source), eventIdFactory, clock));
|
|
524
1338
|
const createStamp = (stampOptions = {}) => {
|
|
525
1339
|
const explicitOccurredAt = stampOptions.occurredAt === void 0 ? void 0 : copyValidEventDate(stampOptions.occurredAt);
|
|
526
1340
|
if (stampOptions.eventId !== void 0) assertNonBlankEventField(stampOptions.eventId, "eventId", "EVENT_ID_INVALID");
|
|
527
1341
|
const eventId = stampOptions.eventId ?? eventIdFactory();
|
|
528
1342
|
assertNonBlankEventField(eventId, "eventId", "EVENT_ID_INVALID");
|
|
529
|
-
const
|
|
1343
|
+
const stamp = {
|
|
530
1344
|
eventId,
|
|
531
1345
|
occurredAt: explicitOccurredAt ?? readEventClock(clock),
|
|
532
|
-
metadata:
|
|
533
|
-
}
|
|
1346
|
+
metadata: cloneOwnedEventData(withFactorySource(stampOptions, source).metadata, "metadata")
|
|
1347
|
+
};
|
|
1348
|
+
const owned = deepFreeze(stamp);
|
|
534
1349
|
FACTORY_OWNED_EVENT_STAMPS.add(owned);
|
|
535
1350
|
return owned;
|
|
536
1351
|
};
|
|
@@ -548,7 +1363,7 @@ function createDomainEventFactory(options = {}) {
|
|
|
548
1363
|
const defaultDomainEventFactory = createDomainEventFactory();
|
|
549
1364
|
/**
|
|
550
1365
|
* Creates a domain event with default values.
|
|
551
|
-
* Sets occurredAt to current date and
|
|
1366
|
+
* Sets occurredAt to current date and schemaVersion to 1 if not provided.
|
|
552
1367
|
*
|
|
553
1368
|
* **Input ownership.** The event is deeply frozen, and `payload` and
|
|
554
1369
|
* `metadata` are deep-cloned first, so the caller's own objects are never
|
|
@@ -559,14 +1374,14 @@ const defaultDomainEventFactory = createDomainEventFactory();
|
|
|
559
1374
|
* over.
|
|
560
1375
|
*
|
|
561
1376
|
* **For aggregate-internal events, prefer `this.createEvent(...)` on
|
|
562
|
-
* `
|
|
1377
|
+
* `StateStoredAggregate` / `EventSourcedAggregate`.** That helper auto-injects
|
|
563
1378
|
* `aggregateId` (from `this.id`) and `aggregateType` (from the
|
|
564
1379
|
* aggregate's declared `aggregateType` property), which downstream
|
|
565
1380
|
* consumers (outbox dispatchers, projection handlers, audit logs)
|
|
566
|
-
* route by. The
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
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
|
|
570
1385
|
* shell before repository persistence or outbox harvest.
|
|
571
1386
|
*
|
|
572
1387
|
* Use `createDomainEvent(...)` directly for events that don't belong to
|
|
@@ -585,46 +1400,39 @@ const defaultDomainEventFactory = createDomainEventFactory();
|
|
|
585
1400
|
* const event = createDomainEvent("OrderCreated", { orderId: "123" });
|
|
586
1401
|
* ```
|
|
587
1402
|
*/
|
|
588
|
-
const
|
|
1403
|
+
const RECORDED_EVENTS = /* @__PURE__ */ new WeakSet();
|
|
589
1404
|
const UNCOMMITTED_EVENTS = /* @__PURE__ */ new WeakSet();
|
|
590
1405
|
const FACTORY_OWNED_EVENT_STAMPS = /* @__PURE__ */ new WeakSet();
|
|
591
|
-
const
|
|
1406
|
+
const RECORDED_BRAND = Symbol.for("@shirudo/ddd-kit.mintedEvent");
|
|
592
1407
|
const UNCOMMITTED_BRAND = Symbol.for("@shirudo/ddd-kit.uncommittedEvent");
|
|
593
|
-
function
|
|
594
|
-
|
|
595
|
-
value: true,
|
|
596
|
-
enumerable: false,
|
|
597
|
-
writable: false,
|
|
598
|
-
configurable: false
|
|
599
|
-
});
|
|
1408
|
+
function stampRecordedBrand(event) {
|
|
1409
|
+
stampCooperativeBrand(event, RECORDED_BRAND);
|
|
600
1410
|
}
|
|
601
1411
|
function stampUncommittedBrand(event) {
|
|
602
|
-
|
|
603
|
-
value: true,
|
|
604
|
-
enumerable: false,
|
|
605
|
-
writable: false,
|
|
606
|
-
configurable: false
|
|
607
|
-
});
|
|
1412
|
+
stampCooperativeBrand(event, UNCOMMITTED_BRAND);
|
|
608
1413
|
}
|
|
609
1414
|
function isFactoryOwnedDomainEventStamp(stamp) {
|
|
610
1415
|
return FACTORY_OWNED_EVENT_STAMPS.has(stamp);
|
|
611
1416
|
}
|
|
612
1417
|
/**
|
|
613
|
-
* Whether `event`
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
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
|
|
617
1424
|
* WeakSet; events minted by ANOTHER copy (duplicate dependency, dual
|
|
618
|
-
* CJS/ESM load) are recognized
|
|
619
|
-
* brand
|
|
620
|
-
* not
|
|
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.
|
|
621
1429
|
*/
|
|
622
|
-
function
|
|
623
|
-
return
|
|
1430
|
+
function isRecordedDomainEvent(event) {
|
|
1431
|
+
return RECORDED_EVENTS.has(event) || hasCooperativeBrand(event, RECORDED_BRAND);
|
|
624
1432
|
}
|
|
625
1433
|
/** Whether a value was created by {@link createUncommittedDomainEvent}. */
|
|
626
1434
|
function isUncommittedDomainEvent(event) {
|
|
627
|
-
return UNCOMMITTED_EVENTS.has(event) || event
|
|
1435
|
+
return UNCOMMITTED_EVENTS.has(event) || hasCooperativeBrand(event, UNCOMMITTED_BRAND);
|
|
628
1436
|
}
|
|
629
1437
|
function createUncommittedDomainEvent(type, payload, options) {
|
|
630
1438
|
assertProducerOwnedEventFields(type, options);
|
|
@@ -633,7 +1441,7 @@ function createUncommittedDomainEvent(type, payload, options) {
|
|
|
633
1441
|
aggregateId: options?.aggregateId,
|
|
634
1442
|
aggregateType: options?.aggregateType,
|
|
635
1443
|
payload: cloneOwnedEventData(payload, "payload"),
|
|
636
|
-
|
|
1444
|
+
schemaVersion: options?.schemaVersion ?? 1
|
|
637
1445
|
};
|
|
638
1446
|
stampUncommittedBrand(event);
|
|
639
1447
|
const uncommitted = deepFreeze(event);
|
|
@@ -659,7 +1467,7 @@ function recordDomainEvent(event, stamp) {
|
|
|
659
1467
|
if (isFactoryOwnedDomainEventStamp(stamp)) return mintRecordedEvent(event, stamp.eventId, stamp.occurredAt, stamp.metadata);
|
|
660
1468
|
assertNonBlankEventField(stamp.eventId, "eventId", "EVENT_ID_INVALID");
|
|
661
1469
|
const occurredAt = deepFreeze(copyValidEventDate(stamp.occurredAt));
|
|
662
|
-
const metadata =
|
|
1470
|
+
const metadata = cloneOwnedEventData(stamp.metadata, "metadata");
|
|
663
1471
|
return mintRecordedEvent(event, stamp.eventId, occurredAt, metadata === void 0 ? void 0 : deepFreeze(metadata));
|
|
664
1472
|
}
|
|
665
1473
|
/**
|
|
@@ -678,27 +1486,27 @@ function mintRecordedEvent(event, eventId, occurredAt, metadata) {
|
|
|
678
1486
|
aggregateType: event.aggregateType,
|
|
679
1487
|
payload: event.payload,
|
|
680
1488
|
occurredAt,
|
|
681
|
-
|
|
1489
|
+
schemaVersion: event.schemaVersion,
|
|
682
1490
|
metadata
|
|
683
1491
|
};
|
|
684
|
-
|
|
1492
|
+
stampRecordedBrand(recorded);
|
|
685
1493
|
Object.freeze(recorded);
|
|
686
|
-
|
|
1494
|
+
RECORDED_EVENTS.add(recorded);
|
|
687
1495
|
return recorded;
|
|
688
1496
|
}
|
|
689
1497
|
/**
|
|
690
|
-
* Brands, freezes, and registers a kit-derived copy of a
|
|
691
|
-
* (e.g. the address-stamped copy `apply()` creates) as
|
|
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.
|
|
692
1500
|
* The copy shares the already-frozen payload/metadata of its source,
|
|
693
1501
|
* so the mint guarantee carries over. Stamping the cooperative brand
|
|
694
1502
|
* before freezing keeps the copy recognizable by another loaded kit
|
|
695
1503
|
* instance as well as by this instance's WeakSet. Module-internal
|
|
696
1504
|
* export; not part of the package entries.
|
|
697
1505
|
*/
|
|
698
|
-
function
|
|
699
|
-
|
|
1506
|
+
function adoptRecordedDomainEvent(copy) {
|
|
1507
|
+
stampRecordedBrand(copy);
|
|
700
1508
|
Object.freeze(copy);
|
|
701
|
-
|
|
1509
|
+
RECORDED_EVENTS.add(copy);
|
|
702
1510
|
return copy;
|
|
703
1511
|
}
|
|
704
1512
|
function createDomainEvent(type, payload, options) {
|
|
@@ -720,7 +1528,7 @@ function mintDomainEvent(type, payload, options, eventIdFactory, clock) {
|
|
|
720
1528
|
const eventId = options?.eventId ?? eventIdFactory();
|
|
721
1529
|
assertNonBlankEventField(eventId, "eventId", "EVENT_ID_INVALID");
|
|
722
1530
|
const occurredAt = options?.occurredAt === void 0 ? readEventClock(clock) : copyValidEventDate(options.occurredAt);
|
|
723
|
-
const
|
|
1531
|
+
const schemaVersion = options?.schemaVersion ?? 1;
|
|
724
1532
|
const event = {
|
|
725
1533
|
eventId,
|
|
726
1534
|
type,
|
|
@@ -728,18 +1536,18 @@ function mintDomainEvent(type, payload, options, eventIdFactory, clock) {
|
|
|
728
1536
|
aggregateType: options?.aggregateType,
|
|
729
1537
|
payload: cloneOwnedEventData(payload, "payload"),
|
|
730
1538
|
occurredAt,
|
|
731
|
-
|
|
732
|
-
metadata:
|
|
1539
|
+
schemaVersion,
|
|
1540
|
+
metadata: cloneOwnedEventData(options?.metadata, "metadata")
|
|
733
1541
|
};
|
|
734
|
-
|
|
1542
|
+
stampRecordedBrand(event);
|
|
735
1543
|
const minted = deepFreeze(event);
|
|
736
|
-
|
|
1544
|
+
RECORDED_EVENTS.add(minted);
|
|
737
1545
|
return minted;
|
|
738
1546
|
}
|
|
739
1547
|
function assertProducerOwnedEventFields(type, options) {
|
|
740
1548
|
assertNonBlankEventField(type, "type", "EVENT_TYPE_INVALID");
|
|
741
|
-
const
|
|
742
|
-
if (!Number.isSafeInteger(
|
|
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");
|
|
743
1551
|
if (options?.aggregateId !== void 0) assertNonBlankEventField(options.aggregateId, "aggregateId", "EVENT_ADDRESS_INVALID");
|
|
744
1552
|
if (options?.aggregateType !== void 0) assertNonBlankEventField(options.aggregateType, "aggregateType", "EVENT_ADDRESS_INVALID");
|
|
745
1553
|
}
|
|
@@ -766,7 +1574,9 @@ function readEventClock(clock) {
|
|
|
766
1574
|
*/
|
|
767
1575
|
function cloneOwnedEventData(value, field) {
|
|
768
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");
|
|
769
1578
|
if (value === null || typeof value !== "object") return value;
|
|
1579
|
+
assertNoHostileOwnProtoKey(value, field === "payload" ? "Event payload" : "Event metadata");
|
|
770
1580
|
assertNoBinaryData(value, field);
|
|
771
1581
|
try {
|
|
772
1582
|
return structuredClone(value);
|
|
@@ -815,7 +1625,7 @@ function assertNoBinaryData(value, field, visited = /* @__PURE__ */ new WeakSet(
|
|
|
815
1625
|
* const newEvent = createDomainEvent(
|
|
816
1626
|
* "OrderShipped",
|
|
817
1627
|
* { orderId: "123" },
|
|
818
|
-
* { metadata: copyMetadata(previousEvent, { causationId: previousEvent.
|
|
1628
|
+
* { metadata: copyMetadata(previousEvent, { causationId: previousEvent.eventId }) }
|
|
819
1629
|
* );
|
|
820
1630
|
* ```
|
|
821
1631
|
*/
|
|
@@ -857,19 +1667,9 @@ function mergeMetadata(...metadataObjects) {
|
|
|
857
1667
|
}
|
|
858
1668
|
return merged;
|
|
859
1669
|
}
|
|
860
|
-
/**
|
|
861
|
-
* Clones event metadata with the loud `__proto__` rejection applied at
|
|
862
|
-
* the SOURCE: structuredClone preserves an own `__proto__` data key, so
|
|
863
|
-
* without this guard a hostile envelope would ride into the frozen
|
|
864
|
-
* event and re-arm downstream.
|
|
865
|
-
*/
|
|
866
|
-
function guardedMetadataClone(metadata) {
|
|
867
|
-
if (metadata !== void 0) assertNoHostileOwnProtoKey(metadata, "Event metadata");
|
|
868
|
-
return cloneOwnedEventData(metadata, "metadata");
|
|
869
|
-
}
|
|
870
1670
|
|
|
871
1671
|
//#endregion
|
|
872
|
-
//#region src/
|
|
1672
|
+
//#region src/messaging/outbox/ports.ts
|
|
873
1673
|
/**
|
|
874
1674
|
* Discriminates a {@link DispatchTrackingOutbox} from a plain
|
|
875
1675
|
* {@link Outbox} at runtime. The single source of truth for the check;
|
|
@@ -887,5 +1687,5 @@ function isDispatchTrackingOutbox(outbox) {
|
|
|
887
1687
|
}
|
|
888
1688
|
|
|
889
1689
|
//#endregion
|
|
890
|
-
export {
|
|
1690
|
+
export { isBuiltInObject as A, voWithValidation as C, builtInTagWithoutInvokingAccessors as D, deepEqual as E, isWeakMap as M, findPropertyDescriptor as O, voEqualsExcept as S, deepOmit as T, stampCooperativeBrand as _, createDomainEvent as a, vo as b, createUncommittedDomainEvent as c, isUncommittedDomainEvent as d, mergeMetadata as f, hasCooperativeBrand as g, SnapshotTimeValidationError as h, copyMetadata as i, isIntrinsicConstructorPrototype as j, hasIntrinsicPrototypeChain as k, defaultDomainEventFactory as l, DomainEventValidationError as m, adoptRecordedDomainEvent as n, createDomainEventFactory as o, recordDomainEvent as p, adoptUncommittedDomainEvent as r, createDomainEventFromFacts as s, isDispatchTrackingOutbox as t, isRecordedDomainEvent as u, ValueObject as v, deepEqualExcept as w, voEquals as x, deepFreeze as y };
|
|
891
1691
|
//# sourceMappingURL=ports.js.map
|