# Cordis Fiber State Transitions During Plugin Load and Unload

> Explore Cordis Fiber state transitions during plugin load and unload. Understand how the finite state machine manages lifecycles through PENDING, LOADING, ACTIVE, UNLOADING, and DISPOSED states.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: internals
- Published: 2026-08-25

---

**Cordis uses a Fiber abstraction with a finite state machine to manage plugin lifecycles, transitioning through `PENDING`, `LOADING`, `ACTIVE`, `UNLOADING`, and `DISPOSED` states via the `_setEpoch`, `_reload`, and `_unload` methods in [`Fiber.ts`](https://github.com/cordiverse/cordis/blob/main/Fiber.ts).**

Managing plugin lifecycles in modern JavaScript frameworks requires deterministic state management to handle initialization, hot-reloading, and cleanup. The **Cordis** framework (from the `cordiverse/cordis` repository) implements a sophisticated **Fiber** architecture that orchestrates **Cordis Fiber state transitions during plugin load and unload** through a finite state machine. Each plugin instance runs inside its own `Fiber`, which tracks its current state and manages the execution of effects and disposables.

## Understanding the Fiber State Machine

The lifecycle of every Cordis plugin is governed by the `Fiber` class defined in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts). This class maintains an internal state machine that ensures predictable transitions between loading, active operation, and disposal.

### The FiberState Enum

