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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@statedelta-libs/event-emitter

v0.0.1

Published

High-performance EventEmitter with caching, wildcards and priority support

Readme

@statedelta-libs/event-emitter

High-performance EventEmitter with O(1) cached emit for hot paths.

Philosophy

Emit é hot path. Em loops de tick (60+ fps), rules engines e resource stores, cada emit() conta.

O problema: EventEmitters tradicionais alocam array a cada emit.

Tradicional:  emit() → [...handlers] → allocation → GC pressure
Otimizado:    emit() → cache.get()   → O(1)       → zero allocation

A solução: Cache lazy com invalidação seletiva.

┌────────────────────────────────────────────────────────────────────────────┐
│                           EMIT HOT PATH                                     │
│                                                                             │
│   emit('tick')                                                              │
│       │                                                                     │
│       ▼                                                                     │
│   ┌────────┐     ┌────────────┐     ┌────────────┐                         │
│   │ CACHE  │────▶│  HANDLERS  │────▶│  EXECUTE   │                         │
│   │ LOOKUP │     │   ARRAY    │     │  INLINE    │                         │
│   │  O(1)  │     │  (cached)  │     │  1,2,N     │                         │
│   └────────┘     └────────────┘     └────────────┘                         │
│                                                                             │
│   Cache HIT: Zero allocation                                                │
│   Cache MISS: 1 array (cached for next emit)                                │
└────────────────────────────────────────────────────────────────────────────┘

Installation

pnpm add @statedelta-libs/event-emitter

Quick Start

import { EventEmitter } from '@statedelta-libs/event-emitter';

// Define events
interface MyEvents {
  'tick': () => void;
  'user:login': (userId: string) => void;
  'data': (payload: { id: number }) => void;
}

// Create emitter
const emitter = new EventEmitter<MyEvents>();

// Register handlers
emitter.on('user:login', (userId) => {
  console.log('User logged in:', userId);
});

// Emit events
emitter.emit('user:login', 'user-123');

Features

| Feature | Description | Default | Overhead | |---------|-------------|---------|----------| | Cache | Zero-allocation emit on cache hit | true | ~100 bytes/event | | Wildcards | Pattern matching (user:*, *) | false | O(d) per emit | | Priority | Handler execution order | false | Sort on cache miss | | Node.js API | Compatible with EventEmitter | Yes | None |

Constructor Options

const emitter = new EventEmitter({
  cached: true,      // Enable handler caching (default: true)
  wildcards: false,  // Enable wildcard patterns (default: false)
  priority: false,   // Enable priority ordering (default: false)
  maxListeners: 10,  // Max listeners warning threshold (default: 10)
});

Configuration Profiles

// Hot path (tick, render loop, rules engine)
const emitter = new EventEmitter({
  cached: true,
  wildcards: false,
  priority: false,
});

// Flexible (plugins, logging, debugging)
const emitter = new EventEmitter({
  cached: true,
  wildcards: true,
  priority: true,
});

// Minimal (many on/off cycles, few emits)
const emitter = new EventEmitter({
  cached: false,
  wildcards: false,
  priority: false,
});

Core API

on / off / emit

// Register handler
emitter.on('event', handler);
emitter.on('event', handler, { priority: 100, once: true, id: 'my-handler' });

// Remove handler
emitter.off('event', handler);   // Remove specific
emitter.off('event');            // Remove all for event

// One-time handler
emitter.once('event', handler);

// Emit event (returns true if had listeners)
const hadListeners = emitter.emit('event', ...args);

Handler Options

interface HandlerOptions {
  priority?: number;  // Execution order (higher first, default: 0)
  once?: boolean;     // Auto-remove after first call (default: false)
  id?: string;        // Identifier for debugging
}

emitter.on('tick', handler, {
  priority: 100,
  once: true,
  id: 'my-tick-handler'
});

Inspection

emitter.listenerCount('event');     // Number of handlers
emitter.hasListeners('event');      // Boolean check
emitter.eventNames();               // All event names with handlers
emitter.listeners('event');         // Handler functions
emitter.rawListeners('event');      // Handler entries with metadata

Node.js Compatibility

Full compatibility with Node.js EventEmitter API.

// Aliases
emitter.addListener('event', handler);     // = on()
emitter.removeListener('event', handler);  // = off()
emitter.removeAllListeners('event');       // Remove all

// Prepend (requires priority: true for effect)
emitter.prependListener('event', handler);
emitter.prependOnceListener('event', handler);

// Max listeners warning
emitter.setMaxListeners(20);
emitter.getMaxListeners();

Caching

Cache eliminates array allocation on emit by storing sorted handler arrays.

const emitter = new EventEmitter({ cached: true });

emitter.on('tick', handler1);
emitter.on('tick', handler2);

// First emit: cache MISS (builds cache)
emitter.emit('tick');

// Subsequent emits: cache HIT (zero allocation)
emitter.emit('tick');  // ~2.5M ops/s
emitter.emit('tick');
emitter.emit('tick');

Cache Invalidation

Cache is automatically invalidated on on() and off() calls.

emitter.on('tick', handler);   // Invalidates 'tick' cache
emitter.off('tick', handler);  // Invalidates 'tick' cache

// Other events unaffected
emitter.emit('other');  // Still cache HIT

Cache Stats

