@asaidimu/utils-events
v1.0.3
Published
A lightweight, type-safe event bus implementation for TypeScript applications.
Downloads
469
Readme
@asaidimu/utils-events
A lightweight, type-safe event bus for TypeScript applications with batching, cross-instance broadcast, and built-in metrics.
Why another event bus? Many existing solutions either lack type safety or force a heavy dependency. This bus provides full TypeScript inference, optional batching to reduce synchronous dispatch overhead, and automatic cross-instance synchronisation – all in a tiny, zero-dependency package.
📚 Table of Contents
- Overview & Features
- Installation
- Quick Start
- API Reference
- Advanced Usage
- Architecture Notes
- Development & Contributing
- License
Overview & Features
@asaidimu/utils-events is a typed event bus designed for modern frontend and Node.js applications. It lets you define a strongly-typed event map once, then enjoy full autocompletion and compile-time checks for every emit, subscribe, and once call.
Key Features
- 🔒 Type-safe – infer payload types from a single
EventMapinterface. - ⚡ Batching (deferred mode) – coalesce rapid-fire events into a single flush to reduce layout thrashing and improve performance.
- 📡 Cross-instance broadcast – synchronise events across browser tabs using
BroadcastChannel(with graceful fallback). - 🩺 Built-in metrics – track total events, active subscriptions, per-event counts, and average dispatch duration.
- 🧹 Cleanup utilities – unsubscribe handles,
.clear()to fully reset the bus. - 🪶 Zero runtime dependencies – small footprint, easy to audit.
Installation
npm install @asaidimu/utils-eventsyarn add @asaidimu/utils-eventspnpm add @asaidimu/utils-eventsRequirements: TypeScript 4.7+ (for
Recordgeneric inference) and a modern runtime that supportsperformance.now()and optionallyBroadcastChannel.
Quick Start
import { createEventBus } from '@asaidimu/utils-events';
// 1. Define your event map
interface AppEvents {
userLogin: { userId: string; name: string };
dataUpdate: { records: number };
error: { message: string; code?: number };
}
// 2. Create the bus
const bus = createEventBus<AppEvents>();
// 3. Subscribe
const unsubscribe = bus.subscribe('userLogin', (payload) => {
console.log(`Welcome ${payload.name}!`);
});
// 4. Emit an event
bus.emit({ name: 'userLogin', payload: { userId: '123', name: 'Alice' } });
// Logs: "Welcome Alice!"
// 5. Unsubscribe when done
unsubscribe();API Reference
createEventBus
function createEventBus<TEventMap extends Record<string, any>>(
options?: EventBusOptions
): EventBus<TEventMap>Creates a new event bus instance.
Options
| Option | Type | Default | Description |
| -------------------- | ----------------------------- | --------------------------- | --------------------------------------------------------------------------- |
| batch.size | number | undefined | Enables deferred mode; flush when queue reaches this size. |
| batch.delay | number | 1000 (if batching) | Quiet period (ms) before flushing a batch. |
| errorHandler | (error: EventError) => void | console.error | Custom error handler for subscriber callbacks. |
| broadcast.channel | string | "event-bus-channel" | Enables cross-instance broadcast using the given BroadcastChannel name. |
If
batch.sizeis provided, the bus runs in deferred mode (events are queued and flushed asynchronously). Otherwise it runs in synchronous mode (events are dispatched immediately).
.subscribe()
subscribe<TEventName extends keyof TEventMap>(
eventName: TEventName,
callback: (payload: TEventMap[TEventName]) => void
): () => voidRegisters a permanent listener. Returns an unsubscribe function.
const off = bus.subscribe('dataUpdate', ({ records }) => {
updateUI(records);
});
// Later
off();.once()
once<TEventName extends keyof TEventMap>(
eventName: TEventName,
callback: (payload: TEventMap[TEventName]) => void
): () => voidRegisters a one-time listener that automatically unsubscribes after the first emission. Returns a cancel function (to unsubscribe before it fires).
bus.once('userLogin', (payload) => {
console.log('First login only');
});
// The callback will run at most once..emit()
emit<TEventName extends keyof TEventMap>(
event: {
name: TEventName;
payload: TEventMap[TEventName];
}
): voidDispatches an event. In synchronous mode all subscribers run immediately. In deferred mode the event is queued and flushed according to batch.size and batch.delay. Cross-instance messages are sent immediately even in deferred mode to avoid latency.
bus.emit({ name: 'dataUpdate', payload: { records: 42 } });.metrics()
metrics(): EventMetricsReturns performance and usage statistics.
console.log(bus.metrics());
// {
// totalEvents: 127,
// activeSubscriptions: 5,
// eventCounts: Map { 'userLogin' => 45, 'dataUpdate' => 82 },
// averageEmitDuration: 0.32 // ms
// }.clear()
clear(): voidRemoves all subscriptions, clears the event queue (if in deferred mode), resets all metrics, and re-opens the BroadcastChannel (if enabled). After calling clear() the bus is fully reusable.
bus.clear(); // fresh startAdvanced Usage
Batching / Deferred Mode
When many events are fired in rapid succession (e.g., keystrokes, scroll handlers), synchronous dispatch can cause performance issues. Batching coalesces them into a single microtask / timer flush.
const batchedBus = createEventBus<MyEvents>({
batch: {
size: 20, // flush after 20 queued events
delay: 100 // or after 100ms of inactivity
}
});- If the queue reaches
batch.sizebefore the timer expires, it flushes immediately. - The timer resets on every new event (quiet period).
- Metrics are still collected per event, so you can measure the real dispatch cost.
Cross-instance Broadcast
Enable the broadcast option to automatically send every emitted event to other instances that share the same channel name. Events received from another instances are dispatched to local subscribers exactly as if they were emitted locally.
const bus = createEventBus<MyEvents>({
broadcast: { channel: 'my-app-events' }
});
// In tab A
bus.emit({ name: 'userLogin', payload: { userId: '1' } });
// In tab B (same origin)
bus.subscribe('userLogin', (payload) => {
console.log('Another tab logged in:', payload.userId);
});⚠️
BroadcastChannelis not supported in Node.js. The bus will log a warning and disable cross-instance functionality gracefully. For Node.js, simply omit thebroadcastoption.
Custom Error Handling
By default, any error thrown inside a subscriber callback is caught and logged to console.error. Override this to send errors to a monitoring service.
const bus = createEventBus<MyEvents>({
errorHandler: (err) => {
myErrorTracker.capture(err, {
eventName: err.eventName,
payload: err.payload
});
}
});The EventError interface extends Error and adds optional eventName and payload fields.
Architecture Notes
- Subscriber snapshotting: When an event is dispatched, the bus iterates over a snapshot of the current subscribers. This prevents bugs where a callback calls
unsubscribe()on itself and stops other listeners from running. - Metrics overhead:
performance.now()is called twice per synchronous event (or per event inside a batch). This overhead is negligible (< 0.01ms) but can be ignored if not needed. - Cross-instance isolation:
BroadcastChanneldoes not send messages to the originating tab. Therefore there is no risk of infinite loops when broadcasting. - Debouncer: The batching mechanism uses an internal
Debouncerutility that guarantees a final flush after the quiet period, even if no new events arrive.
Reporting Issues
Please use the GitHub issue tracker and include:
- A minimal reproduction (code snippet)
- Expected vs actual behaviour
- Environment (browser / Node, version)
License
MIT © Saidimu. See LICENSE for details.
Built with ❤️ for type-safe event-driven architectures.
