Skip to content

Utils Events

GitHub Actions edited this page Sep 18, 2026 · 1 revision

Utils - Events

Type-safe event emitter with protected emission and per-listener isolation.

← Back to Utils

Overview

The Events class provides a typed event surface for classes to expose:

  • Type Safety: the generic parameter maps event names to callback signatures, checked at every on/_emit site
  • Protected Emission: only the class that owns the events can fire them — holders of an instance subscribe via on/once/off but cannot forge lifecycle events
  • Per-Listener Isolation: on the fire-and-forget paths, a listener that throws (or an async listener that rejects) is contained and reported — other listeners still run, and the emitter is unaffected
  • One-Time Listeners: once() auto-removes after a single fire, dedupes like on(), and is removable via off(event, callback)
  • Snapshot Semantics: listeners added during an emission fire from the next emission

Installation

deno add @tundralibs/utils

API Reference

Constructor

import { Events } from '@tundralibs/utils';

type T = { change: (value: string) => void };

class MyClass extends Events<T> {}

Type Parameter T: Object mapping event names to callback signatures.

Public methods (subscription)

  • on(event, callback): Register a listener (or an array of them, each registered independently); duplicates are no-ops
  • once(event, callback): Register a one-time listener (or an array — each fires independently, not as a single group); removable before firing with off(event, callback)
  • off(event, callback?): Remove a listener. Omit callback entirely to clear every listener for the event; pass an array to remove each listed callback.

An EMPTY array (off('event', [])) removes nothing — it is not the same as omitting the argument. Only a fully-omitted callback clears the whole event.

Protected methods (emission — for the owning class)

  • _emit(event, ...args): Fire-and-forget. Listeners run in registration order; sync throws and async rejections are routed to _onListenerError, never propagated
  • _emitSync(event, ...args): Awaits each listener in turn. A throw/rejection propagates to the awaiting caller and stops later listeners — this is the deliberate, handled emission path
  • _emitRaw(event, ...args): Variance-tolerant _emit for generic base classes (typed event key, unknown[] args)
  • _onListenerError(event, error): Hook receiving every contained listener fault; defaults to console.error — override to route into a logger

Usage Examples

An emitting class

import { Events } from '@tundralibs/utils';

type StoreEvents = {
  change: (data: unknown) => void;
  error: (error: Error) => void;
};

class DataStore extends Events<StoreEvents> {
  #data: unknown;

  setData(data: unknown) {
    this.#data = data;
    this._emit('change', data); // emission is the owner's privilege
  }
}

const store = new DataStore();
store.on('change', (data) => console.log('changed:', data));
store.setData({ hello: 'world' });

Awaited emission (_emitSync)

import { Events } from '@tundralibs/utils';

class Pipeline extends Events<{ flush: () => Promise<void> }> {
  async flush() {
    // Each listener completes before the next starts; a rejection
    // surfaces HERE, where the emitter can handle it.
    await this._emitSync('flush');
  }
}

Listener isolation (fire-and-forget)

import { Events } from '@tundralibs/utils';

type StoreEvents = {
  change: (data: unknown) => void;
};

class DataStore extends Events<StoreEvents> {
  setData(data: unknown) {
    this._emit('change', data);
  }
}

const store = new DataStore();

store.on('change', () => {
  throw new Error('listener bug');
});
store.on('change', () => {
  console.log('this still runs'); // isolation: one bad listener
}); //                               cannot stop the others

store.setData(1); // the throw is reported via _onListenerError

Routing listener faults

import { Events } from '@tundralibs/utils';

type ServiceEvents = { start: () => void };

declare const logger: { error(message: string, context: unknown): void };

class Service extends Events<ServiceEvents> {
  protected override _onListenerError(
    event: PropertyKey,
    error: unknown,
  ): void {
    logger.error(`listener failed on '${String(event)}'`, { error });
  }
}

One-time listeners

import { Events } from '@tundralibs/utils';

class App extends Events<{ ready: () => void }> {}

const app = new App();
const startServer = () => console.log('server started');

const onReady = () => startServer();
app.once('ready', onReady);
app.off('ready', onReady); // removable by the ORIGINAL callback

Best Practices

  1. Type your events — always define an event interface
  2. Emit from the owner only — if outside code needs to cause an event, expose a method that does the work and emits
  3. Use _emitSync when the emitter must observe failures; use _emit when emission must never affect the emitter
  4. Clean up — call off() for long-lived emitters

Related Utilities

  • Options - Combines Events with options management
  • BaseError - Error handling with context

← Back to Utils

Clone this wiki locally