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 eventifyQuickstart
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/producethrows and no listeners run (including wildcard andall). - Listener errors: thrown errors or rejected promises are routed to
onErrorand do not stop other listeners. - Invalid callbacks: invoking a non-function throws a
TypeErrorthat is routed toonError. onErrorfailures: errors thrown by the handler are swallowed to avoid crashing producers.iteratebackpressure: if producers emit faster than you consume, the iterator queue grows. UseAbortSignal,return(), or stop iteration.listenTo/listenToOnce: the target must be an Eventify emitter (or another object with compatibleon/once/off).
Constraint tools:
- Payload:
schemas+validateenforce data shape at emit time. - Cardinality:
once/listenToOnce. - Lifetime:
off/stopListening/iterate+AbortSignal. - Namespace scope:
namespaceDelimiter+wildcard. - Errors:
onErrorcentralizes 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
contextdefaults to the emitter. - Duplicate registrations are allowed.
"all"is a compatibility feature (Backbone/Eventify style); it is not a standardEventTargetconcept.
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.mdCHANGELOG.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:localbun run publishci:local requires act installed locally.
License
MIT
