Events
Event payloads
How a VitNode event's Zod schema shapes what emit accepts and listeners receive: dates on the wire, validation versus delivery failures, adapters and the API.
Every VitNode event is defined with a payload schema. c.get("events").emit(event, payload) type-checks the payload against the schema's input, copies it to JSON and validates it before the adapter or any listener sees it. Listeners, and the adapter, receive the schema's output. This page explains what that means for dates, errors and adapters. To define and emit your first event, start with Events.
Input and output
Most schemas have the same input and output. They differ when the schema transforms a value, and the one transform core uses is for dates. The JSON wire has no date type, so a date field takes an ISO string and gives listeners a Date:
import { eventDate } from "@vitnode/core/api/events/wire";
import { z } from "zod";
import { defineEvent } from "@/api/builders";
export const notePinnedEvent = defineEvent({
name: "site-notes.note.pinned",
payload: z.object({
noteId: z.number().int().positive(),
pinnedUntil: eventDate().nullable(),
}),
});// emit: an ISO string
await c.get("events").emit(notePinnedEvent, {
noteId: note.id,
pinnedUntil: until.toISOString(),
});
// listener: a Date
handler: async (c, { pinnedUntil }) => {
if (pinnedUntil && pinnedUntil < new Date()) return;
},Passing a Date to emit is a compile error, and z.date() in an event schema is too. EventInput<typeof event> and EventPayload<typeof event> name the two types. Core's dated fields work the same way: moderation.sanction.applied takes endsAt as an ISO string, and Content Engine events take publishedAt, scheduledFor and hiddenAt as ISO strings. Listeners of all of them get Date values.
Validation failures and delivery failures
| What went wrong | What emit() does |
|---|---|
| The payload is not JSON, or the schema rejects it | Throws EventPayloadValidationError. No listener ran and nothing reached the adapter |
| A different definition is registered under the name | Throws EventDefinitionError |
| A listener throws | Logs it and returns it in failures; the other listeners still run |
| The adapter throws | Logs it and returns one failure with module: "adapter" |
A validation failure is a bug in the emitting code, so it surfaces as an error in that request. A delivery failure is someone else's listener, so it never fails the write that already happened. Error messages name paths and issue codes, never payload values. The schema runs once per emit, and every listener gets the same parsed value.
Local and external adapters
The default local adapter runs listeners in the emitting request and hands them the parsed output directly. A custom adapter receives the same output in envelope.payload, including Date objects, and serializes it itself. envelope.name is the event name as a string.
When an event crosses a process boundary, parse the payload on the receiving side with the same definition: notePinnedEvent.payload.parse(JSON.parse(raw).payload). JSON.stringify writes a Date as an ISO string, which eventDate() reads back, so dates round-trip. Avoid other transforms in schemas of events that leave the process; they may not survive a round trip.
Content Engine events
contentEvent(contentType, action) from @vitnode/core/content returns the definition of content.<id>.<action>. You never define or register these: buildApiPlugin registers one per action for every content type it registers, owned by that plugin, and every call returns the same object.
- Only the actions the content type enables are accepted.
contentEvent(categoryContentType, "published")does not compile for a content type without publication. contentIdis a number for theserialid strategy and a string foruuidandbigint.changedFieldsentries must start with one of the content type's field names.- The schemas accept extra keys, because the engine stamps documented extras such as
versiononto some payloads.
Reference
| Export | Behavior |
|---|---|
defineEvent({ name, payload }) from @/api/builders | Declares an event owned by your plugin. Names use letters, digits and . _ - : / @, up to 200 characters. The unbound version in @vitnode/core/api/lib/event-definition also takes pluginId |
EventDefinition, EventInput<typeof event>, EventPayload<typeof event> | The definition type, what emit accepts and what listeners receive. From @vitnode/core/api/lib/event-definition |
EventPayloadValidationError | Thrown by emit for an invalid payload. reason names paths and codes |
EventDefinitionError | Two different definitions for one name, a plugin registering another plugin's definition, or an invalid name. Thrown at startup, or by emit for a conflicting definition |
eventDate() from @vitnode/core/api/events/wire | A date field: ISO string in, Date out |
buildEventListener({ event, name, handler, description? }) from @vitnode/core/api/lib/events | A listener for one definition. Options type: EventListenerOptions |
EventEnvelope<Payload>, EventsApiPlugin from @vitnode/core/api/models/events | What listeners and adapters receive, and the adapter interface |
contentEvent(contentType, action) from @vitnode/core/content | A Content Engine event definition |
buildApiPlugin({ eventDefinitions }) takes the plugin's own definitions. A definition used by a listener is known without it, so another plugin's event never has to be registered to be listened to.
Related
- Events: define, emit, listen, and how listeners run.
- Built-in events: every event and the export to import for it.