@mubaraksoft/event-emitter
v1.0.5
Published
Simple libaray to manage events in browser
Downloads
37
Maintainers
Readme
Event Emitter
A tiny, strongly‑typed wrapper around the Web Platform’s EventTarget that gives you:
- End‑to‑end TypeScript types for event names and payloads
- Namespaced listeners with easy cleanup via
AbortController - A familiar
dispatchEvent/addEventListenerAPI - Optional per‑namespace subscriber hook for observability/devtools
Works in modern browsers and in Node.js (v16.8+ where
EventTargetandAbortControllerare available; see Compatibility).
Install
npm i @mubaraksoft/event-emitter
# or
pnpm add @mubaraksoft/event-emitter
# or
yarn add @mubaraksoft/event-emitterNo runtime dependencies!
Quick Start
Define your event map once, get type‑safe emitters and listeners everywhere.
// 1) Define your event map (name -> payload)
export type AppEvents = {
"user:login": { id: string; email: string };
"toast:show": { message: string; level?: "info" | "warn" | "error" };
"counter:changed": { value: number };
};
// 2) Create an emitter
const bus = new EventEmitter<AppEvents>();
// 3) Add a listener (fully typed)
const off = bus.addEventListener({
name: "counter:changed",
callback: (e) => {
// e is CustomEvent<{ value: number } & { eventName?: "counter:changed" }>
console.log("count is", e.detail.value, "from", e.detail.eventName);
},
});
// 4) Dispatch an event (payload is validated by TS)
bus.dispatchEvent({ name: "counter:changed", payload: { value: 2 } });Why this package?
EventTarget is great but untyped. This library layers type safety and ergonomics without reinventing the wheel:
- You keep
CustomEventsemantics, bubbling model, and browser performance. - You gain compile‑time checking of event names and payload shapes.
- You get namespaces for grouping listeners and cleaning them up in one go.
Namespaces & Cleanup
Group listeners by a string namespace and dispose them all at once.
const authNS = bus.namespace("auth"); // same underlying bus
const listeners = authNS.addEventsListener({
names: ["user:login", "toast:show"],
callback: (e) => {
// typed as union of the selected events
},
});
// Later: remove a single listener
authNS.removeEventListener(listeners[0]);
// Or nuke the whole namespace (uses AbortController under the hood)
authNS.removeAllListeners();
EventEmitter#removeAllListeners(namespace?)aborts the internalAbortControllerfor that namespace, detaching all listeners registered with it.
Subscriber hook (observability)
Use a per‑namespace subscriber to observe every dispatch going through that namespaced emitter—great for logging or devtools.
const log = bus.namespace("log", (evt) => {
// evt: { name: keyof AppEvents; payload?: AppEvents[keyof AppEvents] }
console.debug("dispatched:", evt.name, evt.payload);
});
log.dispatchEvent({ name: "toast:show", payload: { message: "Saved" } });The subscriber runs after the event has been dispatched on the underlying
EventTarget.
Strong typing end‑to‑end
dispatchEventvalidates name and payload.- Listener
callbackreceivesCustomEvent<Payload & { eventName?: Name }> addEventsListenerinfers a union of payloads when you listen to multiple names.
bus.addEventListener({
name: "toast:show",
callback: (e) => {
// e.detail.level is "info" | "warn" | "error" | undefined
},
});
// ❌ Type error – missing required payload property
bus.dispatchEvent({ name: "counter:changed", payload: {} });API
Types
type EventMapBase = Record<string, unknown>;
type EmittedEventName<M> = keyof M & string;
type GenericEventCallback<M, N> = (
event: CustomEvent<WithEventName<N, M[N]>>
) => void;
type EventListener<M, N = EmittedEventName<M>> = {
name: N;
namespace?: string; // default: "global"
callback?: GenericEventCallback<M, N>;
};
type EventsListener<M> = Omit<EventListener<M>, "name"> & {
names: EmittedEventName<M>[];
};
type EventDispatcher<M, N = EmittedEventName<M>> = {
name: N;
payload?: M[N];
};class EventEmitter<M extends EventMapBase>
addEventListener(listener)⇒EventListener<M, N>- Registers a single listener. Optional
namespace(default"global").
- Registers a single listener. Optional
addEventsListener({ names, callback, namespace })⇒EventListener<M>[]- Register the same callback for multiple event names.
dispatchEvent({ name, payload })⇒void- Dispatches a
CustomEvent(name, { detail })wheredetailmerges the payload with{ eventName: name }.
- Dispatches a
removeEventListener(listener)⇒void- Removes a previously added listener.
removeEventListeners(listeners)⇒void- Convenience to remove an array of listeners.
removeAllListeners(namespace = "global")⇒void- Aborts the namespace’s controller to remove all listeners under it.
namespace(name, subscriber?)⇒EventEmitterNamespace<M>- Returns a namespaced wrapper sharing the same underlying emitter.
class EventEmitterNamespace<M> implements IEventEmitter<M>
A thin façade that automatically injects its namespace into calls and optionally invokes subscriber on each dispatch.
Methods mirror EventEmitter:
addEventListener,addEventsListener,removeEventListener,removeEventListeners,removeAllListeners,dispatchEvent.
Patterns & Recipes
Component‑local bus
export type CounterEvents = { "counter:changed": { value: number } };
export function createCounterBus() {
return new EventEmitter<CounterEvents>();
}Feature‑scoped namespaces
const bus = new EventEmitter<AppEvents>();
const auth = bus.namespace("auth");
const ui = bus.namespace("ui");
ui.addEventListener({ name: "toast:show", callback: showToast });
// teardown
ui.removeAllListeners();Register many, remove later
const ns = bus.namespace("page:settings");
const listeners = ns.addEventsListener({
names: ["toast:show", "counter:changed"],
callback: (e) => {
/* ... */
},
});
// On unmount
ns.removeEventListeners(listeners);Forward events between emitters
function forward<M extends EventMapBase>(
from: EventEmitter<M>,
to: EventEmitter<M>,
names: (keyof M & string)[]
) {
const ns = from.namespace("forward:" + names.join(","));
ns.addEventsListener({
names: names as any,
callback: (e) =>
to.dispatchEvent({ name: e.detail.eventName as any, payload: e.detail }),
});
return () => ns.removeAllListeners();
}Compatibility
- Browsers: Modern browsers supporting
EventTarget,CustomEvent, andAbortController. - Node.js: v16.8+ (global
EventTarget&AbortController). For older versions, bring a polyfill/shim (e.g.event-target-shim) or run in a DOM‑like environment.
This package is framework‑agnostic and has zero deps.
FAQ
Why is eventName included in detail?
It’s convenient in generic handlers and when forwarding/logging. It’s optional and typed as the concrete name that was dispatched.
Does this support event bubbling or capture?
It uses a single EventTarget without a DOM tree, so bubbling/capture semantics aren’t relevant. You can still build higher‑level patterns if you need them.
How do I prevent memory leaks?
Use namespaces and call removeAllListeners(namespace) during teardown (e.g., component unmount). Internally we use AbortController so all listeners under that namespace detach at once.
Can I add multiple callbacks to the same name?
Yes—just call addEventListener multiple times.
Exported surface
export type {
EventMapBase,
EmittedEventName,
GenericEventCallback,
GenericEventListener as EventListener,
GenericEventDispatcher as EventDispatcher,
EventsListener,
IEventEmitter,
};
export { EventEmitter, EventEmitterNamespace };License
MIT © mubaraksoft
