fastevent 2.0.2 → 2.1.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/readme.md CHANGED
@@ -1,362 +1,169 @@
1
1
  # FastEvent
2
2
 
3
- FastEvent is a powerful TypeScript event management library that provides flexible event subscription and publishing mechanisms, supporting features such as event wildcards, scoping, and asynchronous events.
3
+ [WebSite](https://zhangfisher.github.io/fastevent/)
4
4
 
5
- Compared to `EventEmitter2`, `FastEvent` has the following advantages:
6
-
7
- - `FastEvent` performs about `1+` times better than `EventEmitter2` when publishing and subscribing with wildcards.
8
- - `FastEvent` has a package size of `6.3kb`, while `EventEmitter2` is `43.4kb`.
9
- - `FastEvent` offers more comprehensive features.
5
+ `FastEvent` is a well-designed, powerful, type-safe, and thoroughly tested event emitter that provides robust event subscription and publishing mechanisms, suitable for both `nodejs/browser` environments.
10
6
 
11
7
  # Installation
12
8
 
13
- Install using npm:
14
-
15
9
  ```bash
16
10
  npm install fastevent
17
- ```
18
-
19
- Or using yarn:
20
-
21
- ```bash
22
11
  yarn add fastevent
12
+ pnpm add fastevent
13
+ bun add fastevent
23
14
  ```
24
15
 
25
- # Quick Start
16
+ # Guide
17
+
18
+ ## Event Publishing and Subscription
26
19
 
27
- ## Basic Usage
20
+ `FastEvent` provides complete event emission and subscription functionality, with an `API` design inspired by `eventemitter2`.
28
21
 
29
22
  ```typescript
30
23
  import { FastEvent } from 'fastevent';
31
-
32
- // Create event instance
33
24
  const events = new FastEvent();
34
25
 
35
- // Subscribe to event
26
+ // Basic event publishing
27
+ const results = events.emit('user/login', { id: 1 });
28
+
29
+ // Asynchronous event emission
30
+ const results = await events.emitAsync('data/process', { items: [...] });
31
+
32
+ // Event subscription
36
33
  events.on('user/login', (message) => {
37
34
  console.log('User login:', message.payload);
38
- console.log('Event type:', message.type);
39
- console.log('Metadata:', message.meta);
40
35
  });
41
36
 
42
- // Publish event - Method 1: Parameters
43
- events.emit('user/login', { id: 1, name: 'Alice' });
37
+ // One-time listener
38
+ events.once('startup', () => console.log('Application has started'));
44
39
 
45
- // Publish event - Method 2: Message object
46
- events.emit({
47
- type: 'user/login',
48
- payload: { id: 1, name: 'Alice' },
49
- meta: { timestamp: Date.now() },
40
+ // Listener with options
41
+ events.on('data/update', handler, {
42
+ count: 3, // Maximum trigger count
43
+ prepend: true, // Add to the beginning of the queue
44
+ filter: (msg) => msg.payload.important // Only process important updates
50
45
  });
51
46
 
52
- // Using TypeScript with type safety
53
- interface MyEvents {
54
- 'user/login': { id: number; name: string };
55
- 'user/logout': { id: number };
56
- }
57
-
58
- const typedEvents = new FastEvent<MyEvents>();
59
-
60
- // TypeScript will enforce correct event types and payloads
61
- typedEvents.on('user/login', (message) => {
62
- const { id, name } = message.payload; // Properly typed
47
+ // Global listener
48
+ events.onAny((message) => {
49
+ console.log('Event occurred:', message.type);
63
50
  });
64
51
  ```
65
52
 
66
- # Guide
67
-
68
- ## Event Triggering
69
-
70
- FastEvent provides flexible ways to trigger events with different parameter combinations:
53
+ ## Event Messages
71
54
 
72
- ### Basic Event Triggering
55
+ Listener functions receive a `Message` object that contains the following properties:
73
56
 
74
- ```typescript
75
- const events = new FastEvent();
76
-
77
- // Method 1: Parameters form
78
- events.emit('user/login', { id: 1, name: 'Alice' });
79
-
80
- // Method 2: Message object form
81
- events.emit({
82
- type: 'user/login',
83
- payload: { id: 1, name: 'Alice' },
84
- meta: { timestamp: Date.now() },
57
+ ```ts
58
+ events.on('user/login', (message) => {
59
+ // {
60
+ // type: 'user/login', // Event name
61
+ // payload: { id: 1 }, // Event data
62
+ // meta: {...} // Event metadata
63
+ // }
85
64
  });
86
65
  ```
87
66
 
88
- ### Retained Events
67
+ ## Retained Events
89
68
 
90
- Set `retain=true` to store the event for new subscribers:
69
+ Retain the last event data, so subsequent subscribers can immediately receive the event value upon subscription:
91
70
 
92
71
  ```typescript
93
- // Emit and retain the event
94
- events.emit('config/update', { theme: 'dark' }, true);
95
-
96
- // Later subscribers will immediately receive the retained event
97
- events.on('config/update', (message) => {
98
- console.log('Config:', message.payload); // { theme: 'dark' }
99
- });
100
- ```
101
-
102
- ### Event Metadata
103
-
104
- Metadata can be provided at different levels and will be merged:
72
+ const events = new FastEvent();
105
73
 
106
- ```typescript
107
- const events = new FastEvent({
108
- meta: { app: 'MyApp' }, // Global metadata
109
- });
74
+ // Publish and retain event
75
+ events.emit('config/theme', { dark: true }, true);
76
+ // Equivalent to
77
+ events.emit('config/theme', { dark: true }, { retain: true });
110
78
 
111
- // Event-specific metadata
112
- events.emit('order/create', { id: '123' }, false, {
113
- timestamp: Date.now(),
79
+ // Subsequent subscribers immediately receive the retained value
80
+ events.on('config/theme', (message) => {
81
+ console.log('Theme:', message.payload); // Immediately outputs: { dark: true }
114
82
  });
115
-
116
- // Listener receives merged metadata:
117
- // { type: 'order/create', app: 'MyApp', timestamp: ... }
118
- ```
119
-
120
- ### Return Values
121
-
122
- `emit()` returns an array of listener results:
123
-
124
- ```typescript
125
- events.on('calculate', () => 1);
126
- events.on('calculate', () => 2);
127
-
128
- const results = events.emit('calculate');
129
- console.log(results); // [1, 2]
130
- ```
131
-
132
- ### Type-safe Event Triggering
133
-
134
- With TypeScript, event payloads are type-checked:
135
-
136
- ```typescript
137
- interface MyEvents {
138
- 'user/login': { id: number; name: string };
139
- }
140
-
141
- const events = new FastEvent<MyEvents>();
142
-
143
- // Valid - payload matches type
144
- events.emit('user/login', { id: 1, name: 'Alice' });
145
-
146
- // Error - payload type mismatch
147
- events.emit('user/login', { id: '1' }); // TypeScript error
148
- ```
149
-
150
- ## Event Message Format
151
-
152
- FastEvent uses a standardized message format for all events:
153
-
154
- ```typescript
155
- type FastEventMessage<T = string, P = any, M = unknown> = {
156
- type: T; // Event type
157
- payload: P; // Event data
158
- meta: M; // Event metadata
159
- };
160
83
  ```
161
84
 
162
- Event listeners always receive this message object, providing consistent access to event data and metadata.
163
-
164
- ## Event Wildcards
85
+ ## Hierarchical Event Publishing
165
86
 
166
- FastEvent supports two types of wildcards:
87
+ `FastEvent` supports hierarchical event publishing and subscription.
167
88
 
168
- - `*`: Matches a single path level
169
- - `**`: Matches multiple path levels
89
+ - The default event hierarchy delimiter is `/`, which can be modified via `options.delimiter`
90
+ - Two types of wildcards are supported when subscribing to events: `*` matches a single path level, `**` matches multiple path levels (only used at the end of event names)
170
91
 
171
92
  ```typescript
172
93
  const events = new FastEvent();
173
94
 
174
- // Matches user/*/login
95
+ // Match user/*/login
175
96
  events.on('user/*/login', (message) => {
176
97
  console.log('Any user type login:', message.payload);
177
98
  });
178
99
 
179
- // Matches all events under user
100
+ // Match all events under user
180
101
  events.on('user/**', (message) => {
181
102
  console.log('All user-related events:', message.payload);
182
103
  });
183
104
 
184
105
  // Trigger events
185
106
  events.emit('user/admin/login', { id: 1 }); // Both handlers will be called
186
- events.emit('user/admin/profile/update', { name: 'New' }); // Only ** handler will be called
187
- ```
188
-
189
- ## Event Scoping
190
-
191
- Scopes allow you to handle events within specific namespaces. Note that scopes share the same listener table with the parent emitter:
192
-
193
- ```typescript
194
- const events = new FastEvent();
195
-
196
- // Create user-related scope
197
- const userScope = events.scope('user');
198
-
199
- // These are equivalent:
200
- userScope.on('login', handler);
201
- events.on('user/login', handler);
202
-
203
- // These are also equivalent:
204
- userScope.emit('login', data);
205
- events.emit('user/login', data);
206
-
207
- // Clear all listeners in the scope
208
- userScope.offAll(); // Equivalent to events.offAll('user')
209
-
210
- // Nested scopes
211
- const profileScope = userScope.scope('profile');
212
- profileScope.on('update', (message) => {
213
- // Will receive events emitted as 'user/profile/update'
214
- console.log('Profile update:', message.payload);
215
- });
216
-
217
- // Scope with metadata
218
- const adminScope = events.scope('admin', {
219
- meta: { role: 'admin' },
220
- context: { adminId: 1 },
221
- });
222
-
223
- adminScope.on('action', function (message) {
224
- console.log('Admin meta:', message.meta); // Contains { role: 'admin' }
225
- console.log('Context:', this.adminId); // Access to scope context
226
- });
227
-
228
- // Type-safe scopes
229
- interface UserEvents {
230
- login: { id: number };
231
- logout: { id: number };
232
- }
233
-
234
- const typedUserScope = events.scope<'user', UserEvents>('user');
235
- typedUserScope.on('login', (message) => {
236
- const { id } = message.payload; // Properly typed as { id: number }
237
- });
238
- ```
239
-
240
- Scopes provide several benefits:
241
-
242
- 1. Namespace organization - Group related events under a common prefix
243
- 2. Code organization - Separate event handling logic by domain
244
- 3. Metadata inheritance - Share common metadata across related events
245
- 4. Context binding - Provide specific execution context for event handlers
246
- 5. Type safety - Enforce type checking for scoped events
247
-
248
- ## Listener Options
249
-
250
- When subscribing to events, you can specify additional options:
251
-
252
- ```typescript
253
- interface FastEventListenOptions {
254
- // Number of times the listener should be called (0 for unlimited, 1 for once)
255
- count?: number;
256
- // Add the listener to the beginning of the listeners array
257
- prepend?: boolean;
258
- }
259
-
260
- // Example: Listen for first 3 occurrences
261
- events.on('data', handler, { count: 3 });
262
-
263
- // Example: Ensure handler is called before other listeners
264
- events.on('important', handler, { prepend: true });
107
+ events.emit('user/admin/profile/update', { name: 'New' }); // Only the ** handler will be called
265
108
  ```
266
109
 
267
110
  ## Removing Listeners
268
111
 
269
- FastEvent provides multiple ways to remove listeners:
112
+ `FastEvent` provides multiple ways to remove listeners:
270
113
 
271
114
  ```typescript
272
- // Remove specific listener
273
- events.off(listener);
115
+ // Return a subscriber object to remove the listener, recommended approach
116
+ const subscriber = events.on('user/login', handler);
117
+ subscriber.off();
274
118
 
275
- // Remove all listeners for an event
119
+ // Remove a specific listener
120
+ events.off(listener);
121
+ // Remove all listeners for a specific event
276
122
  events.off('user/login');
277
-
278
- // Remove specific listener for an event
123
+ // Remove a specific listener for a specific event
279
124
  events.off('user/login', listener);
280
-
281
- // Remove all listeners with wildcard pattern
125
+ // Remove listeners using wildcard patterns
282
126
  events.off('user/*');
283
-
284
127
  // Remove all listeners
285
128
  events.offAll();
286
-
287
- // Remove all listeners under a prefix
129
+ // Remove all listeners under a specific prefix
288
130
  events.offAll('user');
289
131
  ```
290
132
 
291
- ## One-time Events
292
-
293
- Use `once` to subscribe to events that trigger only once:
294
-
295
- ```typescript
296
- const events = new FastEvent();
297
-
298
- events.once('startup', () => {
299
- console.log('Application started');
300
- });
301
-
302
- // Equivalent to:
303
- events.on('startup', handler, { count: 1 });
304
- ```
305
-
306
- ## Asynchronous Events
133
+ ## Event Scopes
307
134
 
308
- Support for asynchronous event handling:
135
+ Scopes allow you to handle events within a specific namespace.
309
136
 
310
- ```typescript
311
- const events = new FastEvent();
312
-
313
- events.on('data/fetch', async () => {
314
- const response = await fetch('https://api.example.com/data');
315
- return await response.json();
316
- });
317
-
318
- // Async event publishing returns array of results/errors
319
- const results = await events.emitAsync('data/fetch');
320
- console.log('Results from all handlers:', results);
321
- ```
322
-
323
- ## Listener Return Values
324
-
325
- Both `emit` and `emitAsync` methods return the results from all event listeners:
137
+ **Note** that scopes share the same listener table with the parent event emitter:
326
138
 
327
139
  ```typescript
328
140
  const events = new FastEvent();
329
141
 
330
- // Synchronous listeners with return values
331
- events.on('calculate', () => 1);
332
- events.on('calculate', () => 2);
333
- events.on('calculate', () => 3);
142
+ // Create a user-related scope
143
+ const userScope = events.scope('user');
334
144
 
335
- // Get array of return values
336
- const results = events.emit('calculate');
337
- console.log('Results:', results); // [1, 2, 3]
145
+ // The following two approaches are equivalent:
146
+ userScope.on('login', handler);
147
+ events.on('user/login', handler);
338
148
 
339
- // Asynchronous listeners
340
- events.on('process', async () => 'result 1');
341
- events.on('process', async () => 'result 2');
149
+ // The following two approaches are also equivalent:
150
+ userScope.emit('login', data);
151
+ events.emit('user/login', data);
342
152
 
343
- // Get array of resolved values/errors
344
- const asyncResults = await events.emitAsync('process');
345
- console.log('Async results:', asyncResults); // ['result 1', 'result 2']
153
+ // Clear all listeners in the scope
154
+ userScope.offAll(); // Equivalent to events.offAll('user')
346
155
  ```
347
156
 
348
- For asynchronous events, `emitAsync` will wait for all listeners to complete and return an array containing either the resolved values or error objects if a listener fails.
157
+ ## Waiting for Events
349
158
 
350
- ## Event Waiting
351
-
352
- Use `waitFor` to wait for specific events:
159
+ Use `waitFor` to wait for a specific event to occur, with timeout support.
353
160
 
354
161
  ```typescript
355
162
  const events = new FastEvent();
356
163
 
357
164
  async function waitForLogin() {
358
165
  try {
359
- // Wait for login event with 5 seconds timeout
166
+ // Wait for login event with a 5-second timeout
360
167
  const userData = await events.waitFor('user/login', 5000);
361
168
  console.log('User logged in:', userData);
362
169
  } catch (error) {
@@ -365,188 +172,168 @@ async function waitForLogin() {
365
172
  }
366
173
 
367
174
  waitForLogin();
368
- // Later trigger login event
175
+ // Later trigger the login event
369
176
  events.emit('user/login', { id: 1, name: 'Alice' });
370
177
  ```
371
178
 
372
- ## Retain Event Data
179
+ ## Event Hooks
373
180
 
374
- Retain the last event data, new subscribers will receive it immediately:
181
+ `FastEvent` provides multiple hook functions for operations at different stages of the event emitter lifecycle.
375
182
 
376
183
  ```typescript
377
- const events = new FastEvent();
378
-
379
- // Publish event and retain
380
- events.emit('config/update', { theme: 'dark' }, true);
381
-
382
- // Later subscribers will immediately receive the retained data
383
- events.on('config/update', (message) => {
384
- console.log('Config:', message.payload); // Immediately outputs: Config: { theme: 'dark' }
184
+ const otherEvents = new FastEvent();
185
+ const events = new FastEvent({
186
+ // Called when a new listener is added
187
+ onAddListener: (type, listener, options) => {
188
+ console.log('Added new listener:', type);
189
+ // Return false to prevent the listener from being added
190
+ return false;
191
+ // Can directly return a FastEventSubscriber
192
+ // For example: transfer events starting with `@` to another FastEvent
193
+ if (type.startsWith('@')) {
194
+ return otherEvents.on(type, listener, options);
195
+ }
196
+ },
197
+ // Called when a listener is removed
198
+ onRemoveListener: (type, listener) => {
199
+ console.log('Removed listener:', type);
200
+ },
201
+ // Called when listeners are cleared
202
+ onClearListeners: () => {
203
+ console.log('All listeners cleared');
204
+ },
205
+ // Called when a listener throws an error
206
+ onListenerError: (error, listener, message, args) => {
207
+ console.error(`Error in listener for event ${message.type}:`, error);
208
+ },
209
+ // Called before a listener executes
210
+ onBeforeExecuteListener: (message, args) => {
211
+ console.log('Before executing event listener');
212
+ // Return false to prevent listener execution
213
+ return false;
214
+
215
+ // Forward events to another FastEvent
216
+ // For example: forward events starting with `@` to another FastEvent
217
+ if (type.startsWith('@')) {
218
+ return otherEvents.emit(message.type);
219
+ }
220
+ },
221
+ // Called after a listener executes
222
+ onAfterExecuteListener: (message, returns, listeners) => {
223
+ console.log('After executing event listener');
224
+ // Can intercept and modify return values here
225
+ },
385
226
  });
386
227
  ```
387
228
 
388
- ## Multi-level Events and Wildcards
229
+ ## Executors
389
230
 
390
- FastEvent supports hierarchical event structures with powerful wildcard matching capabilities.
231
+ By default, all listeners are executed in parallel when an event is triggered.
391
232
 
392
- ### Event Path Structure
393
-
394
- Events can be organized in a hierarchical structure using path delimiters (default is '/'):
233
+ `FastEvent` provides powerful listener execution mechanisms that allow developers to control how listeners are executed.
395
234
 
396
235
  ```typescript
397
- const events = new FastEvent();
398
-
399
- // Basic multi-level events
400
- events.on('user/profile/update', handler);
401
- events.on('user/settings/theme/change', handler);
402
-
403
- // Custom delimiter
404
- const customEvents = new FastEvent({
405
- delimiter: '.',
236
+ import { race } from 'fastevent/executors';
237
+ const events = new FastEvent({
238
+ executor: race(),
406
239
  });
407
- customEvents.on('user.profile.update', handler);
408
- ```
409
-
410
- ### Wildcard Patterns
411
240
 
412
- FastEvent supports two types of wildcards:
413
-
414
- 1. Single-level wildcard (`*`):
415
- - Matches exactly one level in the event path
416
- - Can be used at any level in the path
417
-
418
- ```typescript
419
- // Match any user type
420
- events.on('user/*/login', (message) => {
421
- console.log('User type:', message.type.split('/')[1]);
422
- // Matches: user/admin/login, user/guest/login, etc.
241
+ events.on('task/start', async () => {
242
+ /* Time-consuming operation 1 */
423
243
  });
424
-
425
- // Match any action
426
- events.on('api/users/*/action/*', (message) => {
427
- const [, , userId, , action] = message.type.split('/');
428
- console.log(`User ${userId} performed ${action}`);
429
- // Matches: api/users/123/action/update, api/users/456/action/delete, etc.
244
+ events.on('task/start', async () => {
245
+ /* Time-consuming operation 2 */
430
246
  });
247
+
248
+ // The two listeners will execute in parallel, returning the fastest result
249
+ await events.emitAsync('task/start');
431
250
  ```
432
251
 
433
- 2. Multi-level wildcard (`**`):
434
- - Matches zero or more levels in the event path
435
- - Must be used at the end of the path pattern
252
+ **Built-in Support**:
436
253
 
437
- ```typescript
438
- // Match all user-related events
439
- events.on('user/**', (message) => {
440
- console.log('User event:', message.type);
441
- // Matches: user/login, user/profile/update, user/settings/theme/change, etc.
442
- });
254
+ | Executor | Description |
255
+ | ----------------------------------------- | ------------------------------------------------------------------------- |
256
+ | `parallel` | Default, concurrent execution |
257
+ | `race` | Parallel executor, uses `Promise.race` for parallel execution |
258
+ | `balance` | Evenly distributed executor |
259
+ | `first` | Execute only the first listener |
260
+ | `last` | Execute only the last listener |
261
+ | `random` | Randomly select a listener |
262
+ | `series` | Serial executor, execute listeners in sequence and return the last result |
263
+ | `waterfall` | Execute listeners in sequence and return the last result, abort on error |
264
+ | `(listeners,message,args,execute)=>any[]` | Custom executor |
443
265
 
444
- // Match all API events
445
- events.on('api/**', (message) => {
446
- console.log('API event:', message.type, message.payload);
447
- // Matches: api/get, api/users/create, api/posts/123/comments/add, etc.
448
- });
449
- ```
266
+ ## Listener Pipes
450
267
 
451
- ### Advanced Wildcard Usage
268
+ Listener pipes are used to wrap listener functions during event subscription to implement various common advanced features.
452
269
 
453
270
  ```typescript
271
+ import { queue } from 'fastevent/pipes';
454
272
  const events = new FastEvent();
455
273
 
456
- // Using single-level wildcards
457
- events.on('service/*/user/update', (message) => {
458
- // Matches patterns like:
459
- // service/auth/user/update
460
- // service/admin/user/update
461
- const parts = message.type.split('/');
462
- const serviceType = parts[1];
463
- console.log(`${serviceType} service user update:`, message.payload);
464
- });
274
+ // default queue size is 10
275
+ events.on(
276
+ 'data/update',
277
+ (data) => {
278
+ console.log('Processing data:', data);
279
+ },
280
+ {
281
+ pipes: [queue({ size: 10 })],
282
+ },
283
+ );
284
+ ```
465
285
 
466
- // Using multi-level wildcard at the end
467
- events.on('service/auth/**', (message) => {
468
- // Matches patterns like:
469
- // service/auth/user/update
470
- // service/auth/user/profile/update
471
- // service/auth/settings/theme/change
472
- console.log('Auth service event:', message.type, message.payload);
473
- });
286
+ **Built-in Support:**
474
287
 
475
- // Type-safe events with TypeScript
476
- interface ApiEvents {
477
- 'api/users/profile': { userId: string; data: any };
478
- 'api/posts/comments': { postId: string; commentId: string; text: string };
479
- }
288
+ | Pipe | Description |
289
+ | ---------- | -------------------------------------------------------------------------------- |
290
+ | `queue` | Queue listener, process messages in queue, supports priority and timeout control |
291
+ | `throttle` | Throttle listener |
292
+ | `debounce` | Debounce listener |
293
+ | `timeout` | Timeout listener |
294
+ | `retry` | Retry listener, for controlling retries after listener execution failure |
295
+ | `memorize` | Cache listener, cache listener execution results |
480
296
 
481
- const typedEvents = new FastEvent<ApiEvents>();
297
+ ## Forwarding Publishing and Subscription
482
298
 
483
- // Exact match with type safety
484
- typedEvents.on('api/users/profile', (message) => {
485
- const { userId, data } = message.payload; // Properly typed
486
- });
299
+ `FastEvent` can elegantly forward publishing and subscription to another `FastEvent` instance.
487
300
 
488
- // Wildcard listeners still work but lose some type safety
489
- typedEvents.on('api/*', (message) => {
490
- // message.payload type is any here
491
- console.log('API event:', message.type);
301
+ ```ts
302
+ const otherEmitter = new FastEvent();
303
+ const emitter = new FastEvent({
304
+ onAddListener: (type, listener, options) => {
305
+ // Subscription forwarding rule: when event name starts with `@/`, forward subscription to another `FastEvent` instance
306
+ if (type.startsWith('@/')) {
307
+ return otherEmitter.on(type.substring(2), listener, options);
308
+ }
309
+ },
310
+ onBeforeExecuteListener: (message, args) => {
311
+ // Event forwarding rule: when event name starts with `@/`, publish to another `FastEvent` instance
312
+ if (message.type.startsWith('@/')) {
313
+ message.type = message.type.substring(2);
314
+ return otherEmitter.emit(message, args);
315
+ }
316
+ },
492
317
  });
493
-
494
- // Wildcard event monitoring
495
- events.on('**', (message) => {
496
- console.log('Event intercepted:', {
497
- type: message.type,
498
- timestamp: new Date(),
499
- payload: message.payload,
500
- });
318
+ const events: any[] = [];
319
+ otherEmitter.on('data', ({ payload }) => {
320
+ events.push(payload);
501
321
  });
502
-
503
- // Example usage
504
- events.emit('service/auth/user/profile/update', { name: 'John' });
505
- events.emit('api/users/123/profile', { userId: '123', data: { age: 30 } });
506
- ```
507
-
508
- ### Important Notes
509
-
510
- 1. Wildcard Limitations:
511
-
512
- - `**` wildcard must be at the end of the path
513
- - `*` can be used multiple times in a path
514
- - Wildcards cannot be combined in a single segment (e.g., 'a/\*\*/b' is invalid)
515
-
516
- 2. Performance Considerations:
517
-
518
- - Specific patterns (without wildcards) are matched faster
519
- - `*` wildcards are more efficient than `**`
520
- - Excessive use of `**` wildcards may impact performance
521
-
522
- 3. Best Practices:
523
- - Use specific patterns when possible
524
- - Limit the use of `**` wildcards
525
- - Consider the event hierarchy carefully
526
- - Use TypeScript interfaces for type safety
527
-
528
- ## Global Event Listening
529
-
530
- Use `onAny` to listen to all events:
531
-
532
- ```typescript
533
- const events = new FastEvent();
534
-
535
- events.onAny((message) => {
536
- console.log(`Event ${message.type} triggered:`, message.payload);
322
+ // Subscribe to otherEmitter's data event
323
+ emitter.on('@/data', ({ payload }) => {
324
+ expect(payload).toBe(1);
325
+ events.push(payload);
537
326
  });
538
-
539
- // Can also use prepend option
540
- events.onAny(handler, { prepend: true });
327
+ // Publish data event to otherEmitter
328
+ const subscriber = emitter.emit('@/data', 1);
329
+ subscriber.off();
541
330
  ```
542
331
 
543
332
  ## Metadata (Meta)
544
333
 
545
- Metadata is a mechanism for providing additional context information for events. You can set metadata at different levels: globally, scope-level, or event-specific.
334
+ Metadata is a mechanism for providing additional contextual information for events.
546
335
 
547
- ### Global Metadata
548
-
549
- Set global metadata when creating a FastEvent instance:
336
+ You can set metadata at different levels: global, scope level, or event-specific level.
550
337
 
551
338
  ```typescript
552
339
  const events = new FastEvent({
@@ -558,171 +345,31 @@ const events = new FastEvent({
558
345
 
559
346
  events.on('user/login', (message) => {
560
347
  console.log('Event data:', message.payload);
561
- console.log('Metadata:', message.meta); // Contains type, version, and environment
562
- });
563
- ```
564
-
565
- ### Scope Metadata
566
-
567
- When creating a scope, you can provide metadata that will be merged with global metadata:
568
-
569
- ```typescript
570
- const events = new FastEvent({
571
- meta: { app: 'MyApp' },
348
+ console.log('Metadata:', message.meta); // Includes type, version, and environment
572
349
  });
573
350
 
351
+ // Using scope-level metadata
574
352
  const userScope = events.scope('user', {
575
353
  meta: { domain: 'user' },
576
354
  });
577
-
578
- userScope.on('login', (message) => {
579
- console.log('Metadata:', message.meta);
580
- // { type: 'user/login', app: 'MyApp', domain: 'user' }
581
- });
582
-
583
- // Nested scopes merge metadata recursively
584
- const profileScope = userScope.scope('profile', {
585
- meta: { section: 'profile' },
586
- });
587
-
588
- profileScope.on('update', (message) => {
589
- console.log('Metadata:', message.meta);
590
- // { type: 'user/profile/update', app: 'MyApp', domain: 'user', section: 'profile' }
591
- });
592
- ```
593
-
594
- ### Event-specific Metadata
595
-
596
- Additional metadata can be passed when publishing events, which will be merged with higher-level metadata:
597
-
598
- ```typescript
599
- const events = new FastEvent({
600
- meta: { app: 'MyApp' },
601
- });
602
-
603
- const userScope = events.scope('user', {
604
- meta: { domain: 'user' },
605
- });
606
-
607
- // Add specific metadata when publishing event
355
+ // Add specific metadata when publishing events
608
356
  userScope.emit(
609
357
  'login',
610
- { userId: '123' }, // Event data
611
- false, // Don't retain
612
- { timestamp: Date.now() }, // Event-specific metadata
358
+ { userId: '123' },
359
+ {
360
+ meta: { timestamp: Date.now() }, // Event-specific metadata
361
+ },
613
362
  );
614
363
 
615
- // Listener receives merged metadata
364
+ // Listeners receive merged metadata
616
365
  userScope.on('login', (message) => {
617
366
  console.log('Metadata:', message.meta);
618
- // { type: 'user/login', app: 'MyApp', domain: 'user', timestamp: ... }
619
- });
620
- ```
621
-
622
- ### Metadata Merge Rules
623
-
624
- 1. Priority (highest to lowest):
625
-
626
- - Event-specific metadata
627
- - Scope metadata (innermost to outermost)
628
- - Global metadata
629
- - System metadata (type is always added)
630
-
631
- 2. Merge behavior:
632
-
633
- - Shallow merge (top-level properties only)
634
- - Later values override earlier ones
635
- - No deep merging of nested objects
636
-
637
- 3. Special cases:
638
- - `type` is always preserved as the full event path
639
- - `undefined` values will remove the property from the result
640
- - Arrays are replaced, not concatenated
641
-
642
- ## Error Handling
643
-
644
- FastEvent provides error handling mechanisms:
645
-
646
- ```typescript
647
- const events = new FastEvent({
648
- ignoreErrors: true, // Default is true, won't throw errors
649
- onListenerError: (type, error) => {
650
- console.error(`Error handling event ${type}:`, error);
651
- },
652
- });
653
-
654
- events.on('process', () => {
655
- throw new Error('Processing failed');
656
- });
657
-
658
- // Won't throw error, will trigger onListenerError instead
659
- events.emit('process');
660
- ```
661
-
662
- ## Generic Parameters
663
-
664
- FastEvent supports three generic type parameters for precise type control:
665
-
666
- ```typescript
667
- class FastEvent<
668
- Events extends Record<string, any> = Record<string, any>,
669
- Meta extends Record<string, any> = Record<string, any>,
670
- Types extends keyof Events = keyof Events
671
- >
672
- ```
673
-
674
- 1. `Events`: Defines the mapping between event types and their payload types
675
- 2. `Meta`: Defines the type of metadata that can be attached to events
676
- 3. `Types`: The union type of all event types (usually inferred from Events)
677
-
678
- ### Basic Type Safety
679
-
680
- ```typescript
681
- // Define event types
682
- interface MyEvents {
683
- 'user/login': { id: number; name: string };
684
- 'user/logout': { id: number };
685
- }
686
-
687
- // Create typed event emitter
688
- const events = new FastEvent<MyEvents>();
689
-
690
- // Type checking for event names and payload
691
- events.on('user/login', (message) => {
692
- // message.payload is typed as { id: number; name: string }
693
- const { id, name } = message.payload;
694
367
  });
695
-
696
- // Error: wrong event name
697
- events.emit('wrong/event', {});
698
-
699
- // Error: wrong payload type
700
- events.emit('user/login', { wrong: 'type' });
701
368
  ```
702
369
 
703
- ### Custom Metadata Types
704
-
705
- ```typescript
706
- // Define metadata structure
707
- interface MyMeta {
708
- timestamp: number;
709
- source: string;
710
- }
711
-
712
- // Define events with custom metadata
713
- const events = new FastEvent<MyEvents, MyMeta>();
714
-
715
- events.on('user/login', (message) => {
716
- // message.meta is typed as MyMeta
717
- const { timestamp, source } = message.meta;
718
- console.log(`Login from ${source} at ${timestamp}`);
719
- });
370
+ ## Event Type Definitions
720
371
 
721
- // Emit with typed metadata
722
- events.emit('user/login', { id: 1, name: 'Alice' }, false, { timestamp: Date.now(), source: 'web' });
723
- ```
724
-
725
- ### Advanced Type Usage
372
+ `FastEvent` has complete `TypeScript` type support.
726
373
 
727
374
  ```typescript
728
375
  // Define events with different payload types
@@ -736,223 +383,21 @@ const events = new FastEvent<ComplexEvents>();
736
383
 
737
384
  // TypeScript ensures type safety for each event
738
385
  events.on('data/number', (message) => {
739
- const sum = message.payload + 1; // payload is typed as number
740
- });
741
-
742
- events.on('data/string', (message) => {
743
- const upper = message.payload.toUpperCase(); // payload is typed as string
744
- });
745
-
746
- events.on('data/object', (message) => {
747
- const value = message.payload.value; // payload is typed as { value: any }
386
+ const sum = message.payload + 1; // payload type is number
748
387
  });
749
388
 
750
- // All emissions are type checked
389
+ // All event emissions are type-checked
751
390
  events.emit('data/number', 42);
752
391
  events.emit('data/string', 'hello');
753
392
  events.emit('data/object', { value: true });
754
393
  ```
755
394
 
756
- ## Event Hooks
757
-
758
- FastEvent provides several hooks for monitoring and debugging the event system:
395
+ ## Unit Testing
759
396
 
760
- ```typescript
761
- const events = new FastEvent({
762
- // Called when a new listener is added
763
- onAddListener: (path: string[], listener: Function) => {
764
- console.log('New listener added for:', path.join('/'));
765
- },
766
-
767
- // Called when a listener is removed
768
- onRemoveListener: (path: string[], listener: Function) => {
769
- console.log('Listener removed from:', path.join('/'));
770
- },
397
+ `FastEvent` has been thoroughly unit tested, with over `280+` cumulative test cases and `99%+` test coverage.
771
398
 
772
- // Called when listeners are cleared
773
- onClearListeners: () => {
774
- console.log('All listeners cleared');
775
- },
776
-
777
- // Called when a listener throws an error
778
- onListenerError: (type: string, error: Error) => {
779
- console.error(`Error in listener for ${type}:`, error);
780
- },
781
-
782
- // Called after listeners are executed (debug mode only)
783
- onExecuteListener: (message, returns, listeners) => {
784
- console.log('Event executed:', {
785
- type: message.type,
786
- payload: message.payload,
787
- results: returns,
788
- listenerCount: listeners.length,
789
- });
790
- },
791
- });
792
- ```
793
-
794
- These hooks provide valuable insights into the event system's operation:
795
-
796
- 1. `onAddListener`: Monitor listener registration
797
-
798
- - Called whenever a new event listener is added
799
- - Receives the event path array and listener function
800
- - Useful for tracking event subscriptions
801
-
802
- 2. `onRemoveListener`: Track listener removal
803
-
804
- - Called when a listener is removed
805
- - Helps monitor event unsubscription patterns
806
- - Receives the same parameters as onAddListener
807
-
808
- 3. `onClearListeners`: Notifies of bulk listener removal
809
-
810
- - Called when offAll() is invoked
811
- - Useful for cleanup monitoring
812
- - No parameters provided
813
-
814
- 4. `onListenerError`: Error handling hook
815
-
816
- - Called when a listener throws an error
817
- - Receives the event type and error object
818
- - Enables centralized error handling
819
- - Only called if ignoreErrors is true
820
-
821
- 5. `onExecuteListener`: Execution monitoring (debug mode)
822
- - Only active when debug: true is set
823
- - Provides detailed execution information
824
- - Includes message, return values, and listener list
825
- - Useful for debugging and performance monitoring
826
-
827
- Example usage:
828
-
829
- ```typescript
830
- const events = new FastEvent({
831
- debug: true, // Enable debug mode for onExecuteListener
832
- onAddListener: (path, listener) => {
833
- console.log(`Listener added for ${path.join('/')}`);
834
- // Track listener count or patterns
835
- },
836
- onListenerError: (type, error) => {
837
- console.error(`Error in ${type}:`, error);
838
- // Log to monitoring system
839
- },
840
- onExecuteListener: (message, returns, listeners) => {
841
- console.log(`Event ${message.type} executed:`, {
842
- executionTime: Date.now(),
843
- listenerCount: listeners.length,
844
- results: returns,
845
- });
846
- // Monitor event execution patterns
847
- },
848
- });
849
-
850
- // Example events that trigger hooks
851
- events.on('user/login', () => {
852
- // onAddListener will be called
853
- });
854
-
855
- events.on('data/process', () => {
856
- throw new Error('Process failed');
857
- // onListenerError will be called
858
- });
859
-
860
- events.emit('user/login', { id: 1 });
861
- // onExecuteListener will be called (if debug: true)
862
-
863
- events.offAll();
864
- // onClearListeners will be called
865
- ```
866
-
867
- # Parameters
868
-
869
- FastEvent constructor accepts the following configuration options:
870
-
871
- ````typescript
872
- interface FastEventOptions<Meta = Record<string, any>, Context = any> {
873
- /**
874
- * Unique identifier for the emitter instance
875
- * @default Randomly generated string
876
- */
877
- id?: string;
878
-
879
- /**
880
- * Whether to enable debug mode
881
- * @default false
882
- * @remarks When true, events can be viewed in Redux DevTools
883
- */
884
- debug?: boolean;
885
-
886
- /**
887
- * Delimiter for event path segments
888
- * @default '/'
889
- * @example
890
- * ```ts
891
- * new FastEvent({ delimiter: '.' }); // Use dot as delimiter
892
- * ```
893
- */
894
- delimiter?: string;
895
-
896
- /**
897
- * Default execution context for event handlers
898
- * @default null
899
- */
900
- context?: Context;
901
-
902
- /**
903
- * Whether to ignore listener errors
904
- * @default true
905
- */
906
- ignoreErrors?: boolean;
907
-
908
- /**
909
- * Global metadata attached to all events
910
- * @default undefined
911
- */
912
- meta?: Meta;
913
-
914
- /**
915
- * Callback when a listener is added
916
- * @param path - Array of path segments
917
- * @param listener - The listener function
918
- */
919
- onAddListener?: (path: string[], listener: Function) => void;
920
-
921
- /**
922
- * Callback when a listener is removed
923
- * @param path - Array of path segments
924
- * @param listener - The listener function
925
- */
926
- onRemoveListener?: (path: string[], listener: Function) => void;
927
-
928
- /**
929
- * Callback when all listeners are cleared
930
- */
931
- onClearListeners?: () => void;
932
-
933
- /**
934
- * Callback when a listener throws an error
935
- * @param type - Event type
936
- * @param error - The error object
937
- */
938
- onListenerError?: (type: string, error: Error) => void;
939
-
940
- /**
941
- * Callback after listeners are executed (debug mode only)
942
- * @param message - Event message
943
- * @param returns - Array of listener return values
944
- * @param listeners - Array of executed listeners
945
- */
946
- onExecuteListener?: (message: FastEventMessage, returns: any[], listeners: (FastEventListener<any, any, any> | [FastEventListener<any, any>, number])[]) => void;
947
- }
948
-
949
- // Debug mode usage
950
- import 'fastevent/devtools';
951
- const emitter = new FastEvent({
952
- debug: true, // Enable debug mode to view events in Redux DevTools
953
- });
954
- ````
399
+ ## License
955
400
 
956
- # Performance
401
+ MIT
957
402
 
958
- ![](./bench.png)
403
+ For more detailed documentation, see [WebSite](https://zhangfisher.github.io/fastevent/)