Events

@bext-stack/framework/events is a typed domain event bus — the app-developer counterpart to Laravel's Events + Listeners. You declare an event map (name → payload type), register listeners, and emit; one event fans out to every listener. It decouples the code that does something from the code that reacts, and it's pure, fully-typed TypeScript.

When To Use It

  • A single action has several independent side effects (welcome email + workspace provisioning + analytics on signup) that shouldn't be hard-wired together.
  • You want to add a reaction to an existing flow without touching the flow.
  • In-request, in-process fan-out — synchronous or async.

Declaring & dispatching

import { createEventBus } from "@bext-stack/framework/events";

type AppEvents = {
  "user.registered": { id: string; email: string };
  "order.paid": { orderId: string; amount: number };
};

const bus = createEventBus<AppEvents>();

bus.on("user.registered", (u) => sendWelcome(u.email));    // payload is typed
bus.on("user.registered", (u) => provisionWorkspace(u.id));

await bus.emit("user.registered", { id: "u1", email: "a@b.co" }); // fans out, awaits async

emit awaits every listener (including async ones). If listeners throw, they all still run and the errors surface afterward (a single error is rethrown; several become an AggregateError) — one bad listener never silently swallows the rest. emitSync is the fire-and-forget variant.

Member Does
on(event, fn) register a listener; returns an unsubscribe fn
once(event, fn) one-shot listener (auto-removed after first emit)
off(event, fn) remove a listener
emit(event, payload) async fan-out, awaits all listeners
emitSync(event, payload) fire-and-forget
listenerCount(event) / removeAll(event?) introspection / teardown

Register many at once (Laravel's $listen map) with subscribe:

import { subscribe } from "@bext-stack/framework/events";

const off = subscribe(bus, {
  "user.registered": [(u) => sendWelcome(u.email), (u) => provisionWorkspace(u.id)],
  "order.paid": (o) => fulfil(o.orderId),
});
Tip

A bus is in-memory and per-V8-isolate. Register listeners at module scope (runs once per isolate, so every isolate has the same set) and emit within the same request/render — the common, deterministic case. For durable, cross-request or cross-process fan-out, reach for the SDK queue (background jobs) or realtime broadcasting instead — this is the synchronous in-app dispatcher, not a durable bus.

Try It

The live events demo fans a single user.registered event out to three independent listeners. Source in sites/demo/src/app/examples/events/page.tsx.

See Also