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'scallbackis 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:
- The dispose callback pushes the fiber into the runtime's
fiberslist and invokes_refresh(). _refresh()computes an epoch string based on injected services. If all required implementations are present, it calls_setEpoch(epoch)._setEpochdetects the transition fromINACTIVEto a non-INACTIVEepoch and creates a reload promise (this.inertia = this._reload()) while setting the state toLOADING._reload()copies the current implementation store, executes the plugin's effect (this._execute(this._runner)), and on success leaves the epoch unchanged, resulting in theACTIVEstate.
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 theinertiapromise chain and triggers_reload()or_unload()as needed._reload: Handles theLOADINGstate transition, executes the plugin runner, and establishes theACTIVEstate upon completion._unload: Manages theUNLOADINGstate, clears disposables, and transitions toDISPOSED.
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_reloadhandling activation and_unloadhandling disposal. - The fiber transitions from
PENDING→LOADING→ACTIVEduring initial load, and throughUNLOADING→LOADING→ACTIVEduring hot-reloads. - Errors transition the fiber to
FAILED, while successful disposal ends inDISPOSED. - The
ctx.plugin()API inpackages/core/src/context.tsand the loader inpackages/loader/src/index.tsdelegate lifecycle management to the Fiber state machine, with comprehensive verification available inpackages/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →