eventify
v3.1.0
Published
Tiny event emitter with strict TypeScript typings and optional schema validation.
Downloads
2,393
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
