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

eventify

v3.1.0

Published

Tiny event emitter with strict TypeScript typings and optional schema validation.

Readme

Eventify

Tiny event emitter with strict TypeScript types, wildcard namespaces, and optional schema validation.

  • ESM only, tree-shakeable
  • Node 20+, Bun, modern browsers
  • Backbone-style semantics (all, listener snapshots, predictable order)

Install

npm install eventify

Quickstart

import { createEmitter } from "eventify";

const emitter = createEmitter();

emitter.on("alert", (message) => {
  console.log(message);
});

emitter.trigger("alert", "hello");

Usage

Mixins

import { decorateWithEvents } from "eventify";

const target = decorateWithEvents({ name: "service" });

Typed Events

type Events = {
  ready: void;
  "change:title": string;
  data: [string, number];
};

const emitter = createEmitter<Events>();

emitter.on("data", (name, count) => {
  console.log(name, count);
});

emitter.trigger("data", "hello", 42);

Event Maps + Space-Delimited Names

emitter.on({
  "change:title": () => console.log("title"),
  "change:author": () => console.log("author"),
});

emitter.on("open close", () => console.log("toggled"));

Namespaces + Wildcards

Event names are split by namespaceDelimiter (default /). The wildcard token (default *) matches segments.

  • * in the middle matches one segment
  • trailing * matches remaining segments
  • * alone matches any event (use "all" if you need the event name)
  • leading/trailing delimiters create empty segments that must match exactly
const emitter = createEmitter({
  namespaceDelimiter: "/",
  wildcard: "*",
});

emitter.on("/product/foo/org/123/user/56/*", () => {
  console.log("any account for that user");
});

emitter.on("/product/foo/org/123/*", () => {
  console.log("any user in org 123");
});

emitter.on("/product/foo/*", () => {
  console.log("any org in product foo");
});

emitter.trigger("/product/foo/org/123/user/56/account/abcd");

Middle-segment wildcards:

emitter.on("/product/foo/org/*/tracked-object/*/assesment", () => {
  console.log("any tracked-object assesment within an org");
});

Colon namespaces:

const emitter = createEmitter({
  namespaceDelimiter: ":",
  wildcard: "*",
});

emitter.on("namespace:foo:*", () => {
  console.log("any sub-event in namespace:foo");
});

Schemas (Zod v4 Compatible)

Any schema with parse or safeParse works. Zod is supported via DI.

import { z } from "zod";
import { createEmitter, setDefaultSchemaValidator } from "eventify";

const schemas = {
  data: z.tuple([z.string(), z.number()]),
  ready: z.undefined(),
};

const emitter = createEmitter({
  schemas,
  validate: setDefaultSchemaValidator,
});

emitter.on("data", (name, count) => {
  console.log(name, count);
});

emitter.trigger("data", "hello", 1);

Validation runs on trigger/emit/produce. If validation fails, the call throws and no listeners run.

Error Handling

Listener failures never crash by default. Errors are routed to onError.

const emitter = createEmitter({
  onError: (error, meta) => {
    console.error(meta.event, error);
  },
});

emitter.on("boom", () => {
  throw new Error("nope");
});

emitter.trigger("boom");

EventTarget Interop

createEmitter and decorateWithEvents expose addEventListener, removeEventListener, and dispatchEvent.

trigger/emit/produce dispatch a CustomEvent with the payload in event.detail. dispatchEvent only uses the EventTarget path (and matching on listeners). It does not run schemas, patterns, or "all". detail is the single payload for 1-arg events, an array for multi-arg events, and undefined for no-arg events.

Async Iteration

const emitter = createEmitter();
const iterator = emitter.iterate("data");

emitter.trigger("data", "a", 1);

const { value } = await iterator.next();
// value -> ["a", 1]

Iterate all events:

const all = emitter.iterate("all");
emitter.trigger("ready");
const { value } = await all.next();
// value -> ["ready"]

Abort with an AbortSignal:

