fastevent 1.0.4 → 1.1.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.
Files changed (53) hide show
  1. package/.prettierrc.js +20 -0
  2. package/.vscode/settings.json +18 -0
  3. package/CHANGELOG.md +22 -6
  4. package/dist/devTools.d.mts +308 -0
  5. package/dist/devTools.d.ts +308 -0
  6. package/dist/devTools.js +3 -0
  7. package/dist/devTools.js.map +1 -0
  8. package/dist/devTools.mjs +3 -0
  9. package/dist/devTools.mjs.map +1 -0
  10. package/dist/index.d.mts +40 -17
  11. package/dist/index.d.ts +40 -17
  12. package/dist/index.js +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.mjs +1 -1
  15. package/dist/index.mjs.map +1 -1
  16. package/example/README.md +54 -0
  17. package/example/eslint.config.js +28 -0
  18. package/example/index.html +13 -0
  19. package/example/package.json +29 -0
  20. package/example/pnpm-lock.yaml +2047 -0
  21. package/example/public/vite.svg +1 -0
  22. package/example/src/App.css +42 -0
  23. package/example/src/App.tsx +60 -0
  24. package/example/src/assets/react.svg +1 -0
  25. package/example/src/index.css +68 -0
  26. package/example/src/main.tsx +10 -0
  27. package/example/src/vite-env.d.ts +1 -0
  28. package/example/tsconfig.app.json +26 -0
  29. package/example/tsconfig.json +7 -0
  30. package/example/tsconfig.node.json +24 -0
  31. package/example/vite.config.ts +7 -0
  32. package/package.json +15 -2
  33. package/readme.md +275 -66
  34. package/readme_cn.md +275 -70
  35. package/src/__tests__/emit.test.ts +68 -69
  36. package/src/__tests__/emitAsync.test.ts +41 -42
  37. package/src/__tests__/many.test.ts +15 -16
  38. package/src/__tests__/meta.test.ts +19 -19
  39. package/src/__tests__/off.test.ts +162 -162
  40. package/src/__tests__/onany.test.ts +97 -98
  41. package/src/__tests__/once.test.ts +42 -43
  42. package/src/__tests__/retain.test.ts +36 -36
  43. package/src/__tests__/scope.test.ts +38 -39
  44. package/src/__tests__/types.test.ts +97 -80
  45. package/src/__tests__/waitFor.test.ts +36 -29
  46. package/src/__tests__/wildcard.test.ts +114 -115
  47. package/src/devTools.ts +166 -0
  48. package/src/event.ts +272 -222
  49. package/src/scope.ts +64 -55
  50. package/src/types.ts +38 -34
  51. package/src/utils/WeakObjectMap.ts +64 -0
  52. package/tsconfig.json +103 -111
  53. package/tsup.config.ts +17 -6
package/readme.md CHANGED
@@ -4,9 +4,9 @@ FastEvent is a powerful TypeScript event management library that provides flexib
4
4
 
5
5
  Compared to `EventEmitter2`, `FastEvent` has the following advantages:
6
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.
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.
10
10
 
11
11
  # Installation
12
12
 
@@ -33,43 +33,67 @@ import { FastEvent } from 'fastevent';
33
33
  const events = new FastEvent();
34
34
 
35
35
  // Subscribe to event
36
- events.on('user/login', (user) => {
37
- console.log('User login:', user);
36
+ events.on('user/login', (message) => {
37
+ console.log('User login:', message.payload);
38
+ console.log('Event type:', message.type);
39
+ console.log('Metadata:', message.meta);
38
40
  });
39
41
 
40
- // Publish event
42
+ // Publish event - Method 1: Parameters
41
43
  events.emit('user/login', { id: 1, name: 'Alice' });
44
+
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() },
50
+ });
42
51
  ```
43
52
 
44
53
  # Guide
45
54
 
55
+ ## Event Message Format
56
+
57
+ FastEvent uses a standardized message format for all events:
58
+
59
+ ```typescript
60
+ type FastEventMessage<T = string, P = any, M = unknown> = {
61
+ type: T; // Event type
62
+ payload: P; // Event data
63
+ meta: M; // Event metadata
64
+ };
65
+ ```
66
+
67
+ Event listeners always receive this message object, providing consistent access to event data and metadata.
68
+
46
69
  ## Event Wildcards
47
70
 
48
71
  FastEvent supports two types of wildcards:
49
- - `*`: Matches a single path level
50
- - `**`: Matches multiple path levels
72
+
73
+ - `*`: Matches a single path level
74
+ - `**`: Matches multiple path levels
51
75
 
52
76
  ```typescript