The possible states are defined by the `FiberState` enum at lines 78-85 of [`Fiber.ts`](https://github.com/cordiverse/cordis/blob/main/Fiber.ts):

- **`PENDING`**: The fiber has been created but not yet started.
- **`LOADING`**: The plugin's `callback` is being executed for the first time.
- **`ACTIVE`**: The plugin has successfully executed and its effects are active.
- **`FAILED`**: Execution threw an error—the fiber stays in a failed state.
- **`UNLOADING`**: The plugin is being disposed (effects are being cleaned up).
- **`DISPOSED`**: The fiber has been fully torn down (all disposables cleared).

## Plugin Lifecycle State Transitions

State transitions are triggered by changes in the fiber's **epoch**—a string representing the current configuration context—and managed by private methods within the `Fiber` class.

### Construction and Initial Load (PENDING → LOADING → ACTIVE)

When a plugin registers via `ctx.plugin()`, the framework instantiates a new `Fiber` via `new Fiber(parent, config, inject, runtime, getOuterStack)`. If a runtime is present, the fiber starts in the `PENDING` state.

The constructor registers a dispose callback via `parent.fiber.effect(...)` that later calls `_setEpoch(INACTIVE)` to trigger unloading. During the first load:

1. The dispose callback pushes the fiber into the runtime's `fibers` list and invokes `_refresh()`.
2. `_refresh()` computes an epoch string based on injected services. If all required implementations are present, it calls `_setEpoch(epoch)`.
3. `_setEpoch` detects the transition from `INACTIVE` to a non-`INACTIVE` epoch and creates a reload promise (`this.inertia = this._reload()`) while setting the state to `LOADING`.
4. `_reload()` copies the current implementation store, executes the plugin's effect (`this._execute(this._runner)`), and on success leaves the epoch unchanged, resulting in the `ACTIVE` state.

### Hot Reloading (ACTIVE → UNLOADING → LOADING → ACTIVE)

When a plugin's configuration updates via `Fiber.update()`, the fiber is forced back to the `INACTIVE` epoch. This causes `_setEpoch` to start an unload (`this.inertia = this._unload()`) followed by a new reload once the unload finishes. The state walks through `UNLOADING → LOADING → ACTIVE` while applying the new configuration.

### Unloading and Disposal (UNLOADING → DISPOSED)

If the fiber's epoch becomes `INACTIVE` (e.g., a required implementation disappears), `_setEpoch` triggers `_unload()`. This method clears the disposable list, awaits each disposer, and empties `this.store`. After the unload promise resolves, the state is set to `DISPOSED`.

### Error Handling (FAILED state)

Any uncaught error during execution or disposal is logged via `this.ctx.logger.error` and the fiber moves to the `FAILED` state, where it remains until explicitly reset.

## Core Implementation Details in Fiber.ts

The transition logic lives in three critical private methods within [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts):

- **`_setEpoch`** (lines 99-112): Orchestrates state transitions by comparing the current epoch with the target epoch. It manages the `inertia` promise chain and triggers `_reload()` or `_unload()` as needed.
- **`_reload`**: Handles the `LOADING` state transition, executes the plugin runner, and establishes the `ACTIVE` state upon completion.
- **`_unload`**: Manages the `UNLOADING` state, clears disposables, and transitions to `DISPOSED`.

State updates are performed by `_updateState` (lines 55-63), which emits `'internal/status'` events for external observers.

## Practical Code Examples

The following examples demonstrate how to interact with the Cordis Fiber state machine in application code.

Registering a plugin triggers the initial state transition from `PENDING` through `LOADING` to `ACTIVE`:

```typescript
// In a Cordis context (e.g., inside a bot entry file)
ctx.plugin({
  // The plugin's main class
  callback: class MyPlugin {
    // Optional init hook; runs before the main effect
    [symbols.initHooks] = [() => console.log('initialising')]

    // The effect that returns disposables or a Promise thereof
    [symbols.init]() {
      ctx.logger.info('MyPlugin activated')
      return () => ctx.logger.info('MyPlugin cleaned up')
    }
  },

  // Optional configuration schema (validated on load)
  Config: {
    '~standard': {
      validate: (conf) => ({ value: conf, issues: [] })
    }
  },

  // Services injected into the plugin's context
  inject: { logger: true }
})

```

Updating a plugin's configuration forces a reload cycle through `UNLOADING` and `LOADING`:

```typescript
// Assuming `myFiber` is the Fiber instance returned by ctx.plugin()
myFiber.update({ verbose: true })

```

Manually unloading a plugin transitions through `UNLOADING` to `DISPOSED`:

```typescript
await myFiber.dispose()   // Returns a Promise that resolves after unload

```

Reacting to state changes allows monitoring of the transition lifecycle:

```typescript
ctx.fiber.effect(() => {
  // Listen to internal status events
  ctx.on('internal/status', (fiber, oldState) => {
    ctx.logger.info(`Fiber ${fiber.name} changed from ${oldState} to ${fiber.state}`)
  })
})

```

## Summary

- Cordis manages plugin lifecycles through a **Fiber** abstraction with a six-state finite state machine defined in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts).
- State transitions are driven by **epoch** changes managed by `_setEpoch`, with `_reload` handling activation and `_unload` handling disposal.
- The fiber transitions from `PENDING` → `LOADING` → `ACTIVE` during initial load, and through `UNLOADING` → `LOADING` → `ACTIVE` during hot-reloads.
- Errors transition the fiber to `FAILED`, while successful disposal ends in `DISPOSED`.
- The `ctx.plugin()` API in [`packages/core/src/context.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/context.ts) and the loader in [`packages/loader/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts) delegate lifecycle management to the Fiber state machine, with comprehensive verification available in [`packages/core/tests/plugin.spec.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/tests/plugin.spec.ts).

## Frequently Asked Questions

### What triggers a Cordis Fiber to transition from LOADING to ACTIVE?

The transition occurs when `_setEpoch` invokes `_reload()`, which executes the plugin's effect via `this._execute(this._runner)`. Once the effect completes successfully without throwing, the fiber leaves the epoch unchanged and `_updateState` sets the state to `ACTIVE`.

### How does Cordis handle plugin configuration hot-reloading?

When `Fiber.update()` is called, it resolves the new configuration and forces the epoch to `INACTIVE`. This triggers `_setEpoch` to begin an unload operation (`_unload`), followed by a new reload once cleanup completes, walking the state through `UNLOADING` → `LOADING` → `ACTIVE`.

### What is the difference between the UNLOADING and DISPOSED states?

`UNLOADING` indicates the fiber is actively cleaning up resources—`_unload()` is executing disposables and clearing the store. `DISPOSED` represents the terminal state where all cleanup has finished and the fiber is fully torn down, as set after the unload promise resolves.

### Where is the Fiber state machine defined in the Cordis source code?

The state machine is defined in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts), specifically in the `FiberState` enum (lines 78-85) and the transition logic within the `Fiber` class methods `_setEpoch` (lines 99-112), `_reload`, `_unload`, and `_updateState` (lines 55-63).