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 Plugin interface with an apply method
  • Runtime: Holds metadata (name, callback, optional Config) and manages a DisposableList of 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 the Runtime object for a specific plugin callback
  • has(plugin): Boolean check for registration status
  • delete(plugin): Removes the runtime and disposes all associated fibers
  • keys() / 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.ts using a Map that associates callback functions with Runtime metadata objects.
  • Each plugin execution creates a Fiber instance with a unique ID, tracked within the runtime's DisposableList for lifecycle management.
  • The plugin() method normalizes inputs through resolve(), 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:

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 →