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