# How Cordis RegistryService Manages Plugin Registration and Tracking

> Learn how Cordis RegistryService manages plugin registration and tracking using an internal map for precise lifecycle management and metadata. Explore the Cordiverse repository for details.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: how-to-guide
- Published: 2026-09-12

---

**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`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts) of the [cordiverse/cordis](https://github.com/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`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts), the `RegistryService` class initializes two critical private properties:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
// Remove specific plugin and cleanup all its fibers
ctx.registry.delete(greetingPlugin)

```

## Summary

- **Cordis RegistryService** centralizes plugin tracking in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/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`](https://github.com/cordiverse/cordis/blob/main/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.