# The lifecycle

Control when a plugin starts and stops with init(), ready(), and dispose(). Covers the startup and shutdown phases, moving a method to another phase, timeouts, and what happens when init() or dispose() fails.

A plugin's setup often depends on timing. A database has to connect before the first handler reads it, a status update needs a logged-in client, and a timer should stop before the process exits. Code in a constructor runs too early for all three.

seedcord calls your plugin at those points instead. `init()` is the one method every plugin writes. If you add `ready()` or `dispose()`, seedcord calls those too.

```ts title="src/plugins/Presence.ts"
import { Plugin } from '@seedcord/gateway';

export class Presence extends Plugin {
    private timer?: NodeJS.Timeout;

    public async init(): Promise<void> {
        this.logger.info('presence plugin starting');
    }

    public override async ready(): Promise<void> {
        this.timer = setInterval(() => {
            this.core.bot.client.user?.setActivity('with types');
        }, 60_000);
    }

    public override async dispose(): Promise<void> {
        clearInterval(this.timer);
    }
}
```

`setActivity` needs a logged-in client, so `Presence` starts its timer in `ready()`. `dispose()` clears that timer, since an interval left running keeps calling a client that has logged out.

> **Gateway only**
>
> `this.core.bot` holds the discord.js client, which only a gateway bot has. Discord takes a presence update over the gateway alone, so an http bot can't run `Presence`, though `init()`, `ready()`, and `dispose()` work the same there.

## Where each one runs

Startup runs three phases in order.

{/* prettier-ignore-start */}

| startup phase   | what has happened                                                   | plugins                       |
| --------------- | ------------------------------------------------------------------- | ----------------------------- |
| `Configuration` | nothing connected                                                   | `init()` runs here by default |
| `Login`         | the gateway session opens, and http runs its REST-only startup work |                               |
| `Ready`         | handlers are live and interactions dispatch                         | `ready()` always runs here    |

{/* prettier-ignore-end */}

Shutdown runs four phases.

{/* prettier-ignore-start */}

| shutdown phase | what happens                                                              | plugins                          |
| -------------- | ------------------------------------------------------------------------- | -------------------------------- |
| `Unbind`       | new interactions and requests stop                                        |                                  |
| `Drain`        | in-flight work finishes and internal services stop                        |                                  |
| `Disconnect`   | external resources close                                                  | `dispose()` runs here by default |
| `Logout`       | the client disconnects last, so a draining handler can still reference it |                                  |

{/* prettier-ignore-end */}

Say you attach `db`, then `settings`, which loads each server's settings from `db`. Both write all three methods and keep the default phases.

```txt output
startup
├─ Configuration
│  ├─ db.init()
│  └─ settings.init()     starts once db.init() has finished
├─ Login
└─ Ready
   ├─ db.ready()          every init() has finished
   └─ settings.ready()

shutdown
├─ Unbind
├─ Drain
├─ Disconnect
│  ├─ settings.dispose()  db is still open
│  └─ db.dispose()
└─ Logout
```

