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

signalcore

v2.0.0

Published

Typed Event-Driven core modules

Readme

signalcore

Typed event-driven architecture for TypeScript. Zero runtime overhead. Full type safety.

interface AppEvents {
    login: { user: string };
}

const emitter = new Emitter<AppEvents>();

emitter.on("login", (t) => console.log(`Welcome, ${t.user}`));
//    ^ action autocompleted, trigger.data fully typed

emitter.emit("login", { user: "Alice" });
//                    ^ only valid shape compiles

Why signalcore?

Most event libraries treat events as strings with any payload. You lose type information the moment you call emit or on. signalcore fixes this at the core: every action is permanently linked to its data type.

  • Full type safetyemit with wrong data is a compile error. Handlers receive exactly the type declared for the action. No any leaks.
  • Tree-shakable — import only what you use. ESM + CJS dual format.
  • Systematic approach — not just an event emitter. A complete typed event-driven toolkit:
    • Emitter — typed event bus
    • EventTargetEmitter — bridge to DOM events
    • NodeEventEmitter — bridge to Node.js EventEmitter
    • Union — compose multiple emitters into one interface
    • Middleware — intercept groups of actions
    • ProxyEmitter — reactive objects: proxy.x = v → emit
    • Stor — reactive store with custom comparators (no emit on equal values)
    • until — async helper: await until(emitter, "ready")
  • Runtime-validatable — every event trigger is instanceof Trigger, so you can check it at runtime.
  • Memory safe — uses WeakMap for once wrappers, no timer leaks, no global caches.
  • Architectural freedom — no framework lock-in. Works in browser, Node.js, or anywhere TypeScript runs.

Quick example

import { Emitter, until } from "signalcore";

type Events = {
    ready: { data: string };
    error: { code: number };
};

const bus = new Emitter<Events>();

// Subscribe with full autocomplete
bus.on("ready", ({ data, emitter }) => {
    console.log(data);  // string
    emitter.emit("error", { code: 0 }); // typed emit from handler
});

// Async wait for exactly one emission
const trigger = await until(bus, "ready");

// Broadcast
bus.emit("ready", { data: "hello" });

More examples are available in the examples/ folder of the repository.


Installation

npm install signalcore
yarn add signalcore
pnpm add signalcore
// ESM
import { Emitter, Union, until } from "signalcore";

// CJS
const { Emitter, Union, until } = require("signalcore");

Documentation

Core: Emitter<ActionTypes>

The foundation. An Emitter maps action names to their data types.

import { Emitter } from "signalcore";

type AppEvents = {
    login: { user: string };
    logout: {};
    error: { code: number; message: string };
};

const e = new Emitter<AppEvents>();

Methods

| Method | Signature | Description | |---|---|---| | emit | (action, data) => boolean | Creates a Trigger, calls all handlers. Returns true if any handler was called. | | on | (action, handler) | Subscribe to an action. | | once | (action, handler) | Subscribe for one emission, then auto-unsubscribe. | | off | (action, handler) | Unsubscribe a specific handler. Handler is typed to the action. | | offAll | (action) | Remove all handlers for an action. | | clear | () | Remove all handlers for all actions. | | hasListeners | (action) => boolean | Check if an action has subscribers. | | listenerCount | (action?) => number | Count subscribers (for an action or total). | | actions | () => (keyof ActionTypes)[] | List all actions that have subscribers. |

Error handling

One failing handler never breaks the chain. emit wraps each handler call in try-catch, logs the error to console.error, and continues to the next handler.

The Trigger object

When an action is emitted, each handler receives a Trigger — an intersection of:

TriggerClass & ActionData & { emitter: Emitter<ActionTypes> }
  • t instanceof Trigger — runtime checkable
  • t.user, t.text, ... — typed action data
  • t.emitter — reference to the emitter (non-enumerable, does not appear in JSON.stringify or for..in)

DOM bridge: EventTargetEmitter<ActionTypes>

Wraps a native EventTarget (DOM element, window, document) and bridges its events into the typed emitter system.

import { EventTargetEmitter } from "signalcore";

