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),
});
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
- Background Jobs / Durable Flows — for durable, cross-process reactions.
- Notifications — a common listener target (email/SMS/Slack/in-app).
- Application Toolkit — the rest of the TypeScript app layer.