@bernierllc/event-emitter 1.0.1
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/.eslintrc.js +29 -0
- package/README.md +313 -0
- package/__tests__/async-handlers.test.ts +260 -0
- package/__tests__/error-handling.test.ts +310 -0
- package/__tests__/event-emitter.test.ts +281 -0
- package/__tests__/logger-integration.test.ts +132 -0
- package/__tests__/wildcard.test.ts +151 -0
- package/coverage/clover.xml +77 -0
- package/coverage/coverage-final.json +3 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/event-emitter.ts.html +655 -0
- package/coverage/lcov-report/factory.ts.html +136 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +149 -0
- package/dist/event-emitter.d.ts +39 -0
- package/dist/event-emitter.js +176 -0
- package/dist/factory.d.ts +6 -0
- package/dist/factory.js +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +14 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +9 -0
- package/jest.config.cjs +29 -0
- package/package.json +44 -0
- package/src/event-emitter.ts +190 -0
- package/src/factory.ts +17 -0
- package/src/index.ts +11 -0
- package/src/types.ts +46 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { EventHandler, EventSubscription, EventEmitterOptions } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Event bus for inter-package communication
|
|
13
|
+
*/
|
|
14
|
+
export class EventEmitter {
|
|
15
|
+
private listeners: Map<string, Set<EventSubscription>>;
|
|
16
|
+
private wildcardListeners: Set<EventSubscription>;
|
|
17
|
+
private options: Required<EventEmitterOptions>;
|
|
18
|
+
private logger?: any;
|
|
19
|
+
|
|
20
|
+
constructor(options: EventEmitterOptions = {}) {
|
|
21
|
+
this.listeners = new Map();
|
|
22
|
+
this.wildcardListeners = new Set();
|
|
23
|
+
this.options = {
|
|
24
|
+
maxListeners: options.maxListeners || 0,
|
|
25
|
+
logEvents: options.logEvents || false,
|
|
26
|
+
captureErrors: options.captureErrors !== false // Default to true
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Try to load logger if available
|
|
30
|
+
try {
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
32
|
+
const { createLogger } = require('@bernierllc/logger');
|
|
33
|
+
this.logger = createLogger({ level: 'debug' });
|
|
34
|
+
} catch {
|
|
35
|
+
// Logger not available, continue without it
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Subscribe to event
|
|
41
|
+
*/
|
|
42
|
+
on<T = any>(eventType: string, handler: EventHandler<T>): EventSubscription {
|
|
43
|
+
// Check max listeners
|
|
44
|
+
if (this.options.maxListeners > 0) {
|
|
45
|
+
const currentCount = this.listenerCount(eventType);
|
|
46
|
+
if (currentCount >= this.options.maxListeners) {
|
|
47
|
+
throw new Error(`Max listeners (${this.options.maxListeners}) exceeded for event type: ${eventType}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const subscription: EventSubscription = {
|
|
52
|
+
id: `${eventType}-${Date.now()}-${Math.random()}`,
|
|
53
|
+
eventType,
|
|
54
|
+
handler,
|
|
55
|
+
once: false,
|
|
56
|
+
unsubscribe: () => this.off(subscription)
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (eventType === '*') {
|
|
60
|
+
this.wildcardListeners.add(subscription);
|
|
61
|
+
} else {
|
|
62
|
+
if (!this.listeners.has(eventType)) {
|
|
63
|
+
this.listeners.set(eventType, new Set());
|
|
64
|
+
}
|
|
65
|
+
this.listeners.get(eventType)!.add(subscription);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (this.options.logEvents && this.logger) {
|
|
69
|
+
this.logger.debug('Event listener added', { eventType, subscriptionId: subscription.id });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return subscription;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Subscribe to event once
|
|
77
|
+
*/
|
|
78
|
+
once<T = any>(eventType: string, handler: EventHandler<T>): EventSubscription {
|
|
79
|
+
const subscription = this.on(eventType, handler);
|
|
80
|
+
subscription.once = true;
|
|
81
|
+
return subscription;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Unsubscribe from event
|
|
86
|
+
*/
|
|
87
|
+
off(subscription: EventSubscription): void {
|
|
88
|
+
if (subscription.eventType === '*') {
|
|
89
|
+
this.wildcardListeners.delete(subscription);
|
|
90
|
+
} else {
|
|
91
|
+
const listeners = this.listeners.get(subscription.eventType);
|
|
92
|
+
if (listeners) {
|
|
93
|
+
listeners.delete(subscription);
|
|
94
|
+
// Clean up empty sets
|
|
95
|
+
if (listeners.size === 0) {
|
|
96
|
+
this.listeners.delete(subscription.eventType);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (this.options.logEvents && this.logger) {
|
|
102
|
+
this.logger.debug('Event listener removed', {
|
|
103
|
+
eventType: subscription.eventType,
|
|
104
|
+
subscriptionId: subscription.id
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Publish event
|
|
111
|
+
*/
|
|
112
|
+
async emit<T = any>(eventType: string, data: T): Promise<void> {
|
|
113
|
+
if (this.options.logEvents && this.logger) {
|
|
114
|
+
this.logger.debug('Event emitted', { eventType, data });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Get specific listeners
|
|
118
|
+
const specificListeners = this.listeners.get(eventType) || new Set();
|
|
119
|
+
|
|
120
|
+
// Combine with wildcard listeners
|
|
121
|
+
const allListeners = [...specificListeners, ...this.wildcardListeners];
|
|
122
|
+
|
|
123
|
+
// Execute handlers
|
|
124
|
+
const promises = allListeners.map(async (subscription) => {
|
|
125
|
+
try {
|
|
126
|
+
await subscription.handler(data);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (this.options.captureErrors) {
|
|
129
|
+
if (this.logger) {
|
|
130
|
+
this.logger.error('Event handler error', {
|
|
131
|
+
eventType,
|
|
132
|
+
subscriptionId: subscription.id,
|
|
133
|
+
error: error instanceof Error ? error.message : String(error)
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
} finally {
|
|
140
|
+
// Remove if once subscription (always happens, even if error occurred)
|
|
141
|
+
if (subscription.once) {
|
|
142
|
+
this.off(subscription);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await Promise.all(promises);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Remove all listeners for event type
|
|
152
|
+
*/
|
|
153
|
+
removeAllListeners(eventType?: string): void {
|
|
154
|
+
if (eventType) {
|
|
155
|
+
if (eventType === '*') {
|
|
156
|
+
this.wildcardListeners.clear();
|
|
157
|
+
} else {
|
|
158
|
+
this.listeners.delete(eventType);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
this.listeners.clear();
|
|
162
|
+
this.wildcardListeners.clear();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (this.options.logEvents && this.logger) {
|
|
166
|
+
this.logger.debug('Listeners removed', { eventType: eventType || 'all' });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get listener count for event type
|
|
172
|
+
*/
|
|
173
|
+
listenerCount(eventType: string): number {
|
|
174
|
+
if (eventType === '*') {
|
|
175
|
+
return this.wildcardListeners.size;
|
|
176
|
+
}
|
|
177
|
+
return this.listeners.get(eventType)?.size || 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get all event types with listeners
|
|
182
|
+
*/
|
|
183
|
+
eventNames(): string[] {
|
|
184
|
+
const names = Array.from(this.listeners.keys());
|
|
185
|
+
if (this.wildcardListeners.size > 0) {
|
|
186
|
+
names.push('*');
|
|
187
|
+
}
|
|
188
|
+
return names;
|
|
189
|
+
}
|
|
190
|
+
}
|
package/src/factory.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { EventEmitter } from './event-emitter';
|
|
10
|
+
import { EventEmitterOptions } from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Factory function for creating event emitters
|
|
14
|
+
*/
|
|
15
|
+
export function createEventEmitter(options?: EventEmitterOptions): EventEmitter {
|
|
16
|
+
return new EventEmitter(options);
|
|
17
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { EventEmitter } from './event-emitter';
|
|
10
|
+
export { createEventEmitter } from './factory';
|
|
11
|
+
export type { EventHandler, EventSubscription, EventEmitterOptions } from './types';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Event handler function
|
|
11
|
+
*/
|
|
12
|
+
export type EventHandler<T = any> = (data: T) => void | Promise<void>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Event subscription
|
|
16
|
+
*/
|
|
17
|
+
export interface EventSubscription {
|
|
18
|
+
/** Unique subscription ID */
|
|
19
|
+
id: string;
|
|
20
|
+
|
|
21
|
+
/** Event type */
|
|
22
|
+
eventType: string;
|
|
23
|
+
|
|
24
|
+
/** Handler function */
|
|
25
|
+
handler: EventHandler;
|
|
26
|
+
|
|
27
|
+
/** Whether this is a one-time subscription */
|
|
28
|
+
once: boolean;
|
|
29
|
+
|
|
30
|
+
/** Unsubscribe function */
|
|
31
|
+
unsubscribe: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Event emitter options
|
|
36
|
+
*/
|
|
37
|
+
export interface EventEmitterOptions {
|
|
38
|
+
/** Maximum listeners per event (0 = unlimited) */
|
|
39
|
+
maxListeners?: number;
|
|
40
|
+
|
|
41
|
+
/** Whether to log events */
|
|
42
|
+
logEvents?: boolean;
|
|
43
|
+
|
|
44
|
+
/** Whether to capture errors */
|
|
45
|
+
captureErrors?: boolean;
|
|
46
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"moduleResolution": "node",
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"noImplicitReturns": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": true
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/**/*"],
|
|
21
|
+
"exclude": ["node_modules", "dist", "__tests__"]
|
|
22
|
+
}
|