const clicks = new EventTargetEmitter<{
    click: MouseEvent;
    submit: SubmitEvent;
}>(document);

clicks.on("click", (t) => {
    t.clientX;  // MouseEvent data
    t.event;    // original Event object
}, { capture: true });

// External signal for cleanup
const ctrl = new AbortController();
clicks.on("click", () => {}, { signal: ctrl.signal });
ctrl.abort();

Node.js bridge: NodeEventEmitter<ActionTypes>

Wraps a Node.js EventEmitter (or any object with on/once/off) into the typed system. Action data is a tuple of event arguments.

import { EventEmitter } from "events";
import { NodeEventEmitter } from "signalcore";

const source = new EventEmitter();

const bridge = new NodeEventEmitter<{
    data: [Buffer];
    end: [];
    error: [Error];
}>(source);

bridge.on("data", ([chunk]) => {
    console.log(chunk.toString());
});

Composition: Union<ActionTypes>

Combines multiple emitters with the same ActionTypes into one interface. Supports dynamic add and remove.

import { Emitter, Union } from "signalcore";

type Events = { tick: number };

const a = new Emitter<Events>();
const b = new Emitter<Events>();
const union = new Union(a, b);

union.on("tick", (t) => console.log(t));
a.emit("tick", 1); // fires
b.emit("tick", 2); // fires

const c = new Emitter<Events>();
union.add(c);   // replays existing subscriptions onto c
union.remove(b); // unsubscribes b from all tracked handlers

Union accepts any EmitterLike — including other Union instances. Unlimited nesting.

const inner = new Union(e1, e2);
const outer = new Union(inner, e3); // nested Union

Middleware: Middleware<ActionTypes, Actions>

Attaches one handler to multiple actions. Useful for logging, metrics, or cross-cutting concerns.

import { Emitter, Middleware } from "signalcore";

const bus = new Emitter<{ login: {}; logout: {} }>();

const logger = new Middleware(bus, ["login", "logout"], (t) => {
    console.log("Action:", t);
});

bus.emit("login", {}); // logs

logger.destroy(); // unsubscribes from both actions

Reactive proxy: createProxyEmitter<Data>

Turns a plain object into a reactive proxy: property assignment automatically emits an event with { from, value }.

import { createProxyEmitter } from "signalcore";

const state = createProxyEmitter({ x: 0, y: 0 });

state.emitter.on("x", ({ from, value }) => {
    console.log(`${from} → ${value}`);
});

state.x = 10; // emits "x" with { from: 0, value: 10 }
state.x = 20; // emits "x" with { from: 10, value: 20 }

// Properties cannot be deleted (type integrity)
delete state.x; // no-op, returns false

// Technical fields (emitter) are non-enumerable
JSON.stringify(state); // "{"x":20,"y":0}"

Reactive store: createStor<Data>

Like createProxyEmitter, but with custom comparators. Does not emit if the new value equals the old one by your comparator.

import { createStor } from "signalcore";

const store = createStor(
    { user: { name: "Alice" }, count: 0 },
    { user: (a, b) => a.name === b.name },
);

store.emitter.on("user", ({ value }) => console.log("changed:", value.name));

store.user = { name: "Alice" }; // silent (comparator says "equal")
store.user = { name: "Bob" };   // emits
store.count = 0;                 // silent (===)
store.count = 1;                 // emits

Async helper: until(emitter, action)

Returns a Promise that resolves with the Trigger on the next emission.

import { Emitter, until } from "signalcore";

async function start() {
    const trigger = await until(bus, "ready");
    console.log(trigger.data);
}

Combine with Promise.race for timeouts:

const result = await Promise.race([
    until(bus, "ready"),
    new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000)),
]);

EmitterLike<ActionTypes> interface

Any object that satisfies the EmitterLike interface — including Emitter, Union, or custom implementations — can be used wherever an event source is expected. This enables unlimited compositional flexibility.

import type { EmitterLike } from "signalcore";

function onTick(target: EmitterLike<{ tick: number }>) {
    target.on("tick", (t) => console.log(t));
}

License

Apache-2.0