Skip to content

API Overview

SymbolPurposeExecution modeCommon gotcha
createBus()Create typed temporal event busSyncemit() and middleware are synchronous
pipeEvents()Forward compatible source eventsSyncPayloads must be assignable to target event
combineSignals()Abort when any input abortsSyncPublic composition has no manual teardown
createTestBus()Record dispatched test eventsSyncAvailable from /testing only

Package Entry Point

ImportPurpose
@vielzeug/heraldRuntime bus, pipes, public types, and errors
@vielzeug/herald/testingcreateTestBus() and TestBus

Core Functions

createBus()

ts
function createBus<T extends EventMap = Record<string, unknown>>(
  options?: BusOptions<T>,
): Bus<T>;

Creates a synchronous bus for future event delivery.

ParameterTypeDescription
optionsBusOptions<T>Optional middleware, validation, error handling, and listener threshold configuration.

Returns: Bus<T>.

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

interface Events {
  count: number;
  ready: void;
}

const bus = createBus<Events>();
bus.emit('count', 1);
bus.emit('ready');
bus.dispose();

pipeEvents()

ts
function pipeEvents<S extends EventMap, T extends EventMap>(
  source: Bus<S>,
  target: Bus<T>,
  entries: readonly [NoInfer<PipeEntry<S, T>>, ...NoInfer<PipeEntry<S, T>>[]],
  opts?: { signal?: AbortSignal },
): Unsubscribe;

Forwards listed compatible events until manually stopped, either bus disposes, or options.signal aborts.

ParameterTypeDescription
sourceBus<S>Bus that emits source events.
targetBus<T>Bus that receives compatible events.
entriesnon-empty PipeEntry tupleSame-name keys or compatible { from, to } mappings.
opts.signalAbortSignalOptional pipe lifetime signal.

Returns: Idempotent Unsubscribe function.

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

interface SourceEvents {
  'auth:login': { id: string };
}

interface TargetEvents {
  'user:authenticated': { id: string };
}

const source = createBus<SourceEvents>();
const target = createBus<TargetEvents>();
const stop = pipeEvents(source, target, [{ from: 'auth:login', to: 'user:authenticated' }]);

stop();
source.dispose();
target.dispose();

combineSignals()

ts
function combineSignals(first: AbortSignal, ...rest: AbortSignal[]): AbortSignal;

Returns a signal aborted with first input signal's reason.

Returns: AbortSignal.

ts
import { combineSignals } from '@vielzeug/herald';

const signal = combineSignals(AbortSignal.timeout(1_000), controller.signal);

Input listeners remain until an input aborts. Bus APIs that accept { signal } clean their internal signal composition when their owned operation ends.

Types

EventMap and EventKey

ts
type EventMap = object;
type EventKey<T extends EventMap> = Extract<keyof T, string>;

EventMap accepts interfaces and type aliases. Only string keys are event names.


BusOptions

ts
type BusOptions<T extends EventMap = EventMap> = {
  maxListeners?: number;
  middleware?: readonly Middleware<T>[];
  name?: string;
  onError?: (context: EmissionErrorContext<T>) => void;
  validatePayload?: <K extends EventKey<T>>(event: K, payload: T[K]) => void;
};
FieldDescription
maxListenersWarn when one event exceeds this active-listener count.
middlewareSynchronous dispatch middleware.
nameDisplay name in disposal errors.
onErrorHandles listener and validation errors instead of rethrowing.
validatePayloadRuns before middleware and listeners.

Bus

ts
type Bus<T extends EventMap> = {
  [Symbol.dispose](): void;
  readonly disposalSignal: AbortSignal;
  dispose(): void;
  readonly disposed: boolean;
  emit<K extends EventKey<T>>(event: K, ...args: T[K] extends void ? [] : [payload: T[K]]): number;
  eventNames(): EventKey<T>[];
  events<K extends EventKey<T>>(event: K, opts?: { maxBuffer?: number; signal?: AbortSignal }): EventStream<T[K]>;
  listenerCount(event?: EventKey<T>): number;
  on<K extends EventKey<T>>(event: K, listener: Listener<T[K]>, opts?: SubscribeOptions): Unsubscribe;
  onAny(listener: (event: EventKey<T>, payload: unknown) => void, opts?: SubscribeOptions): Unsubscribe;
  once<K extends EventKey<T>>(event: K, listener: Listener<T[K]>, opts?: { signal?: AbortSignal }): Unsubscribe;
  tap(handler: (event: HeraldEvent<T>) => void, options?: { signal?: AbortSignal }): Unsubscribe;
  wait<K extends EventKey<T>>(event: K, opts?: { signal?: AbortSignal }): Promise<T[K]>;
  waitAny<const K extends readonly [EventKey<T>, EventKey<T>, ...EventKey<T>[]]>(
    events: K,
    opts?: { signal?: AbortSignal },
  ): Promise<WaitAnyResult<T, K>>;
  wildcardCount(): number;
};

