Cordis Registry Plugin Tracking Mechanism: How Plugins Are Indexed and Managed
The Cordis framework maintains a live index of every loaded plugin through the RegistryService, which stores runtime records containing callback functions, active fiber lists, and optional configuration schemas in an internal Map structure.
The Cordis framework (cordiverse/cordis) implements a sophisticated plugin architecture that requires precise lifecycle tracking. At the heart of this system lies the registry plugin tracking mechanism, which serves as the single source of truth for plugin states within a Context. This article examines how RegistryService manages plugin registrations, from initial resolution through automatic cleanup, based on the actual source implementation.
Core Architecture of the RegistryService
The registry implementation resides in packages/core/src/registry.ts and centers on the RegistryService class. This service maintains a private _internal property (lines 25‑28) that functions as a Map storing the relationship between plugin callbacks and their runtime states.
The Internal Storage Structure
The _internal Map uses the plugin's callback function as the key and a runtime record as the value. According to the source code, each runtime entry contains:
callback– The plugin's function or the object'sapplymethodfibers– An array tracking everyFiberinstance created for this pluginConfig– Optional configuration schema metadata
This structure enables constant-time lookups while preserving the relationship between a plugin definition and all its active instances.
How the Cordis Plugin Registration Flow Works
The registration process is orchestrated through Context.registry.plugin(), which follows a strict four-phase pipeline:
Step 1: Plugin Resolution
When plugin() is invoked, the system first calls RegistryService.resolve() (lines 44‑50) to normalize the plugin input. This method checks whether the supplied plugin is a plain function or an object containing an apply method, ensuring consistent handling regardless of how the plugin was exported.
Step 2: Runtime Creation and Reuse
After resolution, the registry checks _internal.has(callback) to determine if this plugin has been registered previously. If the callback is absent, a new runtime object is instantiated and stored via _internal.set(callback, runtime) (lines 99‑105). This deduplication strategy ensures that reloading the same plugin shares the runtime state rather than creating orphaned entries.
Step 3: Fiber Instantiation and Tracking
If the plugin passes resolution, a new Fiber is constructed with the plugin's config, inject data, and the runtime object (lines 107‑108). During fiber initialization, the instance registers itself with the runtime by executing runtime.fibers.push(this) (lines 71‑72), creating the critical link between the plugin definition and its executing context.
Registry Utility Methods for Plugin Management
The RegistryService exposes several utility methods for interacting with the plugin index:
Checking Plugin Registration Status
The has(plugin) method (lines 57‑60) provides a boolean check for registration status by verifying this._internal.has(key) after resolving the plugin to its callback key.
Retrieving and Deleting Plugin Runtimes
get(plugin)retrieves the runtime entry viathis._internal.get(key)(lines 52‑55)delete(plugin)removes a plugin and disposes all associated fibers by iterating overruntime.fibersand callingdispose()on each (lines 62‑71)sizereturns the current count of distinct plugins viathis._internal.size(lines 40‑42)
Automatic Cleanup and Lifecycle Management
The registry implements automatic garbage collection through fiber disposal callbacks. When a fiber finishes execution—either through explicit disposal or framework reload cycles—it removes itself from the runtime's fibers array. If the array becomes empty, RegistryService.delete() is automatically invoked (see the dispose callback in packages/core/src/fiber.ts lines 81‑86).
This mechanism ensures that the registry never holds stale references. The core test suite validates this behavior in packages/core/tests/plugin.spec.ts (lines 86‑112), specifically verifying that disposing a top-level fiber clears all child registrations.
Practical Examples of Plugin Tracking in Cordis
To interact with the registry plugin tracking mechanism programmatically:
// Register a plugin and inspect its runtime
const fiber = await ctx.plugin(myPlugin, { foo: 'bar' });
const runtime = ctx.registry.get(myPlugin);
console.log('Active fibers:', runtime.fibers.length);
console.log('Callback reference:', runtime.callback);
For introspection and manual lifecycle management:
// Check registry state
console.log('Total plugins loaded:', ctx.registry.size);
// Enumerate all registered plugins
for (const [callback, runtime] of ctx.registry.entries()) {
console.log(`Plugin has ${runtime.fibers.length} active fibers`);
}
// Manually unload a plugin (disposes all fibers)
await ctx.registry.delete(myPlugin);
In testing scenarios, you can verify tracking behavior:
import { expect } from 'vitest';
const ctx = new Context();
await ctx.plugin(async (c) => c.on('event', () => {}));
expect(ctx.registry.size).toBe(1);
const [runtime] = ctx.registry.values();
await ctx.registry.delete(runtime.callback);
expect(ctx.registry.size).toBe(0);
Integration with Hot-Module Replacement
The registry plugin tracking mechanism enables advanced features like Hot-Module Replacement (HMR). The HMR package (packages/hmr/src/index.ts) queries ctx.registry.get(plugin) to retrieve the current runtime before swapping code, ensuring that existing fibers and their states are preserved during reloads. Similarly, the loader utilities (packages/loader/src/index.ts) use ctx.registry.has(plugin) to determine whether a plugin requires fresh initialization or should reuse existing runtime state.
Summary
- The RegistryService maintains a private
_internalMap that associates plugin callbacks with runtime records containing fiber arrays and configuration metadata. - Plugin registration flows through resolution, runtime creation, fiber instantiation, and active tracking phases.
- The registry provides constant-time lookups via
get(),has(), anddelete()methods, withsizereflecting distinct plugin counts. - Automatic cleanup triggers when fiber arrays empty, preventing memory leaks and stale references.
- The mechanism supports HMR, dependency injection, and introspection throughout the Cordis framework.
Frequently Asked Questions
How does Cordis track multiple instances of the same plugin?
Cordis tracks multiple instances through the runtime's fibers array. When you register a plugin multiple times with different configurations, each invocation creates a new Fiber that gets pushed to runtime.fibers (as implemented in packages/core/src/fiber.ts lines 71‑72). The runtime record is shared (stored once in _internal), but the fiber list grows to reflect all active instances. When any fiber disposes, it removes itself from this array; only when the array is empty does the registry delete the entire runtime entry.
What happens to the registry when a plugin fiber is disposed?
When a fiber disposes, it executes a cleanup callback that removes itself from its runtime's fibers list. If this removal leaves the array empty, the registry automatically invokes delete() on the plugin's callback, purging the runtime from _internal (see packages/core/src/fiber.ts lines 81‑86). This ensures that the Cordis registry plugin tracking mechanism never retains references to dead plugins, enabling proper garbage collection.
How can I check if a plugin is already registered in a Cordis context?
Use the ctx.registry.has(plugin) method, which resolves the plugin to its callback key and checks this._internal.has(key) (lines 57‑60). This returns true if the plugin exists in the registry's internal Map, regardless of how many fibers are currently active. For more detailed inspection, ctx.registry.get(plugin) returns the full runtime object including the fiber count and configuration schema.
What is the relationship between RegistryService and Fiber in Cordis?
RegistryService and Fiber form a bidirectional tracking relationship. The registry creates fibers during plugin instantiation and stores them in runtime.fibers, while each fiber maintains a reference to its runtime for lifecycle management. When RegistryService.delete() is called, it iterates through runtime.fibers to dispose each fiber (lines 62‑71). Conversely, when a fiber disposes itself, it notifies the registry to perform cleanup if it was the last active fiber for that plugin.
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 →