# Cordis Registry Plugin Tracking Mechanism: How Plugins Are Indexed and Managed

> Discover the Cordis registry plugin tracking mechanism. Learn how plugins are indexed and managed efficiently within the Cordis framework using the RegistryService for runtime records and internal map storage.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: internals
- Published: 2026-08-25

---

**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`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts) and centers on the `RegistryService` class. This service maintains a private **`_internal`** property (lines [25‑28](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L25-L28)) 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's `apply` method
- **`fibers`** – An array tracking every `Fiber` instance created for this plugin
- **`Config`** – 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](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L44-L50)) 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](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L99-L105)). 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](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L107-L108)). During fiber initialization, the instance registers itself with the runtime by executing `runtime.fibers.push(this)` (lines [71‑72](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts#L71-L72)), 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](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L57-L60)) 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 via `this._internal.get(key)` (lines [52‑55](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L52-L55))
- **`delete(plugin)`** removes a plugin and disposes all associated fibers by iterating over `runtime.fibers` and calling `dispose()` on each (lines [62‑71](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L62-L71))
- **`size`** returns the current count of distinct plugins via `this._internal.size` (lines [40‑42](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L40-L42))

## 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`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts) lines [81‑86](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts#L81-L86)).

This mechanism ensures that the registry never holds stale references. The core test suite validates this behavior in [`packages/core/tests/plugin.spec.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/tests/plugin.spec.ts) (lines [86‑112](https://github.com/cordiverse/cordis/blob/main/packages/core/tests/plugin.spec.ts#L86-L112)), 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:

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

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

```typescript
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`](https://github.com/cordiverse/cordis/blob/main/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`](https://github.com/cordiverse/cordis/blob/main/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 `_internal` Map 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()`, and `delete()` methods, with `size` reflecting 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`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts) lines [71‑72](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts#L71-L72)). 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`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts) lines [81‑86](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts#L81-L86)). 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](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L57-L60)). 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](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L62-L71)). Conversely, when a fiber disposes itself, it notifies the registry to perform cleanup if it was the last active fiber for that plugin.