Event Emitter#
Lightweight typed event emitter for decoupling modules. Emit events from one part of your app, listen from another - no direct imports needed.
Basic Usage#
typescript
import { emitter } from "@buntok/core/emitter";
// Listen to events
emitter.on("user:created", async (data) => {
await sendWelcomeEmail(data.user.email);
});
// Emit events
await emitter.emit("user:created", { user: { email: "test@example.com" } });
// One-time listener
emitter.once("app:ready", () => {
console.log("Buntok is ready!");
});
// Unsubscribe
const unsub = emitter.on("user:created", handler);
unsub(); // Stop listeningAPI#
| Method | Description |
|---|---|
| emitter.on(event, listener) | Subscribe to an event (returns unsubscribe function) |
| emitter.once(event, listener) | Subscribe once, auto-unsubscribes after first call |
| emitter.emit(event, data, options?) | Emit an event (waits for all listeners to finish) |
| emitter.emitSerial(event, data, options?) | Emit listeners serially (one at a time) |
| emitter.off(event?) | Remove all listeners for an event, or all listeners |
| emitter.listenerCount(event) | Get number of listeners for an event |
| emitter.eventNames() | Get all event names with listeners |
Error Isolation#
By default, if any listener throws, the entire emit() call rejects. Use isolatedErrors to prevent one failing listener from blocking others:
typescript
// Default behavior - one failure rejects all
await emitter.emit("user:created", { user }); // Rejects if any listener fails
// With error isolation - failures don't affect other listeners
await emitter.emit("user:created", { user }, {
isolatedErrors: true,
onError: (event, error, listener) => {
console.error(`Listener failed for ${event}:`, error);
},
});Serial Execution#
Use emitSerial() to run listeners one at a time (useful when order matters):
typescript
// Listeners run in order, one after another
await emitter.emitSerial("order:placed", { order });
// With error isolation - continues despite failures
await emitter.emitSerial("order:placed", { order }, {
isolatedErrors: true,
onError: (event, error) => console.error(error),
});Max Listeners#
Prevent memory leaks by limiting the number of listeners per event:
typescript
const emitter = new EventEmitter({
maxListeners: 10, // Max 10 listeners per event
});
// Throws if limit exceeded
emitter.on("test", handler); // ✅
// ... 9 more listeners
emitter.on("test", handler); // ❌ Throws "Max listeners (10) exceeded"
// Increase limit if needed
emitter.increaseMaxListeners(20);Typed Events#
Define your event map for full type safety:
typescript
import { EventEmitter } from "@buntok/core/emitter";
interface AppEvents {
"user:created": { id: number; name: string; email: string };
"user:deleted": { id: number };
"order:placed": { orderId: string; total: number };
"payment:failed": { orderId: string; reason: string };
}
const emitter = new EventEmitter<AppEvents>();
// ✅ Typed - data is { id: number; name: string; email: string }
emitter.on("user:created", async (data) => {
console.log(data.name); // string
});
// ❌ Compile error - wrong event name
emitter.on("user:removed", handler);
// ❌ Compile error - wrong data shape
emitter.emit("user:created", { name: 123 });Default Emitter#
A pre-configured emitter with common app events is available:
typescript
import { emitter } from "@buntok/core/emitter";
// Built-in events:
emitter.on("user:created", async ({ user }) => { /* ... */ });
emitter.on("user:updated", async ({ user }) => { /* ... */ });
emitter.on("user:deleted", async ({ userId }) => { /* ... */ });
emitter.on("user:logged_in", async ({ user }) => { /* ... */ });
emitter.on("user:logged_out", async ({ user }) => { /* ... */ });
emitter.on("request:start", async ({ method, path }) => { /* ... */ });
emitter.on("request:end", async ({ method, path, status, duration }) => { /* ... */ });
emitter.on("request:error", async ({ method, path, error }) => { /* ... */ });
emitter.on("app:ready", async () => { /* ... */ });
emitter.on("app:shutdown", async () => { /* ... */ });
// Custom events also work (extensible via [key: string]: any)
emitter.emit("custom:event", { anything: true });Practical Examples#
Decouple Auth from Notifications#
typescript
// auth.service.ts - emits events, knows nothing about notifications
import { emitter } from "@buntok/core/emitter";
async function createUser(data) {
const user = await db.user.create({ data });
await emitter.emit("user:created", { user });
return user;
}
// notification.service.ts - listens, knows nothing about auth
import { emitter } from "@buntok/core/emitter";
emitter.on("user:created", async ({ user }) => {
await sendWelcomeEmail(user.email);
await sendSlackNotification(`New user: ${user.name}`);
});Audit Trail#
typescript
emitter.on("user:deleted", async ({ userId, ctx }) => {
await db.auditLog.create({
data: {
action: "user:deleted",
entityId: String(userId),
ip: ctx?.ip,
timestamp: new Date(),
},
});
});Cleanup#
typescript
// Remove all listeners for an event
emitter.off("user:created");
// Remove ALL listeners
emitter.off();
// Check listener count
const count = emitter.listenerCount("user:created");