Events
Events
Declare a typed VitNode event in a plugin, emit it after a write, and react to it with a listener registered on a top-level API module, and see exactly how listeners run, fail and scale.
Events let one part of an app react to something another part did, without either importing the other. A route emits site-notes.note.created; any plugin can listen and send an email, update a search index or write an audit entry. In this guide site-notes declares that event, emits it when a note is created and logs it from a listener.
Before you begin
- The
site-notescreate route from Permissions, or any route of your own that writes data.
Declare the event
Events are typed through the VitNodeEvents interface in @vitnode/core/api/models/events. Augment it in src/api/lib/events.ts, with the payload listeners will receive:
declare module "@vitnode/core/api/models/events" {
interface VitNodeEvents {
"site-notes.note.created": { noteId: number; title: string };
}
}
export {};Prefix event names with your plugin id, so they never collide with core's or another plugin's. Then import the file once from src/config.api.ts, so apps and other plugins that import your plugin see the event too:
import { notesModule } from "./api/modules/notes/notes.module";
import "@/api/lib/events"; Emit it after the write
In src/api/modules/admin/notes/create.route.ts, emit the event once the insert has returned, before answering:
handler: async c => {
const values = c.req.valid("json");
const [note] = await c
.get("db")
.insert(site_notes)
.values(values)
.returning({ id: site_notes.id, title: site_notes.title, body: site_notes.body });
await c.get("events").emit("site-notes.note.created", {
noteId: note.id,
title: note.title,
});
return c.json(note, 201);
},emit() is type-checked against VitNodeEvents, so a wrong name or payload is a compile error. Emit after the data is saved. Inside a transaction, emit after it commits, or listeners may read data that is then rolled back.
Create a listener
Create src/api/lib/note-created.listener.ts:
import { buildEventListener } from "@vitnode/core/api/lib/events";
export const noteCreatedListener = buildEventListener({
event: "site-notes.note.created",
name: "log-new-note",
handler: async (c, payload, envelope) => {
await c
.get("log")
.debug(`New note #${payload.noteId}: "${payload.title}" (event ${envelope.eventId})`);
},
});payload is typed from the event name. envelope adds the event id, the emitting plugin, the time and the actor (admin, user or system). name identifies the listener in logs and failure reports.
Register it on a top-level module
Add the listener to the events of notesModule, a module passed to buildApiPlugin directly:
import { buildModule } from "@/api/builders";
import { noteCreatedListener } from "@/api/lib/note-created.listener";
export const notesModule = buildModule({
name: "notes",
routes: [listNotesRoute, showNoteRoute],
events: [noteCreatedListener],
});Listeners are collected only from top-level modules. On a nested module, such as admin/notes, a listener never runs, and nothing warns you.
Check the result
Create a note on the AdminCP page, or with the Create a test note button from the Permissions lesson. Then open Debug Panel ā Log explorer (/admin/core/debug/logs). A debug entry from site-notes reads New note #8: "Lift maintenance" (event ā¦), with your note's id and title. The same line is printed in the pnpm dev terminal.
How listeners run
With the default local adapter:
| Question | Answer |
|---|---|
| When do listeners run? | Inside the emitting request, one after another in registration order. emit() resolves after the last one. |
| Does a failing listener break the request? | No. Its error is logged and returned in emit()'s failures; emit() never throws, and the remaining listeners still run. |
| Is delivery retried or stored? | No. Events live only in that request. Work that must survive a crash or a failure belongs in the queue. |
| Do other instances hear the event? | No, only the instance that emitted it. A custom adapter can deliver elsewhere and report status: "queued". |
emit() resolves to { eventId, status, delivered, failures }. delivered counts the listeners that succeeded; each failure names the plugin, module and listener. Keep listeners quick, because the person who created the note waits for all of them.
Hand slow work to the queue
When a listener has slow work, such as sending email, queue it instead of doing it in the request. A listener runs with the emitter's context, so a dispatch defaults to the emitting plugin. Pass your plugin id when you listen to someone else's event, such as core's user.created:
import { buildEventListener } from "@vitnode/core/api/lib/events";
import { manifest } from "@/manifest.gen";
export const welcomeListener = buildEventListener({
event: "user.created",
name: "queue-welcome-note",
handler: async (c, payload) => {
await c.get("queue").dispatch({
name: "welcome-member",
payload: { userId: payload.userId },
pluginId: manifest.id,
});
},
});welcome-member must be a task your plugin registers; Queue shows how to create one.
Related
- Built-in events: every event core and the Content Engine emit, with payloads.
- Custom event adapter: deliver events through your own transport.