architex-js 1.7.0 → 1.8.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/package.json +3 -2
- package/src/events/DomainEvent.js +24 -0
- package/src/events/EventDispatcher.js +100 -0
- package/src/events/index.js +2 -0
- package/src/index.js +2 -1
- package/test/events.test.js +109 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "architex-js",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.js",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"./pipeline": "./src/pipeline/index.js",
|
|
12
12
|
"./result": "./src/result/index.js",
|
|
13
13
|
"./guards": "./src/guards/index.js",
|
|
14
|
-
"./repository": "./src/repository/index.js"
|
|
14
|
+
"./repository": "./src/repository/index.js",
|
|
15
|
+
"./events": "./src/events/index.js"
|
|
15
16
|
},
|
|
16
17
|
"description": "Architectural Toolbox for JavaScript - Providing high-level building blocks for robust systems.",
|
|
17
18
|
"author": {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for all Domain Events.
|
|
3
|
+
* Extend this class to define strongly-typed events in your domain.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* class UserCreated extends DomainEvent {
|
|
7
|
+
* constructor(userId) {
|
|
8
|
+
* super();
|
|
9
|
+
* this.userId = userId;
|
|
10
|
+
* }
|
|
11
|
+
* }
|
|
12
|
+
*/
|
|
13
|
+
class DomainEvent {
|
|
14
|
+
constructor() {
|
|
15
|
+
/** @type {string} Unique name of this event type */
|
|
16
|
+
this.eventName = this.constructor.name;
|
|
17
|
+
/** @type {Date} When the event was created */
|
|
18
|
+
this.occurredAt = new Date();
|
|
19
|
+
/** @type {string} Unique event id */
|
|
20
|
+
this.eventId = `${this.eventName}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { DomainEvent };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { DomainEvent } from "./DomainEvent.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dispatches domain events to registered handlers.
|
|
5
|
+
* Supports async handlers, multiple handlers per event, and wildcard listeners.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const dispatcher = new EventDispatcher();
|
|
9
|
+
* dispatcher.on(UserCreated, async (event) => sendWelcomeEmail(event.userId));
|
|
10
|
+
* await dispatcher.dispatch(new UserCreated(42));
|
|
11
|
+
*/
|
|
12
|
+
class EventDispatcher {
|
|
13
|
+
constructor() {
|
|
14
|
+
/** @type {Map<string, Array<Function>>} */
|
|
15
|
+
this._handlers = new Map();
|
|
16
|
+
/** @type {Array<Function>} */
|
|
17
|
+
this._wildcards = [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Registers a handler for a specific DomainEvent class.
|
|
22
|
+
* @param {typeof DomainEvent} EventClass - The event class (not an instance).
|
|
23
|
+
* @param {(event: DomainEvent) => void | Promise<void>} handler
|
|
24
|
+
* @returns {() => void} An unsubscribe function.
|
|
25
|
+
*/
|
|
26
|
+
on(EventClass, handler) {
|
|
27
|
+
if (typeof EventClass !== 'function') {
|
|
28
|
+
throw new TypeError('EventDispatcher.on() expects an event class as first argument');
|
|
29
|
+
}
|
|
30
|
+
if (typeof handler !== 'function') {
|
|
31
|
+
throw new TypeError('EventDispatcher.on() expects a function as second argument');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const eventName = EventClass.name;
|
|
35
|
+
|
|
36
|
+
if (!this._handlers.has(eventName)) {
|
|
37
|
+
this._handlers.set(eventName, []);
|
|
38
|
+
}
|
|
39
|
+
this._handlers.get(eventName).push(handler);
|
|
40
|
+
|
|
41
|
+
return () => this.off(EventClass, handler);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Unregisters a handler for a specific event class.
|
|
46
|
+
* @param {typeof DomainEvent} EventClass
|
|
47
|
+
* @param {Function} handler
|
|
48
|
+
*/
|
|
49
|
+
off(EventClass, handler) {
|
|
50
|
+
const eventName = EventClass.name;
|
|
51
|
+
const handlers = this._handlers.get(eventName);
|
|
52
|
+
if (handlers) {
|
|
53
|
+
const idx = handlers.indexOf(handler);
|
|
54
|
+
if (idx > -1) handlers.splice(idx, 1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Registers a wildcard handler that receives ALL dispatched events.
|
|
60
|
+
* @param {(event: DomainEvent) => void | Promise<void>} handler
|
|
61
|
+
* @returns {() => void} An unsubscribe function.
|
|
62
|
+
*/
|
|
63
|
+
onAny(handler) {
|
|
64
|
+
if (typeof handler !== 'function') {
|
|
65
|
+
throw new TypeError('EventDispatcher.onAny() expects a function');
|
|
66
|
+
}
|
|
67
|
+
this._wildcards.push(handler);
|
|
68
|
+
return () => {
|
|
69
|
+
const idx = this._wildcards.indexOf(handler);
|
|
70
|
+
if (idx > -1) this._wildcards.splice(idx, 1);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Dispatches a domain event to all registered handlers (in parallel).
|
|
76
|
+
* @param {DomainEvent} event - An instance of a DomainEvent subclass.
|
|
77
|
+
* @returns {Promise<void>}
|
|
78
|
+
*/
|
|
79
|
+
async dispatch(event) {
|
|
80
|
+
if (!(event instanceof DomainEvent)) {
|
|
81
|
+
throw new TypeError('EventDispatcher.dispatch() expects a DomainEvent instance');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const eventHandlers = this._handlers.get(event.eventName) ?? [];
|
|
85
|
+
const allHandlers = [...eventHandlers, ...this._wildcards];
|
|
86
|
+
|
|
87
|
+
await Promise.all(allHandlers.map(handler => handler(event)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns the number of handlers registered for a given event class.
|
|
92
|
+
* @param {typeof DomainEvent} EventClass
|
|
93
|
+
* @returns {number}
|
|
94
|
+
*/
|
|
95
|
+
listenerCount(EventClass) {
|
|
96
|
+
return (this._handlers.get(EventClass.name) ?? []).length;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { EventDispatcher };
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { DomainEvent } from '../src/events/DomainEvent.js';
|
|
3
|
+
import { EventDispatcher } from '../src/events/EventDispatcher.js';
|
|
4
|
+
|
|
5
|
+
class UserCreated extends DomainEvent {
|
|
6
|
+
constructor(userId) {
|
|
7
|
+
super();
|
|
8
|
+
this.userId = userId;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class OrderPlaced extends DomainEvent {
|
|
13
|
+
constructor(orderId) {
|
|
14
|
+
super();
|
|
15
|
+
this.orderId = orderId;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('DomainEvent', () => {
|
|
20
|
+
it('should set eventName to the class name', () => {
|
|
21
|
+
const event = new UserCreated(1);
|
|
22
|
+
expect(event.eventName).toBe('UserCreated');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should set occurredAt to current date', () => {
|
|
26
|
+
const before = new Date();
|
|
27
|
+
const event = new UserCreated(1);
|
|
28
|
+
const after = new Date();
|
|
29
|
+
expect(event.occurredAt >= before).toBe(true);
|
|
30
|
+
expect(event.occurredAt <= after).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should generate a unique eventId', () => {
|
|
34
|
+
const A = new UserCreated(1);
|
|
35
|
+
const B = new UserCreated(1);
|
|
36
|
+
expect(A.eventId).not.toBe(B.eventId);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('EventDispatcher', () => {
|
|
41
|
+
it('should call a registered handler when event is dispatched', async () => {
|
|
42
|
+
const dispatcher = new EventDispatcher();
|
|
43
|
+
const handler = vi.fn();
|
|
44
|
+
dispatcher.on(UserCreated, handler);
|
|
45
|
+
|
|
46
|
+
const event = new UserCreated(42);
|
|
47
|
+
await dispatcher.dispatch(event);
|
|
48
|
+
|
|
49
|
+
expect(handler).toHaveBeenCalledWith(event);
|
|
50
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should support multiple handlers for the same event', async () => {
|
|
54
|
+
const dispatcher = new EventDispatcher();
|
|
55
|
+
const h1 = vi.fn();
|
|
56
|
+
const h2 = vi.fn();
|
|
57
|
+
dispatcher.on(UserCreated, h1);
|
|
58
|
+
dispatcher.on(UserCreated, h2);
|
|
59
|
+
|
|
60
|
+
await dispatcher.dispatch(new UserCreated(1));
|
|
61
|
+
|
|
62
|
+
expect(h1).toHaveBeenCalled();
|
|
63
|
+
expect(h2).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should not call handlers for a different event type', async () => {
|
|
67
|
+
const dispatcher = new EventDispatcher();
|
|
68
|
+
const handler = vi.fn();
|
|
69
|
+
dispatcher.on(OrderPlaced, handler);
|
|
70
|
+
|
|
71
|
+
await dispatcher.dispatch(new UserCreated(1));
|
|
72
|
+
|
|
73
|
+
expect(handler).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should call wildcard handlers for all events', async () => {
|
|
77
|
+
const dispatcher = new EventDispatcher();
|
|
78
|
+
const wildcard = vi.fn();
|
|
79
|
+
dispatcher.onAny(wildcard);
|
|
80
|
+
|
|
81
|
+
await dispatcher.dispatch(new UserCreated(1));
|
|
82
|
+
await dispatcher.dispatch(new OrderPlaced(2));
|
|
83
|
+
|
|
84
|
+
expect(wildcard).toHaveBeenCalledTimes(2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should unsubscribe correctly via returned function', async () => {
|
|
88
|
+
const dispatcher = new EventDispatcher();
|
|
89
|
+
const handler = vi.fn();
|
|
90
|
+
const unsubscribe = dispatcher.on(UserCreated, handler);
|
|
91
|
+
|
|
92
|
+
unsubscribe();
|
|
93
|
+
await dispatcher.dispatch(new UserCreated(1));
|
|
94
|
+
|
|
95
|
+
expect(handler).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should report correct listenerCount', () => {
|
|
99
|
+
const dispatcher = new EventDispatcher();
|
|
100
|
+
dispatcher.on(UserCreated, () => { });
|
|
101
|
+
dispatcher.on(UserCreated, () => { });
|
|
102
|
+
expect(dispatcher.listenerCount(UserCreated)).toBe(2);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should throw if dispatch receives a non-DomainEvent', async () => {
|
|
106
|
+
const dispatcher = new EventDispatcher();
|
|
107
|
+
await expect(dispatcher.dispatch({ foo: 'bar' })).rejects.toThrow(TypeError);
|
|
108
|
+
});
|
|
109
|
+
});
|