The dispatch context
Pass values from a middleware to the rest of a dispatch with the dispatch context. Covers typing its keys and reading them in a handler, a gate, and a fault card.
A middleware often computes something the rest of the dispatch needs, like the caller's account row or their language. Without somewhere to put it, every handler and gate would load that row again. The dispatch context is where you put that value.
Each interaction gets one bag of values, which your code reads and writes through this.dispatch. The handler, its middleware, its gates, and any card seedcord renders for that interaction all share the same bag.
The bag starts empty. DispatchState is the interface that lists the keys the bag can hold, with a type for each. If you add a key to DispatchState, this.dispatch accepts that key with its type in every file in your project.
import '@seedcord/gateway';
declare module '@seedcord/gateway' {
interface DispatchState {
actor: string;
locale: string;
}
}This file declares two keys, actor for the caller's username and locale for their language. The rest of the page uses both.
Note
TypeScript augments only a module the file already imports. If you leave that first import line out, TypeScript reports "Invalid module name in augmentation".
Writing a value
Fill the bag from a middleware, which runs first on every dispatch.
import {
InteractionMiddleware,
RegisterInteractionMiddleware
} from '@seedcord/gateway';
@RegisterInteractionMiddleware({ priority: -10 })
export class Actor extends InteractionMiddleware {
public async execute(): Promise<void> {
this.dispatch.set('actor', this.event.user.username);
await Promise.resolve();
}
}Reading it back
this.dispatch is on every handler. require returns the value with undefined stripped from its type. If the value is undefined, it throws.
import { class SlashHandler<
Route extends keyof SlashRegistry,
Cache extends CacheType = CacheFor<Route>
>
SlashHandler, function SlashRoute<const Route extends keyof SlashRegistry>(
...routes: Route[]
): <TCtor extends AnyHandlerCtor>(
constructor: AssertSlashRoute<Route, TCtor>
) => void
SlashRoute } from '@seedcord/gateway';
@SlashRoute<"whoami">(...routes: "whoami"[]): <TCtor>(constructor: AssertSlashRoute<"whoami", TCtor>) => voidSlashRoute('whoami')
export class class WhoamiWhoami extends class SlashHandler<
Route extends keyof SlashRegistry,
Cache extends CacheType = CacheFor<Route>
>
SlashHandler<'whoami'> {
public async Whoami.execute(): Promise<void>execute(): interface Promise<T>Promise<void> {
await this.RepliableHandler<ChatInputCommandInteraction<"cached">, Core, SentMessage, BufferResolvable | Stream | JSONEncodable<...> | Attachment | AttachmentBuilder | AttachmentPayload, ReplySender>.reply(response: string | ReplyResponse<BufferResolvable | Stream | JSONEncodable<APIAttachment> | Attachment | AttachmentBuilder | AttachmentPayload>, opts?: SendOpts): Promise<SentMessage>reply(`hello ${this.BaseHandler<ChatInputCommandInteraction<"cached">, Core>.dispatch: DispatchContextdispatch.DispatchContext.require<"actor">(key: "actor"): stringrequire('actor')}`);
}
}Your editor offers the keys DispatchState declares.
this.dispatch.require('- actor
- locale
If the value is undefined, require throws a SeedcordError with the code DispatchStateMissing, and its message names the key you asked for. That covers a key nothing wrote, and a key written as undefined. A null value comes back as is. Usually the middleware that writes the key never registered, or its { kinds } filter skipped this interaction.
Everything this.dispatch carries:
| member | what it does |
|---|---|
id | the one unique id of this dispatch |
set(key, value) | writes a value for the rest of this dispatch |
get(key) | reads a value, or undefined if it isn't set |
require(key) | reads a value, throws DispatchStateMissing if it's undefined |
routeId | the dispatched handler as kind:route, like slash:daily |
The next dispatch of the same route gets a new id. The bus events a dispatch publishes carry its id as dispatchId. Put id in your own log lines to tie a handler's logs to its middleware's.
Use get where the value is genuinely optional.
Reading it from a gate
A gate's context carries the same bag. KnownActor below refuses on something a middleware computed.
import { defineGate, Silence } from '@seedcord/gateway';
export const KnownActor = defineGate('KnownActor', (ctx) => {
if (!ctx.dispatch.get('actor')) throw new Silence('no actor');
});GateContextBase declares the bag, so any gate that reads it fits any handler on either transport. Its other fields carry the caller's ids and permission bits.
Warning
Every middleware runs before the first gate, which is what makes this read safe. If the handler writes a value, every gate has already run, so no gate can read it.
Reading it from a Notice
RenderContext carries the bag too, which matters most when seedcord builds the reply itself, like the one for an unknown fault. seedcord constructs that Notice with only the fault's uuid, so render(ctx) is the one place your code reads the dispatch.
import { Notice } from '@seedcord/gateway';
import type {
RenderContext,
ReplyResponse
} from '@seedcord/gateway';
export class TranslatedNotice extends Notice {
public constructor(private readonly uuid: string) {
super(`unknown fault ${uuid}`);
}
public render(ctx: RenderContext): ReplyResponse {
const locale = ctx.dispatch.get('locale') ?? 'en';
const text = translate(locale, 'fault.generic');
return {
components: [new FaultCard(text, this.uuid).component]
};
}
}Once you point errors.defaultError at it, every unknown fault replies through TranslatedNotice.
import { resolve } from 'node:path';
import { Seedcord } from '@seedcord/gateway';
import { GatewayIntentBits } from 'discord.js';
export const seedcord = new Seedcord({
bot: {
commands: { path: null },
events: { path: null },
clientOptions: { intents: [GatewayIntentBits.Guilds] },
interactions: {
path: resolve(import.meta.dirname, './handlers')
}
},
subscribers: { path: null },
errors: { defaultError: TranslatedNotice }
});On gateway, an event gets a bag too, shared by every handler registered for it. Event middleware covers how that bag fills.