@woltz/rich-domain 1.0.0 → 1.2.0

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.
Files changed (129) hide show
  1. package/CHANGELOG.md +37 -86
  2. package/LICENSE +20 -20
  3. package/dist/base-entity.d.ts +1 -1
  4. package/dist/base-entity.d.ts.map +1 -1
  5. package/dist/base-entity.js +15 -19
  6. package/dist/base-entity.js.map +1 -1
  7. package/dist/constants.js +1 -4
  8. package/dist/constants.js.map +1 -1
  9. package/dist/criteria.d.ts +24 -14
  10. package/dist/criteria.d.ts.map +1 -1
  11. package/dist/criteria.js +154 -67
  12. package/dist/criteria.js.map +1 -1
  13. package/dist/deep-proxy.d.ts.map +1 -1
  14. package/dist/deep-proxy.js +19 -9
  15. package/dist/deep-proxy.js.map +1 -1
  16. package/dist/domain-event-bus.d.ts +5 -6
  17. package/dist/domain-event-bus.d.ts.map +1 -1
  18. package/dist/domain-event-bus.js +4 -17
  19. package/dist/domain-event-bus.js.map +1 -1
  20. package/dist/domain-event.d.ts +1 -31
  21. package/dist/domain-event.d.ts.map +1 -1
  22. package/dist/domain-event.js +4 -7
  23. package/dist/domain-event.js.map +1 -1
  24. package/dist/entity.d.ts +2 -2
  25. package/dist/entity.js +3 -8
  26. package/dist/entity.js.map +1 -1
  27. package/dist/exceptions.d.ts +251 -0
  28. package/dist/exceptions.d.ts.map +1 -0
  29. package/dist/exceptions.js +321 -0
  30. package/dist/exceptions.js.map +1 -0
  31. package/dist/id.d.ts.map +1 -1
  32. package/dist/id.js +14 -7
  33. package/dist/id.js.map +1 -1
  34. package/dist/index.d.ts +2 -4
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +11 -40
  37. package/dist/index.js.map +1 -1
  38. package/dist/mapper.js +1 -5
  39. package/dist/mapper.js.map +1 -1
  40. package/dist/paginated-result.d.ts.map +1 -1
  41. package/dist/paginated-result.js +17 -9
  42. package/dist/paginated-result.js.map +1 -1
  43. package/dist/repository/base-repository.js +4 -11
  44. package/dist/repository/base-repository.js.map +1 -1
  45. package/dist/repository/index.d.ts +1 -2
  46. package/dist/repository/index.d.ts.map +1 -1
  47. package/dist/repository/index.js +3 -26
  48. package/dist/repository/index.js.map +1 -1
  49. package/dist/repository/unit-of-work.d.ts +0 -11
  50. package/dist/repository/unit-of-work.d.ts.map +1 -1
  51. package/dist/repository/unit-of-work.js +2 -43
  52. package/dist/repository/unit-of-work.js.map +1 -1
  53. package/dist/types/criteria.d.ts +31 -7
  54. package/dist/types/criteria.d.ts.map +1 -1
  55. package/dist/types/criteria.js +1 -4
  56. package/dist/types/criteria.js.map +1 -1
  57. package/dist/types/domain-event.d.ts +32 -0
  58. package/dist/types/domain-event.d.ts.map +1 -0
  59. package/dist/types/domain-event.js +2 -0
  60. package/dist/types/domain-event.js.map +1 -0
  61. package/dist/types/domain.d.ts +2 -2
  62. package/dist/types/domain.d.ts.map +1 -1
  63. package/dist/types/domain.js +1 -2
  64. package/dist/types/history-tracker.d.ts +1 -1
  65. package/dist/types/history-tracker.d.ts.map +1 -1
  66. package/dist/types/history-tracker.js +1 -2
  67. package/dist/types/index.d.ts +1 -0
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/dist/types/index.js +7 -22
  70. package/dist/types/index.js.map +1 -1
  71. package/dist/types/standard-schema.js +1 -2
  72. package/dist/types/unit-of-work.js +1 -2
  73. package/dist/types/utils.js +1 -2
  74. package/dist/utils/criteria-operator-validation.d.ts +5 -0
  75. package/dist/utils/criteria-operator-validation.d.ts.map +1 -0
  76. package/dist/utils/criteria-operator-validation.js +143 -0
  77. package/dist/utils/criteria-operator-validation.js.map +1 -0
  78. package/dist/utils/helpers.d.ts +2 -0
  79. package/dist/utils/helpers.d.ts.map +1 -0
  80. package/dist/utils/helpers.js +10 -0
  81. package/dist/utils/helpers.js.map +1 -0
  82. package/dist/validation-error.js +3 -9
  83. package/dist/validation-error.js.map +1 -1
  84. package/dist/value-object.d.ts +1 -1
  85. package/dist/value-object.d.ts.map +1 -1
  86. package/dist/value-object.js +7 -14
  87. package/dist/value-object.js.map +1 -1
  88. package/eslint.config.js +9 -3
  89. package/jest.config.js +1 -1
  90. package/package.json +14 -20
  91. package/src/base-entity.ts +3 -3
  92. package/src/criteria.ts +268 -87
  93. package/src/deep-proxy.ts +50 -38
  94. package/src/domain-event-bus.ts +152 -166
  95. package/src/domain-event.ts +53 -90
  96. package/src/entity.ts +16 -16
  97. package/src/exceptions.ts +435 -0
  98. package/src/id.ts +107 -94
  99. package/src/index.ts +32 -8
  100. package/src/paginated-result.ts +15 -3
  101. package/src/repository/index.ts +1 -6
  102. package/src/repository/unit-of-work.ts +1 -44
  103. package/src/types/criteria.ts +95 -17
  104. package/src/types/domain-event.ts +38 -0
  105. package/src/types/domain.ts +2 -3
  106. package/src/types/history-tracker.ts +1 -1
  107. package/src/types/index.ts +1 -0
  108. package/src/utils/criteria-operator-validation.ts +171 -0
  109. package/src/utils/helpers.ts +6 -0
  110. package/src/validation-error.ts +97 -97
  111. package/src/value-object.ts +3 -6
  112. package/tests/criteria.test.ts +324 -1
  113. package/tests/domain-events.test.ts +431 -445
  114. package/tests/entity-validation.test.ts +1 -1
  115. package/tests/entity.test.ts +33 -33
  116. package/tests/repository.test.ts +4 -2
  117. package/tests/utils.ts +254 -151
  118. package/tests/value-object-validation.test.ts +0 -9
  119. package/tsconfig.json +2 -24
  120. package/.github/workflows/ci.yml +0 -40
  121. package/.husky/commit-msg +0 -1
  122. package/.husky/pre-commit +0 -1
  123. package/.vscode/settings.json +0 -3
  124. package/commitlint.config.js +0 -23
  125. package/dist/repository/in-memory-repository.d.ts +0 -50
  126. package/dist/repository/in-memory-repository.d.ts.map +0 -1
  127. package/dist/repository/in-memory-repository.js +0 -97
  128. package/dist/repository/in-memory-repository.js.map +0 -1
  129. package/src/repository/in-memory-repository.ts +0 -116
