Skip to content
herald logoHeraldEvents
Zero-dependency typed event bus with subscribe/emit, wait(), async streams, AbortSignal support, bus piping, and test helpers.
v0.0.12.4 KB gzip 0 depsBrowser · Node.js · SSR · Deno
createBuscreateBehaviorBuspipeEventscombineSignalsBusDisposedErrordebugBusdebugBehaviorBuscreateTestBus

Why Herald?

Manual event emitters lack TypeScript inference across event names and payloads, and offer no async patterns — awaiting an event or streaming all future emits requires bespoke wiring.

ts
// Before — manual typed event bus
type Handlers = { 'user:login': (p: { userId: string }) => void };
const listeners = new Map<keyof Handlers, Set<Function>>();
function on<K extends keyof Handlers>(event: K, fn: Handlers[K]) {
  /* ... */
}
function emit<K extends keyof Handlers>(event: K, payload: Parameters<Handlers[K]>[0]) {
  /* ... */
}
// No await, no stream, no AbortSignal, no error isolation

// After — Herald
import { createBus } from '@vielzeug/herald';
const bus = createBus<AppEvents>();
bus.on('user:login', ({ userId }) => loadProfile(userId));
bus.emit('user:login', { userId: '42', email: 'alice@example.com' });
const session = await bus.wait('user:login'); // async one-shot
for await (const event of bus.events('cart:updated')) {
} // async stream
FeatureHeraldmittEventEmitter3
Bundle size2.4 KB~200 B~1.5 kB
TypeScript inference Full Basic Basic
Async/await (wait)
Async streaming
AbortSignal
Event piping
Wildcard (onAny)
Disposal signal
Error isolation
Zero dependencies

Use Herald when you need a fully-typed event bus with async patterns (wait, events generator) and AbortSignal-based lifecycle management.

Consider mitt when you only need a bare-minimum synchronous pub/sub with the smallest possible footprint.

Installation

sh
pnpm add @vielzeug/herald
sh
npm install @vielzeug/herald
sh
yarn add @vielzeug/herald

Quick Start

ts
import { BusDisposedError, createBus, pipeEvents } from '@vielzeug/herald';

type AppEvents = {
  'user:login': { userId: string; email: string };
  'user:logout': void;
};

const bus = createBus<AppEvents>();

bus.on('user:login', ({ userId }) => {
  console.log('Logged in:', userId);
});

bus.emit('user:login', { email: 'alice@example.com', userId: '42' });
bus.emit('user:logout');

const nextLogin = await bus.wait('user:login');
const nextSessionChange = await bus.waitAny(['user:login', 'user:logout']);

if (nextSessionChange.event === 'user:login') {
  console.log(nextSessionChange.payload.userId);
}

for await (const payload of bus.events('user:login', { signal: AbortSignal.timeout(5_000) })) {
  console.log(payload.email);
}

// Forward selected events to another bus
const auditBus = createBus<AppEvents>();
const unpipe = pipeEvents(bus, auditBus, ['user:login', 'user:logout']);

// Disposal signal — use as an AbortSignal for external cleanup
otherBus.on('count', handler, { signal: bus.disposalSignal });

try {
  await bus.wait('user:login', { signal: AbortSignal.timeout(500) });
} catch (err) {
  if (err instanceof BusDisposedError) {
    console.log('Bus was disposed');
  }
}

Features

  • Typed event maps for strict event/payload correctness
  • Persistent + one-shot listeners with on and once — each registration is independent, including duplicate handlers
  • Wildcard listeners with onAny — subscribe to all events for cross-cutting concerns like logging and analytics
  • Listener management APIs with unsubscribe handles, wildcardCount(), and eventNames()
  • Async event coordination with wait
  • First-event racing with waitAny
  • Async streaming with events — eager subscription buffers events from the moment events() is called; await using ensures cleanup on early break
  • Event piping with pipeEvents — forward events across buses with optional renaming and automatic teardown
  • Middleware pipeline via createBus({ middleware: [...] }) — intercept or block dispatches before listeners run
  • Payload validation via createBus({ validatePayload: ... }) — schema-level guards applied before middleware
  • Disposal signal via bus.disposalSignal — use as an AbortSignal to tie external lifecycles to the bus
  • Leak detection via maxListeners — warn when a single event accumulates too many listeners
  • Named buses via createBus({ name: 'myBus' }) — name appears in debug log prefixes and BusDisposedError messages for easier debugging across multiple bus instances
  • Debug logging via logger.debug or debugBus() / debugBehaviorBus() (@vielzeug/herald/devtools) — logs subscribe/emit/dispose activity with [herald:*] prefixes; tree-shaken from production bundles
  • Abort-aware APIs for lifecycle-safe teardown
  • onAny() wildcard listener for bus-wide observability; wildcardCount() to inspect active wildcards
  • Custom logger via createBus({ logger: { debug, warn } }) — route or suppress debug and warn output
  • onError hook for listener-error isolation and resilience
  • dispose and [Symbol.dispose] for deterministic cleanup
  • Testing helper via @vielzeug/herald/test
  • Zero dependencies2.4 KB gzipped, 0 dependencies

Documentation

See Also

  • Ripple — reactive signals and computed state that pair naturally with event-driven update patterns
  • Wayfinder — client-side router whose navigation lifecycle hooks integrate with bus-dispatched events
  • Familiar — Web Worker pool that can use a bus to stream task progress and completion events