Engines

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.

yatta/func/events.tsts
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

onts
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

emitts
await events.emit("user.registered", {  userId: user.id,  email: user.email,});
collect errors instead of throwingts
// 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.

wildcardsts
events.on("order.created", handler);   // exactevents.on("order.*", handler);      // prefixevents.on("*.created", handler);    // suffixevents.on("*", handler);            // everything

This is how you attach auditing without touching business logic:

tsts
// 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.

waitForts
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.

pipets
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.

with optionsts
events.pipe(  "order.completed",  "send-receipt",  { delay: "5m", priority: "low" },  (data) => ({ to: lookupEmail(data.orderId), orderId: data.orderId }),);
Note
Because the job runs on a worker, emitting is cheap: the handler returns as soon as the job is queued. That is the difference between a signup request that takes 40ms and one that takes four seconds.

Unsubscribing

on returns an unsubscribe function.

tsts
const off = events.on("user.registered", handler); off();   // stop listening