const controller = new AbortController();
const iterator = emitter.iterate("data", { signal: controller.signal });
controller.abort();

for await with abort:

const controller = new AbortController();

(async () => {
  for await (const value of emitter.iterate("data", {
    signal: controller.signal,
  })) {
    console.log(value);
    controller.abort();
  }
})();

Failure Modes + Constraints

  • Validation failure: trigger/emit/produce throws and no listeners run (including wildcard and all).
  • Listener errors: thrown errors or rejected promises are routed to onError and do not stop other listeners.
  • Invalid callbacks: invoking a non-function throws a TypeError that is routed to onError.
  • onError failures: errors thrown by the handler are swallowed to avoid crashing producers.
  • iterate backpressure: if producers emit faster than you consume, the iterator queue grows. Use AbortSignal, return(), or stop iteration.
  • listenTo/listenToOnce: the target must be an Eventify emitter (or another object with compatible on/once/off).

Constraint tools:

  • Payload: schemas + validate enforce data shape at emit time.
  • Cardinality: once / listenToOnce.
  • Lifetime: off / stopListening / iterate + AbortSignal.
  • Namespace scope: namespaceDelimiter + wildcard.
  • Errors: onError centralizes listener failures.

Semantics

  • Dispatch order: event listeners, then matching patterns, then "all".
  • Listener lists are snapshotted at emit time; mutations during dispatch do not affect the current cycle.
  • The context defaults to the emitter.
  • Duplicate registrations are allowed.
  • "all" is a compatibility feature (Backbone/Eventify style); it is not a standard EventTarget concept.

API

Preferred Named Exports

createEmitter([options]);
decorateWithEvents([target], [options]);
setDefaultSchemaValidator(schema, payload, meta);

createEmitter returns a standalone emitter. decorateWithEvents mixes Eventify methods into an existing object. setDefaultSchemaValidator is the default validator function (no global mutation).

Default Export (Compat)

Eventify.create([options]);
Eventify.enable([target], [options]);
Eventify.defaultSchemaValidator;
Eventify.version;
Eventify.proto;

The Eventify default export remains for compatibility.

Options

type EventifyOptions = {
  schemas?: Record<string, SchemaLike>;
  validate?: SchemaValidator;
  onError?: (
    error: unknown,
    meta: {
      event: string;
      args: unknown[];
      listener?: (...args: unknown[]) => unknown;
      emitter: object;
    },
  ) => void;
  namespaceDelimiter?: string; // default "/"
  wildcard?: string; // default "*"
};

Events

on(event, callback, [context])
on({ event: callback, ... }, [context])
on("a b c", callback, [context])
once(event, callback, [context])
once({ event: callback, ... }, [context])
once("a b c", callback, [context])
off()
off(event, [callback], [context])
off({ event: callback, ... }, [context])
trigger(event, ...args);
emit(event, ...args);
produce(event, ...args);

emit and produce are aliases of trigger.

EventTarget Interop

addEventListener(type, listener, [options]);
removeEventListener(type, listener, [options]);
dispatchEvent(event);

Cross-Emitter Listening

listenTo(other, event, callback)
listenTo(other, { event: callback, ... })
listenToOnce(other, event, callback)
listenToOnce(other, { event: callback, ... })
stopListening([other], [event], [callback]);

Async Iteration

iterate(event, [options]);

For "all", each value is [eventName, ...args]. For other events, a single argument is yielded as a value; multiple arguments are yielded as an array.

Type Exports

EventMap;
EventName;
EventHandler;
EventHandlerMap;
SchemaLike;
SchemaMap;
SchemaValidator;
EventsFromSchemas;

Benchmarks + Changelog

  • BENCHMARKS.md
  • CHANGELOG.md

Development + Release

bun install
bun run format
bun test --coverage
bun run build:all
bunx playwright install --with-deps chromium
bun run test:browser
bun run test:all
bun run ci:local
bun run publish

ci:local requires act installed locally.

License

MIT