@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 ADDED
@@ -0,0 +1,29 @@
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
+ module.exports = {
10
+ parser: '@typescript-eslint/parser',
11
+ parserOptions: {
12
+ ecmaVersion: 2020,
13
+ sourceType: 'module'
14
+ },
15
+ extends: [
16
+ 'eslint:recommended',
17
+ 'plugin:@typescript-eslint/recommended'
18
+ ],
19
+ plugins: ['@typescript-eslint'],
20
+ env: {
21
+ node: true,
22
+ jest: true
23
+ },
24
+ rules: {
25
+ '@typescript-eslint/explicit-function-return-type': 'warn',
26
+ '@typescript-eslint/no-explicit-any': 'off',
27
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }]
28
+ }
29
+ };
package/README.md ADDED
@@ -0,0 +1,313 @@
1
+ # @bernierllc/event-emitter
2
+
3
+ Type-safe event bus for inter-package communication with support for wildcard listeners, async handlers, and comprehensive error handling.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @bernierllc/event-emitter
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - 🎯 **Type-safe event handling** - Full TypeScript support with generics
14
+ - 🌟 **Wildcard listeners** - Subscribe to all events with `*`
15
+ - ⚡ **Async/await support** - Handles both sync and async event handlers
16
+ - 🛡️ **Error handling** - Configurable error capture and propagation
17
+ - 🔄 **One-time subscriptions** - `once()` support for single-use listeners
18
+ - 📊 **Observable** - Track listener counts and event names
19
+ - 🚀 **High performance** - Parallel execution of async handlers
20
+ - 🔌 **Zero dependencies** - Lightweight core with optional logger integration
21
+
22
+ ## Usage
23
+
24
+ ### Basic Event Subscription and Publishing
25
+
26
+ ```typescript
27
+ import { createEventEmitter } from '@bernierllc/event-emitter';
28
+
29
+ // Create event emitter
30
+ const events = createEventEmitter();
31
+
32
+ // Subscribe to events
33
+ const subscription = events.on('user.created', (data) => {
34
+ console.log('User created:', data);
35
+ });
36
+
37
+ // Publish events
38
+ await events.emit('user.created', {
39
+ userId: '123',
40
+ email: 'user@example.com'
41
+ });
42
+
43
+ // Unsubscribe
44
+ subscription.unsubscribe();
45
+ ```
46
+
47
+ ### Type-Safe Events
48
+
49
+ ```typescript
50
+ interface UserCreatedEvent {
51
+ userId: string;
52
+ email: string;
53
+ name: string;
54
+ }
55
+
56
+ // Type-safe subscription
57
+ events.on<UserCreatedEvent>('user.created', (data) => {
58
+ // data is typed as UserCreatedEvent
59
+ console.log(data.userId, data.email, data.name);
60
+ });
61
+
62
+ // Type-safe emission
63
+ await events.emit<UserCreatedEvent>('user.created', {
64
+ userId: '123',
65
+ email: 'user@example.com',
66
+ name: 'John Doe'
67
+ });
68
+ ```
69
+
70
+ ### One-Time Subscriptions
71
+
72
+ ```typescript
73
+ // Subscribe once (automatically unsubscribes after first event)
74
+ events.once('app.ready', () => {
75
+ console.log('App is ready!');
76
+ });
77
+
78
+ await events.emit('app.ready', {});
79
+ await events.emit('app.ready', {}); // Handler won't be called
80
+ ```
81
+
82
+ ### Wildcard Listeners
83
+
84
+ ```typescript
85
+ // Listen to all events
86
+ events.on('*', (data) => {
87
+ console.log('Event received:', data);
88
+ });
89
+
90
+ // Both specific and wildcard listeners will be called
91
+ await events.emit('user.created', { userId: '123' });
92
+ await events.emit('order.placed', { orderId: '456' });
93
+ ```
94
+
95
+ ### Async Event Handlers
96
+
97
+ ```typescript
98
+ // Async handler
99
+ events.on('user.created', async (data) => {
100
+ await sendWelcomeEmail(data.email);
101
+ await createUserProfile(data.userId);
102
+ });
103
+
104
+ // emit() waits for all handlers to complete
105
+ await events.emit('user.created', {
106
+ userId: '123',
107
+ email: 'user@example.com'
108
+ });
109
+ ```
110
+
111
+ ### Error Handling
112
+
113
+ ```typescript
114
+ // With error capture (default)
115
+ const safeEmitter = createEventEmitter({ captureErrors: true });
116
+
117
+ safeEmitter.on('process', () => {
118
+ throw new Error('Processing failed');
119
+ });
120
+
121
+ // Errors are captured and logged, other handlers continue
122
+ await safeEmitter.emit('process', {});
123
+
124
+ // Without error capture
125
+ const strictEmitter = createEventEmitter({ captureErrors: false });
126
+
127
+ strictEmitter.on('process', () => {
128
+ throw new Error('Processing failed');
129
+ });
130
+
131
+ // Errors are thrown
132
+ await strictEmitter.emit('process', {}); // Throws error
133
+ ```
134
+
135
+ ### Max Listeners
136
+
137
+ ```typescript
138
+ // Limit listeners per event
139
+ const limitedEmitter = createEventEmitter({ maxListeners: 10 });
140
+
141
+ // Add 10 listeners
142
+ for (let i = 0; i < 10; i++) {
143
+ limitedEmitter.on('test', () => console.log(i));
144
+ }
145
+
146
+ // This will throw
147
+ limitedEmitter.on('test', () => console.log('too many')); // Error!
148
+ ```
149
+
150
+ ### Managing Listeners
151
+
152
+ ```typescript
153
+ // Get listener count
154
+ const count = events.listenerCount('user.created');
155
+ console.log(`${count} listeners for user.created`);
156
+
157
+ // Get all event names
158
+ const eventNames = events.eventNames();
159
+ console.log('Active events:', eventNames);
160
+
161
+ // Remove all listeners for specific event
162
+ events.removeAllListeners('user.created');
163
+
164
+ // Remove all listeners for all events
165
+ events.removeAllListeners();
166
+ ```
167
+
168
+ ## Configuration
169
+
170
+ ```typescript
171
+ const events = createEventEmitter({
172
+ // Maximum listeners per event (0 = unlimited)
173
+ maxListeners: 100,
174
+
175
+ // Log events (requires @bernierllc/logger)
176
+ logEvents: true,
177
+
178
+ // Capture and suppress handler errors
179
+ captureErrors: true
180
+ });
181
+ ```
182
+
183
+ ### Environment Variables
184
+
185
+ ```bash
186
+ EVENT_EMITTER_MAX_LISTENERS=100
187
+ EVENT_EMITTER_LOG_EVENTS=true
188
+ EVENT_EMITTER_CAPTURE_ERRORS=true
189
+ ```
190
+
191
+ ## API Reference
192
+
193
+ ### `EventEmitter`
194
+
195
+ Main class for event management.
196
+
197
+ #### Methods
198
+
199
+ - `on<T>(eventType: string, handler: EventHandler<T>): EventSubscription` - Subscribe to event
200
+ - `once<T>(eventType: string, handler: EventHandler<T>): EventSubscription` - Subscribe once
201
+ - `off(subscription: EventSubscription): void` - Unsubscribe from event
202
+ - `emit<T>(eventType: string, data: T): Promise<void>` - Publish event
203
+ - `removeAllListeners(eventType?: string): void` - Remove all listeners
204
+ - `listenerCount(eventType: string): number` - Get listener count
205
+ - `eventNames(): string[]` - Get all event names
206
+
207
+ ### `EventSubscription`
208
+
209
+ Subscription object returned by `on()` and `once()`.
210
+
211
+ ```typescript
212
+ interface EventSubscription {
213
+ id: string; // Unique subscription ID
214
+ eventType: string; // Event type
215
+ handler: EventHandler; // Handler function
216
+ once: boolean; // One-time subscription?
217
+ unsubscribe: () => void; // Unsubscribe function
218
+ }
219
+ ```
220
+
221
+ ### `EventHandler<T>`
222
+
223
+ Event handler function type.
224
+
225
+ ```typescript
226
+ type EventHandler<T = any> = (data: T) => void | Promise<void>;
227
+ ```
228
+
229
+ ### `createEventEmitter(options?: EventEmitterOptions): EventEmitter`
230
+
231
+ Factory function for creating event emitters.
232
+
233
+ ## Integration Status
234
+
235
+ - **Logger**: integrated (optional - for event logging and error tracking)
236
+ - **Docs-Suite**: ready (TypeDoc format)
237
+ - **NeverHub**: optional (for distributed event bus across packages)
238
+
239
+ ## Use Cases
240
+
241
+ ### Inter-Package Communication
242
+
243
+ ```typescript
244
+ // In cache-manager package
245
+ events.emit('cache.invalidated', { key: 'user:123' });
246
+
247
+ // In data-sync package
248
+ events.on('cache.invalidated', async (data) => {
249
+ await refreshData(data.key);
250
+ });
251
+ ```
252
+
253
+ ### State Management
254
+
255
+ ```typescript
256
+ // State changes
257
+ events.on('state.changed', (newState) => {
258
+ console.log('State updated:', newState);
259
+ });
260
+
261
+ await events.emit('state.changed', { status: 'active' });
262
+ ```
263
+
264
+ ### Task Lifecycle Events
265
+
266
+ ```typescript
267
+ // Task queue events
268
+ events.on('task.started', (task) => console.log('Started:', task.id));
269
+ events.on('task.completed', (task) => console.log('Completed:', task.id));
270
+ events.on('task.failed', (task) => console.log('Failed:', task.id));
271
+ ```
272
+
273
+ ### Workflow Orchestration
274
+
275
+ ```typescript
276
+ // Multi-step workflow
277
+ events.on('workflow.step.completed', async (data) => {
278
+ if (data.nextStep) {
279
+ await events.emit('workflow.step.start', { step: data.nextStep });
280
+ }
281
+ });
282
+ ```
283
+
284
+ ## Performance
285
+
286
+ - **Parallel Execution**: Async handlers execute in parallel via `Promise.all()`
287
+ - **Memory Efficient**: Automatic cleanup of empty listener sets
288
+ - **Fast Lookups**: O(1) listener retrieval using Map data structure
289
+ - **Zero Dependencies**: Minimal bundle size (~2KB gzipped)
290
+
291
+ ## Testing
292
+
293
+ ```bash
294
+ # Run tests
295
+ npm test
296
+
297
+ # Run tests with coverage
298
+ npm run test:coverage
299
+
300
+ # Run tests in watch mode
301
+ npm run test:watch
302
+ ```
303
+
304
+ ## See Also
305
+
306
+ - [@bernierllc/logger](../logger) - Logging integration for event tracking
307
+ - [@bernierllc/cache-manager](../cache-manager) - Uses events for cache invalidation
308
+ - [@bernierllc/state-machine](../state-machine) - Uses events for state transitions
309
+ - [@bernierllc/task-queue](../task-queue) - Uses events for task lifecycle
310
+
311
+ ## License
312
+
313
+ Copyright (c) 2025 Bernier LLC. All rights reserved.
@@ -0,0 +1,260 @@
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 '../src';
10
+
11
+ describe('EventEmitter - Async Handlers', () => {
12
+ let emitter: EventEmitter;
13
+
14
+ beforeEach(() => {
15
+ emitter = new EventEmitter();
16
+ });
17
+
18
+ describe('async handler support', () => {
19
+ it('should handle async handlers', async () => {
20
+ const results: string[] = [];
21
+
22
+ const asyncHandler = async (data: { message: string }) => {
23
+ await new Promise(resolve => setTimeout(resolve, 10));
24
+ results.push(data.message);
25
+ };
26
+
27
+ emitter.on('test', asyncHandler);
28
+ await emitter.emit('test', { message: 'hello' });
29
+
30
+ expect(results).toEqual(['hello']);
31
+ });
32
+
33
+ it('should wait for all async handlers to complete', async () => {
34
+ const results: number[] = [];
35
+
36
+ const handler1 = async () => {
37
+ await new Promise(resolve => setTimeout(resolve, 30));
38
+ results.push(1);
39
+ };
40
+
41
+ const handler2 = async () => {
42
+ await new Promise(resolve => setTimeout(resolve, 20));
43
+ results.push(2);
44
+ };
45
+
46
+ const handler3 = async () => {
47
+ await new Promise(resolve => setTimeout(resolve, 10));
48
+ results.push(3);
49
+ };
50
+
51
+ emitter.on('test', handler1);
52
+ emitter.on('test', handler2);
53
+ emitter.on('test', handler3);
54
+
55
+ await emitter.emit('test', {});
56
+
57
+ // All handlers should have completed
58
+ expect(results).toHaveLength(3);
59
+ expect(results).toContain(1);
60
+ expect(results).toContain(2);
61
+ expect(results).toContain(3);
62
+ });
63
+
64
+ it('should handle mix of sync and async handlers', async () => {
65
+ const results: string[] = [];
66
+
67
+ const syncHandler = (data: { type: string }) => {
68
+ results.push(`sync-${data.type}`);
69
+ };
70
+
71
+ const asyncHandler = async (data: { type: string }) => {
72
+ await new Promise(resolve => setTimeout(resolve, 10));
73
+ results.push(`async-${data.type}`);
74
+ };
75
+
76
+ emitter.on('test', syncHandler);
77
+ emitter.on('test', asyncHandler);
78
+
79
+ await emitter.emit('test', { type: 'mixed' });
80
+
81
+ expect(results).toHaveLength(2);
82
+ expect(results).toContain('sync-mixed');
83
+ expect(results).toContain('async-mixed');
84
+ });
85
+
86
+ it('should handle async once listeners', async () => {
87
+ const handler = jest.fn(async () => {
88
+ await new Promise(resolve => setTimeout(resolve, 10));
89
+ });
90
+
91
+ emitter.once('test', handler);
92
+
93
+ await emitter.emit('test', { data: 'first' });
94
+ await emitter.emit('test', { data: 'second' });
95
+
96
+ expect(handler).toHaveBeenCalledTimes(1);
97
+ expect(emitter.listenerCount('test')).toBe(0);
98
+ });
99
+
100
+ it('should handle async wildcard listeners', async () => {
101
+ const results: string[] = [];
102
+
103
+ const asyncWildcard = async (data: { event: string }) => {
104
+ await new Promise(resolve => setTimeout(resolve, 10));
105
+ results.push(data.event);
106
+ };
107
+
108
+ emitter.on('*', asyncWildcard);
109
+
110
+ await emitter.emit('event1', { event: 'one' });
111
+ await emitter.emit('event2', { event: 'two' });
112
+
113
+ expect(results).toEqual(['one', 'two']);
114
+ });
115
+ });
116
+
117
+ describe('async handler execution order', () => {
118
+ it('should execute async handlers in parallel', async () => {
119
+ const startTime = Date.now();
120
+ const delays = [50, 50, 50];
121
+ let completedCount = 0;
122
+
123
+ delays.forEach(delay => {
124
+ emitter.on('test', async () => {
125
+ await new Promise(resolve => setTimeout(resolve, delay));
126
+ completedCount++;
127
+ });
128
+ });
129
+
130
+ await emitter.emit('test', {});
131
+
132
+ const duration = Date.now() - startTime;
133
+
134
+ // If executed in parallel, total time should be ~50ms (the max delay)
135
+ // If executed sequentially, total time would be ~150ms
136
+ expect(duration).toBeLessThan(100); // Allow some buffer
137
+ expect(completedCount).toBe(3);
138
+ });
139
+
140
+ it('should handle async handlers that resolve at different times', async () => {
141
+ const order: number[] = [];
142
+
143
+ emitter.on('test', async () => {
144
+ await new Promise(resolve => setTimeout(resolve, 30));
145
+ order.push(1);
146
+ });
147
+
148
+ emitter.on('test', async () => {
149
+ await new Promise(resolve => setTimeout(resolve, 10));
150
+ order.push(2);
151
+ });
152
+
153
+ emitter.on('test', async () => {
154
+ await new Promise(resolve => setTimeout(resolve, 20));
155
+ order.push(3);
156
+ });
157
+
158
+ await emitter.emit('test', {});
159
+
160
+ // Handlers complete in order of their delays, not registration order
161
+ expect(order).toEqual([2, 3, 1]);
162
+ });
163
+ });
164
+
165
+ describe('async handler data flow', () => {
166
+ it('should pass data to async handlers correctly', async () => {
167
+ const receivedData: any[] = [];
168
+
169
+ const handler = async (data: any) => {
170
+ await new Promise(resolve => setTimeout(resolve, 10));
171
+ receivedData.push(data);
172
+ };
173
+
174
+ emitter.on('test', handler);
175
+
176
+ const testData = { id: 123, message: 'test', nested: { value: true } };
177
+ await emitter.emit('test', testData);
178
+
179
+ expect(receivedData).toHaveLength(1);
180
+ expect(receivedData[0]).toEqual(testData);
181
+ });
182
+
183
+ it('should handle async handlers with complex data types', async () => {
184
+ interface ComplexData {
185
+ users: Array<{ id: number; name: string }>;
186
+ metadata: Record<string, any>;
187
+ timestamp: Date;
188
+ }
189
+
190
+ let receivedData: ComplexData | null = null;
191
+
192
+ const handler = async (data: ComplexData) => {
193
+ await new Promise(resolve => setTimeout(resolve, 10));
194
+ receivedData = data;
195
+ };
196
+
197
+ emitter.on('test', handler);
198
+
199
+ const complexData: ComplexData = {
200
+ users: [
201
+ { id: 1, name: 'Alice' },
202
+ { id: 2, name: 'Bob' }
203
+ ],
204
+ metadata: { source: 'api', version: 2 },
205
+ timestamp: new Date()
206
+ };
207
+
208
+ await emitter.emit('test', complexData);
209
+
210
+ expect(receivedData).toEqual(complexData);
211
+ });
212
+ });
213
+
214
+ describe('async handler cleanup', () => {
215
+ it('should handle unsubscribe during async handler execution', async () => {
216
+ let handlerStarted = false;
217
+ let handlerCompleted = false;
218
+
219
+ const handler = async () => {
220
+ handlerStarted = true;
221
+ await new Promise(resolve => setTimeout(resolve, 50));
222
+ handlerCompleted = true;
223
+ };
224
+
225
+ const subscription = emitter.on('test', handler);
226
+
227
+ // Start emission
228
+ const emitPromise = emitter.emit('test', {});
229
+
230
+ // Unsubscribe immediately (while handler is still running)
231
+ await new Promise(resolve => setTimeout(resolve, 10));
232
+ subscription.unsubscribe();
233
+
234
+ // Wait for emission to complete
235
+ await emitPromise;
236
+
237
+ // Handler should have started and completed despite unsubscribe
238
+ expect(handlerStarted).toBe(true);
239
+ expect(handlerCompleted).toBe(true);
240
+
241
+ // But listener should be removed for future events
242
+ expect(emitter.listenerCount('test')).toBe(0);
243
+ });
244
+
245
+ it('should handle async once listeners cleanup', async () => {
246
+ const handler = jest.fn(async () => {
247
+ await new Promise(resolve => setTimeout(resolve, 10));
248
+ });
249
+
250
+ emitter.once('test', handler);
251
+
252
+ expect(emitter.listenerCount('test')).toBe(1);
253
+
254
+ await emitter.emit('test', {});
255
+
256
+ expect(emitter.listenerCount('test')).toBe(0);
257
+ expect(handler).toHaveBeenCalledTimes(1);
258
+ });
259
+ });
260
+ });