npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

event-emission

v0.2.1

Published

Lightweight typed event emitter with DOM EventTarget and TC39 Observable compatibility

Readme

Event Emission

Event Emission is a high-performance, type-safe event primitive designed to bridge the gap between three worlds: DOM EventTarget, TC39 Observable, and AsyncIterator. While standard event emitters often force you into a single consumption pattern, Event Emission gives you the freedom to dispatch once and consume however your logic demands—whether that's standard callbacks, reactive pipelines via RxJS, or clean for await...of loops. By treating events as a first-class, composable primitive rather than just a side-effect, it eliminates race conditions and shared mutable state, providing a unified, zero-dependency foundation for building resilient, concurrent applications in modern JavaScript and TypeScript environments.

A lightweight, zero-dependency, type-safe event system with DOM EventTarget ergonomics and TC39 Observable interoperability. Use one event source with callbacks, async iterators, and RxJS without losing TypeScript safety.

Tasting notes

  • Typed events - Event maps keep payloads and event names in sync
  • DOM compatible - EmissionEvent is a superset of the built-in Event
  • Familiar API - addEventListener, removeEventListener, dispatchEvent
  • TC39 Observable - Fully compliant Observable implementation (passes all es-observable-tests)
  • Async iteration - for await...of over events with backpressure options
  • Wildcard listeners - Listen to * or namespaced user:* patterns
  • Observable state - Proxy any object and emit change events automatically
  • AbortSignal support - Cleanup with AbortController
  • No dependencies - Framework-agnostic, works in Node, Bun, and browsers

When to use it

  • Typed app-level event buses
  • Bridging DOM events to RxJS pipelines
  • State objects that emit change events for UI updates
  • Component/service emitters without stringly-typed payloads

Installation

npm install event-emission
# or
bun add event-emission
# or
pnpm add event-emission

Quick start

import { createEventTarget } from 'event-emission';

type UserEvents = {
  'user:login': { userId: string; timestamp: Date };
  'user:logout': { userId: string };
  error: Error;
};

const events = createEventTarget<UserEvents>();

events.addEventListener('user:login', (event) => {
  console.log(`User ${event.detail.userId} logged in at ${event.detail.timestamp}`);
});

events.dispatchEvent({
  type: 'user:login',
  detail: { userId: '123', timestamp: new Date() },
});

Core concepts

  • Event map: a TypeScript type that maps event names to payload types.
  • Event shape: { type: string; detail: Payload; bubbles?: boolean; cancelable?: boolean; composed?: boolean } for dispatch.
  • Unsubscribe: addEventListener returns a function to remove the listener.

API overview

Core (event-emission):

  • createEventTarget<E>(options?)
  • createEventTarget(target, { observe: true, ... })
  • EventEmission<E> base class

Optional subpaths:

  • Observable<T> compliant implementation (event-emission/observable)
  • Interoperability: fromEventTarget, forwardToEventTarget, pipe (event-emission/interoperability)
  • Observe utilities: isObserved, getOriginal, setupEventForwarding (event-emission/observe)
  • Types-only exports (event-emission/types)

Subpath exports

To keep the core entrypoint lean, optional features are exposed via subpath exports:

import { Observable } from 'event-emission/observable';
import { getOriginal, isObserved, setupEventForwarding } from 'event-emission/observe';
import {
  forwardToEventTarget,
  fromEventTarget,
  pipe,
} from 'event-emission/interoperability';
import type { EventTargetLike } from 'event-emission/types';

createEventTarget

createEventTarget<E>(options?)

Creates a typed event target.

type Events = {
  message: { text: string };
  error: Error;
};

const target = createEventTarget<Events>();

Options

| Option | Type | Description | | ----------------- | ---------------------------------------- | -------------------------------------------- | | onListenerError | (type: string, error: unknown) => void | Custom error handler for listener exceptions |

If a listener throws and no onListenerError is provided, an error event is emitted. If there are no error listeners, the error is re-thrown.

Observable state

Create state objects that emit change events:

const state = createEventTarget({ count: 0, user: { name: 'Ada' } }, { observe: true });

state.addEventListener('update', (event) => {
  console.log('State changed:', event.detail.current);
});

state.addEventListener('update:count', (event) => {
  console.log(
    `Count changed from ${event.detail.previous.count} to ${event.detail.value}`,
  );
});

state.count = 1; // Triggers 'update' and 'update:count'
state.user.name = 'Grace'; // Triggers 'update' and 'update:user.name'

Observe options:

Deep observation is enabled by default.

