Cordis Dispose Mechanism for Fibers and Plugins: Complete Technical Guide
Cordis implements a hierarchical disposal system where every plugin runs inside a lightweight Fiber that automatically cleans up resources, removes registry entries, and awaits pending async work when unloaded via ctx.unload().
In the cordiverse/cordis framework, robust plugin lifecycle management depends on a sophisticated dispose mechanism that treats every loaded plugin as a fiber—a lightweight execution context owning its own Context, configuration, and disposables. This architecture ensures that plugins can be dynamically loaded and unloaded at runtime without dangling references or memory leaks.
How Fibers Model Plugin Execution Contexts
Every plugin registered in Cordis is encapsulated within a Fiber instance defined in packages/core/src/fiber.ts. This fiber acts as the plugin’s runtime boundary, tracking its state, managing injection, and maintaining a DisposableList of cleanup functions. By isolating plugins into fibers, Cordis creates a tree-like hierarchy where parent fibers control the lifecycles of their children.
Creating the Dispose Effect in the Fiber Constructor
When a plugin is registered, the Fiber constructor (lines 22-33) immediately establishes the disposal logic by calling parent.fiber.effect(). This attaches the disposal routine to the parent fiber’s lifecycle, ensuring that when a parent disposes, all child fibers clean up automatically.
The constructor registers the effect using this pattern:
this.dispose = parent.fiber.effect(() => {
// Cleanup logic executes here
}, 'ctx.plugin()')
This assignment at lines 70-100 creates a disposable effect that Cordis invokes when the plugin unloads or the parent fiber terminates.
The Three-Stage Disposal Execution Process
When ctx.unload() triggers or a parent fiber disposes, the effect stored in Fiber.dispose executes a precise three-stage cleanup sequence:
1. Removing the Fiber from the Runtime List
The disposal effect first detaches the fiber from the runtime’s active tracking list. During construction, the fiber stores a removal callback: const remove = runtime.fibers.push(this). When disposal triggers, calling remove() (lines 81-82) eliminates the fiber from runtime.fibers without leaving stale references.
2. Cleaning Up the Plugin Registry
If the disposed fiber represents the last active instance of its plugin, the mechanism cleans the registry entry. As implemented in packages/core/src/fiber.ts (lines 84-86), the code checks whether the fiber list is empty and then executes this.ctx.registry.delete(runtime.callback). This step, defined in packages/core/src/registry.ts, ensures that plugin symbols become unreachable once all associated fibers terminate.
3. Resetting State and Awaiting Pending Work
Finally, the fiber resets its internal state by clearing the uid, emitting an internal/plugin event, and setting the epoch to the INACTIVE placeholder. Crucially, the disposal awaits any pending inertia promises (lines 88-98) to guarantee that all asynchronous operations complete before the fiber terminates, preventing mid-cleanup interruptions.
Hierarchical Resource Cleanup
Because Cordis attaches the dispose effect to the parent fiber’s disposable list, the framework supports recursive teardown. Disposing a parent fiber automatically invokes the disposal effects of all descendant fibers, creating a stack-like cleanup pattern that propagates from the root context down to nested plugins.
All disposables registered via ctx.effect(), ctx.once(), or pushed directly onto Fiber._disposables are collected in a DisposableList. When the fiber disposes, Cordis invokes these functions in reverse order, mirroring the construction sequence and ensuring that dependent resources release before their consumers.
Working with Disposables in Plugin Code
Plugins interact with the dispose mechanism by registering cleanup hooks. The framework supports two primary patterns: implementing the symbols.dispose method for class-based plugins, or using the ctx.effect() API to register functional cleanup routines.
Example implementation demonstrating both approaches:
import { Context, symbols } from 'cordis'
export default class MyPlugin {
[symbols.dispose] = () => {
console.log('Class-level cleanup executed')
}
constructor(public ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => console.log('tick'), 1000)
const connection = openDatabaseConnection()
// Return disposal function
return () => {
clearInterval(timer)
connection.close()
}
})
}
}
The function returned to ctx.effect() automatically enters the fiber’s DisposableList and executes during the stage-three cleanup.
Unloading Plugins at Runtime
To programmatically trigger the dispose mechanism, Cordis exposes ctx.unload() in packages/core/src/service.ts. This method locates the plugin’s fiber and initiates the full disposal chain, including the three-stage process and hierarchical propagation.
Example usage:
// Register the plugin
const fiber = ctx.plugin(MyPlugin, { option: true })
// Later, trigger complete cleanup
await ctx.unload(MyPlugin) // Waits for inertia and all disposables
Calling await ctx.unload() ensures that the fiber’s disposal effect completes, including awaiting any pending inertia promises before returning control to the caller.
Summary
- Every Cordis plugin executes inside a Fiber that functions as a disposable execution context with its own state and resource tracking.
- The
Fiberconstructor registers disposal effects viaparent.fiber.effect(), linking child lifecycles to parent fibers for automatic cascading cleanup. - Disposal executes three defined stages: removal from
runtime.fibers, registry cleanup viathis.ctx.registry.delete(), and state reset with inertia awaiting. - The DisposableList manages cleanup functions in reverse order, ensuring that resources release in the correct dependency order.
- Hierarchical disposal propagates automatically through the fiber tree, allowing nested plugins to unload without manual intervention.
Frequently Asked Questions
What triggers the Cordis fiber dispose mechanism?
The dispose mechanism triggers when ctx.unload() is called on a plugin, when a parent fiber disposes (cascading to children), or during application shutdown. The effect registered in the Fiber constructor (lines 70-100 in packages/core/src/fiber.ts) runs automatically, executing the three-stage cleanup process.
How does Cordis handle asynchronous cleanup during disposal?
The disposal process explicitly awaits pending inertia promises (lines 94-98 in packages/core/src/fiber.ts) before marking the fiber as inactive. This ensures that asynchronous operations like database writes or network requests complete before the fiber’s uid clears and the epoch sets to INACTIVE.
Where does the plugin registry cleanup happen in the source code?
Registry cleanup occurs in packages/core/src/fiber.ts at lines 84-86. When the last fiber for a given runtime is removed, the code executes this.ctx.registry.delete(runtime.callback), which removes the plugin’s callback from the registry defined in packages/core/src/registry.ts.
Can a plugin register multiple cleanup hooks?
Yes. Plugins can define the [symbols.dispose] property for class-based cleanup, use ctx.effect() to register functional cleanup with automatic disposal, or push functions directly onto the fiber’s internal _disposables list. All mechanisms integrate into the same DisposableList that Cordis executes in reverse order during fiber teardown.
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 →