package/src/deep-proxy.ts CHANGED
@@ -27,6 +27,8 @@ export class DeepProxy {
27
27
  const handler: ProxyHandler<any> = {
28
28
  get: (target, prop, receiver) => {
29
29
  const value = Reflect.get(target, prop, receiver);
30
+ if (!this.rootProxy) throw new Error("Root proxy is required");
31
+
30
32
  const currentPath = this.path
31
33
  ? `${this.path}.${String(prop)}`
32
34
  : String(prop);
@@ -44,8 +46,8 @@ export class DeepProxy {
44
46
  if (typeof value === "function") return value.bind(target);
45
47
 
46
48
  if (Array.isArray(value)) {
47
- if (!this.rootProxy!.trackedArraysCloned.has(currentPath)) {
48
- this.rootProxy!.storeArrayState(currentPath, value);
49
+ if (!this.rootProxy.trackedArraysCloned.has(currentPath)) {
50
+ this.rootProxy.storeArrayState(currentPath, value);
49
51
  }
50
52
  return this.createArrayProxy(value, currentPath);
51
53
  }
@@ -62,16 +64,18 @@ export class DeepProxy {
62
64
  const currentPath = this.path
63
65
  ? `${this.path}.${String(prop)}`
64
66
  : String(prop);
67
+ if (!this.rootProxy) throw new Error("Root proxy is required");
68
+
65
69
  const oldValue = Reflect.get(target, prop, receiver);
66
70
  const isArrayAssignment = Array.isArray(newValue);
67
71
 
68
72
  if (!isArrayAssignment && oldValue === newValue) return true;
69
73
 
70
- if (!this.rootProxy!.originalValues.has(currentPath)) {
71
- this.rootProxy!.originalValues.set(currentPath, oldValue);
74
+ if (!this.rootProxy.originalValues.has(currentPath)) {
75
+ this.rootProxy.originalValues.set(currentPath, oldValue);
72
76
  }
73
77
 
74
- this.rootProxy!.history.push({
78
+ this.rootProxy.history.push({
75
79
  path: currentPath,
76
80
  previousValue: oldValue,
77
81
  currentValue: newValue,
@@ -81,28 +85,28 @@ export class DeepProxy {
81
85
  const result = Reflect.set(target, prop, newValue, receiver);
82
86
 
83
87
  if (isArrayAssignment) {
84
- if (!this.rootProxy!.trackedArraysCloned.has(currentPath)) {
88
+ if (!this.rootProxy.trackedArraysCloned.has(currentPath)) {
85
89
  if (Array.isArray(oldValue)) {
86
- this.rootProxy!.storeArrayState(currentPath, oldValue);
90
+ this.rootProxy.storeArrayState(currentPath, oldValue);
87
91
  } else {
88
- this.rootProxy!.storeArrayState(currentPath, []);
92
+ this.rootProxy.storeArrayState(currentPath, []);
89
93
  }
90
94
  }
91
- this.rootProxy!.scheduleNotification(() =>
95
+ this.rootProxy.scheduleNotification(() =>
92
96
  this.rootProxy!.notifyArrayChange(currentPath, newValue)
93
97
  );
94
98
  } else {
95
- this.rootProxy!.scheduleNotification(() =>
99
+ this.rootProxy.scheduleNotification(() =>
96
100
  this.rootProxy!.notifySubscribers(currentPath, oldValue, newValue)
97
101
  );
98
102
  }
99
103
 
100
104
  // Notify global subscribers
101
- this.rootProxy!.scheduleNotification(() =>
105
+ this.rootProxy.scheduleNotification(() =>
102
106
  this.rootProxy!.notifyGlobalSubscribers()
103
107
  );
104
108
 
105
- this.rootProxy!.flushNotifications();
109
+ this.rootProxy.flushNotifications();
106
110
 
107
111
  return result;
108
112
  },
@@ -140,6 +144,8 @@ export class DeepProxy {
140
144
  return new Proxy(array, {
141
145
  get(target, prop, receiver) {
142
146
  const value = Reflect.get(target, prop, receiver);
147
+ if (!self.rootProxy) throw new Error("Root proxy is required");
148
+
143
149
  if (typeof value === "function") {
144
150
  const mutatingMethods = [
145
151
  "push",
@@ -153,23 +159,24 @@ export class DeepProxy {
153
159
  if (mutatingMethods.includes(String(prop))) {
154
160
  return function (...args: any[]) {
155
161
  // Capture state before mutation
162
+ if (!self.rootProxy) throw new Error("Root proxy is required");
156
163
  const oldArray = target.slice();
157
164
 
158
165
  const result = value.apply(target, args);
159
166
 
160
167
  // Add to history
161
- self.rootProxy!.history.push({
168
+ self.rootProxy.history.push({
162
169
  path: path,
163
170
  previousValue: oldArray,
164
171
  currentValue: target.slice(),
165
172
  timestamp: Date.now(),
166
173
  });
167
174
 
168
- if (!self.rootProxy!.trackedArraysCloned.has(path)) {
169
- self.rootProxy!.storeArrayState(path, target);
175
+ if (!self.rootProxy.trackedArraysCloned.has(path)) {
176
+ self.rootProxy.storeArrayState(path, target);
170
177
  }
171
- self.rootProxy!.notifyArrayChange(path, target);
172
- self.rootProxy!.notifyGlobalSubscribers();
178
+ self.rootProxy.notifyArrayChange(path, target);
179
+ self.rootProxy.notifyGlobalSubscribers();
173
180
  return result;
174
181
  };
175
182
  }
@@ -184,24 +191,25 @@ export class DeepProxy {
184
191
  },
185
192
  set(target, prop, newValue, receiver) {
186
193
  if (!isNaN(Number(prop))) {
194
+ if (!self.rootProxy) throw new Error("Root proxy is required");
187
195
  // Capture state before change
188
196
  const oldArray = target.slice();
189
197
 
190
198
  const result = Reflect.set(target, prop, newValue, receiver);
191
199
 
192
200
  // Add to history
193
- self.rootProxy!.history.push({
201
+ self.rootProxy.history.push({
194
202
  path: path,
195
203
  previousValue: oldArray,
196
204
  currentValue: target.slice(),
197
205
  timestamp: Date.now(),
198
206
  });
199
207
 
200
- if (!self.rootProxy!.trackedArraysCloned.has(path)) {
201
- self.rootProxy!.storeArrayState(path, target);
208
+ if (!self.rootProxy.trackedArraysCloned.has(path)) {
209
+ self.rootProxy.storeArrayState(path, target);
202
210
  }
203
- self.rootProxy!.notifyArrayChange(path, target);
204
- self.rootProxy!.notifyGlobalSubscribers();
211
+ self.rootProxy.notifyArrayChange(path, target);
212
+ self.rootProxy.notifyGlobalSubscribers();
205
213
  return result;
206
214
  }
207
215
  return Reflect.set(target, prop, newValue, receiver);
@@ -210,21 +218,24 @@ export class DeepProxy {
210
218
  }
211
219
 
212
220
  subscribe(path: string, callback: Function): void {
221
+ if (!this.rootProxy) throw new Error("Root proxy is required");
222
+
213
223
  if (path === "*") {
214
- this.rootProxy!.globalSubscribers.add(callback);
224
+ this.rootProxy.globalSubscribers.add(callback);
215
225
  return;
216
226
  }
217
227
 
218
- if (!this.rootProxy!.subscribers.has(path)) {
219
- this.rootProxy!.subscribers.set(path, new Set());
228
+ if (!this.rootProxy.subscribers.has(path)) {
229
+ this.rootProxy.subscribers.set(path, new Set());
220
230
  }
221
- this.rootProxy!.subscribers.get(path)!.add(callback);
222
231
 
223
- const actualValue = this.rootProxy!.target[path];
232
+ this.rootProxy.subscribers.get(path)!.add(callback);
233
+
234
+ const actualValue = this.rootProxy.target[path];
224
235
  const isArray = Array.isArray(actualValue);
225
236
 
226
237
  // Fire callback immediately if there's a historical change for this path
227
- const lastChange = this.rootProxy!.getLastChangeForPath(path);
238
+ const lastChange = this.rootProxy.getLastChangeForPath(path);
228
239
  if (lastChange) {
229
240
  if (isArray) {
230
241
  // For arrays, we need to detect changes and fire with toCreate/toUpdate/toDelete
@@ -236,14 +247,14 @@ export class DeepProxy {
236
247
  : actualValue;
237
248
 
238
249
  // Store the initial state before the change for comparison
239
- if (!this.rootProxy!.trackedArraysCloned.has(path)) {
240
- this.rootProxy!.storeArrayState(path, previousArray);
250
+ if (!this.rootProxy.trackedArraysCloned.has(path)) {
251
+ this.rootProxy.storeArrayState(path, previousArray);
241
252
  }
242
253
 
243
254
  // Detect changes between previous and current
244
- const changes = this.rootProxy!.detectArrayChanges(
245
- this.rootProxy!.trackedArraysCloned.get(path) ||
246
- this.rootProxy!.cloneArray(previousArray),
255
+ const changes = this.rootProxy.detectArrayChanges(
256
+ this.rootProxy.trackedArraysCloned.get(path) ||
257
+ this.rootProxy.cloneArray(previousArray),
247
258
  previousArray,
248
259
  currentArray
249
260
  );
@@ -251,7 +262,7 @@ export class DeepProxy {
251
262
  callback({ ...changes, path });
252
263
 
253
264
  // Update tracked state to current
254
- this.rootProxy!.storeArrayState(path, currentArray);
265
+ this.rootProxy.storeArrayState(path, currentArray);
255
266
  } else {
256
267
  // For non-arrays, use the simple property change format
257
268
  callback({
@@ -260,19 +271,20 @@ export class DeepProxy {
260
271
  path: lastChange.path,
261
272
  });
262
273
  }
263
- } else if (isArray && !this.rootProxy!.trackedArraysCloned.has(path)) {
274
+ } else if (isArray && !this.rootProxy.trackedArraysCloned.has(path)) {
264
275
  // No historical changes, just store initial state
265
- this.rootProxy!.storeArrayState(path, actualValue);
276
+ this.rootProxy.storeArrayState(path, actualValue);
266
277
  }
267
278
  }
268
279
 
269
280
  unsubscribe(path: string, callback: Function): void {
281
+ if (!this.rootProxy) throw new Error("Root proxy is required");
270
282
  if (path === "*") {
271
- this.rootProxy!.globalSubscribers.delete(callback);
283
+ this.rootProxy.globalSubscribers.delete(callback);
272
284
  return;
273
285
  }
274
286
 
275
- const subs = this.rootProxy!.subscribers.get(path);
287
+ const subs = this.rootProxy.subscribers.get(path);
276
288
  if (subs) {
277
289
  subs.delete(callback);
278
290
  }
@@ -1,166 +1,152 @@
1
- // ============================================================================
2
- // Domain Event Bus - Pub/Sub for Domain Events
3
- // ============================================================================
4
-
5
- import {
6
- IDomainEvent,
7
- DomainEventHandler,
8
- IDomainEventHandler,
9
- } from "./domain-event";
10
-
11
- type EventConstructor<T extends IDomainEvent = IDomainEvent> = new (
12
- ...args: any[]
13
- ) => T;
14
-
15
- /**
16
- * Domain Event Bus - Singleton pattern for event pub/sub
17
- */
18
- export class DomainEventBus {
19
- private static instance: DomainEventBus;
20
- private handlers: Map<
21
- string,
22
- Set<DomainEventHandler<any> | IDomainEventHandler<any>>
23
- > = new Map();
24
- private wildcardHandlers: Set<
25
- DomainEventHandler<any> | IDomainEventHandler<any>
26
- > = new Set();
27
-
28
- private constructor() {}
29
-
30
- /**
31
- * Get the singleton instance
32
- */
33
- static getInstance(): DomainEventBus {
34
- if (!DomainEventBus.instance) {
35
- DomainEventBus.instance = new DomainEventBus();
36
- }
37
- return DomainEventBus.instance;
38
- }
39
-
40
- /**
41
- * Subscribe to a specific event type
42
- */
43
- subscribe<T extends IDomainEvent>(
44
- eventType: EventConstructor<T> | string,
45
- handler: DomainEventHandler<T> | IDomainEventHandler<T>
46
- ): void {
47
- const eventName =
48
- typeof eventType === "string" ? eventType : eventType.name;
49
-
50
- if (!this.handlers.has(eventName)) {
51
- this.handlers.set(eventName, new Set());
52
- }
53
-
54
- this.handlers.get(eventName)!.add(handler);
55
- }
56
-
57
- /**
58
- * Subscribe to all events (wildcard)
59
- */
60
- subscribeAll(
61
- handler: DomainEventHandler<IDomainEvent> | IDomainEventHandler<IDomainEvent>
62
- ): void {
63
- this.wildcardHandlers.add(handler);
64
- }
65
-
66
- /**
67
- * Unsubscribe from a specific event type
68
- */
69
- unsubscribe<T extends IDomainEvent>(
70
- eventType: EventConstructor<T> | string,
71
- handler: DomainEventHandler<T> | IDomainEventHandler<T>
72
- ): void {
73
- const eventName =
74
- typeof eventType === "string" ? eventType : eventType.name;
75
- const handlers = this.handlers.get(eventName);
76
-
77
- if (handlers) {
78
- handlers.delete(handler);
79
- if (handlers.size === 0) {
80
- this.handlers.delete(eventName);
81
- }
82
- }
83
- }
84
-
85
- /**
86
- * Unsubscribe from all events
87
- */
88
- unsubscribeAll(
89
- handler: DomainEventHandler<IDomainEvent> | IDomainEventHandler<IDomainEvent>
90
- ): void {
91
- this.wildcardHandlers.delete(handler);
92
- }
93
-
94
- /**
95
- * Publish a single event
96
- */
97
- async publish<T extends IDomainEvent>(event: T): Promise<void> {
98
- const eventName = event.eventName;
99
- const handlers = this.handlers.get(eventName) || new Set();
100
-
101
- // Execute specific handlers
102
- const specificPromises = Array.from(handlers).map((handler) =>
103
- this.executeHandler(handler, event)
104
- );
105
-
106
- // Execute wildcard handlers
107
- const wildcardPromises = Array.from(this.wildcardHandlers).map((handler) =>
108
- this.executeHandler(handler, event)
109
- );
110
-
111
- await Promise.all([...specificPromises, ...wildcardPromises]);
112
- }
113
-
114
- /**
115
- * Publish multiple events
116
- */
117
- async publishAll(events: IDomainEvent[]): Promise<void> {
118
- await Promise.all(events.map((event) => this.publish(event)));
119
- }
120
-
121
- /**
122
- * Clear all handlers (useful for testing)
123
- */
124
- clearAllHandlers(): void {
125
- this.handlers.clear();
126
- this.wildcardHandlers.clear();
127
- }
128
-
129
- /**
130
- * Get count of handlers for an event type
131
- */
132
- getHandlerCount(eventType: EventConstructor | string): number {
133
- const eventName =
134
- typeof eventType === "string" ? eventType : eventType.name;
135
- return this.handlers.get(eventName)?.size || 0;
136
- }
137
-
138
- /**
139
- * Execute a handler (function or class)
140
- */
141
- private async executeHandler(
142
- handler: DomainEventHandler<any> | IDomainEventHandler<any>,
143
- event: IDomainEvent
144
- ): Promise<void> {
145
- try {
146
- if (typeof handler === "function") {
147
- await handler(event);
148
- } else {
149
- await handler.handle(event);
150
- }
151
- } catch (error) {
152
- console.error(
153
- `Error handling event ${event.eventName}:`,
154
- error
155
- );
156
- // Don't throw - we don't want one handler failure to break others
157
- }
158
- }
159
- }
160
-
161
- /**
162
- * Convenience function to get the event bus instance
163
- */
164
- export function getEventBus(): DomainEventBus {
165
- return DomainEventBus.getInstance();
166
- }
1
+ import { IDomainEvent, DomainEventHandler, IDomainEventHandler } from ".";
2
+
3
+ type EventConstructor<T extends IDomainEvent = IDomainEvent> = new (
4
+ ...args: any[]
5
+ ) => T;
6
+
7
+ /**
8
+ * Domain Event Bus - Singleton pattern for event pub/sub
9
+ */
10
+ export class DomainEventBus {
11
+ private static instance: DomainEventBus;
12
+ private handlers: Map<
13
+ string,
14
+ Set<DomainEventHandler<any> | IDomainEventHandler<any>>
15
+ > = new Map();
16
+ private wildcardHandlers: Set<
17
+ DomainEventHandler<any> | IDomainEventHandler<any>
18
+ > = new Set();
19
+
20
+ private constructor() {}
21
+
22
+ /**
23
+ * Get the singleton instance
24
+ */
25
+ static getInstance(): DomainEventBus {
26
+ if (!DomainEventBus.instance) {
27
+ DomainEventBus.instance = new DomainEventBus();
28
+ }
29
+ return DomainEventBus.instance;
30
+ }
31
+
32
+ /**
33
+ * Subscribe to a specific event type
34
+ */
35
+ subscribe<T extends IDomainEvent>(props: {
36
+ event: EventConstructor<T> | string;
37
+ handler: DomainEventHandler<T> | IDomainEventHandler<T>;
38
+ }): void {
39
+ const { event, handler } = props;
40
+ const eventName = typeof event === "string" ? event : event.name;
41
+
42
+ if (!this.handlers.has(eventName)) {
43
+ this.handlers.set(eventName, new Set());
44
+ }
45
+
46
+ this.handlers.get(eventName)!.add(handler);
47
+ }
48
+
49
+ /**
50
+ * Subscribe to all events (wildcard)
51
+ */
52
+ subscribeAll(
53
+ handler:
54
+ | DomainEventHandler<IDomainEvent>
55
+ | IDomainEventHandler<IDomainEvent>
56
+ ): void {
57
+ this.wildcardHandlers.add(handler);
58
+ }
59
+
60
+ /**
61
+ * Unsubscribe from a specific event type
62
+ */
63
+ unsubscribe<T extends IDomainEvent>(
64
+ eventType: EventConstructor<T> | string,
65
+ handler: DomainEventHandler<T> | IDomainEventHandler<T>
66
+ ): void {
67
+ const eventName =
68
+ typeof eventType === "string" ? eventType : eventType.name;
69
+ const handlers = this.handlers.get(eventName);
70
+
71
+ if (handlers) {
72
+ handlers.delete(handler);
73
+ if (handlers.size === 0) {
74
+ this.handlers.delete(eventName);
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Unsubscribe from all events
81
+ */
82
+ unsubscribeAll(
83
+ handler:
84
+ | DomainEventHandler<IDomainEvent>
85
+ | IDomainEventHandler<IDomainEvent>
86
+ ): void {
87
+ this.wildcardHandlers.delete(handler);
88
+ }
89
+
90
+ /**
91
+ * Publish a single event
92
+ */
93
+ async publish<T extends IDomainEvent>(event: T): Promise<void> {
94
+ const eventName = event.eventName;
95
+ const handlers = this.handlers.get(eventName) || new Set();
96
+
97
+ // Execute specific handlers
98
+ const specificPromises = Array.from(handlers).map((handler) =>
99
+ this.executeHandler(handler, event)
100
+ );
101
+
102
+ // Execute wildcard handlers
103
+ const wildcardPromises = Array.from(this.wildcardHandlers).map((handler) =>
104
+ this.executeHandler(handler, event)
105
+ );
106
+
107
+ await Promise.all([...specificPromises, ...wildcardPromises]);
108
+ }
109
+
110
+ /**
111
+ * Publish multiple events
112
+ */
113
+ async publishAll(events: IDomainEvent[]): Promise<void> {
114
+ await Promise.all(events.map((event) => this.publish(event)));
115
+ }
116
+
117
+ /**
118
+ * Clear all handlers (useful for testing)
119
+ */
120
+ clearAllHandlers(): void {
121
+ this.handlers.clear();
122
+ this.wildcardHandlers.clear();
123
+ }
124
+
125
+ /**
126
+ * Get count of handlers for an event type
127
+ */
128
+ getHandlerCount(eventType: EventConstructor | string): number {
129
+ const eventName =
130
+ typeof eventType === "string" ? eventType : eventType.name;
131
+ return this.handlers.get(eventName)?.size || 0;
132
+ }
133
+
134
+ /**
135
+ * Execute a handler (function or class)
136
+ */
137
+ private async executeHandler(
138
+ handler: DomainEventHandler<any> | IDomainEventHandler<any>,
139
+ event: IDomainEvent
140
+ ): Promise<void> {
141
+ try {
142
+ if (typeof handler === "function") {
143
+ await handler(event);
144
+ } else {
145
+ await handler.handle(event);
146
+ }
147
+ } catch (error) {
148
+ console.error(`Error handling event ${event.eventName}:`, error);
149
+ // Don't throw - we don't want one handler failure to break others
150
+ }
151
+ }
152
+ }