npm.io
3.0.0 • Published yesterday

@supercat1337/event-emitter

Licence
MIT
Version
3.0.0
Deps
0
Size
44 kB
Vulns
0
Weekly
0
Stars
1

@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 EventEmitterLite or full-featured EventEmitter.
  • First‑class TypeScript – deep generic support for event names and argument validation.
  • Promise‑based Waiting – native waitForEvent and waitForAnyEvent with built‑in timeout support.
  • Lifecycle Tracking – monitor when a specific event gains or loses listeners (onHasEventListeners, onNoEventListeners) – only in EventEmitter.
  • 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 AbortSignal in on, once, and onAny.
  • 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: onHasEventListeners and onNoEventListeners now 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 the AbortSignal listener. If you remove a listener via removeListener(), the AbortSignal handler 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 logErrors is true (default), errors are printed to console.error.
  • Even with logErrors: false, you can still intercept errors globally using onListenerError for 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 Symbol to avoid collisions with user events.

Browser and Node.js Support

Platform Version
Node.js 14+
Modern browsers ES2022+
TypeScript 4.0+

License

MIT supercat1337

Keywords