How Cordis Supports Dynamic Loading and Unloading of Plugins
Cordis implements dynamic plugin management through a Loader service that orchestrates Entry objects and Fiber instances, enabling runtime loading, hot-reloading, and cleanup via RegistryService.
Cordis (cordiverse/cordis) is a progressive TypeScript framework designed for modular application development. Its architecture centers on dynamic loading and unloading of plugins, allowing developers to add, update, or remove functionality at runtime without restarting the application.
The Loader Service Architecture
The dynamic plugin system relies on the Loader service defined in packages/loader/src/index.ts. This service maintains a tree of Entry objects that represent individual plugin instances and their configurations.
Entry Objects and Plugin Resolution
Each plugin is wrapped in an Entry class (packages/loader/src/config/entry.ts). When loading occurs:
- The entry resolves the module using
loader.unwrapExports - It invokes
ctx.registry.pluginto instantiate the plugin's runtimeFiber - The
Entry._initmethod establishes the plugin's context and dependency injection
The RegistryService (packages/core/src/registry.ts) stores a Plugin.Runtime for each plugin function and tracks active Fiber instances.
Fiber Lifecycle Management
Every plugin instance runs inside a Fiber, which represents the active execution context. The RegistryService.plugin method returns a Fiber that manages the plugin's lifecycle, including updates and disposal.
Runtime Loading Process
When you invoke ctx.plugin(), Cordis executes the following sequence:
ctx.plugin(plugin, config)delegates toRegistryService.plugin- The registry creates a runtime representation and instantiates a new
Fiber - For file-based loading,
Loader.read()parses configuration files and constructsEntryobjects - The entry calls
ctx.registry.pluginto activate the plugin
Hot-Reloading Configuration Updates
Cordis supports hot-module replacement through the 'internal/update' event. When a plugin's configuration changes:
- The
Loaderdetects the change and triggersEntry.update - The entry emits
'loader/partial-dispose'and patches the context - It calls
fiber.updateto apply new configuration without destroying the plugin instance
This process occurs in packages/loader/src/index.ts (lines 74-86), allowing stateful updates without service interruption.
Unloading and Resource Cleanup
Removing plugins requires careful resource management to prevent memory leaks.
Explicit Unloading via Registry
To completely remove a plugin:
- Call
ctx.registry.delete(plugin)to invokeRegistryService.delete(lines 62-70 inpackages/core/src/registry.ts) - This removes the runtime from the internal map
- All associated fibers are disposed, releasing event listeners and injected dependencies
Group Management and Partial Disposal
For grouped plugins, EntryGroup (packages/loader/src/config/group.ts) manages collections atomically. Removing a plugin from a group:
- Invokes
EntryGroup.remove, which disposes the specific fiber - Emits
'loader/partial-dispose'to notify the system - Preserves other plugins in the group
The Loader also listens to 'internal/plugin' events to handle implicit unloads (lines 88-124 in packages/loader/src/index.ts), logging the operation and marking entries as disabled.
Practical Implementation Examples
Here is how to dynamically manage plugins in Cordis applications:
Loading and updating a plugin programmatically:
import { Context } from 'cordis'
// Dynamically load a plugin
await ctx.plugin(MyPlugin, { foo: 'bar' })
// Update its configuration later (hot‑reload)
await ctx.registry.plugin(MyPlugin).update({ foo: 'baz' })
// Unload the plugin
await ctx.registry.delete(MyPlugin) // removes runtime and disposes all fibers
Using the Loader for file-based plugins:
import { Loader } from 'cordis/loader'
const loader = ctx.loader as Loader
// Load a plugin from a file path
await loader.read([{ name: './plugins/example', id: 'example' }])
// Reload after editing the file (HMR)
await loader.update(loader.tree.get('example'), { config: { enabled: true } })
// Unload the plugin
await loader.tree.store['example'].fiber?.dispose()
Summary
- Cordis enables dynamic loading and unloading of plugins through the
Loaderservice andRegistryServicecoordination. - Entry objects in
packages/loader/src/config/entry.tsmanage individual plugin lifecycles and configuration resolution. - The Fiber system tracks active plugin instances, supporting hot-reloading via
fiber.updatewithout recreation. - Group management through
EntryGroupallows atomic operations on plugin collections. - Explicit cleanup occurs through
ctx.registry.delete, which removes runtimes and disposes all associated fibers defined inpackages/core/src/registry.ts.
Frequently Asked Questions
How does Cordis handle plugin updates without restarting the application?
Cordis listens for the 'internal/update' event in the Loader service. When triggered, the corresponding Entry emits 'loader/partial-dispose', patches the context with new configuration, and invokes fiber.update to refresh the plugin state while preserving its identity and injected dependencies.
What is the difference between ctx.plugin() and direct Loader usage?
ctx.plugin() provides a programmatic API for immediate plugin instantiation with configuration, while the Loader service (accessed via ctx.loader) manages file-based plugin discovery and hot-reloading. The Loader constructs Entry objects from configuration files and orchestrates updates through the internal event system.
How does Cordis prevent memory leaks when unloading plugins?
The RegistryService.delete method (located in packages/core/src/registry.ts) removes the Plugin.Runtime from its internal Map and explicitly disposes every Fiber associated with that plugin. This ensures all event listeners, asynchronous operations, and injected services are properly terminated.
Can plugins be organized into groups for bulk operations?
Yes, the EntryGroup class in packages/loader/src/config/group.ts enables collective management. You can add, update, or remove multiple plugins simultaneously, with the group handling partial disposal events and maintaining consistency across the plugin tree.
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 →