Cordis Fiber State Transitions During Plugin Load and Unload

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.

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. 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:

  • 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:

  • _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:

// 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:

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

Manually unloading a plugin transitions through UNLOADING to DISPOSED:

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

Reacting to state changes allows monitoring of the transition lifecycle:

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.
  • 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 and the loader in packages/loader/src/index.ts delegate lifecycle management to the Fiber state machine, with comprehensive verification available in 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, 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).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →