@supercat1337/event-emitter
v2.1.1
Published
Lightweight typed event emitter with Symbol support, one-time listeners, and zero dependencies. Works in Node.js and browsers.
Maintainers
Readme
@supercat1337/event-emitter
A modern, feature-rich EventEmitter implementation for JavaScript and TypeScript with advanced capabilities and industry-leading type safety.
Features
- ✅ Dual Implementation – choose between lightweight
EventEmitterLiteor full-featuredEventEmitter. - ✅ First‑class TypeScript – deep generic support for event names and argument validation.
- ✅ Promise‑based Waiting – native
waitForEventandwaitForAnyEventwith built‑in timeout support. - ✅ Lifecycle Tracking – monitor when events gain or lose listeners (
onHasEventListeners,onNoEventListeners) – only inEventEmitter. - ✅ Centralized Error Handling – intercept listener errors globally via
onListenerError. - ✅ Global Listeners – subscribe to all events with
onAny– perfect for logging, debugging, or metrics. - ✅ AbortSignal Support – cancel subscriptions using standard
AbortSignalinon,once, andonAny. - ✅ Introspection Methods – inspect listeners, counts, and event names with
hasListeners,listenerCount,eventNames,getListeners. - ✅ Memory‑Efficient – automatic cleanup of unused event keys and dedicated
destroy()lifecycle. - ✅ Immutable Emission – listener arrays are snapshotted during emission, making it safe to modify listeners inside callbacks.
- ✅ Modern ES2022+ – leverages native private fields and optimised logic.
Installation
npm install @supercat1337/event-emitterQuick Start
Lightweight version (EventEmitterLite)
For simple scenarios where you only need on / once / off / emit:
import { EventEmitterLite } from '@supercat1337/event-emitter';
const emitter = new EventEmitterLite();
const unsubscribe = emitter.on('data', msg => console.log(msg));
emitter.emit('data', 'Hello, World!');
unsubscribe(); // remove listenerFull‑featured version (EventEmitter)
With all advanced features:
import { EventEmitter } from '@supercat1337/event-emitter';
const emitter = new EventEmitter();
emitter.on('ready', () => console.log('Ready!'));
emitter.emit('ready');Classes
| Class | Description |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| EventEmitterLite | Core implementation: on, once, off, removeListener, emit, onAny, offAny, introspection, and cleanup. |
| EventEmitter | Extends EventEmitterLite, adding: waitForEvent, waitForAnyEvent, lifecycle hooks (onHasEventListeners, onNoEventListeners, onListenerError), and destroy. |
Key difference: EventEmitter adds async waiting and lifecycle monitoring.
API
Common Properties
| Property | Type | Description |
| ------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| logErrors | boolean | If true (default), errors in listeners are logged to console.error. Even when false, errors can still be caught via onListenerError (in EventEmitter). |
| isDestroyed | boolean | EventEmitter only. true after destroy() is called. |
Methods available in both classes
| Method | Description |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| on(event, listener, options?) | Subscribes to an event. Returns an unsubscribe() function. options.signal accepts an AbortSignal. |
| once(event, listener, options?) | Subscribes for a single invocation, then auto‑removes. Supports AbortSignal. |
| off(event, listener) | Removes a specific listener. |
| removeListener(event, listener) | Alias for off(). |
| emit(event, ...args) | Triggers all listeners for the event with provided arguments. |
| onAny(listener, options?) | Subscribes a listener that is invoked for every emitted event. Receives (eventName, ...args). Supports AbortSignal. |
| offAny(listener) | Removes a listener added via onAny. |
| hasListeners(event) | Returns true if the event has any listeners. |
| listenerCount(event) | Returns the number of listeners for a specific event. |
| eventNames() | Returns an array of event names that have at least one listener (including symbols). |
| getListeners(event) | Debug only. Returns a copy of the listeners array for the event. |
| removeAllListeners() | Removes all listeners from all events. The emitter remains functional. In EventEmitter, this also emits #no‑listeners for each event that had listeners. |
| removeAllListenersOf(event) | Removes all listeners for the specified event. In EventEmitter, emits #no‑listeners if any listener was removed. |
| clear() | Deprecated. Use removeAllListeners() instead. |
| clearEventListeners(event) | Deprecated. Use removeAllListenersOf(event) instead. |
Methods available only in EventEmitter
| Method | Description |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| waitForEvent(event, maxWaitMs = 0) | Returns Promise<boolean>. Resolves true when the event fires, or false on timeout. If maxWaitMs === 0, waits indefinitely. |
| waitForAnyEvent(events, maxWaitMs = 0) | Waits for the first occurring event from an array of event names. |
| destroy() | Completely destroys the emitter: removes all listeners (including internal), clears internal state, and prevents further operations. |
| onHasEventListeners(callback) | Subscribes to the system event emitted when any event gains its first listener. Callback receives the event name. |
| onNoEventListeners(callback) | Subscribes to the system event emitted when any event loses its last listener. Callback receives the event name. |
| onListenerError(callback) | Subscribes to errors thrown by listeners. Callback receives (error, eventName, ...args). |
Examples
AbortSignal support
import { EventEmitter } from '@supercat1337/event-emitter';
const emitter = new EventEmitter();
const controller = new AbortController();
emitter.on('message', text => console.log(text), { signal: controller.signal });
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);Works with once and onAny as well.
Global listener with onAny
emitter.onAny((event, ...args) => {
console.log(`Event "${event}" fired with`, args);
});
emitter.emit('foo', 1, 2); // logs: Event "foo" fired with [1, 2]Introspection
emitter.on('foo', () => {});
emitter.on('foo', () => {});
console.log(emitter.hasListeners('foo')); // true
console.log(emitter.listenerCount('foo')); // 2
console.log(emitter.eventNames()); // ['foo']
const listeners = emitter.getListeners('foo'); // copy, safe to iterateLifecycle monitoring (only in EventEmitter)
emitter.onHasEventListeners(event => {
console.log(`First listener added for ${event}`);
});
emitter.onNoEventListeners(event => {
console.log(`Last listener removed for ${event}`);
});
emitter.on('test', () => {});
// => "First listener added for test"
emitter.removeAllListenersOf('test');
// => "Last listener removed for test"Error handling
emitter.onListenerError((err, event, ...args) => {
console.error(`Error in "${event}":`, err);
// send to monitoring service
});
emitter.on('crash', () => {
throw new Error('Boom!');
});
emitter.emit('crash'); // error is caught and passed to the callbackCleanup and destruction
// Remove all listeners, but keep the emitter functional
emitter.removeAllListeners();
// Permanently destroy the emitter
emitter.destroy();
console.log(emitter.isDestroyed); // true
emitter.on('test', () => {}); // throws: "EventEmitter is destroyed"Listener context (this)
When a listener is invoked, the this context inside the listener function refers to the EventEmitter (or EventEmitterLite) instance. This is consistent with Node.js EventEmitter behavior.
emitter.on('event', function () {
console.log(this); // points to the emitter instance
});
// To preserve a custom context, use an arrow function or .bind()
const obj = { name: 'MyObj' };
emitter.on('event', () => {
console.log(this); // lexical this
});
emitter.on(
'event',
function () {
console.log(this.name);
}.bind(obj)
);TypeScript
Simple string union
type MyEvents = 'start' | 'stop';
const emitter = new EventEmitter<MyEvents>();
emitter.emit('start'); // OK
emitter.emit('unknown'); // Type errorFull type safety with arguments
type AppEvents = {
'user:created': [id: number, name: string];
ping: [];
};
const emitter = new EventEmitter<AppEvents>();
emitter.on('user:created', (id, name) => {
// id: number, name: string
});
emitter.emit('user:created', 1, 'Alice'); // OK
emitter.emit('user:created', '1'); // Type errorError Handling
All listener errors are caught to prevent the emitter from crashing.
- If
logErrorsistrue(default), errors are printed toconsole.error. - Even with
logErrors: false, you can still intercept errors globally usingonListenerErrorfor custom logging or reporting.
Performance Notes
- Snapshotted iteration: Listener arrays are copied before emission. If a listener calls
off()during emission, the current cycle continues safely without skipping elements. - Zero dependencies: Ultra‑small bundle size.
- Memory management: Event keys are deleted when the last listener is removed.
- Symbol‑based internal events: Internal lifecycle events use
Symbolto avoid collisions with user events.
Browser and Node.js Support
| Platform | Version | | --------------- | ------- | | Node.js | 14+ | | Modern browsers | ES2022+ | | TypeScript | 4.0+ |
License
MIT © supercat1337
