@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
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
|
+
});
|