53
77
  const events = new FastEvent();
54
78
 
55
79
  // Matches user/*/login
56
- events.on('user/*/login', (data) => {
57
- console.log('Any user type login:', data);
80
+ events.on('user/*/login', (message) => {
81
+ console.log('Any user type login:', message.payload);
58
82
  });
59
83
 
60
84
  // Matches all events under user
61
- events.on('user/**', (data) => {
62
- console.log('All user-related events:', data);
85
+ events.on('user/**', (message) => {
86
+ console.log('All user-related events:', message.payload);
63
87
  });
64
88
 
65
89
  // Trigger events
66
- events.emit('user/admin/login', { id: 1 }); // Both handlers will be called
67
- events.emit('user/admin/profile/update', { name: 'New' }); // Only ** handler will be called
90
+ events.emit('user/admin/login', { id: 1 }); // Both handlers will be called
91
+ events.emit('user/admin/profile/update', { name: 'New' }); // Only ** handler will be called
68
92
  ```
69
93
 
70
94
  ## Event Scoping
71
95
 
72
- Scopes allow you to handle events within specific namespaces:
96
+ Scopes allow you to handle events within specific namespaces. Note that scopes share the same listener table with the parent emitter:
73
97
 
74
98
  ```typescript
75
99
  const events = new FastEvent();
@@ -77,13 +101,59 @@ const events = new FastEvent();
77
101
  // Create user-related scope
78
102
  const userScope = events.scope('user');
79
103
 
80
- // Subscribe to events within the scope
81
- userScope.on('login', (data) => {
82
- console.log('User login:', data);
83
- });
104
+ // These are equivalent:
105
+ userScope.on('login', handler);
106
+ events.on('user/login', handler);
107
+
108
+ // These are also equivalent:
109
+ userScope.emit('login', data);
110
+ events.emit('user/login', data);
111
+
112
+ // Clear all listeners in the scope
113
+ userScope.offAll(); // Equivalent to events.offAll('user')
114
+ ```
115
+
116
+ ## Listener Options
117
+
118
+ When subscribing to events, you can specify additional options:
119
+
120
+ ```typescript
121
+ interface FastEventListenOptions {
122
+ // Number of times the listener should be called (0 for unlimited, 1 for once)
123
+ count?: number;
124
+ // Add the listener to the beginning of the listeners array
125
+ prepend?: boolean;
126
+ }
127
+
128
+ // Example: Listen for first 3 occurrences
129
+ events.on('data', handler, { count: 3 });
130
+
131
+ // Example: Ensure handler is called before other listeners
132
+ events.on('important', handler, { prepend: true });
133
+ ```
134
+
135
+ ## Removing Listeners
136
+
137
+ FastEvent provides multiple ways to remove listeners:
84
138
 
85
- // Equivalent to events.emit('user/login', data)
86
- userScope.emit('login', { id: 1 });
139
+ ```typescript
140
+ // Remove specific listener
141
+ events.off(listener);
142
+
143
+ // Remove all listeners for an event
144
+ events.off('user/login');
145
+
146
+ // Remove specific listener for an event
147
+ events.off('user/login', listener);
148
+
149
+ // Remove all listeners with wildcard pattern
150
+ events.off('user/*');
151
+
152
+ // Remove all listeners
153
+ events.offAll();
154
+
155
+ // Remove all listeners under a prefix
156
+ events.offAll('user');
87
157
  ```
88
158
 
89
159
  ## One-time Events
@@ -94,11 +164,11 @@ Use `once` to subscribe to events that trigger only once:
94
164
  const events = new FastEvent();
95
165
 
96
166
  events.once('startup', () => {
97
- console.log('Application started');
167
+ console.log('Application started');
98
168
  });
99
169
 
100
- events.emit('startup'); // Output: Application started
101
- events.emit('startup'); // No output, listener has been removed
170
+ // Equivalent to:
171
+ events.on('startup', handler, { count: 1 });
102
172
  ```
103
173
 
104
174
  ## Asynchronous Events
@@ -109,15 +179,42 @@ Support for asynchronous event handling:
109
179
  const events = new FastEvent();
110
180
 
111
181
  events.on('data/fetch', async () => {
112
- const response = await fetch('https://api.example.com/data');
113
- return await response.json();
182
+ const response = await fetch('https://api.example.com/data');
183
+ return await response.json();
114
184
  });
115
185
 
116
- // Async event publishing
186
+ // Async event publishing returns array of results/errors
117
187
  const results = await events.emitAsync('data/fetch');
118
188
  console.log('Results from all handlers:', results);
119
189
  ```
120
190
 
191
+ ## Listener Return Values
192
+
193
+ Both `emit` and `emitAsync` methods return the results from all event listeners:
194
+
195
+ ```typescript
196
+ const events = new FastEvent();
197
+
198
+ // Synchronous listeners with return values
199
+ events.on('calculate', () => 1);
200
+ events.on('calculate', () => 2);
201
+ events.on('calculate', () => 3);
202
+
203
+ // Get array of return values
204
+ const results = events.emit('calculate');
205
+ console.log('Results:', results); // [1, 2, 3]
206
+
207
+ // Asynchronous listeners
208
+ events.on('process', async () => 'result 1');
209
+ events.on('process', async () => 'result 2');
210
+
211
+ // Get array of resolved values/errors
212
+ const asyncResults = await events.emitAsync('process');
213
+ console.log('Async results:', asyncResults); // ['result 1', 'result 2']
214
+ ```
215
+
216
+ 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.
217
+
121
218
  ## Event Waiting
122
219
 
123
220
  Use `waitFor` to wait for specific events:
@@ -126,13 +223,13 @@ Use `waitFor` to wait for specific events:
126
223
  const events = new FastEvent();
127
224
 
128
225
  async function waitForLogin() {
129
- try {
130
- // Wait for login event with 5 seconds timeout
131
- const userData = await events.waitFor('user/login', 5000);
132
- console.log('User logged in:', userData);
133
- } catch (error) {
134
- console.log('Login wait timeout');
135
- }
226
+ try {
227
+ // Wait for login event with 5 seconds timeout
228
+ const userData = await events.waitFor('user/login', 5000);
229
+ console.log('User logged in:', userData);
230
+ } catch (error) {
231
+ console.log('Login wait timeout');
232
+ }
136
233
  }
137
234
 
138
235
  waitForLogin();
@@ -151,8 +248,8 @@ const events = new FastEvent();
151
248
  events.emit('config/update', { theme: 'dark' }, true);
152
249
 
153
250
  // Later subscribers will immediately receive the retained data
154
- events.on('config/update', (config) => {
155
- console.log('Config:', config); // Immediately outputs: Config: { theme: 'dark' }
251
+ events.on('config/update', (message) => {
252
+ console.log('Config:', message.payload); // Immediately outputs: Config: { theme: 'dark' }
156
253
  });
157
254
  ```
158
255
 
@@ -164,7 +261,7 @@ By default, '/' is used as the event path delimiter, but you can use custom deli
164
261
 
165
262
  ```typescript
166
263
  const events = new FastEvent({
167
- delimiter: '.'
264
+ delimiter: '.',
168
265
  });
169
266
  ```
170
267
 
@@ -175,12 +272,12 @@ Use `onAny` to listen to all events:
175
272
  ```typescript
176
273
  const events = new FastEvent();
177
274
 
178
- events.onAny((data, meta) => {
179
- console.log(`Event ${meta.type} triggered:`, data);
275
+ events.onAny((message) => {
276
+ console.log(`Event ${message.type} triggered:`, message.payload);
180
277
  });
181
278
 
182
- events.emit('user/login', { id: 1 }); // Output: Event user/login triggered: { id: 1 }
183
- events.emit('system/error', 'Connection failed'); // Output: Event system/error triggered: Connection failed
279
+ // Can also use prepend option
280
+ events.onAny(handler, { prepend: true });
184
281
  ```
185
282
 
186
283
  ## Metadata (Meta)
@@ -193,15 +290,15 @@ Set global metadata when creating a FastEvent instance:
193
290
 
194
291
  ```typescript
195
292
  const events = new FastEvent({
196
- meta: {
197
- version: '1.0',
198
- environment: 'production'
199
- }
293
+ meta: {
294
+ version: '1.0',
295
+ environment: 'production',
296
+ },
200
297
  });
201
298
 
202
- events.on('user/login', (data, meta) => {
203
- console.log('Event data:', data);
204
- console.log('Metadata:', meta); // Contains type, version, and environment
299
+ events.on('user/login', (message) => {
300
+ console.log('Event data:', message.payload);
301
+ console.log('Metadata:', message.meta); // Contains type, version, and environment
205
302
  });
206
303
  ```
207
304
 
@@ -211,20 +308,21 @@ Additional metadata can be passed when publishing events, which will be merged w
211
308
 
212
309
  ```typescript
213
310
  const events = new FastEvent({
214
- meta: { app: 'MyApp' }
311
+ meta: { app: 'MyApp' },
215
312
  });
216
313
 
217
314
  // Add specific metadata when publishing event
218
- events.emit('order/create',
219
- { orderId: '123' }, // Event data
220
- false, // Don't retain
221
- { timestamp: Date.now() } // Event-specific metadata
315
+ events.emit(
316
+ 'order/create',
317
+ { orderId: '123' }, // Event data
318
+ false, // Don't retain
319
+ { timestamp: Date.now() }, // Event-specific metadata
222
320
  );
223
321
 
224
322
  // Listener receives merged metadata
225
- events.on('order/create', (data, meta) => {
226
- console.log('Order:', data); // { orderId: '123' }
227
- console.log('Metadata:', meta); // { type: 'order/create', app: 'MyApp', timestamp: ... }
323
+ events.on('order/create', (message) => {
324
+ console.log('Order:', message.payload); // { orderId: '123' }
325
+ console.log('Metadata:', message.meta); // { type: 'order/create', app: 'MyApp', timestamp: ... }
228
326
  });
229
327
  ```
230
328
 
@@ -234,20 +332,114 @@ FastEvent provides error handling mechanisms:
234
332
 
235
333
  ```typescript
236
334
  const events = new FastEvent({
237
- ignoreErrors: true, // Default is true, won't throw errors
238
- onListenerError: (type, error) => {
239
- console.error(`Error handling event ${type}:`, error);
240
- }
335
+ ignoreErrors: true, // Default is true, won't throw errors
336
+ onListenerError: (type, error) => {
337
+ console.error(`Error handling event ${type}:`, error);
338
+ },
241
339
  });
242
340
 
243
341
  events.on('process', () => {
244
- throw new Error('Processing failed');
342
+ throw new Error('Processing failed');
245
343
  });
246
344
 
247
345
  // Won't throw error, will trigger onListenerError instead
248
346
  events.emit('process');
249
347
  ```
250
348
 
349
+ ## Generic Parameters
350
+
351
+ FastEvent supports three generic type parameters for precise type control:
352
+
353
+ ```typescript
354
+ class FastEvent<
355
+ Events extends Record<string, any> = Record<string, any>,
356
+ Meta extends Record<string, any> = Record<string, any>,
357
+ Types extends keyof Events = keyof Events
358
+ >
359
+ ```
360
+
361
+ 1. `Events`: Defines the mapping between event types and their payload types
362
+ 2. `Meta`: Defines the type of metadata that can be attached to events
363
+ 3. `Types`: The union type of all event types (usually inferred from Events)
364
+
365
+ ### Basic Type Safety
366
+
367
+ ```typescript
368
+ // Define event types
369
+ interface MyEvents {
370
+ 'user/login': { id: number; name: string };
371
+ 'user/logout': { id: number };
372
+ }
373
+
374
+ // Create typed event emitter
375
+ const events = new FastEvent<MyEvents>();
376
+
377
+ // Type checking for event names and payload
378
+ events.on('user/login', (message) => {
379
+ // message.payload is typed as { id: number; name: string }
380
+ const { id, name } = message.payload;
381
+ });
382
+
383
+ // Error: wrong event name
384
+ events.emit('wrong/event', {});
385
+
386
+ // Error: wrong payload type
387
+ events.emit('user/login', { wrong: 'type' });
388
+ ```
389
+
390
+ ### Custom Metadata Types
391
+
392
+ ```typescript
393
+ // Define metadata structure
394
+ interface MyMeta {
395
+ timestamp: number;
396
+ source: string;
397
+ }
398
+
399
+ // Define events with custom metadata
400
+ const events = new FastEvent<MyEvents, MyMeta>();
401
+
402
+ events.on('user/login', (message) => {
403
+ // message.meta is typed as MyMeta
404
+ const { timestamp, source } = message.meta;
405
+ console.log(`Login from ${source} at ${timestamp}`);
406
+ });
407
+
408
+ // Emit with typed metadata
409
+ events.emit('user/login', { id: 1, name: 'Alice' }, false, { timestamp: Date.now(), source: 'web' });
410
+ ```
411
+
412
+ ### Advanced Type Usage
413
+
414
+ ```typescript
415
+ // Define events with different payload types
416
+ interface ComplexEvents {
417
+ 'data/number': number;
418
+ 'data/string': string;
419
+ 'data/object': { value: any };
420
+ }
421
+
422
+ const events = new FastEvent<ComplexEvents>();
423
+
424
+ // TypeScript ensures type safety for each event
425
+ events.on('data/number', (message) => {
426
+ const sum = message.payload + 1; // payload is typed as number
427
+ });
428
+
429
+ events.on('data/string', (message) => {
430
+ const upper = message.payload.toUpperCase(); // payload is typed as string
431
+ });
432
+
433
+ events.on('data/object', (message) => {
434
+ const value = message.payload.value; // payload is typed as { value: any }
435
+ });
436
+
437
+ // All emissions are type checked
438
+ events.emit('data/number', 42);
439
+ events.emit('data/string', 'hello');
440
+ events.emit('data/object', { value: true });
441
+ ```
442
+
251
443
  ## Custom Options
252
444
 
253
445
  The FastEvent constructor supports multiple options:
@@ -255,18 +447,18 @@ The FastEvent constructor supports multiple options:
255
447
  ```typescript
256
448
  const events = new FastEvent({
257
449
  // Event path delimiter, default is '/'
258
- delimiter: '.',
450
+ delimiter: '.',
259
451
  // Context for event handlers
260
- context: null,
452
+ context: null,
261
453
  // Metadata, passed to all event handlers
262
454
  meta: { ... },
263
-
455
+
264
456
  // Error handling
265
457
  ignoreErrors: true,
266
458
  onListenerError: (type, error) => {
267
459
  console.error(`Event error:`, type, error);
268
460
  },
269
-
461
+
270
462
  // Callbacks for listener addition/removal
271
463
  onAddListener: (path, listener) => {
272
464
  console.log('Listener added:', path);
@@ -274,9 +466,26 @@ const events = new FastEvent({
274
466
  onRemoveListener: (path, listener) => {
275
467
  console.log('Listener removed:', path);
276
468
  }
469
+ onClearListeners: () => {
470
+ console.log('清空监听器:', path);
471
+ },
472
+ onExecuteListener: (message: FastEventMessage, returns: any[], listeners: (FastEventListener<any, any, any> | [FastEventListener<any, any>, number])[]) => {
473
+ console.log('监听器执行后的回调:');
474
+ }
475
+ });
476
+ ```
477
+
478
+ ### debug
479
+
480
+ Use `debug` option to enable debug mode and import `fastevent/devtools`, so that you can see the events in `Redux Dev Tools`.
481
+
482
+ ```ts
483
+ import 'fastevent/devtools';
484
+ const emitter = new FastEvent({
485
+ debug: true,
277
486
  });
278
487
  ```
279
488
 
280
489
  # Performance
281
490
 
282
- ![](./bench.png)
491
+ ![](./bench.png)