const stats = emitter.getCacheStats();
// {
//   size: 3,        // Cached event types
//   hits: 1000,     // Cache hits
//   misses: 3,      // Cache misses
//   hitRate: 0.997  // Hit rate (0-1)
// }

// Manual control
emitter.clearCache();

Wildcards

Match events by pattern when wildcards are enabled.

const emitter = new EventEmitter({ wildcards: true });

// Prefix wildcard - matches user:login, user:logout, user:profile:update
emitter.on('user:*', (data) => {
  console.log('User event:', data);
});

// Global wildcard - matches ALL events
emitter.onAny((event, ...args) => {
  console.log('Event:', event, args);
});

// Emit triggers exact + wildcards
emitter.emit('user:login', 'user-123');
// Triggers: 'user:login' handlers + 'user:*' handlers + onAny handlers

Pattern Matching

| Pattern | Matches | Storage | |---------|---------|---------| | user:login | Exact match only | _listeners | | user:* | user:login, user:logout, user:profile:update | _wildcards['user'] | | * | All events | _anyHandlers |

Wildcard API

// Register
emitter.on('user:*', handler);  // Prefix wildcard
emitter.onAny(handler);         // Global wildcard

// Remove
emitter.off('user:*', handler);
emitter.offAny(handler);

Wildcard Matching Algorithm

emit('user:profile:update', data)
    │
    ├── 1. Exact: _listeners.get('user:profile:update')
    │
    ├── 2. Prefixes (bottom-up):
    │      'user' → _wildcards.get('user')           // user:*
    │      'user:profile' → _wildcards.get('user:profile')  // user:profile:*
    │
    └── 3. Any: _anyHandlers

Priority

Execute handlers in priority order (higher first).

const emitter = new EventEmitter({ priority: true });

emitter.on('tick', () => console.log('second'), { priority: 50 });
emitter.on('tick', () => console.log('first'), { priority: 100 });
emitter.on('tick', () => console.log('third'), { priority: 10 });
emitter.on('tick', () => console.log('fourth'));  // priority: 0 (default)

emitter.emit('tick');
// Output: first, second, third, fourth

Priority Semantics

Priority 100  → executes first
Priority 50   → executes second
Priority 0    → default
Priority -10  → executes last

Sort happens only on cache build (not on every emit).

Performance

Optimized for hot paths where emit is called frequently.

Benchmarks (expected)

| Scenario | ops/s | |----------|-------| | emit (1 handler, cache hit) | ~3M | | emit (3 handlers, cache hit) | ~2.5M | | emit (3 handlers, no cache) | ~1.2M | | emit (with priority) | ~2.2M | | emit (with wildcards) | ~1.8M | | on + off cycle | ~500K |

Complexity

| Operation | Time | Allocation | |-----------|------|------------| | on() | O(1) | 1 entry | | off() | O(h) | 0 | | emit() cache hit | O(h) | 0 | | emit() cache miss | O(h log h)* | 1 array | | listenerCount() | O(1) | 0 |

*O(h log h) only if priority enabled (sort)

Memory

| Component | Size | |-----------|------| | Empty EventEmitter | ~200 bytes | | Per event (no handlers) | ~64 bytes | | Per handler entry | ~32 bytes | | Cache per event | ~24 bytes + array |

TypeScript

Full type inference for events and handlers.

interface Events {
  'user:login': (userId: string, timestamp: number) => void;
  'user:logout': (userId: string) => void;
  'error': (error: Error) => void;
}

const emitter = new EventEmitter<Events>();

// Type-safe handlers
emitter.on('user:login', (userId, timestamp) => {
  // userId: string, timestamp: number (inferred)
});

// Type-safe emit
emitter.emit('user:login', 'user-123', Date.now());  // OK
emitter.emit('user:login', 123);  // Error: expected string

Exported Types

import type {
  // Core types
  EventMap,
  EventHandler,
  EventParams,

  // Options
  HandlerOptions,
  EventEmitterOptions,

  // Internal
  HandlerEntry,
  CacheStats,
  AnyHandler,

  // Interfaces
  IEventEmitter,
  IFastEventEmitter,
} from '@statedelta-libs/event-emitter';

import {
  EventEmitter,
  createEventEmitter,
} from '@statedelta-libs/event-emitter';

Factory Function

import { createEventEmitter } from '@statedelta-libs/event-emitter';

const emitter = createEventEmitter<MyEvents>({
  cached: true,
  wildcards: true,
  priority: true,
});

Integration with StateDelta

import { EventEmitter } from '@statedelta-libs/event-emitter';

// TickManager - hot path (60+ fps)
class TickManager {
  private emitter = new EventEmitter<{
    'tick:before': (tick: number) => void;
    'tick:after': (tick: number) => void;
  }>({ cached: true });

  tick() {
    this.emitter.emit('tick:before', this.tickCount);
    // ... process
    this.emitter.emit('tick:after', this.tickCount);
  }
}

// RulesEngine - with wildcards for debugging
class RulesEngine {
  private emitter = new EventEmitter<{
    'rule:matched': (rule: Rule) => void;
    'rule:executed': (rule: Rule, result: any) => void;
  }>({ cached: true, wildcards: true });
}

Scripts

pnpm build          # Build (ESM + CJS + .d.ts)
pnpm test           # Run tests
pnpm test:watch     # Watch mode
pnpm test:coverage  # Coverage report
pnpm typecheck      # Type checking
pnpm bench          # Benchmarks

License

MIT