emit() returns listener count or 0 after disposal, blocked middleware, or handled validation rejection.

tap() receives every emit, subscribe, unsubscribe, listener-error, and dispose event as a HeraldEvent. It is the supported way to observe bus activity for logging and diagnostics. The returned Unsubscribe stops the tap; pass { signal } to bind its lifetime to an AbortSignal.

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

const bus = createBus<AppEvents>();
const stop = bus.tap((event) => console.debug(`herald:${event.type}`, event));

Listener, SubscribeOptions, and Unsubscribe

ts
type Listener<T> = (payload: T) => void;
type SubscribeOptions = { once?: boolean; signal?: AbortSignal };
type Unsubscribe = () => void;

HeraldEvent

ts
type HeraldEvent<T extends EventMap = EventMap> =
  | { type: 'emit'; event: EventKey<T>; payload: unknown; timestamp: number }
  | { type: 'subscribe'; event: EventKey<T>; timestamp: number }
  | { type: 'unsubscribe'; event: EventKey<T>; timestamp: number }
  | { type: 'listener-error'; event: EventKey<T>; err: unknown; timestamp: number }
  | { type: 'dispose'; timestamp: number };

Discriminated union delivered to tap() handlers. Narrow on event.type to access type-specific fields.


EmissionErrorContext and Middleware

ts
type EmissionErrorContext<T extends EventMap = EventMap> = {
  err: unknown;
  event: EventKey<T>;
  payload: unknown;
  timestamp: number;
};

type Middleware<T extends EventMap = EventMap> = (
  event: EventKey<T>,
  payload: unknown,
  next: () => void,
) => void;

Call middleware next() synchronously at most once. Omit it to block dispatch.


EventStream and WaitAnyResult

ts
type EventStream<T> = AsyncGenerator<T> & AsyncDisposable;

type WaitAnyResult<T extends EventMap, K extends readonly EventKey<T>[]> = {
  [I in keyof K]: K[I] extends EventKey<T> ? { event: K[I]; payload: T[K[I]] } : never;
}[number];

PipeableKey, RenamedPipeEntry, and PipeEntry

ts
type PipeableKey<S extends EventMap, T extends EventMap> = {
  [K in EventKey<S> & EventKey<T>]: S[K] extends T[K] ? K : never;
}[EventKey<S> & EventKey<T>];

type RenamedPipeEntry<S extends EventMap, T extends EventMap> = {
  [From in EventKey<S>]: {
    [To in EventKey<T>]: S[From] extends T[To] ? { from: From; to: To } : never;
  }[EventKey<T>];
}[EventKey<S>];

type PipeEntry<S extends EventMap, T extends EventMap> =
  | PipeableKey<S, T>
  | RenamedPipeEntry<S, T>;

Testing

createTestBus()

ts
function createTestBus<T extends EventMap = Record<string, unknown>>(
  options?: BusOptions<T>,
): TestBus<T>;

Creates a bus that records dispatched payloads.

Returns: TestBus<T>.

TestBus

ts
type TestBus<T extends EventMap> = Bus<T> & {
  allEmitted(): { [K in EventKey<T>]?: T[K][] };
  emitted<K extends EventKey<T>>(event: K): T[K][];
  emittedCount<K extends EventKey<T>>(event: K): number;
  reset(): void;
};

Errors

ErrorTriggerNotable properties
BusDisposedErrorwait() or waitAny() interrupted by disposalBus name appears when configured.
HeraldConfigErrorInvalid stream buffer, empty pipe entries, or fewer than two waitAny() events
HeraldErrorBase class for Herald-originated errorsinstanceof HeraldError narrows subclasses.