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

zephyr-events

v1.6.0

Published

Tiny typed event emitter with race-condition safety

Readme

Zephyr Events — Tiny TypeScript Event Emitter

A lightweight, type-safe event emitter for TypeScript and JavaScript. Zero dependencies, under 1KB gzipped, with built-in race-condition safety that larger alternatives like EventEmitter3 and Node.js EventEmitter lack.

npm version npm downloads bundle size TypeScript license

Why Zephyr Events?

Most event emitter libraries break when handlers modify the listener list during emission — a handler that calls off() on itself can skip the next handler, or adding a new listener mid-emit can cause infinite loops. Zephyr Events solves this with snapshot-based iteration, making it safe for real-world use in UI frameworks, state machines, and plugin systems.

  • Under 1KB gzipped — 4.5KB ESM, 866B gzipped, zero dependencies, tree-shakeable
  • Race-condition safe — handlers can subscribe, unsubscribe, or clear during emit without side effects
  • Fast modezephyrEventsFast skips the snapshot for ~50% faster emit once an event has four or more handlers
  • Full TypeScript support — generic event maps, strict handler signatures, IDE autocompletion
  • Universal builds — ESM, CommonJS, and UMD for browsers, Node.js, and bundlers
  • Wildcard listeners — subscribe to all events with *
  • Shared state — pass a handler map between emitters for cross-module communication

Installation

npm install zephyr-events
yarn add zephyr-events
pnpm add zephyr-events

Quick Start

TypeScript Event Emitter

import zephyrEvents from 'zephyr-events';

// Define your event types
type AppEvents = {
  'user:login': { id: number; name: string }
  'user:logout': { id: number }
  'error': Error
}

const emitter = zephyrEvents<AppEvents>();

// Subscribe — returns an unsubscribe function for easy cleanup
const unsubscribe = emitter.on('user:login', (user) => {
  console.log(`Welcome, ${user.name}`);
});

// Emit with full type checking
emitter.emit('user:login', { id: 1, name: 'Alice' });

// Clean up when done
unsubscribe();

Fast Mode

zephyrEventsFast walks the live handler array instead of copying it. That pays off once an event type has four or more handlers — the copy is what zephyrEvents spends its time on:

import { zephyrEventsFast } from 'zephyr-events';

const emitter = zephyrEventsFast<AppEvents>();
// Same API — on(), off(), emit(), all

Below four handlers, don't. zephyrEvents snapshots one to three handlers into locals without allocating, which makes it faster than fast mode there — by 11% at one handler and by over 70% at three. Reach for fast mode only once you have measured a hot event with four or more listeners. See Performance Benchmarks.

JavaScript Event Emitter

const zephyrEvents = require('zephyr-events');
const { zephyrEventsFast } = require('zephyr-events');

const emitter = zephyrEvents();       // safe (snapshot)
const fast = zephyrEventsFast();       // fast (no snapshot)

emitter.on('message', (data) => {
  console.log('Received:', data);
});

emitter.emit('message', { text: 'Hello' });

API Reference

zephyrEvents<Events>(all?)

Creates a new typed event emitter with snapshot-safe emission. Optionally accepts an existing handler map to share event state between emitters.

const emitter = zephyrEvents<{
  message: string
  data: { value: number }
}>();

// Share handlers between emitters
const shared = new Map();
const emitterA = zephyrEvents(shared);
const emitterB = zephyrEvents(shared);

zephyrEventsFast<Events>(all?)

Creates a non-snapshot event emitter. Same API as zephyrEvents, but emit() iterates the live handler array instead of a copy — roughly 50% faster once an event has four or more handlers, and slower below that.

Because the array is live, handlers that modify the listener list change the cycle in flight. Removing a handler shifts the rest down, so one that had not run yet can be skipped, and a handler added during the cycle can land in a vacated slot and run. It will not crash and it will not run away: the loop never invokes more handlers than were registered when emit() was called. But the delivery set is not stable — use zephyrEvents when handlers may subscribe or unsubscribe during emission.

import { zephyrEventsFast } from 'zephyr-events';

const emitter = zephyrEventsFast<{ tick: number }>();

emitter.on(type, handler): Unsubscribe

