How Cordis RegistryService Manages Plugin Registration and Tracking
Cordis RegistryService maintains a central _internal Map that associates plugin callback functions with Runtime objects containing metadata and active Fiber instances, enabling precise tracking and lifecycle management of every loaded plugin.
The RegistryService class, located in packages/core/src/registry.ts of the cordiverse/cordis repository, serves as the backbone for plugin management in the Cordis framework. It provides a systematic approach to Cordis RegistryService plugin registration and tracking by maintaining unique identifiers for each plugin instance and managing their execution contexts through a sophisticated fiber-based architecture.
Core Architecture: Runtime and Fiber
The registry implements a three-tier tracking system that separates plugin definitions from their executions:
- Plugin: Any function, class constructor, or object implementing the
Plugininterface with anapplymethod - Runtime: Holds metadata (
name,callback, optionalConfig) and manages aDisposableListof active Fiber instances - Fiber: The execution unit tracking configuration, injected dependencies, and disposal lifecycle for a specific plugin instance
Internal Data Structures
In packages/core/src/registry.ts, the RegistryService class initializes two critical private properties:
export class RegistryService {
private _counter = 0
private _internal = new Map<Function, Plugin.Runtime>()
// ...
}
The _counter provides unique IDs for plugin instances, while _internal maps plugin callback functions to their corresponding Runtime objects. The constructor stores a reference to the current Context to enable fiber creation and event emission:
constructor(public ctx: Context) {
defineProperty(this, symbols.tracker, {
property: 'ctx',
noShadow: true,
})
}
The Registration Workflow
Resolving Plugin Callbacks
Before registration, the service normalizes plugin values into callable functions through the resolve method:
resolve(plugin: Plugin): Function | undefined {
try {
if (typeof plugin === 'function') return plugin
if (isApplicable(plugin)) return plugin.apply
} catch {}
}
The isApplicable helper checks for objects containing an apply method, allowing both functional and object-based plugins.
Creating Plugin Instances
The primary entry point RegistryService.plugin handles the complete registration process:
plugin(plugin: Plugin, config?: any, getOuterStack = buildOuterStack()) {
const callback = this.resolve(plugin)
if (!callback) throw new Error('invalid plugin …')
this.ctx.fiber.assertActive()
// ① Retrieve or create the runtime entry
let runtime = this._internal.get(callback)
if (!runtime) {
let name = plugin.name
if (name === 'apply') name = undefined
runtime = { name, callback, fibers: new DisposableList(), Config: plugin.Config }
this._internal.set(callback, runtime)
}
// ② Create a new Fiber for this execution
const fiber = new Fiber(this.ctx, config, Inject.resolve(plugin.inject), runtime, getOuterStack)
const wrapped = Object.create(fiber) as Fiber & PromiseLike<Fiber>
wrapped.then = (onFulfilled, onRejected) => {
return fiber.await().then(onFulfilled, onRejected)
}
return wrapped
}
Step 1: If the plugin callback has never been registered, the method creates a new Runtime object containing the plugin name, callback reference, empty DisposableList for fibers, and optional Config schema, then stores it in _internal.
Step 2: A new Fiber instance receives the current context, configuration object, resolved injections, and the runtime reference. The fiber automatically registers its unique uid (derived from _counter) with the runtime's fiber list. The returned object behaves both as a Fiber and a Promise, allowing await on plugin completion.
Lifecycle Tracking and Cleanup
Tracking Active Executions
Each Runtime maintains its active executions through a DisposableList<Fiber>. When a fiber initializes, it pushes itself into the runtime's collection:
this.dispose = parent.fiber.effect(() => {
const remove = runtime.fibers.push(this)
// ...
}, 'ctx.plugin()')
Automatic Cleanup
The disposal mechanism ensures proper cleanup when a plugin is removed:
return async () => {
if (this.ctx.registry.has(runtime.callback)) {
remove()
if (!runtime.fibers.length) {
this.ctx.registry.delete(runtime.callback)
}
}
}
When a fiber disposes, it removes itself from the runtime's fiber list. If the list becomes empty, the entire runtime entry is purged from _internal via registry.delete, preventing memory leaks.
Manual Deletion
The delete method provides explicit cleanup by disposing all fibers associated with a plugin:
delete(plugin: Plugin) {
const key = this.resolve(plugin)
const runtime = key && this._internal.get(key)
if (!runtime) return
this._internal.delete(key)
for (const fiber of runtime.fibers) {
fiber.dispose()
}
return runtime
}
Querying the Registry
RegistryService exposes a Map-like interface for inspecting registered plugins:
get(plugin): Returns theRuntimeobject for a specific plugin callbackhas(plugin): Boolean check for registration statusdelete(plugin): Removes the runtime and disposes all associated fiberskeys()/values()/entries()/forEach(): Standard iteration methods over the internal registry
These methods all utilize the resolve helper to normalize plugin references before lookup.
Practical Usage Examples
Basic Plugin Registration
import { Context } from 'cordis'
function helloPlugin(ctx: Context) {
ctx.logger.info('Hello from plugin!')
}
const ctx = new Context()
await ctx.registry.plugin(helloPlugin)
Registration with Configuration and Injection
import { Context, Inject } from 'cordis'
interface Config {
greeting: string
}
function greetingPlugin(ctx: Context, config: Config) {
ctx.logger.info(`${config.greeting} from ${ctx.name}`)
}
// Register with configuration
await ctx.registry.plugin(greetingPlugin, { greeting: 'Hello' })
Using the Inject Decorator
import { Inject } from 'cordis'
class DatabaseService {
@Inject('logger')
private logger: any
connect() {
this.logger.info('Connecting to database...')
}
}
// Class-based plugin registration
await ctx.registry.plugin(DatabaseService)
Manual Plugin Removal
// Remove specific plugin and cleanup all its fibers
ctx.registry.delete(greetingPlugin)
Summary
- Cordis RegistryService centralizes plugin tracking in
packages/core/src/registry.tsusing aMapthat associates callback functions withRuntimemetadata objects. - Each plugin execution creates a Fiber instance with a unique ID, tracked within the runtime's
DisposableListfor lifecycle management. - The
plugin()method normalizes inputs throughresolve(), creates runtimes on-demand, and returns Promise-like fiber objects. - Automatic cleanup occurs when fibers dispose, removing empty runtimes from the registry to prevent memory leaks.
- The service provides Map-like querying methods (
get,has,delete) for inspecting and managing the plugin catalog.
Frequently Asked Questions
How does RegistryService handle different plugin types?
The resolve method in packages/core/src/registry.ts normalizes inputs by checking typeof plugin === 'function' for functional plugins or isApplicable(plugin) for objects with an apply method. Both types map to the same callback-based tracking system in the internal registry.
What happens when a plugin context is disposed?
When a fiber's context disposes, the effect cleanup callback removes the fiber from its runtime's DisposableList. If no fibers remain for that runtime, registry.delete automatically purges the runtime entry from _internal, ensuring complete cleanup of plugin metadata and instances.
Can I check if a plugin is already registered without triggering registration?
Yes. Use ctx.registry.has(plugin) to perform a boolean check. This method resolves the plugin to its callback function and checks for existence in the internal Map without creating new runtime entries or fiber instances.
What is the difference between Runtime and Fiber in the registry?
A Runtime represents the plugin definition itself—containing the callback function, name, Config schema, and a list of all active executions. A Fiber represents a single execution instance of that plugin with specific configuration, injections, and a unique UID. One Runtime can manage multiple Fibers if the same plugin is registered multiple times with different configurations.
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 →