Events
Decouple what happens from where it happens. Emit an event, and anything can listen — or the event can become a background job without a line of glue code.
Setup
The bus is typed the same way jobs are: declare your event names once and every on, emit and pipe call is checked.
import { createEvents } from "yatta/jobs";import { jobs } from "./jobs"; export interface AppEvents { "user.registered": { userId: string; email: string }; "order.completed": { orderId: string; amount: number }; "payment.failed": { orderId: string; reason: string };} declare module "yatta/jobs" { interface EventRegister extends AppEvents {}} export const events = createEvents(jobs);Listening
events.on("user.registered", (data) => { console.log(`new user: ${data.email}`);}); events.once("order.completed", (data) => { console.log(`first order only: ${data.orderId}`);});Listeners may be async, and run concurrently. A listener that throws does not stop the others.
Emitting
await events.emit("user.registered", { userId: user.id, email: user.email,});// By default a throwing listener propagates.try { await events.emit("user.registered", data);} catch (err) { console.error("listener failed", err);} // Or collect every failure and throw an AggregateError at the end.await events.emit("user.registered", data, { throwOnError: true });Patterns
Besides exact names, the bus matches on prefix, suffix and a global wildcard.
events.on("order.created", handler); // exactevents.on("order.*", handler); // prefixevents.on("*.created", handler); // suffixevents.on("*", handler); // everythingThis is how you attach auditing without touching business logic:
// Any event can be audited without knowing its name in advance.events.on("*", async (data, eventName) => { await db.auditLog.insert({ event: eventName, payload: JSON.stringify(data), });});Waiting for an event
Sometimes you need to block on something rather than react to it. waitFor resolves when the event fires, or rejects on timeout.
const payment = await events.waitFor("payment.confirmed", "1m");// → the event payload // Rejects with a QueueError if the event never arrivestry { await events.waitFor("never.happens", "20s");} catch (err) { // QueueError: Timeout waiting for event "never.happens"}Piping to jobs
This is the reason to use events over direct calls. The event fires, and a background job is enqueued — the handler runs on a worker, so a slow or failing email cannot slow down or break the request that triggered it.
events.pipe( "user.registered", "send-welcome", undefined, (data) => ({ to: data.email, name: data.name }),);The signature matters. The third argument is enqueue options; the fourth maps the event payload to the job payload. Passing a payload in the third position will not type-check.
events.pipe( "order.completed", "send-receipt", { delay: "5m", priority: "low" }, (data) => ({ to: lookupEmail(data.orderId), orderId: data.orderId }),);Unsubscribing
on returns an unsubscribe function.
const off = events.on("user.registered", handler); off(); // stop listening