Registers an event handler. Returns an unsubscribe function for automatic cleanup — no need to keep a reference to the handler. The returned function is idempotent: calling it more than once is harmless and never removes another registration of the same handler.

// Type-safe subscription
const unsub = emitter.on('message', (msg) => {
  console.log(msg);
});

// Wildcard listener — receives every event
emitter.on('*', (type, event) => {
  console.log(`[${String(type)}]`, event);
});

// Unsubscribe when no longer needed
unsub();

emitter.off(type, handler?)

Removes a specific handler, or all handlers for an event type.

// Remove a specific handler
emitter.off('message', myHandler);

// Remove all handlers for an event type
emitter.off('message');

// Remove every wildcard listener
emitter.off('*');

emitter.emit(type, event)

Emits an event to all registered handlers. Handlers run synchronously from a snapshot, so the listener list can be safely modified during emission.

emitter.emit('message', 'Hello World!');
emitter.emit('data', { value: 42 });

Handlers are called in registration order, and matching wildcard listeners run after them.

Errors are not caught. If a handler throws, the exception propagates out of emit() and every remaining handler — including wildcard listeners — is skipped. This matters when a wildcard is doing audit logging or metrics: an unrelated subscriber that throws will silently take it down with it. Guard handlers you do not control:

emitter.on('order:created', (order) => {
  try {
    riskyThirdPartyHook(order);
  } catch (err) {
    logger.error({ err }, 'hook failed');
  }
});

emitter.all

The underlying Map<EventType, Handler[]> that stores all registered handlers. Can be inspected, serialized, or shared between emitter instances.

Use Cases

Event Bus for React Components

import zephyrEvents from 'zephyr-events';

type UIEvents = {
  'modal:open': { id: string }
  'modal:close': { id: string }
  'toast': { message: string; severity: 'info' | 'error' }
}

// Create a shared event bus
export const uiBus = zephyrEvents<UIEvents>();

// In a React component — clean up on unmount
useEffect(() => {
  const unsub = uiBus.on('toast', (toast) => {
    showToast(toast.message, toast.severity);
  });
  return unsub;
}, []);

Pub/Sub in Node.js Microservices

import zephyrEvents from 'zephyr-events';

type ServiceEvents = {
  'order:created': { orderId: string; total: number }
  'order:shipped': { orderId: string; trackingId: string }
  'inventory:low': { sku: string; remaining: number }
}

const events = zephyrEvents<ServiceEvents>();

// Multiple subscribers
events.on('order:created', sendConfirmationEmail);
events.on('order:created', updateAnalytics);
events.on('inventory:low', notifyWarehouse);

// Audit logging with wildcard
events.on('*', (type, data) => {
  logger.info({ event: type, payload: data });
});

Plugin System

import zephyrEvents from 'zephyr-events';

type PluginEvents = {
  'init': { config: Record<string, unknown> }
  'transform': { input: string }
  'destroy': undefined
}

function createPluginHost() {
  const emitter = zephyrEvents<PluginEvents>();

  return {
    register(plugin: (on: typeof emitter.on) => void) {
      plugin(emitter.on.bind(emitter));
    },
    emit: emitter.emit.bind(emitter),
  };
}

Race-Condition Safety

Unlike most event emitters, Zephyr Events handles listener modification during emission safely. Each emit() call captures both the typed handler list and the wildcard list when it is entered, so adding or removing handlers mid-emit never causes skipped handlers or infinite loops:

const emitter = zephyrEvents();

// Safe: handler removes itself during emit
emitter.on('data', function once(data) {
  emitter.off('data', once);
  process(data); // next handler still fires
});

// Safe: handler adds new listeners during emit
emitter.on('init', () => {
  emitter.on('init', () => {
    // this will NOT fire during the current emit cycle
  });

  emitter.on('*', () => {
    // neither will a wildcard added mid-emit
  });
});

The rule is the same in both directions and for both lists: the set of handlers an emit() delivers to is fixed at the moment emit() is called. A listener added during the cycle waits for the next event; a listener removed during the cycle still receives the current one.

This applies to zephyrEvents only. zephyrEventsFast deliberately drops it — see Fast Mode.

Comparison with Other Event Emitters

