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

@youzi9601/typed-event-bus

v0.3.2

Published

Define Once, Use Everywhere — Type-safe event bus with zero duplicate declarations

Readme

typed-event-bus

Define Once, Use Everywhere — Type-safe event bus with zero duplicate declarations.

npm version License: MIT TypeScript Bundle Size


Why typed-event-bus?

In TypeScript projects, event names are usually raw strings and payload types are separate interfaces. The connection between them only exists in the developer's mind:

// Traditional approach: strings + any, error-prone during refactoring
bus.emit('user.created', { id: '123', name: 'Alice' })
bus.on('user.created', (payload) => {
  // payload is implicitly any, IDE cannot autocomplete, refactor errors go unnoticed
})

typed-event-bus solves this:

// Define once → inferred everywhere automatically
const userCreated = defineEvent('user.created').payload<{ id: string; name: string }>()

bus.emit(userCreated, { id: '123', name: 'Alice' })  // ✅ Type-checked
bus.emit(userCreated, { id: 123 })                    // ❌ Compile error: id should be string

bus.on(userCreated, (payload) => {
  payload.id    // string ✅ IDE autocomplete
  payload.name  // string ✅
})

Key advantages:

  • Zero duplicate declarations — EventDefinition is the single source of truth
  • Compile-time payload checking — tsserver red squigglies in real time
  • Native IDE support — Autocomplete, Rename Symbol, Go to Definition
  • Wildcard correlation narrowingonAll(namespace, ({ event, payload }) => { ... }) correctly narrows types
  • Explicit sync/async separationemit (fire-and-forget) / emitAsync (await all)
  • < 3 KB gzipped — zero dependencies, extremely lightweight
  • Transport agnostic — Browser / Node / Worker / Electron / Deno / Bun / Edge compatible

Installation

pnpm add @youzi9601/typed-event-bus
# or
npm install @youzi9601/typed-event-bus
# or
yarn add @youzi9601/typed-event-bus

Peer Dependency: typescript >= 5.0.0


Quick Start

import { defineEvent, defineEvents, createEventBus } from '@youzi9601/typed-event-bus'

// 1. Define events (single or namespace)
const userCreated = defineEvent('user.created').payload<{ id: string; name: string }>()

const userEvents = defineEvents('user', {
  created: defineEvent('created').payload<{ id: string; name: string }>(),
  deleted: defineEvent('deleted').payload<{ id: string }>(),
  updated: defineEvent('updated').payload<{ id: string; name: string; version: number }>(),
})

// 2. Create Event Bus
const bus = createEventBus(userEvents)

// 3. Emit event (sync fire-and-forget)
bus.emit(userEvents.created, { id: '123', name: 'Alice' })

// 4. Subscribe (payload fully type-inferred)
bus.on(userEvents.created, (payload) => {
  console.log(payload.id, payload.name)  // Type-safe
})

// 5. Wildcard subscription (correlation narrowing)
bus.onAll(userEvents, ({ event, payload }) => {
  if (event === 'user.created') {
    payload.id  // string ✅ auto-narrowed
  }
  if (event === 'user.deleted') {
    payload.id  // string ✅
  }
})

// 6. Async emission (await all async listeners)
await bus.emitAsync(userEvents.created, { id: '123', name: 'Alice' })

// 7. Unsubscribe
const sub = bus.on(userEvents.created, (payload) => {
  console.log(payload.id, payload.name)
})
sub.unsubscribe()

API Overview

Event Definition

| Function | Description | |----------|-------------| | defineEvent(name).payload<T>() | Create a single event definition (builder chain) | | defineEvents(prefix, definitions) | Create a namespace with auto-prefixed names |

Event Bus Creation

| Function | Description | |----------|-------------| | createEventBus(registry, options?) | Create bus, supports single or merged namespaces |

Emit

| Method | Behavior | |--------|----------| | bus.emit(event, payload) | Sync fire-and-forget, catches exceptions → onError → continues | | bus.emitAsync(event, payload, options?) | Async, awaits all async listeners in parallel (Promise.all), aggregates exceptions as MultiError. Pass { sequential: true } to execute in registration order (Node strict order) |

Subscribe

| Method | Description | |--------|-------------| | bus.on(event, listener, options?) | Subscribe, returns Subscription | | bus.once(event, listener, options?) | Subscribe once | | bus.prependListener(event, listener, options?) | Subscribe at the front of the listener order (Node parity) | | bus.prependOnceListener(event, listener, options?) | Subscribe once, at the front of the listener order (Node parity) | | bus.onAll(namespace, handler, options?) | Namespace wildcard, handler receives { event, payload } |

Meta Events (Node parity)

