Skip to content

The handler context

A handler is called as handler(input, ctx). What ctx carries depends on how it was called, and the generated types say which members each handler gets.

Member Where What it does
ctx.log(message) every handler Writes one line to the host log under the realm’s name.
ctx.gateway.<api>.<operation>(args) every handler Calls an approved API operation. The host attaches the owner’s bound credential; the guest never sees it. See APIs.
ctx.gateway.<graphql name>.<operation>({variables}) every handler Runs one of the realm’s persisted GraphQL queries. See GraphQL.
ctx.gateway.cypher.query({cypher, params}) every handler Reads the owner’s graph under the separate cypher_query approval. See graph queries.
ctx.gateway.channel.publish(...), .position(...), .publishBatch(...) every handler Files events on the realm’s own sources. See publishing events.
ctx.deps.<name> every handler, when dependencies are declared A mounted dependency, such as SQLite. See dependencies.
ctx.writePropose(proposal) every handler Files a write for the owner to accept. See write proposals.
ctx.frame, ctx.cursor, ctx.publish(...) channel handlers The frame that caused the dispatch, the cursor the last dispatch kept, and a publish shortcut.
ctx.headers webhook handlers The headers of the verified request.
ctx.stream.send(text), ctx.stream.close() websocket handlers Writes to the channel’s own socket, or asks the host to reconnect it.
ctx.frame, ctx.cursor, ctx.publish(...) consumers declared in connecting The published event, the consumer’s cursor, and a publish shortcut.
ctx.assistant.chat(text, thread) consumers that declared assistant: true Asks the owner’s assistant and resolves to its final message.

The generated ctx.gateway types each API namespace and the GraphQL namespace the realm declares, and ctx.gateway.channel with the receipts the host returns (see publishing events). Every other namespace, cypher included, is typed unknown, so a handler that calls one gives it a type of its own; the graph queries section shows one.

A channel or consumer handler has an alias of its own, named after the verb it points at:

Declared as Alias Called as Resolves to
websocket onFrame or keepalive.handler <Verb>FrameHandler (frame, ctx: ChannelCtx & StreamCtx) {cursor?}
long-poll onResponse <Verb>ResponseHandler (frame, ctx: ChannelCtx) {cursor?, cursorParam?}
webhook onRequest <Verb>RequestHandler (frame, ctx: ChannelCtx & RequestCtx) {cursor?}
connecting consumer <Verb>ConsumerHandler (event, ctx: HandlerContext & ConsumerCtx), plus AssistantCtx with assistant: true a JSON object

A frame is the provider’s text or a control frame (Frame). Only the handler a channel names as its keepalive.handler is typed to receive the keepalive tick; every other channel handler gets FrameWithoutKeepalive. Two channels or consumers pointing at one verb share its alias, and synth refuses them when they would need different ones. A dataPipes consumer gets the plain <Verb>Handler and HandlerContext. The generated names may not collide with a type the realm declares, so synth refuses a type named RealmTypes, HandlerContext, Handlers or WriteProposal, and, in a realm with channels or consumers, Frame and the other channel declarations.

Every ctx.gateway.<namespace>.<operation> call and ctx.writePropose returns a promise, which resolves to the result or rejects. A refusal carries one fixed message and never says which rule failed, so treat it as “not allowed at the moment” and carry on. The assistant call is the one exception: it rejects with an AssistantRefusal, an Error whose code is ASSISTANT_NOT_GRANTED (the consumer has no assistant grant) or SENDER_NOT_PAIRED (the owner has not paired this sender), so a realm can answer with its own pairing hint. The code is a property of the error and never part of its message. A refusal that gives no reason has no code. The generated types export AssistantRefusal and AssistantRefusalCode for a realm that declares channels or consumers.

import type { AssistantRefusal, ReplyConsumerHandler } from "../.embabel/realm.d.ts";
export const reply: ReplyConsumerHandler = async (event, ctx) => {
const chatId = String(event.chatId);
try {
const answer = await ctx.assistant.chat(String(event.text), chatId);
await ctx.gateway.telegram.sendMessage({ body: { chat_id: chatId, text: answer } });
} catch (e) {
if ((e as AssistantRefusal).code === "SENDER_NOT_PAIRED") {
await ctx.gateway.telegram.sendMessage({
body: { chat_id: chatId, text: "Ask the owner for a pairing code, then send: pair <code>" },
});
}
}
return {};
};