Skip to content

The core object

Read your config and your plugins from any handler through this.core, and run work while your bot starts and stops.

Your /ping handler already read this.core.uptime. Other handlers read your own config, a database, or the rate limiter that a cooldown charges. You read all of it from this.core, one object that seedcord builds while your bot starts.

src/handlers/Ping.ts
import { SlashHandler, SlashRoute } from '@seedcord/gateway';

@SlashRoute('ping')
export class Ping extends SlashHandler<'ping'> {
    public async execute(): Promise<void> {
        const owners = this.core.config.ownerIds;
const owners: string[] | undefined
await this.reply( `${owners?.length ?? 0} owners configured.` ); } }

this.core.config is the object you passed to new Seedcord(). The type under the ownerIds line marks it optional, which is why the sample guards it with owners?.length. OwnerOnly reads the same key.

Other code reaches the same object. A gate, which is a check that runs before a handler, reads it as ctx.core. A subscriber, which listens for your own events, and a plugin read this.core like a handler does.

What it carries

memberholds
configthe object you passed to new Seedcord()
restDiscord's REST client, which is the discord.js client's own on gateway
applicationIdyour bot's Discord application id
rateLimiterthe counters behind Cooldown, which you can also use directly
buspublishing and subscribing for framework and application events
startupaddTask, for work that runs while your bot starts
shutdownaddTask, for work that runs while it stops
boton gateway, bot.client is the discord.js client and bot.botToken is your token. Http has no bot

CoreBase is the part both transports share. Each one adds its own members to it, and a plugin you attach adds a key of its own, as the last section shows.

Running work while your bot starts and stops

Suppose your bot uses a cache of its own. It has to open before the first command runs and close when the bot stops. Without seedcord, you listen for SIGTERM yourself to close it.

You write a function that opens the cache and another that closes it. You register them with startup.addTask and shutdown.addTask, each in a phase, which is a stage of startup or shutdown. seedcord runs the phases in order. If the cache needs options of its own or its own log channel, write a plugin instead. A plugin starts and stops in these same phases.

src/bot.ts
import {
    Seedcord,
    ShutdownPhase,
    StartupPhase
} from '@seedcord/gateway';
import { GatewayIntentBits } from 'discord.js';

export const seedcord = new Seedcord({
    bot: {
        clientOptions: { intents: [GatewayIntentBits.Guilds] },
        interactions: { path: './handlers' },
        commands: { path: './commands' },
        events: { path: null }
    },
    subscribers: { path: null }
});

seedcord.startup.addTask(
    StartupPhase.Configuration,
    'open-cache',
    (signal) => openCache(signal)
);

seedcord.shutdown.addTask(
    ShutdownPhase.Disconnect,
    'close-cache',
    closeCache
);

export default seedcord;

Each call takes the phase, a name for the logs, the function to run, and an optional timeout in milliseconds. A startup task gets 10000 unless you pass a timeout. A shutdown task gets 5000.

Add every task before the bot starts, because addTask throws LifecycleTaskChangeRejected once startup is running or finished. The sample does it at the top level of bot.ts.

A startup task's function receives an AbortSignal, and the sample passes it to openCache. Pass it to any request the task makes, because it aborts if the task times out or startup stops. A shutdown task gets a signal too, and it aborts if that task times out.

Pick the phase by what must already be ready when your task runs. The sample registers open-cache in Configuration, before anything connects, and close-cache in Disconnect, the phase for closing external resources. Plugin lifecycle has a table of every startup and shutdown phase and what has run before it.

Tasks in the same phase run at the same time, including seedcord's own tasks. The next phase starts once all of them have finished. If one task needs another task's result, put the task that needs it in a later phase.

If a startup task throws, startup fails and no later phase runs. If a shutdown task fails, the phases after it still run.

All the shutdown phases share one deadline of 25000 milliseconds, and after them seedcord waits three more seconds to flush the logs. To finish before your platform stops the process, set lifecycle.shutdownDeadline lower in the object you pass to new Seedcord().

Gateway only

core.applicationId comes from the discord.js client, so it resolves during the Login phase. If you read it in a Configuration task, it throws. Read it in a handler or in a Ready task. core.rest has no token before Login either.

Http only

On Cloudflare Workers, startup tasks run on the first POST request each worker instance receives, in the same phases and with the same timeouts. Requests that arrive meanwhile wait for them. If a task fails, the next request runs the tasks again.

shutdown.addTask throws CoreLifecycleUnavailable there, because Cloudflare doesn't have a shutdown hook for a worker. Do your cleanup inside a handler. A Workers config doesn't take a lifecycle.

Attach a plugin to the core

If you construct a plugin yourself, you pass it down through imports and order its setup by hand. Attaching it puts it on the core, so every handler already reads it from this.core.

attach() takes a key, a plugin class, and the options that plugin's constructor expects. The key becomes a property on the same object your handlers read.

import { Seedcord } from '@seedcord/gateway';
import { KyselyPostgres } from '@seedcord/plugin-kysely-postgres';
import { GatewayIntentBits } from 'discord.js';

export default seedcord.attach('db', KyselyPostgres, {
    dir: './services',
    connectionString: 'postgres://localhost:5432/seedcord',
    migrations: {
        dir: './migrations'
    }
});

attach() sets the property at runtime. The type that makes this.core.db compile in a handler is defined in seedcord-gen.d.ts. seedcord codegen writes that file, and seedcord dev rewrites it each time the bot starts. Both dir values are relative to root in seedcord.config.ts, like the folders in bot.ts.

The scaffold's bot.ts attaches its Uptime plugin with .attach('uptime', Uptime), so the ping handler can read this.core.uptime.