newListener fires before a listener is registered, removeListener after it is removed — including once auto-removal and removeAllListeners. Subscribe with the built-in definitions:

import { newListenerEvent, removeListenerEvent } from '@youzi9601/typed-event-bus'

bus.on(newListenerEvent, listener => { /* listener being registered */ })
bus.on(removeListenerEvent, listener => { /* listener being removed */ })

Meta events run with the same listener semantics as emit (snapshot, once auto-removal, errors → onError) but do not run middleware.

Subscription

const sub = bus.on(event, handler)
sub.unsubscribe()        // Primary API
sub.signal               // Optional AbortSignal
sub.unsubscribed         // Whether subscription has been cancelled

Other

| API | Description | |-----|-------------| | bus.use(middleware) | Register middleware | | bus.listenerCount(event) | Get listener count | | bus.eventNames() | Get all registered event names | | bus.rawListeners(event) | Get listener functions in registration order (once listeners returned unwrapped, unlike Node) | | bus.removeAllListeners(event?) | Remove all listeners | | bus.off(event, listener) | Remove a specific listener (removes the last matching registration, Node parity) |

Bus Options

| Option | Description | |--------|-------------| | maxListeners | Warn via console.warn when a single event exceeds this many listeners (Node parity) | | debug | Log subscribe/unsubscribe/emit activity to console.debug |


Advanced Usage

Multi-Namespace Merge (Cross-module Decoupling)

// src/user/events.ts
export const userEvents = defineEvents('user', { ... })

// src/order/events.ts
export const orderEvents = defineEvents('order', { ... })

// src/main.ts
import { userEvents } from './user/events'
import { orderEvents } from './order/events'

const bus = createEventBus({
  user: userEvents,
  order: orderEvents,
})

Error Handling

const bus = createEventBus(userEvents, {
  onError: (error, event, payload) => {
    // Custom error handling (defaults to console.error)
    sentry.captureException(error, { extra: { event: event.name, payload } })
  }
})

Middleware

// Built-in middleware factories
bus.use(createLoggingMiddleware())
bus.use(createTimingMiddleware())
bus.use(createMetricsMiddleware((name, size) => metrics.record(name, size)))

// Custom middleware
bus.use((event, payload, next) => {
  console.log(`[emit] ${event.name}`, payload)
  next()
})

Electron / Worker Cross-Process

// Core package has zero dependencies, use with @typed-event-bus/adapter-*
// (adapters are separate packages, published after 1.0)

Development Guide

# Install dependencies
pnpm install

# Dev mode (watch)
pnpm dev

# Single build
pnpm build

# Testing
pnpm test              # runtime tests
pnpm test:types        # type tests (vitest expectTypeOf)
pnpm test:watch        # watch mode

# Code quality
pnpm lint              # biome check
pnpm lint:fix          # biome check --write
pnpm format            # biome format --write

# Benchmarking
pnpm bench
node scripts/check-budget.js   # pure Node (zlib), works on any platform

# Full check (CI equivalent)
pnpm check

Project Structure

src/
├── bus.ts                  # createEventBus factory function (main entry)
├── constants.ts            # internal metadata keys + meta event definitions (newListenerEvent, removeListenerEvent)
├── bus/
│   ├── context.ts          # BusContext - internal state (listeners, middlewares, options, registry)
│   ├── emit.ts             # sync emit (fire-and-forget)
│   ├── emit-async.ts       # async emit (await all, aggregates MultiError)
│   ├── on.ts               # subscribe (sync/async/prepend), newListener meta event
│   ├── on-all.ts           # wildcard with correlation narrowing
│   ├── once.ts             # subscribe once
│   ├── off.ts              # unsubscribe single (lastIndexOf semantics, removeListener meta event)
│   └── utils.ts            # runListeners, emitMetaEvent, listenerCount, eventNames, rawListeners, removeAllListeners, use
├── define.ts               # defineEvent (builder chain), defineEvents, type guards
├── errors.ts               # MultiError, defaultErrorHandler
├── middleware.ts           # executeMiddleware, createLogging/Timing/MetricsMiddleware
├── subscription.ts         # EventSubscription class, createSubscription
├── types.ts                # Core types: EventDefinition, EventNamespace, EventsOf, WildcardHandler, Subscription, ErrorHandler, Middleware, BusOptions, PayloadOf, NameOf
└── index.ts                # Public API exports (barrel file)

tests/
├── runtime/        # Vitest runtime tests
└── types/          # Vitest type tests (*.test-d.ts)

bench/              # Benchmark tests
scripts/            # CI helper scripts

Contributing

Contributions welcome! Please read:


License

MIT License © 2026 Youzi9601


Related Links