-
Notifications
You must be signed in to change notification settings - Fork 2
Utils Events
GitHub Actions edited this page Sep 18, 2026
·
1 revision
Type-safe event emitter with protected emission and per-listener isolation.
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/_emitsite -
Protected Emission: only the class that owns the events can fire
them — holders of an instance subscribe via
on/once/offbut 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 likeon(), and is removable viaoff(event, callback) - Snapshot Semantics: listeners added during an emission fire from the next emission
deno add @tundralibs/utilsimport { 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.
-
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 withoff(event, callback) -
off(event, callback?): Remove a listener. Omitcallbackentirely 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-omittedcallbackclears the whole event.
-
_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_emitfor generic base classes (typed event key,unknown[]args) -
_onListenerError(event, error): Hook receiving every contained listener fault; defaults toconsole.error— override to route into a logger
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' });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');
}
}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 _onListenerErrorimport { 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 });
}
}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- Type your events — always define an event interface
- Emit from the owner only — if outside code needs to cause an event, expose a method that does the work and emits
-
Use
_emitSyncwhen the emitter must observe failures; use_emitwhen emission must never affect the emitter -
Clean up — call
off()for long-lived emitters