@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 a specific event gains or loses 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-emitter
Quick 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 listener
Full‑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, lifecycle monitoring, and fine‑grained control over internal listeners.
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 (works with listeners added via once as well). |
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. Returns Promise<boolean>. |
destroy() |
Completely destroys the emitter: removes all listeners (including internal), clears internal state, and prevents further operations. |
onHasEventListeners(event, callback) |
Subscribes to the system event emitted when the specified external event gains its first listener. Callback receives the event name and any additional arguments. |
onNoEventListeners(event, callback) |
Subscribes to the system event emitted when the specified external event loses its last listener. Callback receives the event name and any additional arguments. |
onListenerError(callback) |
Subscribes to errors thrown by listeners. Callback receives (error, eventName, ...args). |
removeAllInternalListenersOf(event) |
Removes all internal listeners (e.g., those added via onHasEventListeners or onNoEventListeners) that are associated with the given external event. |
Note:
onHasEventListenersandonNoEventListenersnow require the event name as the first argument. They no longer support global subscriptions without a filter. This change was made to enable fine‑grained control and prevent unintended side effects.
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.
Tip: Calling the returned
unsubscribe()function cleans up both the listener and theAbortSignallistener. If you remove a listener viaremoveListener(), theAbortSignalhandler will remain attached until the signal aborts or is garbage-collected.
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 iterate
Lifecycle monitoring (only in EventEmitter)
// Subscribe to "#has-listeners" for the specific event "test"
emitter.onHasEventListeners('test', event => {
console.log(`First listener added for ${event}`);
});
emitter.onNoEventListeners('test', event => {
console.log(`Last listener removed for ${event}`);
});
emitter.on('test', () => {});
// => "First listener added for test"
emitter.removeAllListenersOf('test');
// => "Last listener removed for test"
Important: The callback is invoked only for the event name you specified. If you need to monitor multiple events, subscribe separately for each.
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 callback
Waiting for events
// Wait for a single event with timeout
const [fired] = await Promise.all([emitter.waitForEvent('ready', 3000), someAsyncTask()]);
if (!fired) {
console.log('Task timed out!');
}
// Wait for the first of multiple events
const success = await emitter.waitForAnyEvent(['complete', 'error'], 5000);
if (success) {
console.log('One of the events fired in time');
} else {
console.log('Timeout');
}
Cleanup and destruction
// Remove all external listeners, but keep internal listeners intact
emitter.removeAllListeners();
// Remove internal listeners for a specific event (e.g., added via onHasEventListeners)
emitter.removeAllInternalListenersOf('test');
// Permanently destroy the emitter (clears everything)
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 error
Full 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 error
Error 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