Hooks and events
A hook runs before a write and may change or refuse it. When something worth knowing happens, it says so with an event, and agents elsewhere act on it: send the email, write the audit row, say the next thing. Each piece stays small, and none knows about the others.
Hooks
A file in hooks/ named after a collection runs before each write to it. beforeCreate, beforeUpdate and beforeDelete each get the record and return it, changed if need be; reject refuses the write with a message.
export function beforeCreate({ record, reject, alert }) {
if (String(record.note ?? "").includes("<script")) {
alert("A script in an order's note", { email: record.email });
reject("That note is not allowed.");
}
// Card numbers never reach the database.
return { ...record, note: String(record.note ?? "").replace(/\d{16}/g, "[card]") };
}
export function beforeUpdate({ record, previous, emit }) {
// `record` is what changes; `previous` is the row as it was.
const order = { ...previous, ...record };
if (order.paid && !previous.paid) emit("order.paid", { id: order.id, email: order.email });
return record;
}hooks/_all.ts runs before every collection’s own hook, for what applies everywhere.
Events
emit(name, data) says something happened. From a hook, it is said only once the write has gone through: a refused write says nothing, except its alerts. Server functions and agents can emit too.
An agent that watches an event hears each one, in order:
export const watch = { event: "order.paid" };
export async function act({ event, mail, emit }) {
mail({ to: event.data.email, subject: "Paid, thank you", text: `Order ${event.data.id} is paid.` });
emit("receipt.sent", { order: event.data.id });
}And another may listen to that one:
export const watch = { event: "receipt.sent" };
export async function act({ event, server }) {
server.collection("audit").create({ what: `receipt for ${event.data.order}` });
}The hook does not send email; it says the order is paid. What follows from that is each agent’s business, and adding one changes nothing that was there. A chain of events more than eight deep is refused, so two agents answering each other cannot run forever.
Alerts
alert(message, details) is for what someone should look at. It is written to the log, counted in the development overlay, and said as an alert event, so an agent can pass it on:
export const watch = { event: "alert" };
export async function act({ event, mail }) {
mail({ to: "ops@example.com", subject: "Alert", text: event.data.message });
}Events from outside
Another program can say an event too. Over HTTP, as a superuser:
curl -X POST http://localhost:8090/api/events/door.opened \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"door": 3}'JSON, MessagePack (application/msgpack) and CBOR (application/cbor) all arrive as JSON. Other bytes arrive as { "bytes": "<base64>" }. ?app=reports says the event only to the app mounted as reports.
On the same machine, without a token, the server can listen over UDP:
sluurp serve --public ./app --events
# from anything else on this machine:
sluurp emit door.opened '{"door": 3}'--events listens on 127.0.0.1:9900 unless given another address. Each datagram is a map of name, data and, optionally, app, written in JSON, MessagePack or CBOR. A sensor, a script or a cron job can each send one in a line of code.