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 listening

API#

MethodDescription
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");