Your plugins start in the order you attach them and stop in the *reverse* order. Attach a plugin before any plugin that reads from it. seedcord puts all of a phase's plugin calls in one task, so they run one after another while the phase's [other tasks](/core#running-work-while-your-bot-starts-and-stops) run in parallel with them. Your handlers can already receive interactions and events while `ready()` is still running. Set up anything a handler needs in `init()`.

## Moving a method to another phase

The defaults fit most plugins, but some don't. If your `dispose()` writes a final batch into a database that another plugin connects, that write has to finish before the database plugin disconnects. Your `dispose()` then belongs in Drain, one phase earlier. Pass a [`PluginLifecycleSpec`](https://docs.seedcord.org/packages/core/latest/plugin-lifecycle-spec) as the second argument to `super()`. If you leave a field out, it keeps its default.

```ts
export class Presence extends Plugin {
    public constructor(host: CoreBase) {
        super(host, {
            init: { phase: StartupPhase.Login },
            ready: { timeout: 20_000 },
            dispose: { phase: ShutdownPhase.Drain, timeout: 30_000 }
        });
    }

    public async init(): Promise<void> {}
}
```

`Presence` moves `init()` to Login, gives `ready()` 20 seconds, and moves `dispose()` to Drain with 30 seconds. The `ready` field takes a timeout without a phase, since `ready()` always runs in the Ready phase. If you move an `init()` to Ready, it still runs before every plugin's `ready()`.

## Timeouts

An `init()` waiting on a service that never answers would hang your bot's startup forever, so each method gets its own timeout. `init()` and `ready()` default to 15000ms, and `dispose()` defaults to 10000ms.

If a method runs past its timeout, seedcord throws `LifecycleTaskTimeout`. The task name in the message carries the attach key and which method ran.

```txt output
Task "Plugin (db)" timed out after 15000ms.
Task "Plugin:db:ready" timed out after 15000ms.
Task "Plugin:db:dispose" timed out after 10000ms.
```

A timeout of zero, a negative number, or `Infinity` throws `PluginInvalidLifecycleTimeout` in the constructor, before any of your code runs.

### The shutdown deadline

Those timeouts limit one method each. Your hosting platform kills the process if shutdown as a whole takes too long, so seedcord also bounds the whole shutdown with a single deadline, 25000ms by default. Once it elapses seedcord skips the phases it never reached and exits. A task already running keeps going until the process exits three seconds later. A deadline of zero, a negative number, or `Infinity` throws `LifecycleInvalidShutdownDeadline`.

If shutdown starts while the bot is still starting, it waits for startup to finish, and that wait counts against the same deadline. If the deadline passes first, seedcord doesn't dispose the plugins that finish starting after that.

The 25000ms default fits the 30 seconds Kubernetes waits before SIGKILL, with the three-second exit delay added. `docker stop` allows 10 seconds, so set `shutdownDeadline` to 5000 there, which leaves two seconds after the exit delay. You can also widen the window with `docker stop -t`.

```ts title="src/bot.ts"
import { resolve } from 'node:path';

import { Seedcord } from '@seedcord/gateway';
import { GatewayIntentBits } from 'discord.js';

const seedcord = new Seedcord({
    bot: {
        clientOptions: { intents: [GatewayIntentBits.Guilds] },
        interactions: {
            path: resolve(import.meta.dirname, './handlers')
        },
        commands: { path: null },
        events: { path: null }
    },
    subscribers: { path: null },
    // 25s plus the 3s log flush fits inside kubernetes' 30s window
    lifecycle: { shutdownDeadline: 25_000 }
});
```

## When init fails

An `init()` usually fails because a service it connects to is down or can't be reached. When that happens, startup stops and `start()` rejects with `LifecyclePhaseFailures`, carrying your error in its `errors` array. What gets cleaned up depends on whether `init()` threw or ran out of time.

### When it throws

seedcord calls `dispose()` on every plugin whose `init()` had already finished, in reverse attach order. The plugin that threw doesn't get a `dispose()` call. Anything its `init()` opened before the error stays open unless `init()` closes it.

```ts title="src/plugins/Feeds.ts"
export class Feeds extends Plugin {
    private readonly client = new FeedClient();

    public async init(): Promise<void> {
        await this.client.connect();
        try {
            await this.client.subscribe('updates');
        } catch (error) {
            await this.client.close();
            throw error;
        }
    }
}
```

If `subscribe` throws, `Feeds` closes the connection in its `catch`, then rethrows so startup still fails.

### When it times out

Startup fails, but your `init()` keeps running in the background. What happens next depends on whether it finishes:

* **It finishes later.** seedcord calls that plugin's `dispose()` as soon as it does, without waiting for the other plugins' `dispose()` calls. If that `dispose()` throws, seedcord only logs a warning.
* **It never finishes.** `dispose()` never runs. Give whatever `init()` waits on its own timeout, so a hang turns into an error your `catch` handles.

Once startup has failed, the same `Seedcord` can't start again. Calling `start()` on it a second time throws `LifecycleRestartAfterFailure`, so exit and let your process manager start a fresh one. When using `seedcord dev`, press `r` instead, which loads your bot into a fresh `Seedcord`.

## When dispose fails

If one `dispose()` fails, the others still run. With `db` and `settings` from earlier, a throw from `settings.dispose()` still lets `db.dispose()` close the connection. Once they've all run, seedcord throws the failure it caught. If several failed, it throws one `AggregateError` carrying `PluginDisposeFailures`, with each error in its `errors` array.