| Option | Type | Default | Description | | --------------- | ------------------------------- | -------- | ------------------------------------------------------------------ | | observe | boolean | false | Enable property change observation | | deep | boolean | true | Observe nested objects | | cloneStrategy | 'shallow' \| 'deep' \| 'path' | 'path' | How to clone previous state | | deepClone | <T>(value: T) => T | - | Optional deep clone fallback when structuredClone is unavailable |

Note: cloneStrategy: 'deep' uses structuredClone by default, or deepClone if provided.

Example fallback:

const state = createEventTarget(
  { count: 0, user: { name: 'Ada' } },
  {
    observe: true,
    cloneStrategy: 'deep',
    deepClone: (value) => JSON.parse(JSON.stringify(value)),
  },
);

Update event details:

  • update and update:path events include { value, current, previous }.
  • Array mutators emit method events like update:items.push with { method, args, added, removed, current, previous }.

Event listeners

on(type, options?)

Creates an Observable for a specific event type. This follows the ObservableEventTarget proposal, allowing for powerful composition.

const clicks = button.on('click', { passive: true });

clicks.subscribe((event) => {
  console.log('Clicked!', event.detail);
});

Options:

| Option | Type | Default | Description | | -------------- | ------------- | ------- | -------------------------------------------------------------------------------------- | | capture | boolean | false | If true, listen during the capture phase | | receiveError | boolean | false | If true, listen for "error" events and forward them to the observer's error method | | handler | Function | null | Optional function to run stateful actions (like preventDefault()) before dispatching | | once | boolean | false | If true, the observable completes after the first event is dispatched | | passive | boolean | false | Indicates that the callback will not cancel the event | | signal | AbortSignal | - | Abort signal to remove the listener when aborted |

addEventListener(type, listener, options?)

Adds a listener and returns an unsubscribe function.

const unsubscribe = events.addEventListener('message', (event) => {
  console.log(event.detail.text);
});

unsubscribe();

Options: (or pass true/false for capture)

| Option | Type | Description | | --------- | ------------- | -------------------------------------------- | | capture | boolean | Listen during the capture phase | | once | boolean | Remove listener after first invocation | | passive | boolean | Listener will not call preventDefault() | | signal | AbortSignal | Abort signal to remove listener when aborted |

Note: preventDefault() only affects events dispatched with cancelable: true. dispatchEvent returns false when a cancelable event is prevented.

once(type, listener, options?)

Adds a one-time listener.

removeEventListener(type, listener, options?)

Removes a specific listener.

removeAllListeners(type?)

Removes all listeners, or all listeners for a type.

Wildcard listeners

events.addWildcardListener('*', (event) => {
  console.log(`Got ${event.originalType}:`, event.detail);
});

events.addWildcardListener('user:*', (event) => {
  console.log(`User event: ${event.originalType}`);
});

Wildcard events include { type: pattern, originalType, detail }.

Async iteration

You can use for await...of to consume events. This is great for stream-processing events with backpressure.

// Simple iteration
for await (const event of events.events('message')) {
  console.log('Received:', event.detail.text);
}

// With options for backpressure and cleanup
const iterator = events.events('message', {
  bufferSize: 16,
  overflowStrategy: 'drop-oldest',
  signal: abortController.signal,
});

for await (const event of iterator) {
  // ...
}

Iterator options:

| Option | Type | Default | Description | | ------------------ | ------------------------------------------- | --------------- | ------------------------------ | | signal | AbortSignal | - | Abort signal to stop iteration | | bufferSize | number | Infinity | Maximum buffered events | | overflowStrategy | 'drop-oldest' \| 'drop-latest' \| 'throw' | 'drop-oldest' | Behavior when buffer is full |

When overflowStrategy is throw, the iterator throws BufferOverflowError.

Observable interoperability

subscribe(type, observer)

const subscription = events.subscribe('message', {
  next: (event) => console.log(event.detail),
  error: (err) => console.error(err),
  complete: () => console.log('Done'),
});

subscription.unsubscribe();

toObservable()

Returns an Observable that emits all events.

import { from } from 'rxjs';
import { filter, map } from 'rxjs/operators';

const observable = from(events);

observable
  .pipe(
    filter((event) => event.type === 'message'),
    map((event) => event.detail.text),
  )
  .subscribe(console.log);

Observable class

A fully compliant implementation of the TC39 Observable proposal.

import { Observable } from 'event-emission/observable';

// Create from items
const numbers = Observable.of(1, 2, 3);

// Create from any iterable or observable-like
const fromArray = Observable.from([10, 20, 30]);

