npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

npm version License: MIT Build Status Tests


📚 Table of Contents


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 BroadcastChannel API.
  • 🚨 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/events

Configuration

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.js

Usage 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 errorHandler

Cross-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 callback function to a specific eventName.
    • The callback receives the event's payload, correctly typed according to TEventMap.
    • Returns: An unsubscribe function. Call this function to remove the subscription.
  • emit<TEventName extends keyof TEventMap>(event: { name: TEventName; payload: TEventMap[TEventName] }): void

    • Emits an event with the specified name and payload.
    • The payload must match the type defined for TEventName in TEventMap.
    • All subscribed listeners for that event will be invoked (either synchronously or asynchronously based on configuration).
  • getMetrics(): EventMetrics

    • Retrieves various performance and usage metrics of the event bus.
    • Returns: An EventMetrics object.
  • clear(): void

    • Removes all active subscriptions from the event bus.
    • Resets all internal metrics (totalEvents, eventCounts, etc.) to zero.
    • Closes the BroadcastChannel if crossTab was 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

  1. Clean up subscriptions: Always call the unsubscribe function returned by subscribe when a listener is no longer needed. This prevents memory leaks, especially in single-page applications with dynamic components.
  2. Batch processing: For scenarios involving frequent event emissions (e.g., mouse movements, sensor data), enable async: true and adjust batchSize and batchDelay to optimize performance and prevent blocking the main thread.
  3. Error handling: Provide a custom errorHandler in production environments to gracefully handle exceptions in subscriber callbacks and integrate with your application's logging or monitoring systems.
  4. Monitor performance: Regularly use getMetrics() to understand the event bus's performance characteristics in your application, identify bottlenecks, or detect unexpected behavior.
  5. 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 channelName options to prevent unintended cross-communication.
  6. 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 file

Core Components

  • createEventBus Function: 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 for subscribe, emit, getMetrics, and clear methods.
  • EventMetrics Interface: Specifies the structure of performance data returned by getMetrics, allowing consistent monitoring.
  • subscribers Map: An internal Map that stores Sets of callback functions, efficiently mapping event names to their registered listeners.
  • eventQueue Array: Used when async processing is enabled, this array temporarily holds events before they are processed in batches.
  • BroadcastChannel API: Leveraged for enabling communication between different browser tabs or windows, allowing events to propagate across the user's open sessions.

Data Flow

  1. Initialization: createEventBus is called, setting up internal maps, queues, and (optionally) a BroadcastChannel.
  2. Subscription: When subscribe(eventName, callback) is called, the callback is added to a Set associated with eventName in the subscribers map. An internal cached array of callbacks is updated for quick iteration.
  3. Emission (Synchronous): When emit({ name, payload }) is called with async: false (default), the associated callbacks from the cachedArrays are immediately invoked. Metrics are updated. If crossTab is enabled, the event is also posted to BroadcastChannel.
  4. Emission (Asynchronous): When emit({ name, payload }) is called with async: true, the event is pushed into eventQueue. A debounced function (debouncedProcess) schedules a batch processing. If batchSize is met, processBatch is called immediately. The event is also immediately posted to BroadcastChannel if crossTab is enabled.
  5. Batch Processing: processBatch takes events from eventQueue, iterates through them, and invokes their respective listeners. Metrics are updated after each event.
  6. Cross-Tab Reception: If crossTab is enabled, any event posted to the BroadcastChannel by another tab is received, and its listeners are invoked on the receiving bus instance.
  7. Unsubscription: Calling the function returned by subscribe removes the specific callback from its Set, and the cachedArrays are updated accordingly. If no callbacks remain for an event, its entry is removed from the subscribers map.
  8. Clearing: clear() empties all subscriber maps, resets metrics, and closes the BroadcastChannel.

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:

  1. Clone the repository:
    git clone https://github.com/asaidimu/events.git
    cd events
  2. Install dependencies: This project uses bun as its package manager, but npm, yarn, or pnpm should also work.
    bun install
    # or
    # npm install
    # yarn install
    # pnpm install
  3. 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 the dist/ build directory.
  • bun run prebuild: Executes clean and 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). Uses tsup.
  • bun run postbuild: Copies README.md, LICENSE.md, and dist.package.json into the dist/ folder, preparing for publishing.
  • bun run test: Runs unit and integration tests using vitest.
  • 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 test

To run tests in watch mode during development:

bun test --watch

To run tests specifically in a browser environment (requires Playwright setup):

bun test:browser

Contributing Guidelines

Please follow these guidelines when contributing:

  1. Fork the repository and clone it locally.
  2. Create a new branch for your feature or bug fix: git checkout -b feature/your-feature-name or fix/issue-description.
  3. Make your changes.
  4. Ensure your code adheres to the project's coding standards (linting and formatting are enforced).
  5. Write tests for your changes to ensure functionality and prevent regressions.
  6. Ensure all existing tests pass.
  7. 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.
  8. Push your branch to your fork.
  9. Open a Pull Request to the main branch 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 supported warning: 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 the BroadcastChannel API. 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 batchDelay is too high, you might not see immediate results. Check your batchSize and batchDelay configurations.
  • Memory leaks: If your application experiences increasing memory usage, double-check that you are calling the unsubscribe() function for every subscribe() 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 TEventMap interface 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.