Events
Custom adapter
Replace or extend VitNode's in-process event transport with a custom EventsApiPlugin, for example one that runs local listeners and also forwards every event to a Redis stream for other services.
By default, c.get("events").emit() runs the matching listeners inside the emitting request, on that instance only. An events adapter decides what emit() does instead. This guide builds a hybrid adapter. It runs VitNode's listeners exactly as before, then appends each event to a Redis stream that other services can read.
Only the adapter runs listeners. An adapter that just forwards events
somewhere means no buildEventListener in any plugin runs again, and the
service reading the broker cannot run them either, because a listener needs the
request's Hono context. Call LocalEventsAdapter first, as below, unless you
really mean to move every listener out of VitNode.
Before you begin
- A Redis server, and the
redispackage (node-redis) in the API app's dependencies. - The API config of your app,
src/vitnode.api.config.ts.
Build the adapter
Create src/lib/events/redis-stream-adapter.ts in the API app:
import type { EventsApiPlugin } from "@vitnode/core/api/models/events";
import { LocalEventsAdapter } from "@vitnode/core/api/adapters/events/local";
import { createClient } from "redis";
const redis = createClient({ url: process.env.REDIS_URL });
// node-redis throws "error" events as uncaught exceptions without a listener.
redis.on("error", () => {});
void redis.connect().catch(() => {});
const local = LocalEventsAdapter();
export const RedisStreamEventsAdapter = (): EventsApiPlugin => ({
name: "redis-stream",
publish: async (c, envelope) => {
// VitNode's own listeners, unchanged: in this request, one after another.
const result = await local.publish(c, envelope);
try {
await redis.xAdd("vitnode:events", "*", { event: JSON.stringify(envelope) });
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
await c.get("log").error(`Forwarding "${envelope.name}" to Redis failed: ${error}`);
result.failures.push({
error,
listener: "redis-stream",
module: "adapter",
pluginId: envelope.pluginId,
});
}
return result;
},
});- The client is yours. The adapter creates its own Redis connection, once per process. VitNode's cache connection is not available on
c. publishreturns what the local adapter returned. That isstatus: "delivered", the listeners that ran and their failures, plus one failure if forwarding failed.emit()never throws, so a broker outage costs a log line, not a failed save.- The envelope holds
eventId,name,payload,pluginId,actorandemittedAt, whichJSON.stringifywrites as an ISO string.
Register it in the API config
Set events.adapter in buildApiConfig:
import { buildApiConfig } from "@vitnode/core/vitnode.config";
import { RedisStreamEventsAdapter } from "./lib/events/redis-stream-adapter";
export const vitNodeApiConfig = buildApiConfig({
// ...plugins, metadata and the rest of your config
events: {
adapter: RedisStreamEventsAdapter(),
},
});Every emit() in core and every plugin now goes through the adapter.
Check the result
Restart the API and trigger an event, such as creating a note in the Events guide. The listener's log line appears as before, and the stream holds the event:
redis-cli XRANGE vitnode:events - +Each entry has one field, event, holding the JSON envelope.
Reading the stream elsewhere
Whatever reads vitnode:events, with XREAD or a consumer group, runs its own code: a search indexer, an analytics pipeline, another app. It does not run VitNode listeners and has no Hono context. Work that must happen reliably inside VitNode, with retries, belongs in a queue task a listener dispatches.
The interface
import type { EventEnvelope, EventEmitResult } from "@vitnode/core/api/models/events";
import type { Context } from "hono";
interface EventsApiPlugin {
/** Shown in logs and returned by c.get("events").name(). */
name: string;
publish: (c: Context, envelope: EventEnvelope) => Promise<EventEmitResult>;
}EventEmitResult | Description |
|---|---|
eventId | The envelope's id. |
status | "delivered" when listeners ran before publish resolved; "queued" when the adapter handed the event off. |
delivered | How many listeners succeeded. |
failures | { pluginId, module, listener, error } for each listener, or the adapter, that failed. |
emit() catches an adapter that throws, logs it, and reports it as one failure with module: "adapter". To run listeners yourself, read them from c.get("core").events.listeners, as LocalEventsAdapter does.
Related
- Events: emit and listen, and how listeners run.
- Built-in events: every event core and the Content Engine emit.