@asaidimu/events
v1.1.2
Published
A lightweight, type-safe event bus implementation for TypeScript applications with zero dependencies.
Readme
@asaidimu/events
A lightweight, type-safe event bus implementation for TypeScript applications with zero dependencies.
📚 Table of Contents
- Overview & Features
- Installation & Setup
- Usage Documentation
- API Reference
- Performance Tips
- Project Architecture
- Development & Contributing
- Additional Information
Overview & Features
@asaidimu/events provides a robust, highly performant, and fully type-safe event bus solution for modern TypeScript applications. Designed with a minimalist philosophy, it boasts zero external runtime dependencies, ensuring a lean footprint and maximum compatibility. This library is ideal for managing application-wide communication, decoupling components, and facilitating seamless data flow in both single-page applications and multi-tab browser environments.
Beyond basic publish-subscribe patterns, @asaidimu/events offers advanced features like asynchronous event processing with configurable batching, built-in performance metrics to monitor event dispatch and listener execution, and robust cross-tab communication leveraging the BroadcastChannel API. It empowers developers to build scalable and responsive applications by providing a centralized, efficient, and predictable messaging system.
Key Features
- 🎯 Fully Type-Safe: Define event names and their corresponding payload types for compile-time safety and autocompletion.
- ⚡ High-Performance: Optimized for fast event emission and subscription, with internal caching of subscribers for efficiency.
- 🎮 Async & Batched Processing: Supports asynchronous event processing with configurable batch size and delay, preventing UI freezes and optimizing resource usage for high-frequency events.
- 📊 Built-in Performance Monitoring: Provides real-time metrics on total events emitted, active subscriptions, event-specific counts, and average emission duration.
- 🧹 Automatic Memory Management: Returns unsubscribe functions for easy cleanup, helping prevent memory leaks in long-running applications or dynamic components.
- 🌐 Cross-Tab Notifications: Seamlessly broadcast events across multiple browser tabs or windows using the
BroadcastChannelAPI. - 🚨 Custom Error Handling: Integrate your own error logging and recovery logic for event processing failures.
- 0️⃣ Zero Dependencies: A truly lightweight solution, ensuring minimal bundle size and no dependency conflicts.
Installation & Setup
Prerequisites
- Node.js (v18 or higher recommended) or Bun (v1.0.0 or higher recommended)
- TypeScript (v5.0.3 or higher recommended)
Installation Steps
Install @asaidimu/events using your preferred package manager:
# npm
npm install @asaidimu/events
# yarn
yarn add @asaidimu/events
# pnpm
pnpm add @asaidimu/events
# bun
bun add @asaidimu/eventsConfiguration
The createEventBus function accepts an optional options object to customize its behavior.
import { createEventBus } from '@asaidimu/events';
interface AppEvents {
'user:loggedIn': { userId: string };
'data:fetched': { data: any[] };
}
const defaultOptions = {
async: false, // Process events synchronously by default
batchSize: 1000, // Max events to process in one async batch
batchDelay: 16, // Delay in milliseconds before processing a batch
errorHandler: (error: EventError) => console.error('EventBus Error:', error), // Default error handler
crossTab: false, // Disable cross-tab communication by default
channelName: 'event-bus-channel' // Default channel name for BroadcastChannel
};
// Example with custom options
const bus = createEventBus<AppEvents>({
async: true,
batchSize: 500,
batchDelay: 32,
errorHandler: (err) => {
console.error('Caught EventBus error:', err.eventName, err.payload, err.message);
},
crossTab: true,
channelName: 'my-custom-app-channel'
});Verification
To verify the installation, create a simple TypeScript file (e.g., test.ts) and run it:
// test.ts
import { createEventBus } from '@asaidimu/events';
interface MyEvents {
'ping': string;
'pong': number;
}
const bus = createEventBus<MyEvents>();
const unsubscribePing = bus.subscribe('ping', (message) => {
console.log(`Received ping: ${message}`);
});
bus.emit({ name: 'ping', payload: 'hello' });
bus.emit({ name: 'pong', payload: 123 }); // This will not trigger a listener as nothing is subscribed to 'pong'
// Expected output: Received ping: hello
unsubscribePing();
console.log('Ping subscription unsubscribed.');
// No output after this:
bus.emit({ name: 'ping', payload: 'another ping' });Run with ts-node or compile and run:
# Using ts-node
npx ts-node test.ts
# Or compile and run
npx tsc test.ts
node test.jsUsage Documentation
Basic Usage
Define your event map interface, then create a type-safe event bus instance.
import { createEventBus } from '@asaidimu/events';
// 1. Define your application's event types
interface AppEvents {
'user:created': { id: string; name: string; email: string };
'order:placed': { orderId: string; userId: string; amount: number };
'notification:sent': { type: 'email' | 'sms'; recipient: string; message: string };
}
// 2. Create a type-safe event bus instance
const appBus = createEventBus<AppEvents>();
// 3. Subscribe to events
const unsubscribeUserCreated = appBus.subscribe('user:created', (user) => {
console.log(`[Event: user:created] New user registered: ${user.name} (${user.email})`);
});
const unsubscribeOrderPlaced = appBus.subscribe('order:placed', (order) => {
console.log(`[Event: order:placed] Order ${order.orderId} placed by user ${order.userId} for $${order.amount}`);
});
// A single event can have multiple listeners
appBus.subscribe('user:created', (user) => {
console.log(`[Secondary Listener] Sending welcome email to: ${user.email}`);
appBus.emit({ name: 'notification:sent', payload: { type: 'email', recipient: user.email, message: 'Welcome!' } });
});
appBus.subscribe('notification:sent', (notification) => {
console.log(`[Event: notification:sent] ${notification.type} to ${notification.recipient}: "${notification.message}"`);
});
// 4. Emit events with their corresponding payloads
appBus.emit({
name: 'user:created',
payload: { id: 'usr-123', name: 'Alice Smith', email: '[email protected]' }
});
appBus.emit({
name: 'order:placed',
payload: { orderId: 'ord-456', userId: 'usr-123', amount: 99.99 }
});
// Example of incorrect type usage (will cause a TypeScript error)
// appBus.emit({ name: 'user:created', payload: { userId: 'abc' } }); // Error: 'userId' does not exist in type '{ id: string; name: string; email: string; }'
// 5. Unsubscribe when listeners are no longer needed
// This is crucial for preventing memory leaks in SPAs or dynamic components.
unsubscribeUserCreated();
console.log('Unsubscribed from user:created events.');
// This event will not be logged by the first 'user:created' listener, but the secondary one will still fire.
appBus.emit({
name: 'user:created',
payload: { id: 'usr-456', name: 'Bob Johnson', email: '[email protected]' }
});
// To clear all subscriptions and reset metrics (useful for testing or full app shutdown)
appBus.clear();
console.log('Event bus cleared. All subscriptions removed.');Advanced Configuration
The createEventBus function accepts an optional configuration object to tailor its behavior.
import { createEventBus, EventError } from '@asaidimu/events';
interface HighFrequencyEvents {
'mouse:move': { x: number; y: number };
'sensor:data': { sensorId: string; value: number };
}
const highLoadBus = createEventBus<HighFrequencyEvents>({
async: true, // Process events asynchronously to avoid blocking the main thread
batchSize: 500, // Process up to 500 events in a single batch
batchDelay: 50, // Wait 50ms before processing a batch (if batchSize not met)
errorHandler: (error: EventError) => { // Custom error handling for failed listener executions
console.error(`[Custom Error Handler] Event '${error.eventName}' failed:`, error.message, 'Payload:', error.payload);
// You might send this error to a logging service, e.g., Sentry, Bugsnag
},
crossTab: true, // Enable cross-tab notifications (events will be broadcast to other tabs)
channelName: 'my-app-events-channel' // Unique channel name for BroadcastChannel
});
// Example usage with high frequency events
highLoadBus.subscribe('mouse:move', (coords) => {
// console.log(`Mouse moved to (${coords.x}, ${coords.y})`); // Too noisy for console
});
for (let i = 0; i < 10000; i++) {
highLoadBus.emit({
name: 'mouse:move',
payload: { x: Math.random() * 1000, y: Math.random() * 800 }
});
}
// Emitting an event that might cause an error in a listener
highLoadBus.subscribe('sensor:data', (data) => {
if (data.value < 0) {
throw new Error('Negative sensor value detected!');
}
console.log(`Sensor ${data.sensorId} reading: ${data.value}`);
});
highLoadBus.emit({ name: 'sensor:data', payload: { sensorId: 'temp-001', value: 25.5 } });
highLoadBus.emit({ name: 'sensor:data', payload: { sensorId: 'temp-002', value: -5.0 } }); // This will trigger the errorHandlerCross-Tab Notifications
When crossTab is enabled, events emitted in one browser tab are automatically broadcasted to other tabs with the same event bus configuration (channelName). This feature leverages the browser's BroadcastChannel API.
// --- In Browser Tab 1 (e.g., index.html) ---
import { createEventBus } from '@asaidimu/events';
interface ChatEvents {
'chat:message': { sender: string; text: string; timestamp: number };
'user:typing': { username: string };
}
// Create a bus instance with crossTab enabled and a unique channel name
const chatBus = createEventBus<ChatEvents>({
crossTab: true,
channelName: 'my-chat-app-channel'
});
// Tab 1 subscribes to messages
chatBus.subscribe('chat:message', (msg) => {
console.log(`[Tab 1] Received message from ${msg.sender}: "${msg.text}" at ${new Date(msg.timestamp).toLocaleTimeString()}`);
});
chatBus.subscribe('user:typing', (data) => {
console.log(`[Tab 1] ${data.username} is typing...`);
});
console.log('[Tab 1] Chat bus initialized, waiting for messages...');
// Tab 1 emits a message after a delay
setTimeout(() => {
chatBus.emit({
name: 'chat:message',
payload: { sender: 'Alice', text: 'Hello everyone!', timestamp: Date.now() }
});
console.log('[Tab 1] Emitted: Hello everyone!');
}, 2000);
// --- In Browser Tab 2 (e.g., another_page.html) ---
// (Imagine this code runs in a separate browser tab/window)
import { createEventBus } from '@asaidimu/events';
interface ChatEvents { // Must use the same event map interface
'chat:message': { sender: string; text: string; timestamp: number };
'user:typing': { username: string };
}
const chatBus2 = createEventBus<ChatEvents>({
crossTab: true,
channelName: 'my-chat-app-channel' // Crucially, use the same channel name
});
// Tab 2 also subscribes to messages
chatBus2.subscribe('chat:message', (msg) => {
console.log(`[Tab 2] Received message from ${msg.sender}: "${msg.text}" at ${new Date(msg.timestamp).toLocaleTimeString()}`);
});
chatBus2.subscribe('user:typing', (data) => {
console.log(`[Tab 2] ${data.username} is typing...`);
});
console.log('[Tab 2] Chat bus initialized, waiting for messages...');
// Tab 2 emits a typing event
setTimeout(() => {
chatBus2.emit({
name: 'user:typing',
payload: { username: 'Bob' }
});
console.log('[Tab 2] Emitted: Bob is typing...');
}, 4000);
// Expected Output (in both tabs, potentially with slight timing differences):
// [Tab 1] Chat bus initialized, waiting for messages...
// [Tab 2] Chat bus initialized, waiting for messages...
// ... (2 seconds later) ...
// [Tab 1] Emitted: Hello everyone!
// [Tab 1] Received message from Alice: "Hello everyone!" at [time]
// [Tab 2] Received message from Alice: "Hello everyone!" at [time]
// ... (2 seconds later) ...
// [Tab 2] Emitted: Bob is typing...
// [Tab 1] Bob is typing...
// [Tab 2] Bob is typing...Note: If BroadcastChannel is not supported by the browser environment, a warning will be logged, and cross-tab functionality will be disabled gracefully.
Performance Monitoring
Access real-time metrics about your event bus's activity and performance using the getMetrics() method.
import { createEventBus } from '@asaidimu/events';
interface MetricsEvents {
'data:processed': { count: number };
'api:request': { endpoint: string };
}
const perfBus = createEventBus<MetricsEvents>();
perfBus.subscribe('data:processed', (data) => {
// Simulate some work
for (let i = 0; i < 10000; i++) Math.sqrt(i);
console.log(`Processed ${data.count} items.`);
});
perfBus.subscribe('api:request', (req) => {
console.log(`API request to ${req.endpoint}.`);
});
perfBus.emit({ name: 'data:processed', payload: { count: 10 } });
perfBus.emit({ name: 'api:request', payload: { endpoint: '/users' } });
perfBus.emit({ name: 'data:processed', payload: { count: 25 } });
// After some operations, retrieve metrics
const metrics = perfBus.getMetrics();
console.log('\n--- Event Bus Metrics ---');
console.log(`Total Events Emitted: ${metrics.totalEvents}`); // Total count of all emitted events
console.log(`Active Subscriptions: ${metrics.activeSubscriptions}`); // Total number of currently active subscriptions
console.log('Event Emission Counts:');
metrics.eventCounts.forEach((count, eventName) => {
console.log(` - ${eventName}: ${count} times`); // How many times each specific event was emitted
});
console.log(`Average Emit Duration: ${metrics.averageEmitDuration.toFixed(2)} ms`); // Average time taken to emit an event and execute its listeners
console.log('-------------------------');
/* Expected example output:
--- Event Bus Metrics ---
Total Events Emitted: 3
Active Subscriptions: 2
Event Emission Counts:
- data:processed: 2 times
- api:request: 1 times
Average Emit Duration: XX.XX ms (will vary based on system performance)
-------------------------
*/Memory Management
Always remember to unsubscribe from events when the listener is no longer needed (e.g., when a component unmounts in a UI framework). The subscribe method returns an unsubscribe function for this purpose.
import { createEventBus } from '@asaidimu/events';
interface ComponentEvents {
'component:mounted': void;
'component:unmounted': void;
'data:updated': { value: number };
}
const componentBus = createEventBus<ComponentEvents>();
class MyComponent {
private name: string;
private unsubscribes: Array<() => void> = [];
constructor(name: string) {
this.name = name;
}
mount() {
console.log(`[${this.name}] Component mounted.`);
// Store the unsubscribe function
this.unsubscribes.push(
componentBus.subscribe('data:updated', this.handleDataUpdate)
);
// Subscribe to a general event, but ensure it's cleaned up
this.unsubscribes.push(
componentBus.subscribe('component:unmounted', this.cleanupInternalState)
);
componentBus.emit({ name: 'component:mounted', payload: undefined });
}
private handleDataUpdate = (data: { value: number }) => {
console.log(`[${this.name}] Data updated: ${data.value}`);
};
private cleanupInternalState = () => {
console.log(`[${this.name}] Internal state cleaned up.`);
// Perform any other cleanup specific to this component instance
}
unmount() {
console.log(`[${this.name}] Component unmounting. Cleaning up subscriptions.`);
// Call all stored unsubscribe functions
this.unsubscribes.forEach(unsub => unsub());
this.unsubscribes = []; // Clear the array
componentBus.emit({ name: 'component:unmounted', payload: undefined });
}
}
const componentA = new MyComponent('ComponentA');
const componentB = new MyComponent('ComponentB');
componentA.mount();
componentB.mount();
componentBus.emit({ name: 'data:updated', payload: { value: 100 } }); // Both components will receive
componentA.unmount(); // ComponentA's subscriptions are removed
componentBus.emit({ name: 'data:updated', payload: { value: 200 } }); // Only ComponentB will receive
componentB.unmount(); // ComponentB's subscriptions are removed
// No output after this:
componentBus.emit({ name: 'data:updated', payload: { value: 300 } });Custom Error Handling
You can provide a custom errorHandler function in the createEventBus options. This function will be called if any subscriber callback throws an uncaught error during synchronous event emission.
import { createEventBus, EventError } from '@asaidimu/events';
interface ErrorProneEvents {
'process:item': { itemId: string; data: any };
}
const errorBus = createEventBus<ErrorProneEvents>({
errorHandler: (error: EventError) => {
console.error('--- Custom Error Handler ---');
console.error(`Error processing event: "${error.eventName}"`);
console.error(`Payload was:`, JSON.stringify(error.payload, null, 2));
console.error(`Original error: ${error.message}`);
if (error.stack) {
console.error('Stack trace:', error.stack);
}
console.error('--------------------------');
// Here, you would typically log to an external service or display a user notification
}
});
errorBus.subscribe('process:item', (item) => {
if (!item.data || item.data.value === undefined) {
throw new Error(`Invalid data for item ${item.itemId}: 'value' property missing.`);
}
if (item.data.value < 0) {
throw new Error(`Negative value not allowed for item ${item.itemId}.`);
}
console.log(`Successfully processed item ${item.itemId} with value ${item.data.value}`);
});
// This will be processed successfully
errorBus.emit({ name: 'process:item', payload: { itemId: 'A1', data: { value: 42 } } });
// This will trigger the custom error handler due to missing 'value'
errorBus.emit({ name: 'process:item', payload: { itemId: 'B2', data: { status: 'incomplete' } } });
// This will trigger the custom error handler due to negative value
errorBus.emit({ name: 'process:item', payload: { itemId: 'C3', data: { value: -10 } } });
console.log('Events emitted. Check console for custom error handling output.');API Reference
createEventBus<TEventMap extends Record<string, any>>(options?: EventBusOptions)
A factory function that creates and returns a new event bus instance.
The TEventMap generic type argument is crucial for providing type-safety to your events.
EventBusOptions (Type Definition)
interface EventBusOptions {
/**
* Whether events should be processed asynchronously in batches.
* If false, events are processed synchronously.
* @default false
*/
async?: boolean;
/**
* The maximum number of events to batch together before processing.
* Only applicable when `async` is true.
* @default 1000
*/
batchSize?: number;
/**
* The delay in milliseconds before processing a batch of events.
* Events are also processed immediately if `batchSize` is reached.
* Only applicable when `async` is true.
* @default 16
*/
batchDelay?: number;
/**
* A custom error handler function to be called if an error occurs
* within a subscriber callback during event emission.
* @default (error) => console.error('EventBus Error:', error)
*/
errorHandler?: (error: EventError) => void;
/**
* Whether to enable cross-tab communication using `BroadcastChannel`.
* Events emitted will be broadcast to other browser tabs/windows
* initialized with the same `channelName`.
* @default false
*/
crossTab?: boolean;
/**
* A unique name for the `BroadcastChannel`.
* Essential for isolating event buses across different parts of an application
* or different applications running on the same domain.
* Only applicable when `crossTab` is true.
* @default 'event-bus-channel'
*/
channelName?: string;
}Returns
An EventBus<TEventMap> instance with the following methods:
subscribe<TEventName extends keyof TEventMap>(eventName: TEventName, callback: (payload: TEventMap[TEventName]) => void): () => void- Subscribes a
callbackfunction to a specificeventName. - The
callbackreceives the event's payload, correctly typed according toTEventMap. - Returns: An
unsubscribefunction. Call this function to remove the subscription.
- Subscribes a
emit<TEventName extends keyof TEventMap>(event: { name: TEventName; payload: TEventMap[TEventName] }): void- Emits an event with the specified
nameandpayload. - The
payloadmust match the type defined forTEventNameinTEventMap. - All subscribed listeners for that event will be invoked (either synchronously or asynchronously based on configuration).
- Emits an event with the specified
getMetrics(): EventMetrics- Retrieves various performance and usage metrics of the event bus.
- Returns: An
EventMetricsobject.
clear(): void- Removes all active subscriptions from the event bus.
- Resets all internal metrics (
totalEvents,eventCounts, etc.) to zero. - Closes the
BroadcastChannelifcrossTabwas enabled.
EventMetrics (Type Definition)
interface EventMetrics {
/** Total number of events emitted since initialization or last clear. */
totalEvents: number;
/** Number of active subscriptions. */
activeSubscriptions: number;
/** Map of event names to their emission counts. */
eventCounts: Map<string, number>;
/** Average duration of event emission in milliseconds. */
averageEmitDuration: number;
}EventError (Type Definition)
interface EventError extends Error {
/** Optional name of the event that caused the error. */
eventName?: string;
/** Optional payload that caused the error. */
payload?: unknown;
}Performance Tips
- Clean up subscriptions: Always call the
unsubscribefunction returned bysubscribewhen a listener is no longer needed. This prevents memory leaks, especially in single-page applications with dynamic components. - Batch processing: For scenarios involving frequent event emissions (e.g., mouse movements, sensor data), enable
async: trueand adjustbatchSizeandbatchDelayto optimize performance and prevent blocking the main thread. - Error handling: Provide a custom
errorHandlerin production environments to gracefully handle exceptions in subscriber callbacks and integrate with your application's logging or monitoring systems. - Monitor performance: Regularly use
getMetrics()to understand the event bus's performance characteristics in your application, identify bottlenecks, or detect unexpected behavior. - Cross-tab uniqueness: If you have multiple distinct event buses within the same application or different applications on the same domain, ensure they use unique
channelNameoptions to prevent unintended cross-communication. - Payload Size: While the event bus itself is fast, large or complex payloads can still impact performance due to serialization/deserialization (especially with
BroadcastChannel) or heavy processing by listeners. Keep payloads concise and relevant.
Project Architecture
The library is structured for clarity, maintainability, and optimal performance.
.
├── dist/ # Compiled output (JavaScript, Declaration Files, and assets for NPM)
├── src/
│ ├── index.ts # Core EventBus implementation (createEventBus function, logic)
│ └── types.ts # TypeScript type definitions for EventBus interfaces and options
├── index.ts # Main entry point for the package (re-exports from src/)
├── package.json # Project metadata, dependencies, scripts, and build configurations
├── tsconfig.json # TypeScript compiler configuration
├── vitest.config.ts # Vitest testing framework configuration (including browser tests)
├── CHANGELOG.md # Automatically generated project changelog
├── LICENSE.md # MIT License details
└── README.md # This documentation fileCore Components
createEventBusFunction: The central factory function responsible for instantiating the event bus. It encapsulates the core logic, including subscriber management, event queuing, and metric tracking.EventBus<TEventMap>Interface: Defines the public API contract for the event bus instance, ensuring strict type-checking forsubscribe,emit,getMetrics, andclearmethods.EventMetricsInterface: Specifies the structure of performance data returned bygetMetrics, allowing consistent monitoring.subscribersMap: An internalMapthat storesSets of callback functions, efficiently mapping event names to their registered listeners.eventQueueArray: Used whenasyncprocessing is enabled, this array temporarily holds events before they are processed in batches.BroadcastChannelAPI: Leveraged for enabling communication between different browser tabs or windows, allowing events to propagate across the user's open sessions.
Data Flow
- Initialization:
createEventBusis called, setting up internal maps, queues, and (optionally) aBroadcastChannel. - Subscription: When
subscribe(eventName, callback)is called, thecallbackis added to aSetassociated witheventNamein thesubscribersmap. An internal cached array of callbacks is updated for quick iteration. - Emission (Synchronous): When
emit({ name, payload })is called withasync: false(default), the associated callbacks from thecachedArraysare immediately invoked. Metrics are updated. IfcrossTabis enabled, the event is also posted toBroadcastChannel. - Emission (Asynchronous): When
emit({ name, payload })is called withasync: true, the event is pushed intoeventQueue. A debounced function (debouncedProcess) schedules a batch processing. IfbatchSizeis met,processBatchis called immediately. The event is also immediately posted toBroadcastChannelifcrossTabis enabled. - Batch Processing:
processBatchtakes events fromeventQueue, iterates through them, and invokes their respective listeners. Metrics are updated after each event. - Cross-Tab Reception: If
crossTabis enabled, any event posted to theBroadcastChannelby another tab is received, and its listeners are invoked on the receiving bus instance. - Unsubscription: Calling the function returned by
subscriberemoves the specific callback from itsSet, and thecachedArraysare updated accordingly. If no callbacks remain for an event, its entry is removed from thesubscribersmap. - Clearing:
clear()empties all subscriber maps, resets metrics, and closes theBroadcastChannel.
Development & Contributing
Contributions are highly welcome! Whether it's bug reports, feature requests, or code contributions, your help makes this project better.
Development Setup
To set up the project for local development:
- Clone the repository:
git clone https://github.com/asaidimu/events.git cd events - Install dependencies:
This project uses
bunas its package manager, butnpm,yarn, orpnpmshould also work.bun install # or # npm install # yarn install # pnpm install - Build the project:
bun run build
Available Scripts
The package.json defines several scripts for common development tasks:
bun run ci: Installs project dependencies.bun run clean: Removes thedist/build directory.bun run prebuild: Executescleanand a custom script (.sync-package.ts) before building.bun run build: Compiles TypeScript source files into CommonJS and ES modules, and generates TypeScript declaration files (.d.ts). Usestsup.bun run postbuild: CopiesREADME.md,LICENSE.md, anddist.package.jsoninto thedist/folder, preparing for publishing.bun run test: Runs unit and integration tests usingvitest.bun run test:ci: Runs tests in a CI environment (headless).bun run test:browser: Runs tests specifically in a browser environment using Playwright.
Testing
Tests are written with Vitest and can be run from the command line. The project also supports browser testing via Playwright.
To run all tests:
bun testTo run tests in watch mode during development:
bun test --watchTo run tests specifically in a browser environment (requires Playwright setup):
bun test:browserContributing Guidelines
Please follow these guidelines when contributing:
- Fork the repository and clone it locally.
- Create a new branch for your feature or bug fix:
git checkout -b feature/your-feature-nameorfix/issue-description. - Make your changes.
- Ensure your code adheres to the project's coding standards (linting and formatting are enforced).
- Write tests for your changes to ensure functionality and prevent regressions.
- Ensure all existing tests pass.
- Write a clear and concise commit message following Conventional Commits specifications (e.g.,
feat: add new feature,fix: resolve bug). This helps with automated changelog generation and semantic versioning. - Push your branch to your fork.
- Open a Pull Request to the
mainbranch of the original repository. Provide a detailed description of your changes.
Issue Reporting
If you find a bug or have a feature request, please open an issue on the GitHub Issues page. When reporting a bug, please include:
- A clear and concise description of the issue.
- Steps to reproduce the behavior.
- Expected behavior.
- Actual behavior.
- Any relevant code snippets or screenshots.
- Your environment details (Node.js version, browser, OS).
Additional Information
Troubleshooting
BroadcastChannel is not supportedwarning: If you see this warning, it means your current JavaScript environment (e.g., an older browser, Node.js without a polyfill, or certain test environments like JSDOM without specific configurations) does not support theBroadcastChannelAPI. Cross-tab communication will automatically be disabled in this case, but the event bus will function normally within the single environment.- Events not firing in async mode: Ensure your application's event loop has time to process batches. If you're emitting very few events or your
batchDelayis too high, you might not see immediate results. Check yourbatchSizeandbatchDelayconfigurations. - Memory leaks: If your application experiences increasing memory usage, double-check that you are calling the
unsubscribe()function for everysubscribe()call when listeners are no longer needed, especially in dynamic UI components. - TypeScript errors: If you encounter TypeScript errors related to event names or payloads, verify that your
TEventMapinterface correctly defines all expected event names and their corresponding payload types. The type-safety is strict by design.
FAQ
Q: What is an event bus? A: An event bus is a pattern that enables different parts of your application to communicate with each other without direct dependencies. It allows components to "publish" (emit) events and other components to "subscribe" (listen) to those events, promoting a decoupled and modular architecture.
Q: Why is @asaidimu/events type-safe?
A: By defining an EventMap interface, TypeScript can enforce that event names used in emit and subscribe calls are valid, and that the payload matches the expected type for that specific event. This eliminates common runtime errors related to misspelled event names or incorrect data structures, improving development experience and code reliability.
Q: Why "zero dependencies"?
A: "Zero dependencies" means the published library (dist/package.json) does not require any other NPM packages to function at runtime. This keeps the bundle size minimal, reduces potential dependency conflicts, and simplifies maintenance. Development dependencies (like typescript, tsup, vitest) are only needed for building and testing the library itself.
Q: How does cross-tab communication work?
A: @asaidimu/events uses the browser's native BroadcastChannel API. When crossTab: true is set, events emitted by one instance of the event bus in a browser tab are automatically sent through a shared channel to all other instances of the event bus initialized with the same channelName in other tabs or windows of the same origin.
Q: Can I use this in Node.js?
A: Yes, the core event bus functionality (subscribe, emit, metrics, clear) works perfectly in Node.js environments. The crossTab feature, however, relies on BroadcastChannel, which is a browser API. While some Node.js environments might have polyfills, it's primarily designed for browser-based cross-tab communication.
Changelog
For a detailed history of changes, new features, and bug fixes, please refer to the CHANGELOG.md file.
License
This project is licensed under the MIT License. See the LICENSE.md file for full details.
Acknowledgments
This project is developed and maintained by Saidimu. Special thanks to the open-source community for the inspiration and tools that make such libraries possible. Built with ❤️ and TypeScript.
