@statedelta-libs/event-emitter
v0.0.1
Published
High-performance EventEmitter with caching, wildcards and priority support
Maintainers
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 allocationA 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-emitterQuick 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 metadataNode.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 HITCache 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 handlersPattern 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: _anyHandlersPriority
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, fourthPriority Semantics
Priority 100 → executes first
Priority 50 → executes second
Priority 0 → default
Priority -10 → executes lastSort 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 stringExported 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 # BenchmarksLicense
MIT
