UNPKG

typescript-event-emitter

Version:

Versatile and feature-rich TypeScript library for event management, providing a solid foundation for building event-driven applications in TypeScript.

864 lines (685 loc) 26.8 kB
# typescript-event-emitter Versatile and feature-rich TypeScript library for event management, providing a solid foundation for building event-driven applications in TypeScript. | \* | Version | Supported | | --- | --------- | ------------------ | | npm | >= 7.24.0 | :white_check_mark: | # main features 1. Throttling and Debouncing: - flexibility for handling events in scenarios where rapid or frequent triggering needs to be controlled. - advanced Throttling and Debouncing which supports options with leading/trailing edge control. This allows for fine-grained control over when event listeners are executed. 2. Wildcard Listeners: - supporting wildcard listeners with the '\*' namespace, allowing global event handling. 3. Namespace Support: - namespace support is a great way to organize and manage different types of events within your system. 4. Priority Queue: - prioritizing listeners based on a priority value ensures that critical listeners can be given precedence, offering more control over event execution order. 5. Event Filtering: - the ability to filter events based on registered filters provides a mechanism for selectively emitting events. 6. Async/Await Pattern: - leveraging async/await for asynchronous operations ensures that it can handle asynchronous listeners gracefully. 7. Global Event Bus Integration: - the integration with a global event bus enhances the modularity and usability of the event system. 8. Error Handling: - logging errors to the console. 9. Custom separator per listener and Global configs: - ability to set custom separator per listener which would override global separator dedicated for listeners. - ability to change global separator which is used for listeners where separator is not provided. 10. Concurrency: - Limits the number of concurrent executions for listeners, ensuring efficient handling of multiple events at once. 11. Subscription Management: - Allows users to easily manage and view their subscriptions to specific event types. 12. Event History: - Event history provides a mechanism to record and retrieve detailed logs of all events and listener executions. 13. Hooks / lifecycle methods: - Hooks or lifecycle methods allow you to execute custom logic at specific stages of an event’s lifecycle, such as before dispatch, after execution, or on listener completion. 14. Integrations: - Seamless integration hooks and composables are provided for Angular, React, and Vue, allowing you to easily subscribe to events, emit events, and access the full EventEmitter API in your favorite frontend framework. - React: Supports React 17 and above (including React 18+). The integration uses hooks and is compatible with modern React functional components. - Angular: Supports Angular 12 and above. The integration uses Angular services and RxJS, matching the structure of Angular 12+ projects. - Vue: Supports Vue 3 and above. The integration uses the Composition API (composables), which is standard in Vue 3+. ## installation ```bash $ npm install --save typescript-event-emitter ``` ## usage After installation, the only thing you need to do is require the module: ```bash import { EventEmitter } from 'typescript-event-emitter'; ``` or ```bash const { EventEmitter } = require('typescript-event-emitter'); ``` And you're ready to create your own EventEmitter instances. ### Base usage ```bash const emitter = new EventEmitter(); let context = { test: 'Some metada' } const onEventNameEmitted = (eventname:string, data:any) => { console.log(data === context) // true console.log(eventname === 'event-name') // true }; emitter.on('event-name', onEventNameEmitted); // adds listener emitter.emit('event-name', context); // emits listener ``` ### Remove listener ```bash const emitter = new EventEmitter(); const onEventNameEmitted = (eventname:string, data:any) => { console.log(eventname,data ) }; emitter.on('event-name', onEventNameEmitted);// adds listener emitter.off('event-name', onEventNameEmitted); // removes listener ``` ### Throttling 1. First emit: - The 'throttleEvent' is emitted. - The throttled listener is executed immediately, and callCount becomes 1. 2. Second emit (within the 100-millisecond throttle delay): - The 'throttleEvent' is emitted again. - This emit is ignored because throttling prevents the listener from being executed within the 100-millisecond throttle period. 3. Third emit (within the 100-millisecond throttle delay): - The 'throttleEvent' is emitted once more. - This emit is also ignored due to throttling. ```bash const emitter = new EventEmitter(); let callCount = 0; emitter.on( 'throttleEvent', () => { callCount++; }, { throttle: 100 } ); emitter.emit('throttleEvent'); emitter.emit('throttleEvent'); emitter.emit('throttleEvent'); ``` #### Advanced Throttling ```typescript // Simple throttling (executes on both leading and trailing edges) emitter.on('event', listener, { throttle: 1000 // 1 second }); // Advanced throttling with edge control emitter.on('event', listener, { throttle: { delay: 1000, // 1 second leading: true, // Execute on first call trailing: true // Execute on last call } }); ``` - **Leading Edge**: Best for actions that should happen instantly but not too often, like a "fire" button in a game that has a cooldown period. - **Trailing Edge**: Ideal for periodically processing a continuous stream of data, such as updating a live data chart every second with the latest value. - **Both Edges**: Perfect for responsive UI updates during continuous actions, like immediately indicating a window is being resized (leading) and then rendering the final content once the resizing pauses (trailing). ### Debouncing 1. First emit: - The 'debounceEvent' is emitted. - The debounced listener is called but not immediately executed due to the debounce delay. - The debounced function is scheduled to be executed after 100 milliseconds. 2. Second emit (within the debounce delay): - The 'debounceEvent' is emitted. - The debounced listener is called again, but the previous scheduled execution is canceled, and a new one is scheduled for 100 milliseconds from the latest emit. 3. Third emit (within the debounce delay): - The 'debounceEvent' is emitted. - The debounced listener is called once more, canceling the previous scheduled execution again and scheduling a new one for 100 milliseconds from this emit. So basically for the given example, the listener will be executed after 300 millisecond delay ```bash const emitter = new EventEmitter(); let callCount = 0; emitter.on( 'debounceEvent', () => { callCount++; }, { debounce: 100 } ); emitter.emit('debounceEvent'); emitter.emit('debounceEvent'); emitter.emit('debounceEvent'); ``` #### Advanced Debouncing ```typescript // Simple debouncing (executes only on trailing edge) emitter.on('event', listener, { debounce: 1000 // 1 second }); // Advanced debouncing with edge control emitter.on('event', listener, { debounce: { delay: 1000, // 1 second leading: false, // Don't execute on first call trailing: true // Execute on last call } }); ``` - **Leading Edge**: Useful for preventing accidental double-submissions, like clicking a "Purchase" button once and ignoring further frantic clicks. - **Trailing Edge**: Perfect for auto-saving a form or running a search query only after the user has finished typing for a moment. - **Both Edges**: Ideal for providing instant feedback and a final action, such as immediately showing a "saving..." spinner on the first keypress (leading) and then confirming "Saved!" after the user stops typing (trailing). ### Wildcard ```bash const emitter = new EventEmitter(); emitter.on('*', () => { // listener will be executed for both emits, wildcard listens to anything console.log("Executed") }); emitter.emit('someEvent'); emitter.emit('namespace.someEvent'); ``` ```bash const emitter = new EventEmitter(); emitter.on('namespace1.*', () => { // listener will be executed 2 times, wildcard for namespace listens to anything within that namespace console.log("Executed 1") }); emitter.on('namespace2.*', () => { // listener will not be executed console.log("Executed 2") }); emitter.emit('other.event1'); emitter.emit('namespace1.event1'); emitter.emit('namespace1.event2'); ``` ```bash const emitter = new EventEmitter(); emitter.on('*.someEvent', () => { // wildcard listeners as namespace for event console.log("Executed") }); emitter.emit('other.event1'); // No match, no listener executed emitter.emit('other.someEvent'); // Matches the pattern, listener executed emitter.emit('namespace1.event1'); // No match, no listener executed emitter.emit('namespace1.someEvent'); // Matches the pattern, listener executed ``` ### Namespace ```bash const emitter = new EventEmitter(); emitter.on('someEvent', () => { console.log('Listener'); }); emitter.on('namespace.someEvent', () => { console.log('Listener'); }); emitter.emit('other.event'); // No match, no listener executed emitter.emit('namespace.someEvent'); // Matches the pattern, listener executed emitter.emit('namespace.event'); // No match, no listener executed ``` ### Priority Queue ```bash const emitter = new EventEmitter(); //last to be executed emitter.on('priorityEvent', () => { console.log('Low Priority Listener'); }); //first to be executed emitter.on( 'priorityEvent', () => { console.log('High Priority Listener'); }, { priority: 2 } ); //second to be executed emitter.on( 'priorityEvent', () => { console.log('Medium Priority Listener'); }, { priority: 1 } ); emitter.emit('priorityEvent'); ``` ### Event Filtering ```bash interface Message { id: number; content: string; messageType: string; sender: string; } const emitter = new EventEmitter(); const currentUser = { username: 'example_user' }; const notificationFilter: EventFilter = (eventName, namespace) => { if (namespace === 'dm') { return true; } if (eventName === 'notification') { return true; } if (namespace === 'mention' && currentUser.username === eventName) { return true; } return false; }; const receivedNotifications: Message[] = []; emitter.on( '*', (_event, message: Message) => { receivedNotifications.push(message); //array will have: directMessage, generalNotification, mentionNotification objects }, { filter: notificationFilter } ); const directMessage: Message = { id: 1, content: 'Hello!', messageType: 'dm', sender: 'user123' }; const generalNotification: Message = { id: 2, content: 'General update', messageType: 'announcement', sender: 'system' }; const mentionNotification: Message = { id: 3, content: 'You were mentioned!', messageType: 'mention', sender: 'other_user' }; const unrelatedEvent: Message = { id: 4, content: 'Irrelevant event', messageType: 'other', sender: 'unknown' }; emitter.emit('other.event', unrelatedEvent), emitter.emit('notification', generalNotification), emitter.emit('dm.newMessage', directMessage), emitter.emit('mention.example_user', mentionNotification) ``` ### Async/Await Pattern ```bash const emitter = new EventEmitter(); let flag = false; emitter.on('asyncEvent', async () => { return new Promise<void>(resolve => { setTimeout(() => { flag = true; resolve(); }, 100); }); }); await emitter.emit('asyncEvent'); console.log(flag); // will be true ``` ### Error Handling ```bash const emitter = new EventEmitter(); emitter.on('errorEvent', () => { throw new Error('Listener Error'); }); try { await emitter.emit('errorEvent'); } catch (error) { console.log(error) // will be 'Listener Error' } ``` ```bash const emitter = new EventEmitter(); let firstListenerInvoked = false; let secondListenerInvoked = false; emitter.on('errorEvent', () => { throw new Error('Listener Error'); }); emitter.on('errorEvent', () => { firstListenerInvoked = true; }); emitter.on('errorEvent', () => { secondListenerInvoked = true; }); emitter.emit('errorEvent'); //all 3 will be fired and event flow won't be disrupted ``` ### Global Event Bus The global event bus is a singleton that contains an instance of an event emitter. Functionality/features, etc is just a centralized mechanism for communication across different parts of an application. ```bash import { globalEventBus } from 'typescript-event-emitter'; ``` or ```bash const { globalEventBus } = require('typescript-event-emitter'); ``` ```bash let context = { test: 'Some metada' } const onEventNameEmitted = (eventname:string, data:any) => { console.log(data === context) // true console.log(eventname === 'event-name') // true }; globalEventBus.on('event-name', onEventNameEmitted); // adds listener globalEventBus.emit('event-name', context); // emits listener ``` ### Custom separator per listener and Global configs ```bash const eventEmitter: EventEmitter = new EventEmitter({ separator: ':' }); // setting global separator if not provided it will revert to default "." eventEmitter.on("namespace:someEvent", () => {}); ``` ```bash const eventEmitter: EventEmitter = new EventEmitter(); // default separator '.' eventEmitter.setGlobalOptions({ separator: "-" }); // sets global separator which can be provided via constructor aswell eventEmitter.on("namespace-someEvent", () => {}); eventEmitter.off("namespace-someEvent"); eventEmitter.on("namespace:someEvent1", () => {}, { separator: ":" }); // listener separator will be ':' eventEmitter.off("namespace:someEvent1"); eventEmitter.on("namespace:someEvent2", () => {}, { separator: ":" }); // listener separator will be ':' eventEmitter.on("namespace-someEvent3", () => {}); // listener separator will be '-' as it was set via setGlobalOptions eventEmitter.off("namespace:someEvent2"); eventEmitter.off("namespace-someEvent3"); ``` ```bash const eventEmitter: EventEmitter = new EventEmitter({ separator: ':' }); // setting global separator if not provided it will revert to default "." globalEventBus.setGlobalOptions({ separator: "-" }); // sets global separator globalEventBus.on("namespace:someEvent", () => {}, { separator: ":" }); // listener separator will be ':' ``` ### Concurrency ```bash // Step 1: Create an instance of EventEmitter const emitter = new EventEmitter(); // Step 2: Add event listeners emitter.on('data:received', async (eventName, data) => { console.log(`Listener 1 started processing: ${data}`); await new Promise(resolve => setTimeout(resolve, 150)); // Simulate processing time console.log(`Listener 1 finished processing: ${data}`); }, { concurrency: 2 }); // Limit this listener to 2 concurrent executions emitter.on('data:received', async (eventName, data) => { console.log(`Listener 2 started processing: ${data}`); await new Promise(resolve => setTimeout(resolve, 200)); // Simulate processing time console.log(`Listener 2 finished processing: ${data}`); }, { concurrency: 1 }); // Limit this listener to 1 concurrent execution emitter.on('data:received', async (eventName, data) => { console.log(`Listener 3 started processing: ${data}`); await new Promise(resolve => setTimeout(resolve, 250)); // Simulate processing time console.log(`Listener 3 finished processing: ${data}`); }, { concurrency: 3 }); // Limit this listener to 3 concurrent executions // Step 3: Emit an event console.log('Emitting event: data:received'); await Promise.all([ emitter.emit('data:received', 'Payload 1'), emitter.emit('data:received', 'Payload 2'), emitter.emit('data:received', 'Payload 3'), emitter.emit('data:received', 'Payload 4'), ]); // Output // Emitting event: data:received // Listener 1 started processing: Payload 1 // Listener 2 started processing: Payload 1 // Listener 3 started processing: Payload 1 // Listener 1 started processing: Payload 2 // Listener 3 started processing: Payload 2 // Listener 3 started processing: Payload 3 // Listener 1 finished processing: Payload 1 // Listener 1 started processing: Payload 3 // Listener 1 finished processing: Payload 2 // Listener 1 started processing: Payload 4 // Listener 2 finished processing: Payload 1 // Listener 2 started processing: Payload 2 // Listener 3 finished processing: Payload 1 // Listener 3 started processing: Payload 4 // Listener 3 finished processing: Payload 2 // Listener 3 finished processing: Payload 3 // Listener 1 finished processing: Payload 3 // Listener 1 finished processing: Payload 4 // Listener 2 finished processing: Payload 2 // Listener 2 started processing: Payload 3 // Listener 3 finished processing: Payload 4 // Listener 2 finished processing: Payload 3 // Listener 2 started processing: Payload 4 // Listener 2 finished processing: Payload 4 ``` ### Subscription Management ```bash const emitter = new EventEmitter(); const event1 = 'namespace.event1'; const event2 = 'namespace.event2'; const listener1 = () => {}; const listener2 = () => {}; emitter.on(event1, listener1, { priority: 1, concurrency: 5 }); emitter.on(event1, listener2, { priority: 2 }); emitter.on(event2, listener1, { priority: 3 }); const subscriptions = emitter.subscriptions(); // Output // Lists array of all subscriptions to event key and listener count //[ // { // "event":"namespace.event1", // "listenerCount":2 // }, // { // "event":"namespace.event2", // "listenerCount":1 // } //] const result1 = emitter.inspectSubscription(event1); // Output of result1 // {UUID1} & {UUID2} are UUID string // Lists array of all subscriptions details for particular event // [ // { // id: {UUID1}, // priority: 2, // concurrency: null, // eventInfo: { separator: '.', event: 'namespace.event1' } // }, // { // id: {UUID2}, // priority: 1, // concurrency: 5, // eventInfo: { separator: '.', event: 'namespace.event1' } // } // ]; // replace {UUID1} with actual id emitter.removeSubscription(event1, {UUID1}); const result2 = emitter.inspectSubscription(event1); // Output // {UUID2} is UUID string // [ // { // id: {UUID2}, // priority: 1, // concurrency: 5, // eventInfo: { separator: '.', event: 'namespace.event1' } // } // ]; ``` ### Event History ```bash const emitter = new EventEmitter(); const testEvent1 = 'testEvent1'; const testEvent2 = 'testEvent2'; emitter.on(testEvent1, () => {}); emitter.on(testEvent2, () => {}); await emitter.emit(testEvent1); await emitter.emit(testEvent1); await emitter.emit(testEvent2); const allHistory = emitter.getAllEventHistory(); const specificHistory = emitter.getSpecificEventHistory(testEvent2); // Outcome allHistory // [ // { // event: 'testEvent1', // listenerId: '08f743fc-cd34-41af-9542-cef704f8892e', // timestamp: 1734170275281, // args: [] // }, // { // event: 'testEvent1', // listenerId: '08f743fc-cd34-41af-9542-cef704f8892e', // timestamp: 1734170275281, // args: [] // }, // { // event: 'testEvent2', // listenerId: '96ca9cd5-9ef6-4c6c-984a-d9657c98ea31', // timestamp: 1734170275281, // args: [] // } // ] // Outcome specificHistory // [ // { // event: 'testEvent2', // listenerId: '96ca9cd5-9ef6-4c6c-984a-d9657c98ea31', // timestamp: 1734170275281, // args: [] // } // ] ``` ### Hooks / lifecycle methods Lifecycle hooks allow you to register functions that are triggered before or after an event is emitted, regardless of which event it is. This is incredibly useful for cross-cutting concerns like logging, analytics, or transaction management. - **`before(hook)`**: Registers a function to run _before_ any listeners for an event are executed. - **`after(hook)`**: Registers a function to run _after_ all listeners for an event have completed. ```typescript const emitter = new EventEmitter(); const executionOrder = []; // Register a "before" hook for logging emitter.before((eventName, ...args) => { console.log(`About to emit '${eventName}' with args:`, args); executionOrder.push('before'); }); // Register an "after" hook for metrics emitter.after(eventName => { console.log(`Finished emitting '${eventName}'.`); executionOrder.push('after'); }); // Register a standard listener emitter.on('user:created', user => { console.log('User created:', user); executionOrder.push('listener'); }); await emitter.emit('user:created', { id: 1, name: 'John Doe' }); // Output will be logged in this order: // 1. "About to emit 'user:created' with args: [ { id: 1, name: 'John Doe' } ]" // 2. "User created: { id: 1, name: 'John Doe' }" // 3. "Finished emitting 'user:created'." console.log(executionOrder); // Will be ['before', 'listener', 'after'] // You can also remove hooks const myHook = () => console.log('This will be removed.'); emitter.before(myHook); emitter.removeBefore(myHook); // The hook is no longer active ``` ## Integrations ### Angular 1. Inject the service into your component or service: ```typescript import { Component, OnInit, OnDestroy } from '@angular/core'; import { EventEmitterService } from 'typescript-event-emitter/angular'; import { Subscription } from 'rxjs'; @Component({ selector: 'app-demo', template: `...` }) export class DemoComponent implements OnInit, OnDestroy { private subscription: Subscription; constructor(private eventEmitterService: EventEmitterService) {} ngOnInit() { this.subscription = this.eventEmitterService.fromEvent<string>('myEvent').subscribe( payload => { console.log('Received:', payload); }, { throttle: 500 } ); } ngOnDestroy() { this.subscription.unsubscribe(); } } ``` 2.To emit an event from anywhere you have access to the EventEmitter instance: ```typescript this.eventEmitterService['emitter'].emit('myEvent', 'Hello from Angular!'); // Or, if using the global event bus this.globalEventBus.emit('globalEvent', 'data'); ``` 3. Accessing the Full EventEmitter API in Angular can be via instance ```typescript this.eventEmitterService.instance.getAllEventHistory(); ``` ### React 1. Use the hook in your component to subscribe to events: ```typescript import React, { useEffect } from 'react'; import { useEventEmitter, EventEmitter } from 'typescript-event-emitter/react'; // Create or import your EventEmitter instance const emitter = new EventEmitter(); function DemoComponent() { // Subscribe to an event with options (e.g., throttle) useEventEmitter<string>( emitter, 'myEvent', (payload) => { console.log('Received:', payload); }, { throttle: 500 } ); useEffect(() => { // Emit an event emitter.emit('myEvent', 'Hello from React!'); }, []); return <div>Check the console for events!</div>; } ``` 2. Accessing the Full EventEmitter API in React ```typescript useEffect(() => { emitter.setGlobalOptions({ separator: ':' }); const history = emitter.getAllEventHistory(); console.log(history); }, []); ``` 3.Using the Global Event Bus in React ```typescript import { useGlobalEventBus, globalEventBus } from 'typescript-event-emitter/react'; function GlobalDemoComponent() { useGlobalEventBus<string>( 'globalEvent', (payload) => { console.log('Global event received:', payload); }, { debounce: 300 } ); useEffect(() => { globalEventBus.emit('globalEvent', 'Hello from the global bus!'); }, []); return <div>Check the console for global events!</div>; } ``` ### Vue 1. Use the composable in your component to subscribe to events: ```typescript <script setup lang="ts"> import { useEventEmitter, EventEmitter } from 'typescript-event-emitter/vue'; // Create or import your EventEmitter instance const emitter = new EventEmitter(); // Subscribe to an event with options (e.g., throttle) useEventEmitter<string>( emitter, 'myEvent', (payload) => { console.log('Received:', payload); }, { throttle: 500 } ); // To emit an event emitter.emit('myEvent', 'Hello from Vue!'); </script> ``` 2. Accessing the Full EventEmitter API in Vue ```typescript emitter.setGlobalOptions({ separator: ':' }); const history = emitter.getAllEventHistory(); console.log(history); ``` 3. Using the Global Event Bus in Vue ```typescript <script setup lang="ts"> import { useGlobalEventBus, globalEventBus } from 'typescript-event-emitter/vue'; // Subscribe to a global event useGlobalEventBus<string>( 'globalEvent', (payload) => { console.log('Global event received:', payload); }, { debounce: 300 } ); // Emit a global event globalEventBus.emit('globalEvent', 'Hello from the global bus!'); </script> ``` ## Tests This module is well-tested. You can run: `npm run test` to run the tests under Node.js. <br/> `npm run test:nyc` to run the tests under Node.js and get the coverage Tests are not included in the npm package. If you want to play with them, you must clone the GitHub repository. ## contributing Please read our [Contribution Guidelines](CONTRIBUTING.md) before contributing to this project. ## security Please read our [SECURITY REPORTS](SECURITY.md) ## license [MIT](LICENSE)