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:

  1. The entry resolves the module using loader.unwrapExports
  2. It invokes ctx.registry.plugin to instantiate the plugin's runtime Fiber
  3. The Entry._init method 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 to RegistryService.plugin
  • The registry creates a runtime representation and instantiates a new Fiber
  • For file-based loading, Loader.read() parses configuration files and constructs Entry objects
  • The entry calls ctx.registry.plugin to activate the plugin

Hot-Reloading Configuration Updates

Cordis supports hot-module replacement through the 'internal/update' event. When a plugin's configuration changes:

  • The Loader detects the change and triggers Entry.update
  • The entry emits 'loader/partial-dispose' and patches the context
  • It calls fiber.update to 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 invoke RegistryService.delete (lines 62-70 in packages/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 Loader service and RegistryService coordination.
  • Entry objects in packages/loader/src/config/entry.ts manage individual plugin lifecycles and configuration resolution.
  • The Fiber system tracks active plugin instances, supporting hot-reloading via fiber.update without recreation.
  • Group management through EntryGroup allows atomic operations on plugin collections.
  • Explicit cleanup occurs through ctx.registry.delete, which removes runtimes and disposes all associated fibers defined in packages/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:

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 →