drimion 0.1.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.
- package/LICENSE +7 -0
- package/README.md +955 -0
- package/dist/cli/index.js +1045 -0
- package/dist/cli/templates/aggregate.ts.hbs +22 -0
- package/dist/cli/templates/entity.ts.hbs +16 -0
- package/dist/cli/templates/repository.ts.hbs +24 -0
- package/dist/cli/templates/use-case.ts.hbs +20 -0
- package/dist/cli/templates/value-object.ts.hbs +16 -0
- package/dist/kernel/core/aggregate.ts +234 -0
- package/dist/kernel/core/entity.ts +348 -0
- package/dist/kernel/core/id.ts +207 -0
- package/dist/kernel/core/index.ts +5 -0
- package/dist/kernel/core/repository.ts +81 -0
- package/dist/kernel/core/value-object.ts +309 -0
- package/dist/kernel/events/browser-event-manager.ts +241 -0
- package/dist/kernel/events/domain-event.ts +76 -0
- package/dist/kernel/events/event-bus.ts +158 -0
- package/dist/kernel/events/event-context.ts +95 -0
- package/dist/kernel/events/event-manager.ts +20 -0
- package/dist/kernel/events/event-utils.ts +19 -0
- package/dist/kernel/events/index.ts +7 -0
- package/dist/kernel/events/server-event-manager.ts +169 -0
- package/dist/kernel/helpers/auto-mapper.ts +222 -0
- package/dist/kernel/helpers/domain-classes.ts +162 -0
- package/dist/kernel/helpers/domain-error.ts +52 -0
- package/dist/kernel/helpers/getters-setters.ts +385 -0
- package/dist/kernel/helpers/index.ts +7 -0
- package/dist/kernel/index.ts +73 -0
- package/dist/kernel/libs/crypto.ts +33 -0
- package/dist/kernel/libs/index.ts +5 -0
- package/dist/kernel/libs/iterator.ts +298 -0
- package/dist/kernel/libs/result.ts +252 -0
- package/dist/kernel/libs/utils.ts +260 -0
- package/dist/kernel/libs/validator.ts +353 -0
- package/dist/kernel/types/adapter.types.ts +26 -0
- package/dist/kernel/types/command.types.ts +37 -0
- package/dist/kernel/types/entity.types.ts +60 -0
- package/dist/kernel/types/event.types.ts +129 -0
- package/dist/kernel/types/index.ts +26 -0
- package/dist/kernel/types/iterator.types.ts +39 -0
- package/dist/kernel/types/result.types.ts +122 -0
- package/dist/kernel/types/uid.types.ts +18 -0
- package/dist/kernel/types/utils.types.ts +120 -0
- package/dist/kernel/types/value-object.types.ts +20 -0
- package/dist/kernel/utils/date.utils.ts +111 -0
- package/dist/kernel/utils/index.ts +32 -0
- package/dist/kernel/utils/number.utils.ts +341 -0
- package/dist/kernel/utils/object.utils.ts +61 -0
- package/dist/kernel/utils/string.utils.ts +128 -0
- package/dist/kernel/utils/type.utils.ts +33 -0
- package/package.json +59 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { DomainEventPayload, EventEntry } from "../types/event.types";
|
|
2
|
+
import { BaseEventManager } from "./event-manager";
|
|
3
|
+
import { ValidateEventName } from "./event-utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @description
|
|
7
|
+
* Duck-typed interface for the browser `window` object.
|
|
8
|
+
*
|
|
9
|
+
* Using a structural type instead of `Window & typeof globalThis` keeps this
|
|
10
|
+
* module free of DOM lib dependencies, making the library usable in projects
|
|
11
|
+
* that do not include `"lib": ["DOM"]` in their tsconfig.
|
|
12
|
+
*/
|
|
13
|
+
export interface WindowLike {
|
|
14
|
+
dispatchEvent(event: Event): boolean;
|
|
15
|
+
addEventListener(
|
|
16
|
+
type: string,
|
|
17
|
+
listener: (event: Event) => void,
|
|
18
|
+
options?: boolean | AddEventListenerOptions,
|
|
19
|
+
): void;
|
|
20
|
+
removeEventListener(
|
|
21
|
+
type: string,
|
|
22
|
+
listener: (event: Event) => void,
|
|
23
|
+
options?: boolean | EventListenerOptions,
|
|
24
|
+
): void;
|
|
25
|
+
sessionStorage: {
|
|
26
|
+
getItem(key: string): string | null;
|
|
27
|
+
setItem(key: string, value: string): void;
|
|
28
|
+
removeItem(key: string): void;
|
|
29
|
+
};
|
|
30
|
+
CustomEvent: new <T>(
|
|
31
|
+
type: string,
|
|
32
|
+
eventInitDict?: { bubbles?: boolean; detail?: T },
|
|
33
|
+
) => Event;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @description
|
|
38
|
+
* Browser-side singleton event manager backed by the native `window` CustomEvent API.
|
|
39
|
+
*
|
|
40
|
+
* Manages application-level event subscriptions and dispatches for browser environments.
|
|
41
|
+
* Subscriptions are persisted in `sessionStorage` so their existence can be detected
|
|
42
|
+
* across re-renders. Listeners are automatically cleaned up on page unload.
|
|
43
|
+
*
|
|
44
|
+
* Supports wildcard event dispatch using `*` as a glob-style pattern.
|
|
45
|
+
*
|
|
46
|
+
* Use `EventContext.resolve()` to obtain this instance rather than constructing it directly.
|
|
47
|
+
* For most use cases, prefer `EventBus` — use this only when you need to tap into
|
|
48
|
+
* the native browser CustomEvent system.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* const manager = BrowserEventManager.instance(window);
|
|
53
|
+
*
|
|
54
|
+
* manager.subscribe('order:placed', (event) => {
|
|
55
|
+
* console.log(event.detail);
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* manager.dispatchEvent('order:placed', { orderId: '123' });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export class BrowserEventManager extends BaseEventManager {
|
|
62
|
+
private static _instance: BrowserEventManager;
|
|
63
|
+
private readonly entries: EventEntry[] = [];
|
|
64
|
+
private readonly storagePrefix = "browser:event:";
|
|
65
|
+
private initialized = false;
|
|
66
|
+
|
|
67
|
+
private constructor(private readonly win: WindowLike) {
|
|
68
|
+
super();
|
|
69
|
+
if (typeof win === "undefined" || win === null) {
|
|
70
|
+
throw new Error("BrowserEventManager requires a valid window object.");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @description
|
|
76
|
+
* Returns the singleton `BrowserEventManager` instance, creating it if necessary.
|
|
77
|
+
*
|
|
78
|
+
* @param win The browser `window` object (or any `WindowLike` implementation).
|
|
79
|
+
* @returns The singleton `BrowserEventManager`.
|
|
80
|
+
* @throws {Error} If called outside a browser environment.
|
|
81
|
+
*/
|
|
82
|
+
public static instance(win: WindowLike): BrowserEventManager {
|
|
83
|
+
if (!BrowserEventManager._instance) {
|
|
84
|
+
BrowserEventManager._instance = new BrowserEventManager(win);
|
|
85
|
+
}
|
|
86
|
+
return BrowserEventManager._instance;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @description
|
|
91
|
+
* Checks whether an event with the given name is currently registered.
|
|
92
|
+
*
|
|
93
|
+
* Checks both the internal entry list and `sessionStorage` for persistence
|
|
94
|
+
* across re-renders.
|
|
95
|
+
*
|
|
96
|
+
* @param eventName The event name to check.
|
|
97
|
+
* @returns `true` if the event is registered.
|
|
98
|
+
*/
|
|
99
|
+
public exists(eventName: string): boolean {
|
|
100
|
+
const inStorage = !!this.win.sessionStorage.getItem(
|
|
101
|
+
this.storagePrefix + eventName,
|
|
102
|
+
);
|
|
103
|
+
const inEntries = this.entries.some((e) => e.eventName === eventName);
|
|
104
|
+
return inStorage || inEntries;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @description
|
|
109
|
+
* Dispatches a custom event by name, forwarding optional arguments as the `detail` payload.
|
|
110
|
+
*
|
|
111
|
+
* Supports wildcard patterns — if `eventName` contains `*`, all registered events
|
|
112
|
+
* whose names match the resulting regex are dispatched.
|
|
113
|
+
*
|
|
114
|
+
* @param eventName The event name or wildcard pattern to dispatch.
|
|
115
|
+
* @param args Additional arguments forwarded as `detail` to handlers.
|
|
116
|
+
*/
|
|
117
|
+
public dispatchEvent(eventName: string, ...args: unknown[]): void {
|
|
118
|
+
ValidateEventName(eventName);
|
|
119
|
+
|
|
120
|
+
if (eventName.includes("*")) {
|
|
121
|
+
const regex = new RegExp(eventName.replace("*", ".*"));
|
|
122
|
+
for (const entry of this.entries) {
|
|
123
|
+
if (regex.test(entry.eventName)) {
|
|
124
|
+
this.win.dispatchEvent(
|
|
125
|
+
new this.win.CustomEvent(entry.eventName, {
|
|
126
|
+
bubbles: true,
|
|
127
|
+
detail: args,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.win.dispatchEvent(
|
|
136
|
+
new this.win.CustomEvent(eventName, {
|
|
137
|
+
bubbles: true,
|
|
138
|
+
detail: args,
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @description
|
|
145
|
+
* Removes a registered event, cleaning up its listener and `sessionStorage` entry.
|
|
146
|
+
*
|
|
147
|
+
* @param eventName The name of the event to remove.
|
|
148
|
+
* @returns `true` if the event was found and removed; `false` otherwise.
|
|
149
|
+
*/
|
|
150
|
+
public removeEvent(eventName: string): boolean {
|
|
151
|
+
// this.win.sessionStorage.removeItem(this.storagePrefix + eventName);
|
|
152
|
+
// const entry = this.entries.find((e) => e.eventName === eventName);
|
|
153
|
+
// if (!entry) return false;
|
|
154
|
+
// this.entries.splice(this.entries.indexOf(entry), 1);
|
|
155
|
+
// // Cast through unknown — the callback is structurally compatible at runtime
|
|
156
|
+
// // (CustomEvent extends Event), but TypeScript cannot verify this statically.
|
|
157
|
+
// this.win.removeEventListener(
|
|
158
|
+
// eventName,
|
|
159
|
+
// entry.callback as unknown as (event: Event) => void,
|
|
160
|
+
// );
|
|
161
|
+
// return true;
|
|
162
|
+
const matching = this.entries.filter((e) => e.eventName === eventName);
|
|
163
|
+
|
|
164
|
+
if (matching.length === 0) return false;
|
|
165
|
+
|
|
166
|
+
// Remove from entries
|
|
167
|
+
this.entries.splice(
|
|
168
|
+
0,
|
|
169
|
+
this.entries.length,
|
|
170
|
+
...this.entries.filter((e) => e.eventName !== eventName),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Remove all listeners
|
|
174
|
+
for (const entry of matching) {
|
|
175
|
+
this.win.removeEventListener(
|
|
176
|
+
eventName,
|
|
177
|
+
entry.callback as unknown as (event: Event) => void,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Remove storage
|
|
182
|
+
this.win.sessionStorage.removeItem(this.storagePrefix + eventName);
|
|
183
|
+
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @description
|
|
189
|
+
* Subscribes a callback to the specified event name using the native `window` event system.
|
|
190
|
+
*
|
|
191
|
+
* Persists the subscription in `sessionStorage` and registers a `beforeunload`
|
|
192
|
+
* cleanup listener. If the event is already registered, this call is a no-op.
|
|
193
|
+
*
|
|
194
|
+
* Event names must follow the `context:EventName` format.
|
|
195
|
+
*
|
|
196
|
+
* @param eventName The event name to subscribe to.
|
|
197
|
+
* @param fn The callback to invoke when the event is dispatched.
|
|
198
|
+
*
|
|
199
|
+
* @throws {DomainError} If the event name does not follow the required format.
|
|
200
|
+
*/
|
|
201
|
+
public subscribe(
|
|
202
|
+
eventName: string,
|
|
203
|
+
fn: (event: DomainEventPayload) => void | Promise<void>,
|
|
204
|
+
): void {
|
|
205
|
+
ValidateEventName(eventName);
|
|
206
|
+
|
|
207
|
+
this.entries.push({ eventName, callback: fn });
|
|
208
|
+
this.win.sessionStorage.setItem(
|
|
209
|
+
this.storagePrefix + eventName,
|
|
210
|
+
Date.now().toString(),
|
|
211
|
+
);
|
|
212
|
+
this.win.addEventListener(
|
|
213
|
+
eventName,
|
|
214
|
+
fn as unknown as (event: Event) => void,
|
|
215
|
+
);
|
|
216
|
+
if (!this.initialized) {
|
|
217
|
+
this.win.addEventListener("beforeunload", () => {
|
|
218
|
+
for (const entry of this.entries) {
|
|
219
|
+
this.win.sessionStorage.removeItem(
|
|
220
|
+
this.storagePrefix + entry.eventName,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
this.initialized = true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @description
|
|
231
|
+
* Resets the cached event manager instance.
|
|
232
|
+
*
|
|
233
|
+
* Intended for use in tests where environment switching or clean state
|
|
234
|
+
* between test cases is required. Not recommended in production code.
|
|
235
|
+
*
|
|
236
|
+
* @internal
|
|
237
|
+
*/
|
|
238
|
+
public static __reset(): void {
|
|
239
|
+
BrowserEventManager._instance = undefined as unknown as BrowserEventManager;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { DomainEvent } from "../types/event.types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @description
|
|
5
|
+
* Abstract base class for defining strongly-typed domain events.
|
|
6
|
+
*
|
|
7
|
+
* Extend this class when you want an OOP-style event definition with a fixed
|
|
8
|
+
* `type` string baked into the class. The `aggregateId`, `aggregateName`, and
|
|
9
|
+
* `occurredAt` fields are set automatically by `Aggregate.emit()` — subclasses
|
|
10
|
+
* only need to declare the `payload` shape.
|
|
11
|
+
*
|
|
12
|
+
* Using this base class is **optional**. `Aggregate.emit()` accepts any object
|
|
13
|
+
* conforming to `DomainEvent<TPayload>` — a plain object literal works equally
|
|
14
|
+
* well. Prefer this class when you want a reusable, self-describing event type
|
|
15
|
+
* that can be imported and checked with `instanceof`.
|
|
16
|
+
*
|
|
17
|
+
* @template TPayload The shape of the event-specific data.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* // Define the event
|
|
22
|
+
* interface OrderPlacedPayload { total: number; customerId: string }
|
|
23
|
+
*
|
|
24
|
+
* class OrderPlacedEvent extends BaseDomainEvent<OrderPlacedPayload> {
|
|
25
|
+
* static readonly type = 'order:placed' as const;
|
|
26
|
+
*
|
|
27
|
+
* constructor(aggregateId: string, payload: OrderPlacedPayload) {
|
|
28
|
+
* super(OrderPlacedEvent.type, aggregateId, 'Order', payload);
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* // Inside an Aggregate subclass
|
|
33
|
+
* class Order extends Aggregate<OrderProps> {
|
|
34
|
+
* public place(): void {
|
|
35
|
+
* this.change('status', 'placed');
|
|
36
|
+
* this.emit(new OrderPlacedEvent(this.id.value(), {
|
|
37
|
+
* total: this.get('total'),
|
|
38
|
+
* customerId: this.get('customerId'),
|
|
39
|
+
* }));
|
|
40
|
+
* }
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* // Application layer
|
|
44
|
+
* order.place();
|
|
45
|
+
* await repo.save(order);
|
|
46
|
+
* await eventBus.publishAll(order.pullEvents());
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export abstract class BaseDomainEvent<TPayload = unknown>
|
|
50
|
+
implements DomainEvent<TPayload>
|
|
51
|
+
{
|
|
52
|
+
readonly type: string;
|
|
53
|
+
readonly aggregateId: string;
|
|
54
|
+
readonly aggregateName: string;
|
|
55
|
+
readonly occurredAt: Date;
|
|
56
|
+
readonly payload: TPayload;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param type The event type identifier, e.g. `'order:placed'`.
|
|
60
|
+
* @param aggregateId The `id.value()` of the aggregate that emitted this event.
|
|
61
|
+
* @param aggregateName The class name of the aggregate, e.g. `'Order'`.
|
|
62
|
+
* @param payload Event-specific data. Keep flat and serializable.
|
|
63
|
+
*/
|
|
64
|
+
constructor(
|
|
65
|
+
type: string,
|
|
66
|
+
aggregateId: string,
|
|
67
|
+
aggregateName: string,
|
|
68
|
+
payload: TPayload,
|
|
69
|
+
) {
|
|
70
|
+
this.type = type;
|
|
71
|
+
this.aggregateId = aggregateId;
|
|
72
|
+
this.aggregateName = aggregateName;
|
|
73
|
+
this.occurredAt = new Date();
|
|
74
|
+
this.payload = payload;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DomainEvent,
|
|
3
|
+
EventSubscriber,
|
|
4
|
+
IEventBus,
|
|
5
|
+
SubscriberEntry,
|
|
6
|
+
} from "../types/event.types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @description
|
|
10
|
+
* A simple, in-process event bus that ships with the library as a zero-config
|
|
11
|
+
* default. Suitable for monolithic applications, unit tests, and any context
|
|
12
|
+
* where events do not need to cross process or network boundaries.
|
|
13
|
+
*
|
|
14
|
+
* `EventBus` implements `IEventBus` so it can be swapped for any external
|
|
15
|
+
* transport (Redis, NATS, BullMQ, etc.) without touching domain code.
|
|
16
|
+
*
|
|
17
|
+
* Subscribers are registered per `type` string and invoked in registration
|
|
18
|
+
* order. Each subscriber runs independently — a failure in one does not
|
|
19
|
+
* prevent the others from running.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const bus = new EventBus();
|
|
24
|
+
*
|
|
25
|
+
* // Register subscribers (application layer)
|
|
26
|
+
* bus.subscribe('order:placed', async (event) => {
|
|
27
|
+
* await sendConfirmationEmail(event.payload.customerId);
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // Publish after repository.save() (application layer)
|
|
31
|
+
* await repo.save(order);
|
|
32
|
+
* await bus.publishAll(order.pullEvents());
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class EventBus implements IEventBus {
|
|
36
|
+
private readonly entries: SubscriberEntry[] = [];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @description
|
|
40
|
+
* Registers a subscriber for a specific event type.
|
|
41
|
+
*
|
|
42
|
+
* Multiple subscribers for the same `type` are all invoked when that event
|
|
43
|
+
* is published. Subscribing the same function twice for the same type will
|
|
44
|
+
* register it twice — callers are responsible for deduplication if needed.
|
|
45
|
+
*
|
|
46
|
+
* @param type The event type to listen for, e.g. `'order:placed'`.
|
|
47
|
+
* @param subscriber The callback to invoke when a matching event is published.
|
|
48
|
+
*/
|
|
49
|
+
public subscribe<TPayload = unknown>(
|
|
50
|
+
type: string,
|
|
51
|
+
subscriber: EventSubscriber<TPayload>,
|
|
52
|
+
): void {
|
|
53
|
+
this.entries.push({
|
|
54
|
+
type,
|
|
55
|
+
subscriber: subscriber as EventSubscriber,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @description
|
|
61
|
+
* Removes all subscribers for a given event type.
|
|
62
|
+
*
|
|
63
|
+
* @param type The event type whose subscribers should be removed.
|
|
64
|
+
* @returns The number of subscribers that were removed.
|
|
65
|
+
*/
|
|
66
|
+
public unsubscribe(type: string): number {
|
|
67
|
+
const before = this.entries.length;
|
|
68
|
+
const remaining = this.entries.filter((e) => e.type !== type);
|
|
69
|
+
this.entries.splice(0, this.entries.length, ...remaining);
|
|
70
|
+
return before - this.entries.length;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @description
|
|
75
|
+
* Publishes a single domain event, invoking all matching subscribers in
|
|
76
|
+
* registration order.
|
|
77
|
+
*
|
|
78
|
+
* Subscriber errors are caught and re-thrown after all subscribers have
|
|
79
|
+
* had a chance to run, collected as an `AggregateError`.
|
|
80
|
+
*
|
|
81
|
+
* @param event The domain event to publish.
|
|
82
|
+
* @throws {AggregateError} If one or more subscribers throw.
|
|
83
|
+
*/
|
|
84
|
+
public async publish(event: DomainEvent): Promise<void> {
|
|
85
|
+
const matching = this.entries.filter((e) => e.type === event.type);
|
|
86
|
+
const errors: unknown[] = [];
|
|
87
|
+
|
|
88
|
+
for (const entry of matching) {
|
|
89
|
+
try {
|
|
90
|
+
await entry.subscriber(event);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
errors.push(err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (errors.length > 0) {
|
|
97
|
+
throw new AggregateError(
|
|
98
|
+
errors,
|
|
99
|
+
`EventBus: ${errors.length} subscriber(s) failed for event "${event.type}".`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @description
|
|
106
|
+
* Publishes all domain events in the provided array, in order.
|
|
107
|
+
* Each event is published independently — a failure in one event's
|
|
108
|
+
* subscribers does not prevent subsequent events from being published.
|
|
109
|
+
*
|
|
110
|
+
* Errors from all events are collected and thrown together as an
|
|
111
|
+
* `AggregateError` after all events have been processed.
|
|
112
|
+
*
|
|
113
|
+
* @param events The array of domain events to publish.
|
|
114
|
+
* @throws {AggregateError} If any subscriber across any event throws.
|
|
115
|
+
*/
|
|
116
|
+
public async publishAll(events: ReadonlyArray<DomainEvent>): Promise<void> {
|
|
117
|
+
const errors: unknown[] = [];
|
|
118
|
+
|
|
119
|
+
for (const event of events) {
|
|
120
|
+
try {
|
|
121
|
+
await this.publish(event);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
// AggregateError from publish() — unwrap its inner errors
|
|
124
|
+
if (err instanceof AggregateError) {
|
|
125
|
+
errors.push(...err.errors);
|
|
126
|
+
} else {
|
|
127
|
+
errors.push(err);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (errors.length > 0) {
|
|
133
|
+
throw new AggregateError(
|
|
134
|
+
errors,
|
|
135
|
+
`EventBus: ${errors.length} error(s) occurred while publishing ${events.length} event(s).`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @description
|
|
142
|
+
* Returns the number of subscribers currently registered for a given type.
|
|
143
|
+
* Useful for testing and diagnostics.
|
|
144
|
+
*
|
|
145
|
+
* @param type The event type to count subscribers for.
|
|
146
|
+
*/
|
|
147
|
+
public subscriberCount(type: string): number {
|
|
148
|
+
return this.entries.filter((e) => e.type === type).length;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @description
|
|
153
|
+
* Removes all registered subscribers. Useful for test teardown.
|
|
154
|
+
*/
|
|
155
|
+
public clear(): void {
|
|
156
|
+
this.entries.splice(0, this.entries.length);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { BrowserEventManager } from "./browser-event-manager";
|
|
2
|
+
import type { BaseEventManager } from "./event-manager";
|
|
3
|
+
import { ServerEventManager } from "./server-event-manager";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @description
|
|
7
|
+
* Internal cache for the resolved event manager instance.
|
|
8
|
+
*/
|
|
9
|
+
let managerCache: BaseEventManager | null = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @description
|
|
13
|
+
* Provides a platform-aware mechanism for resolving the appropriate built-in
|
|
14
|
+
* event manager at runtime.
|
|
15
|
+
*
|
|
16
|
+
* `EventContext` detects whether the current environment is Node.js or a browser,
|
|
17
|
+
* then initialises and returns the corresponding singleton `BaseEventManager`:
|
|
18
|
+
* - Node.js / Bun / Deno → `ServerEventManager`
|
|
19
|
+
* - Browser → `BrowserEventManager`
|
|
20
|
+
*
|
|
21
|
+
* Detection uses `process.versions?.node` rather than just `process` to avoid
|
|
22
|
+
* false positives in bundled browser environments (Webpack/Vite) where `process`
|
|
23
|
+
* may be polyfilled.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* `EventContext` is an **optional, convenience utility** for the built-in event
|
|
27
|
+
* managers. Application code that uses `EventBus` or a custom `IEventBus`
|
|
28
|
+
* implementation does not need this at all — it exists only to simplify access
|
|
29
|
+
* to `BrowserEventManager` / `ServerEventManager` when you choose to use them.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* // Resolve the platform-appropriate manager once at startup
|
|
34
|
+
* const manager = EventContext.resolve();
|
|
35
|
+
* manager.subscribe('order:placed', handler);
|
|
36
|
+
*
|
|
37
|
+
* // After aggregate mutations and repository.save():
|
|
38
|
+
* for (const event of order.pullEvents()) {
|
|
39
|
+
* manager.dispatchEvent(event.type, event);
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export const EventContext = {
|
|
44
|
+
/**
|
|
45
|
+
* @description
|
|
46
|
+
* Resolves and returns the platform-appropriate `BaseEventManager` singleton.
|
|
47
|
+
*
|
|
48
|
+
* Detection order:
|
|
49
|
+
* 1. Node.js / Bun — detected via `process.versions?.node`
|
|
50
|
+
* 2. Browser — detected via `globalThis.window`
|
|
51
|
+
*
|
|
52
|
+
* The resolved instance is cached after first resolution. Call `reset()`
|
|
53
|
+
* in tests if you need a clean state between environment switches.
|
|
54
|
+
*
|
|
55
|
+
* @returns The resolved `BaseEventManager` instance.
|
|
56
|
+
* @throws {Error} If the runtime environment cannot be determined.
|
|
57
|
+
*/
|
|
58
|
+
resolve(): BaseEventManager {
|
|
59
|
+
if (managerCache) return managerCache;
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
typeof process !== "undefined" &&
|
|
63
|
+
process.versions?.node !== undefined
|
|
64
|
+
) {
|
|
65
|
+
managerCache = ServerEventManager.instance();
|
|
66
|
+
return managerCache;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
typeof globalThis !== "undefined" &&
|
|
71
|
+
typeof globalThis.window !== "undefined"
|
|
72
|
+
) {
|
|
73
|
+
managerCache = BrowserEventManager.instance(
|
|
74
|
+
globalThis.window as Window & typeof globalThis,
|
|
75
|
+
);
|
|
76
|
+
return managerCache;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new Error(
|
|
80
|
+
"EventContext: unable to determine the runtime environment. " +
|
|
81
|
+
"Neither a Node.js-compatible runtime nor a browser window was detected.",
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @description
|
|
87
|
+
* Resets the cached event manager instance.
|
|
88
|
+
*
|
|
89
|
+
* Intended for use in tests where environment switching or clean state
|
|
90
|
+
* between test cases is required. Not recommended in production code.
|
|
91
|
+
*/
|
|
92
|
+
__reset(): void {
|
|
93
|
+
managerCache = null;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DomainEventPayload } from "../types/event.types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @description
|
|
5
|
+
* Abstract base class for the built-in event manager implementations
|
|
6
|
+
* (`BrowserEventManager`, `ServerEventManager`).
|
|
7
|
+
*
|
|
8
|
+
* Application code that depends only on `IEventBus` does not need this class.
|
|
9
|
+
* It is exposed for library consumers who want to extend or replace the
|
|
10
|
+
* built-in transport layer.
|
|
11
|
+
*/
|
|
12
|
+
export abstract class BaseEventManager {
|
|
13
|
+
abstract subscribe(
|
|
14
|
+
eventName: string,
|
|
15
|
+
fn: (event: DomainEventPayload) => void | Promise<void>,
|
|
16
|
+
): void;
|
|
17
|
+
abstract exists(eventName: string): boolean;
|
|
18
|
+
abstract removeEvent(eventName: string): boolean;
|
|
19
|
+
abstract dispatchEvent(eventName: string, ...args: unknown[]): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DomainError } from "../helpers";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @description
|
|
5
|
+
* Validates that an event name follows the required `context:EventName` format.
|
|
6
|
+
*
|
|
7
|
+
* @param eventName The event name to validate.
|
|
8
|
+
* @throws {DomainError} If the event name does not contain a colon separator.
|
|
9
|
+
*/
|
|
10
|
+
export const ValidateEventName = (eventName: string): void => {
|
|
11
|
+
if (!eventName.includes(":")) {
|
|
12
|
+
throw new DomainError(
|
|
13
|
+
`Invalid event name "${eventName}". ` +
|
|
14
|
+
'Event names must follow the pattern "context:EventName" ' +
|
|
15
|
+
'(e.g., "order:placed", "user:registered").',
|
|
16
|
+
{ context: "DomainEvent" },
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { BrowserEventManager, type WindowLike } from "./browser-event-manager";
|
|
2
|
+
export { BaseDomainEvent } from "./domain-event";
|
|
3
|
+
export { EventBus } from "./event-bus";
|
|
4
|
+
export { EventContext } from "./event-context";
|
|
5
|
+
export { BaseEventManager } from "./event-manager";
|
|
6
|
+
export { ValidateEventName } from "./event-utils";
|
|
7
|
+
export { ServerEventManager } from "./server-event-manager";
|