The bus
Send your own events between parts of your bot with the bus. Covers declaring a key, writing a subscriber, publishing, running once, listening with a callback, and the keys seedcord publishes.
Say a member levels up in /award, and three things should follow: an announcement, a role, and a log line. If the handler calls all three itself, adding a fourth follow-up means editing /award again.
The bus separates the two sides. /award publishes one named event, so its file holds only the award itself. Each follow-up moves into its own subscriber class, which runs when that event is published. A fourth follow-up is one more subscriber file, with /award left as it is.
Event handlers, middleware, subscribers, and plugin services get core too, so any of them can publish. If members also level up by chatting, a messageCreate handler publishes the same levelUp. The same three follow-ups run with none of their code copied.
Everything on the bus stays inside your bot. To show members something, like the announcement, a subscriber sends that message to Discord itself.
Start by declaring the key and its payload. Declaration merging puts them on the Subscriptions interface that your transport package exports.
// the augmentation below fails to compile without this import
import '@seedcord/gateway';
declare module '@seedcord/gateway' {
interface Subscriptions {
levelUp: { userId: string; level: number };
}
}Now levelUp and its payload type exist everywhere in your project. @Subscribe offers it next to the keys seedcord publishes.
@Subscribe('- unknownException
- handledException
- unhandledInteractionError
- interactionDispatched
- responseAttempted
- commandsDeployed
- unhandledEventError
- eventDispatching
- eventDispatched
- anyInteraction
- levelUp
A subscriber class runs each time its key is published.
import { function Subscribe<TSubscriber extends SubscriptionKey>(
subscriber: TSubscriber,
options?: SubscribeOptions
): <HandlerCtor extends Constructor<Subscriber<TSubscriber, CoreBase>>>(
constructor: HandlerCtor
) => void
Subscribe, class Subscriber<KeyOfSubscribers extends SubscriptionKey>Subscriber } from '@seedcord/gateway';
@Subscribe<"levelUp">(subscriber: "levelUp", options?: SubscribeOptions): <HandlerCtor>(constructor: HandlerCtor) => voidSubscribe('levelUp')
export class class AnnounceAnnounce extends class Subscriber<KeyOfSubscribers extends SubscriptionKey>Subscriber<'levelUp'> {
public async Announce.execute(): Promise<void>execute(): interface Promise<T>Promise<void> {
const { const userId: stringuserId, const level: numberlevel } = this.Subscriber<"levelUp", Core>.data: {
userId: string;
level: number;
}
data;
this.Subscriber<"levelUp", Core>.logger: Loggerlogger.Logger.info(msg: string, ...args: unknown[]): voidinfo(`${const userId: stringuserId} reached ${const level: numberlevel}`);
}
}You write the key in @Subscribe and again in the generic on Subscriber. The generic sets the type of this.data, here { userId, level }. If the key in @Subscribe and the generic differ, the class fails to compile.
Publishing
core.bus.publish takes the key and its payload. TypeScript checks both against what you declared.
import { SlashHandler, SlashRoute } from '@seedcord/gateway';
@SlashRoute('award')
export class Award extends SlashHandler<'award'> {
public async execute(): Promise<void> {
const member = this.options.getUser('member');
this.core.bus.publish('levelUp', {
userId: member.id,
level: 12
});
await this.reply(`Levelled up <@${member.id}>.`);
}
}Warning
publish returns before your subscribers finish, so code after the call can't read anything a subscriber wrote. If a subscriber or listener throws, seedcord catches the error and logs it on the subscribers log channel. The call to publish never sees it.
publish returns true if the key had at least one on listener. It counts those listeners only, so a key with three subscribers and no listeners returns false.
Subscribers on the same key start at the same time, so any of them can finish first. Don't make one subscriber depend on another's result, since nothing guarantees the order.
Running once
Some follow-ups belong to the first publish only, like a "first level-up on the server" message. @Subscribe takes a frequency option for that, the same one @RegisterEvent takes.
import { Subscribe, Subscriber } from '@seedcord/gateway';
@Subscribe('levelUp', { frequency: 'once' })
export class FirstLevel extends Subscriber<'levelUp'> {
public async execute(): Promise<void> {
this.logger.info('somebody levelled for the first time');
}
}seedcord marks FirstLevel as run as soon as it starts. If it throws partway through, it still counts as run. The next publish won't retry it.
Where to put subscribers
seedcord loads subscribers from the folder you set in subscribers.path. The scaffold sets it to null, so point it at your folder when you write your first subscriber.
export const seedcord = new Seedcord({
bot,
subscribers: {
path: resolve(import.meta.dirname, './subscribers')
}
});If a class in that folder doesn't have @Subscribe, it still loads but never runs. The subscriber-missing-decorators lint rule flags the missing decorator while you write the class.
Listening without a class
core.bus is a TypedEventEmitter, so a plain callback works too.
core.bus.on('levelUp', (data) => {
console.log(data.level);
});
const [data] = await core.bus.waitFor('levelUp', {
timeoutMs: 5000
});on, once, and off each take a key and a callback. waitFor resolves with the next payload. Its optional second argument takes three fields:
filter, a function that receives the payload and returns a booleantimeoutMs, how long to waitsignal, an abort signal
If the timeout passes or the signal aborts, waitFor rejects with a WaitForError. Its reason is 'timeout' or 'aborted'.
Write a subscriber by default. It gets this.core and this.logger without any setup. seedcord loads its file at startup, so nothing in your code has to register it. Each publish runs a new instance, so a field on the class doesn't keep its value between publishes. A callback registered with on suits code that already holds core, like a plugin. waitFor suits code that has to pause until the next publish.
Warning
Don't call core.bus.emit. Your editor still offers it beside publish, crossed out as deprecated. It throws a SeedcordTypeError with the code CoreBusEmitUnavailable. Call publish, which reaches your subscribers and your on listeners together.
The framework's own keys
seedcord publishes its own keys onto the same bus, like interactionDispatched once per dispatch. You subscribe to them the same way you subscribe to levelUp.
@Subscribe<"interactionDispatched">(subscriber: "interactionDispatched", options?: SubscribeOptions): <HandlerCtor>(constructor: HandlerCtor) => voidSubscribe('interactionDispatched')
export class class DispatchTimesDispatchTimes extends class Subscriber<KeyOfSubscribers extends SubscriptionKey>Subscriber<'interactionDispatched'> {
public async DispatchTimes.execute(): Promise<void>execute(): interface Promise<T>Promise<void> {
const { const routeId: stringrouteId, const durationMs: numberdurationMs } = this.Subscriber<"interactionDispatched", Core>.data: {
readonly dispatchId: string;
readonly routeId: string;
readonly interactionId: string;
readonly kind: `${InteractionKind}`;
readonly outcome: DispatchOutcome;
readonly fallback: boolean;
readonly userId: string | null;
readonly guildId: string | null;
readonly durationMs: number;
readonly queuedMs: number;
}
data;
this.Subscriber<"interactionDispatched", Core>.logger: Loggerlogger.Logger.info(msg: string, ...args: unknown[]): voidinfo(`${const routeId: stringrouteId} took ${const durationMs: numberdurationMs}ms`);
}
}durationMs is how long that dispatch took. Only seedcord publishes these keys, so calling publish with one is a compile error.
// publish accepts only the keys you declared, levelUp on this page
core.bus.publish('interactionDispatched', {} as never);The framework's bus keys lists every key, which transport publishes it, and its payload. To post one of them to a Discord channel, write a WebhookLog. Reporting faults shows how.