// Manual creation
const custom = new Observable((observer) => {
  observer.next('Hello');
  observer.complete();
});

Lifecycle

complete()

Marks the event target as complete, clears listeners, and ends iterators.

clear()

Removes all listeners without marking as complete.

EventEmission base class

Extend EventEmission to build typed emitters:

import { EventEmission } from 'event-emission';

class UserService extends EventEmission<{
  'user:created': { id: string; name: string };
  'user:deleted': { id: string };
  error: Error;
}> {
  createUser(name: string) {
    const id = crypto.randomUUID();
    this.dispatchEvent({ type: 'user:created', detail: { id, name } });
    return id;
  }
}

DOM interoperability

fromEventTarget(domTarget, eventTypes, options?)

import { fromEventTarget } from 'event-emission/interoperability';

type ButtonEvents = {
  click: MouseEvent;
  focus: FocusEvent;
};

const button = document.getElementById('my-button');
const events = fromEventTarget<ButtonEvents>(button, ['click', 'focus']);

events.addEventListener('click', (event) => {
  console.log('Button clicked!', event.detail);
});

events.destroy();

forwardToEventTarget(source, domTarget, options?)

import { createEventTarget } from 'event-emission';
import { forwardToEventTarget } from 'event-emission/interoperability';

const events = createEventTarget<{ custom: { value: number } }>();
const element = document.getElementById('target');

const unsubscribe = forwardToEventTarget(events, element);

events.dispatchEvent({ type: 'custom', detail: { value: 42 } });

unsubscribe();

pipe(source, target, options?)

import { createEventTarget } from 'event-emission';
import { pipe } from 'event-emission/interoperability';

const componentEvents = createEventTarget<{ ready: void }>();
const appBus = createEventTarget<{ ready: void }>();

const unsubscribe = pipe(componentEvents, appBus);

unsubscribe();

Note: Both pipe(source, target) and events.pipe(target) forward all events via a wildcard listener. Use a map function to transform events or return null to filter.

React integration

Event Emission works beautifully with React's useSyncExternalStore for predictable, race-condition-free state synchronization.

Observing an event emitter

import { useSyncExternalStore } from 'react';
import { createEventTarget } from 'event-emission';

const bus = createEventTarget<{ log: string }>();

function useBusEvent() {
  return useSyncExternalStore(
    (callback) => bus.addEventListener('log', callback),
    () => getLatestLogValue(), // implementation depends on your needs
  );
}

Syncing with Observable State

The observe feature is perfect for building high-performance global stores or local controllers that live outside the React render cycle.

import { useSyncExternalStore } from 'react';
import { createEventTarget } from 'event-emission';

// 1. Create your store outside of React
const store = createEventTarget(
  { count: 0, lastUpdated: new Date() },
  { observe: true }
);

// 2. Create a generic hook to sync with any observed target
export function useObservable<T extends object>(target: T) {
  return useSyncExternalStore(
    (onStoreChange) => (target as any).addEventListener('update', onStoreChange),
    () => target
  );
}

// 3. Components re-render only when the store actually mutates
function Counter() {
  const state = useObservable(store);
  return (
    <button onClick={() => state.count++}>
      Count is {state.count}
    </button>
  );
}

Svelte integration

Event Emission works naturally with Svelte 5 Runes to create reactive stores that live outside your component tree.

Creating a Reactive Rune

import { createEventTarget } from 'event-emission';

// 1. Define your external state
const store = createEventTarget({ count: 0 }, { observe: true });

// 2. Create a generic Rune to sync with any observed target
export function useObservable<T extends object>(target: T) {
  let state = $state(target);

  $effect(() => {
    return (target as any).addEventListener('update', () => {
      state = target; // Trigger Svelte reactivity
    });
  });

  return state;
}

// 3. Use it in your components
const state = useObservable(store);

Utilities

isObserved(obj)

Checks if an object is an observed proxy.

import { isObserved } from 'event-emission/observe';

getOriginal(proxy)

Returns the original unproxied object.

import { getOriginal } from 'event-emission/observe';

TypeScript types

import type {
  EmissionEvent,
  EventTargetLike,
  ObservableLike,
  Observer,
  Subscription,
  WildcardEvent,
  AddEventListenerOptionsLike,
} from 'event-emission/types';

import type {
  ObservableEventMap,
  PropertyChangeDetail,
  ArrayMutationDetail,
} from 'event-emission/observe';

import type { Subscriber, SubscriptionObserver } from 'event-emission/observable';