@hg-ts/events 0.5.17 → 0.5.18

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hg-ts/events",
3
- "version": "0.5.17",
3
+ "version": "0.5.18",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -18,10 +18,10 @@
18
18
  "test:dev": "vitest watch"
19
19
  },
20
20
  "devDependencies": {
21
- "@hg-ts-config/typescript": "0.5.17",
22
- "@hg-ts/linter": "0.5.17",
23
- "@hg-ts/tests": "0.5.17",
24
- "@hg-ts/types": "0.5.17",
21
+ "@hg-ts-config/typescript": "0.5.18",
22
+ "@hg-ts/linter": "0.5.18",
23
+ "@hg-ts/tests": "0.5.18",
24
+ "@hg-ts/types": "0.5.18",
25
25
  "@types/node": "22.19.1",
26
26
  "@vitest/coverage-v8": "4.0.14",
27
27
  "eslint": "9.18.0",
@@ -0,0 +1,29 @@
1
+ export type BaseEventDto = {
2
+ name: string;
3
+ body: unknown;
4
+ occurredOn: Date;
5
+ traceId: Nullable<string>;
6
+ }
7
+
8
+ export type EventParams<Body> = {
9
+ body: Body;
10
+ occurredOn?: Date;
11
+ name?: string;
12
+ traceId?: Nullable<string>;
13
+ }
14
+
15
+ export abstract class BaseEvent<Body = unknown> {
16
+ public readonly occurredOn: Date;
17
+ public readonly name: string;
18
+ public readonly body: Body;
19
+
20
+ public readonly traceId: Nullable<string>;
21
+
22
+ public constructor({ name, body, occurredOn = new Date(), traceId }: EventParams<Body>) {
23
+ this.body = body;
24
+ this.occurredOn = occurredOn;
25
+ this.name = name ?? this.constructor.name;
26
+
27
+ this.traceId = traceId ?? null;
28
+ }
29
+ }
@@ -0,0 +1 @@
1
+ export * from './listen.decorator.js';
@@ -0,0 +1,38 @@
1
+ import type { BaseEvent } from '../base.event.js';
2
+
3
+ const LISTEN_META_KEY = Symbol('LISTEN_META_KEY');
4
+ const LISTEN_METHODS_META_KEY = Symbol('LISTEN_METHODS_META_KEY');
5
+
6
+ export type EventHandler<T extends BaseEvent = BaseEvent> = (event: T) => any;
7
+
8
+ export const Listen = <T extends BaseEvent>(eventClass: Class<T, any[]>) => (
9
+ proto: Object,
10
+ propertyKey: string | symbol,
11
+ _descriptor: TypedPropertyDescriptor<EventHandler<T>>,
12
+ ): void => {
13
+ const eventsToHandle = getEventsToHandle(proto, propertyKey);
14
+ const listenMethods = getListenMethods(proto);
15
+
16
+ eventsToHandle.push(eventClass);
17
+ if (!listenMethods.includes(propertyKey)) {
18
+ listenMethods.push(propertyKey);
19
+ }
20
+
21
+ Reflect.defineMetadata(LISTEN_META_KEY, eventsToHandle, proto, propertyKey);
22
+ Reflect.defineMetadata(LISTEN_METHODS_META_KEY, listenMethods, proto);
23
+ };
24
+
25
+ export function getEventsToHandle(
26
+ proto: Object,
27
+ propertyKey: string | symbol,
28
+ ): Class<BaseEvent>[] {
29
+ return Reflect.getMetadata(LISTEN_META_KEY, proto, propertyKey) ?? [];
30
+ }
31
+
32
+ export function getListenMethods(proto: Object): (string | symbol)[] {
33
+ return Reflect.getMetadata(LISTEN_METHODS_META_KEY, proto) ?? [];
34
+ }
35
+
36
+ export function isEventListener(proto: Object): boolean {
37
+ return getListenMethods(proto).length > 0;
38
+ }
@@ -0,0 +1,7 @@
1
+ import { BaseEvent } from './base.event.js';
2
+
3
+ export class EmptyEvent extends BaseEvent {
4
+ public constructor() {
5
+ super({ body: null });
6
+ }
7
+ }
@@ -0,0 +1,5 @@
1
+ import type { BaseEvent } from '../base.event.js';
2
+
3
+ export abstract class BaseEventTransport {
4
+ public abstract publish(event: BaseEvent | BaseEvent[]): Promise<void>;
5
+ }
@@ -0,0 +1,15 @@
1
+ import type { BaseEvent } from '../base.event.js';
2
+ import type { BaseEventTransport } from './base.event-transport.js';
3
+ import type { EventPublisher } from './event-publisher.js';
4
+
5
+ export class EventEmitter {
6
+ private readonly transports: BaseEventTransport[];
7
+
8
+ public constructor(eventPublisher: EventPublisher) {
9
+ this.transports = [eventPublisher];
10
+ }
11
+
12
+ public async emit(event: BaseEvent | BaseEvent[]): Promise<void> {
13
+ await Promise.all(this.transports.map(async transport => transport.publish(event)));
14
+ }
15
+ }
@@ -0,0 +1,37 @@
1
+ import 'reflect-metadata';
2
+
3
+ import {
4
+ EventHandler,
5
+ getEventsToHandle,
6
+ getListenMethods,
7
+ isEventListener,
8
+ } from '../decorators/index.js';
9
+
10
+ import type { EventPublisher } from './event-publisher.js';
11
+
12
+ export class EventListenerManager {
13
+ private readonly eventPublisher: EventPublisher;
14
+
15
+ public constructor(eventPublisher: EventPublisher) {
16
+ this.eventPublisher = eventPublisher;
17
+ }
18
+
19
+ public addListener(instance: unknown): void {
20
+ const proto = Object.getPrototypeOf(instance);
21
+ if (!proto || !isEventListener(proto)) {
22
+ return;
23
+ }
24
+
25
+ const listenMethods = getListenMethods(proto);
26
+ const listener: Record<string | symbol, any> = instance as any;
27
+
28
+ listenMethods.forEach(methodName => {
29
+ const method = listener[methodName].bind(listener) as EventHandler;
30
+ const eventsToHandle = getEventsToHandle(proto, methodName);
31
+
32
+ eventsToHandle.forEach(eventCtor => {
33
+ this.eventPublisher['addListener'](eventCtor, method);
34
+ });
35
+ });
36
+ }
37
+ }
@@ -0,0 +1,40 @@
1
+ import assert from 'node:assert/strict';
2
+
3
+ import { BaseEvent } from '../base.event.js';
4
+ import type { EventHandler } from '../decorators/index.js';
5
+ import type { BaseEventTransport } from './base.event-transport.js';
6
+
7
+ export class EventPublisher implements BaseEventTransport {
8
+ private readonly listeners: Map<Class<BaseEvent>, EventHandler[]> = new Map();
9
+
10
+ public async publish(event: BaseEvent | BaseEvent[]): Promise<void> {
11
+ const events = Array.isArray(event) ? event : [event];
12
+
13
+ await Promise.all(events.map(async event => this.publishOne(event)));
14
+ }
15
+
16
+ protected async publishOne(event: BaseEvent): Promise<void> {
17
+ assert.ok(event instanceof BaseEvent);
18
+ const listeners = this.findListeners(event);
19
+
20
+ await Promise.all(listeners.map(async listener => listener(event)));
21
+ }
22
+
23
+ protected addListener<Event extends BaseEvent>(eventCtor: Class<Event>, listener: EventHandler<Event>): void {
24
+ const listeners: EventHandler<Event>[] = this.listeners.get(eventCtor) ?? [];
25
+
26
+ listeners.push(listener);
27
+
28
+ this.listeners.set(eventCtor, listeners as EventHandler[]);
29
+ }
30
+
31
+ private findListeners(event: BaseEvent): EventHandler[] {
32
+ const eventCtor = Object.getPrototypeOf(event)!.constructor;
33
+ const listenEvents = [...this.listeners.keys()];
34
+
35
+ return listenEvents
36
+ .filter(ctor => eventCtor.prototype instanceof ctor || eventCtor === ctor)
37
+ .sort((a, b) => (a.prototype instanceof b ? 1 : -1))
38
+ .flatMap(ctor => this.listeners.get(ctor) ?? []);
39
+ }
40
+ }
@@ -0,0 +1,4 @@
1
+ export * from './event-emitter.js';
2
+ export * from './event-publisher.js';
3
+ export * from './event-listener-manager.js';
4
+ export * from './base.event-transport.js';
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './base.event.js';
2
+ export * from './empty.event.js';
3
+
4
+ export * from './decorators/index.js';
5
+ export * from './event-handling/index.js';
@@ -0,0 +1,76 @@
1
+ // eslint-disable-next-line max-classes-per-file
2
+ import {
3
+ Describe,
4
+ expect,
5
+ Suite,
6
+ Test,
7
+ } from '@hg-ts/tests';
8
+
9
+ import { BaseEvent } from '../base.event.js';
10
+ import { Listen } from '../decorators/index.js';
11
+ import { EmptyEvent } from '../empty.event.js';
12
+
13
+ import {
14
+ EventListenerManager,
15
+ EventPublisher,
16
+ } from '../event-handling/index.js';
17
+
18
+ @Describe()
19
+ export class EventPublisherTestSuite extends Suite {
20
+ private publisher: EventPublisher;
21
+ private eventListenerManager: EventListenerManager;
22
+
23
+ @Test()
24
+ public async addListener(): Promise<void> {
25
+ class TestEvent extends BaseEvent<{}> {
26
+ public constructor() {
27
+ super({ body: {} });
28
+ }
29
+ }
30
+ class ExtendedEvent extends TestEvent {}
31
+ class Listener {
32
+ public called = new Map<Class<BaseEvent>, number>();
33
+
34
+ @Listen(EmptyEvent)
35
+ public async empty(event: EmptyEvent): Promise<void> {
36
+ this.check(event, EmptyEvent);
37
+ }
38
+
39
+ @Listen(TestEvent)
40
+ public async test(event: TestEvent): Promise<void> {
41
+ this.check(event, TestEvent);
42
+ }
43
+
44
+ @Listen(ExtendedEvent)
45
+ public async extended(event: ExtendedEvent): Promise<void> {
46
+ this.check(event, ExtendedEvent);
47
+ }
48
+
49
+ private check(event: BaseEvent, ctor: Class<BaseEvent, any[]>): void {
50
+ let count = this.called.get(ctor) ?? 0;
51
+
52
+ expect(event).toBeInstanceOf(ctor);
53
+ count++;
54
+
55
+ this.called.set(ctor, count);
56
+ }
57
+ }
58
+ const listener = new Listener();
59
+
60
+ this.eventListenerManager.addListener(listener);
61
+
62
+ await this.publisher.publish(new EmptyEvent());
63
+ await this.publisher.publish(new TestEvent());
64
+ await this.publisher.publish(new ExtendedEvent());
65
+
66
+ expect(listener.called.get(EmptyEvent)).toBe(1);
67
+ expect(listener.called.get(TestEvent)).toBe(2);
68
+ expect(listener.called.get(ExtendedEvent)).toBe(1);
69
+
70
+ expect.assertions(7);
71
+ }
72
+ public override async beforeEach(): Promise<void> {
73
+ this.publisher = new EventPublisher();
74
+ this.eventListenerManager = new EventListenerManager(this.publisher);
75
+ }
76
+ }
@@ -0,0 +1,132 @@
1
+ // eslint-disable-next-line max-classes-per-file
2
+ import {
3
+ Describe,
4
+ expect,
5
+ Suite,
6
+ Test,
7
+ } from '@hg-ts/tests';
8
+
9
+ import { BaseEvent } from '../base.event.js';
10
+ import { EmptyEvent } from '../empty.event.js';
11
+
12
+ import { EventPublisher } from '../event-handling/index.js';
13
+
14
+ @Describe()
15
+ export class EventPublisherTest extends Suite {
16
+ private publisher: EventPublisher;
17
+
18
+ @Test()
19
+ public async publish(): Promise<void> {
20
+ const listener = async(event: BaseEvent): Promise<void> => {
21
+ expect(event).toBeInstanceOf(EmptyEvent);
22
+ };
23
+
24
+ this.publisher['addListener'](EmptyEvent, listener);
25
+
26
+ await this.publisher.publish(new EmptyEvent());
27
+
28
+ expect.assertions(1);
29
+ }
30
+
31
+ @Test()
32
+ public async multipleListeners(): Promise<void> {
33
+ let callIndex = 1;
34
+ const listener1 = async(event: BaseEvent): Promise<void> => {
35
+ expect(event).toBeInstanceOf(EmptyEvent);
36
+ expect(callIndex).toBe(1);
37
+ callIndex++;
38
+ };
39
+ const listener2 = async(event: BaseEvent): Promise<void> => {
40
+ expect(event).toBeInstanceOf(EmptyEvent);
41
+ expect(callIndex).toBe(2);
42
+ callIndex++;
43
+ };
44
+
45
+ this.publisher['addListener'](EmptyEvent, listener1);
46
+ this.publisher['addListener'](EmptyEvent, listener2);
47
+
48
+ await this.publisher.publish(new EmptyEvent());
49
+
50
+ expect.assertions(4);
51
+ }
52
+
53
+ @Test()
54
+ public async multipleEvents(): Promise<void> {
55
+ class TestEvent extends BaseEvent<{}> {
56
+ public constructor() {
57
+ super({ body: {} });
58
+ }
59
+ }
60
+ const listener1 = async(event: BaseEvent): Promise<void> => {
61
+ expect(event).toBeInstanceOf(EmptyEvent);
62
+ };
63
+ const listener2 = async(event: BaseEvent): Promise<void> => {
64
+ expect(event).toBeInstanceOf(TestEvent);
65
+ };
66
+
67
+ this.publisher['addListener'](EmptyEvent, listener1);
68
+ this.publisher['addListener'](TestEvent, listener2);
69
+
70
+ await this.publisher.publish(new TestEvent());
71
+
72
+ expect.assertions(1);
73
+ }
74
+
75
+ @Test()
76
+ public async extendingEventsInherited(): Promise<void> {
77
+ class TestEvent extends BaseEvent<{}> {
78
+ public constructor() {
79
+ super({ body: {} });
80
+ }
81
+ }
82
+ class ExtendedEvent extends TestEvent {}
83
+ const listener1 = async(event: BaseEvent): Promise<void> => {
84
+ expect(event).toBeInstanceOf(EmptyEvent);
85
+ };
86
+ const listener2 = async(event: BaseEvent): Promise<void> => {
87
+ expect(event).toBeInstanceOf(TestEvent);
88
+ };
89
+ const listener3 = async(event: BaseEvent): Promise<void> => {
90
+ expect(event).toBeInstanceOf(ExtendedEvent);
91
+ };
92
+
93
+ this.publisher['addListener'](EmptyEvent, listener1);
94
+ this.publisher['addListener'](TestEvent, listener2);
95
+ this.publisher['addListener'](ExtendedEvent, listener3);
96
+
97
+ await this.publisher.publish(new ExtendedEvent());
98
+
99
+ expect.assertions(2);
100
+ }
101
+
102
+ @Test()
103
+ public async extendingEventsParent(): Promise<void> {
104
+ class TestEvent extends BaseEvent<{}> {
105
+ public constructor() {
106
+ super({ body: {} });
107
+ }
108
+ }
109
+ class ExtendedEvent extends TestEvent {}
110
+ const listener1 = async(event: BaseEvent): Promise<void> => {
111
+ expect(event).toBeInstanceOf(EmptyEvent);
112
+ };
113
+ const listener2 = async(event: BaseEvent): Promise<void> => {
114
+ expect(event).toBeInstanceOf(TestEvent);
115
+ };
116
+ const listener3 = async(event: BaseEvent): Promise<void> => {
117
+ expect(event).toBeInstanceOf(ExtendedEvent);
118
+ };
119
+
120
+ this.publisher['addListener'](EmptyEvent, listener1);
121
+ this.publisher['addListener'](TestEvent, listener2);
122
+ this.publisher['addListener'](ExtendedEvent, listener3);
123
+
124
+ await this.publisher.publish(new TestEvent());
125
+
126
+ expect.assertions(1);
127
+ }
128
+
129
+ public override async beforeEach(): Promise<void> {
130
+ this.publisher = new EventPublisher();
131
+ }
132
+ }