@thuum/event-emitter
Installation
Section titled “Installation”npm install @thuum/event-emitterOverview
Section titled “Overview”@thuum/event-emitter provides a unified, type-safe interface for pub/sub communication across different runtime backends:
eventTarget— Adapter for the DOMEventTargetAPI (browsers, Deno, Node 15+)messagePort— Adapter forBroadcastChannel,MessagePort,Worker, andWindow.postMessagenodeEvents— Adapter for Node.jsEventEmitterrxjs— Adapter for RxJSSubject/Observableconnect— Utility to forward events between any listener→emitter pair
Each adapter exposes the same two-function surface — emitter(source) and listener(source) — making it trivial to swap transports without changing application logic.
Features
Section titled “Features”- 🎯 Type Safe — Event names and payloads are checked at compile time
- 🔌 Multi-Backend — One interface, four transports: DOM, MessagePort, Node.js, RxJS
- 🧩 Composable — Mix and match adapters; connect any listener to any emitter
- 🪶 Zero Dependencies — No external runtime dependencies
- ❄️ Frozen Instances — All returned objects are
Object.freezed for safety - 🛑 Deterministic Cleanup — Every subscription returns a
{ stop() }handle
Choosing an Adapter
Section titled “Choosing an Adapter”| Adapter | Import Path | Transport | Use Case |
|---|---|---|---|
| eventTarget | @thuum/event-emitter/event-target | DOM EventTarget | In-process pub/sub, custom elements, service workers |
| messagePort | @thuum/event-emitter/message-port | postMessage channel | Cross-window, cross-worker, BroadcastChannel communication |
| nodeEvents | @thuum/event-emitter/node-events | Node.js EventEmitter | Server-side event buses, streams, CLI tools |
| rxjs | @thuum/event-emitter/rxjs | RxJS Subject | Reactive pipelines, multiplexed event streams |
When to Choose Which
Section titled “When to Choose Which”eventTarget— You want in-process pub/sub that interops with native DOM event listeners andCustomEvent.messagePort— You need to send typed events across execution contexts (windows, workers, iframes).nodeEvents— You’re in Node.js and want type safety on top of the built-inEventEmitter.rxjs— You’re already using RxJS and want to multiplex typed events through a singleSubject.connect— You want to bridge two systems together, forwarding specific events from one to another.
Architecture
Section titled “Architecture”The library is built around two core interfaces that all adapters implement:
interface Emitter<Map extends EventMap> { emit<K extends keyof Map>(name: K & string, value: Map[K & string]): void;}
interface Listener<Map extends EventMap> { on<K extends keyof Map>( name: K & string, handler: (value: Map[K & string]) => void, ): { stop(): void };}An EventMap is simply a record mapping string event names to payload types:
type EventMap = { [name: string]: unknown };This shared contract means you can write transport-agnostic code and swap the underlying backend without changing your event handling logic.
Core Types
Section titled “Core Types”import type { EventMap, EventUnion, Emitter, Listener } from "@thuum/event-emitter";EventMap
Section titled “EventMap”Base constraint for event maps — a record of string keys to payload types.
type MyEvents = { message: string; error: Error; disconnect: undefined;};EventUnion<Map>
Section titled “EventUnion<Map>”Converts an event map into a discriminated union of { name, value } pairs. Used internally by the message-port and rxjs adapters to multiplex events through a single channel.
type MyEvents = { message: string; error: Error };
type Union = EventUnion<MyEvents>;// => { name: "message"; value: string } | { name: "error"; value: Error }Emitter<Map>
Section titled “Emitter<Map>”Interface for type-safe event dispatching.
declare const emitter: Emitter<MyEvents>;
emitter.emit("message", "hello"); // ✓emitter.emit("error", new Error("fail")); // ✓emitter.emit("message", 42); // ✗ type errorListener<Map>
Section titled “Listener<Map>”Interface for type-safe event subscription. Returns a { stop() } handle for deterministic cleanup.
declare const listener: Listener<MyEvents>;
const sub = listener.on("message", (value) => { console.log(value); // typed as `string`});
sub.stop(); // unsubscribeeventTarget
Section titled “eventTarget”import { emitter, listener } from "@thuum/event-emitter/event-target";Adapts a DOM EventTarget. Events are dispatched as CustomEvent with payloads in the detail property.
Emitting Events
Section titled “Emitting Events”import { emitter } from "@thuum/event-emitter/event-target";
type Events = { login: { userId: string }; logout: undefined;};
const target = new EventTarget();const emit = emitter<Events>(target);
emit.emit("login", { userId: "abc-123" });emit.emit("logout", undefined);Listening for Events
Section titled “Listening for Events”import { emitter, listener } from "@thuum/event-emitter/event-target";
type Events = { message: { text: string; from: string }; disconnect: undefined;};
const target = new EventTarget();const emit = emitter<Events>(target);const listen = listener<Events>(target);
// Handler receives the typed payload directly:const sub = listen.on("message", (value) => { console.log(value.text); // string console.log(value.from); // string});
emit.emit("message", { text: "hello", from: "server" });
// Unsubscribe when done:sub.stop();Multiple Independent Subscriptions
Section titled “Multiple Independent Subscriptions”const target = new EventTarget();const listen = listener<{ data: string }>(target);
const sub1 = listen.on("data", (v) => console.log("A:", v));const sub2 = listen.on("data", (v) => console.log("B:", v));
// Stop one without affecting the other:sub1.stop();messagePort
Section titled “messagePort”import { emitter, listener } from "@thuum/event-emitter/message-port";Adapts BroadcastChannel, MessagePort, Worker, or Window. Events are sent as { name, value } objects via postMessage and filtered by name on the receiving side.
Cross-Tab Communication
Section titled “Cross-Tab Communication”import { emitter, listener } from "@thuum/event-emitter/message-port";
type Events = { "user:login": { id: string; role: string }; "user:logout": { id: string };};
// In Tab A — send events:const channel = new BroadcastChannel("auth");const emit = emitter<Events>(channel);
emit.emit("user:login", { id: "abc", role: "admin" });// In Tab B — receive events:const channel = new BroadcastChannel("auth");const listen = listener<Events>(channel);
const sub = listen.on("user:login", (payload) => { console.log(payload.id); // "abc" console.log(payload.role); // "admin"});
// Cleanup:sub.stop();Worker Communication
Section titled “Worker Communication”import { emitter, listener } from "@thuum/event-emitter/message-port";
type WorkerEvents = { "task:start": { taskId: string }; "task:done": { taskId: string; result: unknown };};
// Main thread → Workerconst worker = new Worker("./worker.js");const emit = emitter<WorkerEvents>(worker);
emit.emit("task:start", { taskId: "t1" });
// Worker → Main thread (inside worker.js)const listen = listener<WorkerEvents>(self);listen.on("task:start", (task) => { // process task...});nodeEvents
Section titled “nodeEvents”import { emitter, listener } from "@thuum/event-emitter/node-events";Wraps a Node.js EventEmitter with type-safe emit and on methods.
Basic Usage
Section titled “Basic Usage”import { EventEmitter } from "node:events";import { emitter, listener } from "@thuum/event-emitter/node-events";
type Events = { message: string; error: Error };
const ee = new EventEmitter();const emit = emitter<Events>(ee);const listen = listener<Events>(ee);
const sub = listen.on("message", (msg) => { console.log(msg); // typed as string});
emit.emit("message", "hello");
// Unsubscribe:sub.stop();Multiple Event Types
Section titled “Multiple Event Types”import { EventEmitter } from "node:events";import { emitter, listener } from "@thuum/event-emitter/node-events";
type Events = { request: { method: string; url: string }; response: { status: number; body: string }; error: Error;};
const bus = new EventEmitter();const emit = emitter<Events>(bus);const listen = listener<Events>(bus);
listen.on("request", (req) => { console.log(`${req.method} ${req.url}`);});
listen.on("error", (err) => { console.error(err.message);});
emit.emit("request", { method: "GET", url: "/api/users" });emit.emit("error", new Error("timeout"));import { emitter, listener } from "@thuum/event-emitter/rxjs";Multiplexes typed events as { name, value } objects through a single RxJS-compatible Subject. Useful for reactive pipelines where multiple event types share one stream.
Basic Usage
Section titled “Basic Usage”import { Subject } from "rxjs";import { emitter, listener } from "@thuum/event-emitter/rxjs";import type { EventUnion } from "@thuum/event-emitter";
type Events = { message: string; disconnect: { reason: string };};
const subject = new Subject<EventUnion<Events>>();const emit = emitter<Events>(subject);const listen = listener<Events>(subject);
const sub = listen.on("message", (text) => { console.log("received:", text);});
emit.emit("message", "hello");
// Unsubscribe (calls subscription.unsubscribe() internally):sub.stop();Filtering by Event Name
Section titled “Filtering by Event Name”import { Subject } from "rxjs";import { emitter, listener } from "@thuum/event-emitter/rxjs";import type { EventUnion } from "@thuum/event-emitter";
type Events = { ping: undefined; pong: undefined };
const subject = new Subject<EventUnion<Events>>();const emit = emitter<Events>(subject);const listen = listener<Events>(subject);
// Only fires for "pong" events:listen.on("pong", () => { console.log("pong received");});
emit.emit("ping", undefined); // handler not calledemit.emit("pong", undefined); // logs: "pong received"connect(source, sink, ...names)
Section titled “connect(source, sink, ...names)”Bridges a listener (source) to an emitter (sink), forwarding all specified events from one side to the other.
import { connect } from "@thuum/event-emitter";Forward Events Between Targets
Section titled “Forward Events Between Targets”import { connect } from "@thuum/event-emitter";import { emitter, listener } from "@thuum/event-emitter/event-target";
type Events = { message: { text: string }; error: { code: number };};
const source = new EventTarget();const dest = new EventTarget();
const connection = connect( listener<Events>(source), emitter<Events>(dest), "message", "error",);
// Events dispatched on `source` are now forwarded to `dest`.// Stop forwarding:connection.stop();Bidirectional Bridge
Section titled “Bidirectional Bridge”import { connect } from "@thuum/event-emitter";import { emitter, listener } from "@thuum/event-emitter/event-target";
type Events = { ping: undefined; pong: undefined };
const a = new EventTarget();const b = new EventTarget();
// Wire A → B and B → A:const ab = connect(listener<Events>(a), emitter<Events>(b), "ping");const ba = connect(listener<Events>(b), emitter<Events>(a), "pong");
// Cleanup both directions:ab.stop();ba.stop();Cross-Transport Bridging
Section titled “Cross-Transport Bridging”Because all adapters share the same Emitter/Listener interfaces, you can bridge different transports:
import { connect } from "@thuum/event-emitter";import { listener } from "@thuum/event-emitter/node-events";import { emitter } from "@thuum/event-emitter/message-port";import { EventEmitter } from "node:events";
type Events = { update: { id: string; data: unknown } };
const ee = new EventEmitter();const channel = new BroadcastChannel("updates");
// Forward Node.js events → BroadcastChannel:const bridge = connect( listener<Events>(ee), emitter<Events>(channel), "update",);
bridge.stop();Patterns
Section titled “Patterns”Selective Forwarding
Section titled “Selective Forwarding”Only forward a subset of events between systems:
import { connect } from "@thuum/event-emitter";import { emitter, listener } from "@thuum/event-emitter/event-target";
type Events = { mousedown: { x: number; y: number }; mousemove: { x: number; y: number }; mouseup: { x: number; y: number };};
const input = new EventTarget();const renderer = new EventTarget();
// Only forward mousedown and mouseup, skip the noisy mousemove:connect( listener<Events>(input), emitter<Events>(renderer), "mousedown", "mouseup",);Shared Event Bus
Section titled “Shared Event Bus”Create a centralized, type-safe event bus:
import { emitter, listener } from "@thuum/event-emitter/event-target";
type AppEvents = { "auth:login": { userId: string }; "auth:logout": undefined; "nav:change": { route: string }; "theme:toggle": undefined;};
const bus = new EventTarget();
export const publish = emitter<AppEvents>(bus);export const subscribe = listener<AppEvents>(bus);// In any module:import { publish, subscribe } from "./event-bus";
subscribe.on("auth:login", ({ userId }) => { console.log(`User ${userId} logged in`);});
publish.emit("auth:login", { userId: "user-42" });Transport-Agnostic Modules
Section titled “Transport-Agnostic Modules”Write modules that accept the abstract interfaces, making them testable and transport-independent:
import type { Emitter, Listener } from "@thuum/event-emitter";
type NotificationEvents = { notify: { title: string; body: string }; dismiss: { id: string };};
function createNotificationService( emit: Emitter<NotificationEvents>, listen: Listener<NotificationEvents>,) { listen.on("dismiss", ({ id }) => { console.log(`Dismissed notification ${id}`); });
return { show(title: string, body: string) { emit.emit("notify", { title, body }); }, };}Sub-path Exports
Section titled “Sub-path Exports”| Path | Contents |
|---|---|
@thuum/event-emitter | Core types + connect + all adapter namespaces |
@thuum/event-emitter/event-target | emitter, listener for DOM EventTarget |
@thuum/event-emitter/message-port | emitter, listener for MessagePort/BroadcastChannel |
@thuum/event-emitter/node-events | emitter, listener for Node.js EventEmitter |
@thuum/event-emitter/rxjs | emitter, listener for RxJS Subject/Observable |
License
Section titled “License”ISC