| Feature | Zephyr Events | mitt | EventEmitter3 | Node.js EventEmitter | |---------|:---:|:---:|:---:|:---:| | Bundle size | 866B gzip | ~200B | ~7KB | Built-in | | TypeScript types | Native | Native | Bundled | @types/node | | Race-condition safe | Yes | No | No | No | | Fast (no-snapshot) mode | Yes | No | No | No | | Wildcard listeners | Yes | Yes | No | No | | Unsubscribe function | Yes | No | No | No | | Shared handler maps | Yes | Yes | No | No | | Zero dependencies | Yes | Yes | Yes | N/A | | Tree-shakeable ESM | Yes | Yes | Yes | No |

Sizes are not measured the same way: Zephyr Events ships unminified, so 866B is the gzipped size of the published ESM bundle as-is (a bundler's minifier shrinks it further). The mitt and EventEmitter3 figures are the minified+gzipped numbers those projects publish.

Performance Benchmarks

Tested on Apple Silicon M-series (ARM64), Node.js v25.2.1. Every benchmark runs in its own V8 process, median of 3 runs. Run node benchmark-compare.js to reproduce.

Safe vs Fast

| Operation | Safe (snapshot) | Fast (no snapshot) | Delta | |-----------|----------------:|-------------------:|------:| | Creation | 52.2M ops/s | 51.9M ops/s | −1% | | Emit (1 handler) | 97.5M ops/s | 79.8M ops/s | −18% | | Emit (2 handlers) | 94.3M ops/s | 82.2M ops/s | −13% | | Emit (3 handlers) | 88.6M ops/s | 78.6M ops/s | −11% | | Emit (4 handlers) | 38.1M ops/s | 67.3M ops/s | +77% | | Emit (10 handlers) | 27.8M ops/s | 41.8M ops/s | +51% | | Emit (100 handlers) | 7.7M ops/s | 11.5M ops/s | +50% | | Emit (1 handler + wildcard) | 76.7M ops/s | 77.9M ops/s | +2% | | Wildcard only | 90.0M ops/s | 87.7M ops/s | −3% | | On + unsub cycle | 11.3M ops/s | 11.4M ops/s | +1% | | Off (specific handler) | 12.0M ops/s | 12.1M ops/s | +1% | | Mixed (on/emit/unsub) | 9.7M ops/s | 9.5M ops/s | −1% |

The crossover is at four handlers. zephyrEvents snapshots one, two, or three handlers by holding them in locals — an exact snapshot with no allocation — so below four there is nothing for fast mode to save and the safe emitter is 11–18% ahead. From four handlers up, the array copy starts to dominate and fast mode pulls ahead by 50–77%.

Unrolling further was tried and rejected: extending the locals path to four or six handlers won that case but made the one-handler and wildcard-only paths about 8% slower, which are far more common.

Each benchmark runs in a fresh process with its own warmup. That isolation matters: measured in a single shared process, ordering and accumulated GC pressure moved these numbers by more than 2×. No-op handlers measure emitter overhead only — real-world throughput depends on handler complexity.

Bundle Formats

Zephyr Events ships three bundle formats for maximum compatibility:

| Format | File | Size (raw / gzip) | Use case | |--------|------|------------------:|----------| | ESM | dist/zephyr-events.mjs | 4.5KB / 866B | Modern bundlers (Vite, Rollup, webpack 5+) | | CommonJS | dist/zephyr-events.js | 4.5KB / 886B | Node.js require(), older bundlers | | UMD | dist/zephyr-events.umd.js | 4.9KB / 1013B | Script tags, AMD loaders, legacy environments |

All three are generated from the same compiled source by build.js, which loads and smoke-tests each one before the build succeeds.

Requirements

  • Node.js >= 18
  • TypeScript >= 4.7 (for type features; runtime has no TS dependency)
  • Works in all modern browsers (ES2020+ support)

Changelog

Release history and upgrade notes are in CHANGELOG.md.

Contributing

Contributions are welcome. Please read CONTRIBUTING.md before opening a pull request.

Acknowledgments

Zephyr Events is a modernization of mitt by Jason Miller. Built on the same simple API with added type safety, race-condition handling, and universal module support.

License

MIT

Original mitt: MIT (c) Jason Miller