# How the Cordis Plugin Registry Handles Registration, Tracking, and Disposal

> Discover how the Cordis plugin registry manages registration, tracking, and disposal with a centralized RegistryService. Learn about runtime management and automatic cleanup.

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

---

**The Cordis plugin registry uses a centralized `RegistryService` class that maintains a private `Map` of plugin runtimes, assigns unique incremental identifiers, tracks active fibers per plugin, and automatically cleans up runtime entries when the last fiber is disposed.**

Cordis is a lightweight, modular plugin system for TypeScript applications. At its heart lies the **plugin registry**, which manages the complete lifecycle of every plugin—from initial registration through active tracking to final disposal. This article examines how `RegistryService` in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts) implements this orchestration.

---

## Registration: Creating Plugin Runtimes

When you register a plugin via `ctx.registry.plugin(...)`, the registry performs two core operations: it ensures a **runtime record** exists for that plugin and instantiates a new **fiber** to run it.

### Runtime Record Creation

The registry stores plugin state in a private `_internal` map typed as `Map<Function, Plugin.Runtime>`:

```ts
// packages/core/src/registry.ts
export class RegistryService {
  private _internal = new Map<Function, Plugin.Runtime>()

  plugin(callback: Plugin, config?: any) {
    let runtime = this._internal.get(callback)
    if (!runtime) {
      runtime = this.createRuntime(callback)
      this._internal.set(callback, runtime)
    }
    // ... fiber creation
  }
}

```

Each runtime receives a **unique identifier** through the `counter` getter, which increments a private `_counter` field on every access. This provides deterministic ordering for plugin operations.

### Fiber Instantiation

Every plugin registration creates a `Fiber` object (defined in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts)). The fiber represents the actual execution context of that plugin instance:

```ts
// Inside RegistryService.plugin()
const fiber = new Fiber(this.ctx, runtime, config)
await fiber.init()

```

The fiber immediately registers itself with its runtime:

```ts
// packages/core/src/fiber.ts
export class Fiber {
  constructor(...) {
    const remove = runtime.fibers.push(this)
    this.disposers.push(remove)
  }
}

```

Here, `runtime.fibers` is a `DisposableList<Fiber>`—a specialized collection that returns a disposal function (`remove`) when items are pushed.

---

## Tracking: Monitoring Active Plugins

The registry exposes a complete inspection API that operates on the internal `_internal` map. This allows applications to query plugin state at any time.

### Registry Inspection Methods

| Method | Purpose |
|--------|---------|
| `keys()` | Iterate over registered plugin constructors |
| `values()` | Iterate over runtime records |
| `entries()` | Iterate over `[plugin, runtime]` pairs |
| `forEach(fn)` | Execute callback for each entry |
| `size` | Total number of active plugin runtimes |
| `has(plugin)` | Check if a plugin has an active runtime |
| `get(plugin)` | Retrieve the runtime for a specific plugin |

These methods enable runtime introspection:

```ts
// Check registration status
console.log('Active plugins:', ctx.registry.size)
console.log('Has myPlugin?', ctx.registry.has(myPlugin))

// Iterate over all runtimes
ctx.registry.forEach((runtime, plugin) => {
  console.log(plugin.name, 'has', runtime.fibers.length, 'active fibers')
})

```

### Per-Plugin Fiber Tracking

Each runtime maintains its own `fibers` list, enabling fine-grained tracking of plugin instances:

```ts
// Runtime structure (simplified)
interface Plugin.Runtime {
  callback: Plugin
  fibers: DisposableList<Fiber>
  // ... other metadata
}

```

This design supports **multiple concurrent instances** of the same plugin—each gets its own fiber, all tracked under a single runtime entry.

---

## Disposal: Cleaning Up Plugin Resources

The disposal system operates bidirectionally: fibers can dispose themselves (triggering registry cleanup), or the registry can forcibly dispose all fibers for a plugin.

### Automatic Fiber-Triggered Cleanup

When a fiber's `dispose()` method is called, it executes a coordinated cleanup sequence:

```ts
// packages/core/src/fiber.ts
async dispose() {
  // 1. Execute all registered disposers (effects, listeners, etc.)
  await Promise.all(this.disposers.map(d => d()))
  
  // 2. Remove self from runtime's fiber list
  if (this.ctx.registry.has(runtime.callback)) {
    remove()
    
    // 3. If no fibers remain, delete the runtime from registry
    if (!runtime.fibers.length) {
      this.ctx.registry.delete(runtime.callback)
    }
  }
}

```

This **reference-counting approach** ensures that plugin runtimes persist only while active fibers exist.

### Registry-Forced Disposal via `delete()`

Applications can also trigger disposal externally through `RegistryService.delete()`:

```ts
// packages/core/src/registry.ts
delete(plugin: Plugin): boolean {
  const runtime = this.resolve(plugin)
  if (!runtime) return false
  
  // Dispose every fiber synchronously
  for (const fiber of runtime.fibers) {
    fiber.dispose()
  }
  
  // Remove runtime from internal map
  return this._internal.delete(runtime.callback)
}

```

Key characteristics of this method:

- **Eager execution**: All fibers are disposed immediately via synchronous iteration
- **Complete cleanup**: The runtime entry is removed from `_internal` regardless of fiber disposal success
- **Boolean return**: Indicates whether a runtime was actually found and removed

---

## Complete Lifecycle Example

The following example demonstrates the full registration-to-disposal flow:

```ts
import { Context } from '@cordis/core'

const ctx = new Context()

// Step 1: Register a plugin
const myPlugin = (ctx: Context, config: { greeting: string }) => {
  ctx.on('ready', () => {
    console.log(config.greeting)
  })
  return () => console.log('Plugin disposed')
}

await ctx.registry.plugin(myPlugin, { greeting: 'Hello, Cordis!' })
// Runtime created, fiber instantiated and initialized

// Step 2: Inspect registry state
console.log(ctx.registry.size)           // → 1
console.log(ctx.registry.get(myPlugin))  // → Plugin.Runtime { fibers: [Fiber] }

// Step 3: Register same plugin again (creates second fiber)
await ctx.registry.plugin(myPlugin, { greeting: 'Second instance' })
console.log(ctx.registry.get(myPlugin)!.fibers.length)  // → 2

// Step 4: Dispose one fiber manually
const runtime = ctx.registry.get(myPlugin)!
const firstFiber = runtime.fibers[0]
await firstFiber.dispose()
console.log(runtime.fibers.length)       // → 1 (runtime still exists)

// Step 5: Dispose remaining fiber (triggers automatic runtime deletion)
await runtime.fibers[0].dispose()
console.log(ctx.registry.has(myPlugin))  // → false
console.log(ctx.registry.size)           // → 0

// Alternative: Force disposal of all fibers at once
await ctx.registry.plugin(myPlugin, { greeting: 'Temporary' })
ctx.registry.delete(myPlugin)            // Disposes fiber, removes runtime

```

---

## Implementation Files

| File | Responsibility |
|------|--------------|
| [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts) | `RegistryService` class: `_internal` map, `plugin()`, `delete()`, `resolve()`, inspection API |
| [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts) | `Fiber` class: instantiation, `dispose()`, integration with runtime fiber lists |
| [`packages/core/src/context.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/context.ts) | Binds registry service to context instances |
| [`packages/core/src/reflect.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/reflect.ts) | Mixin definitions exposing registry methods on `Context` |

---

## Summary

- **Registration** creates a `Plugin.Runtime` on first use, assigns a unique counter ID, and instantiates a `Fiber` that self-registers with the runtime
- **Tracking** occurs through the `RegistryService` inspection API and per-runtime `fibers` lists that act as reference counters
- **Disposal** supports both fiber-initiated cleanup (automatic runtime deletion when `fibers.length` reaches zero) and registry-forced disposal via `delete()` which iterates and disposes all fibers
- The **private `_internal` map** serves as the single source of truth for plugin lifetimes, with automatic garbage collection of empty runtimes

---

## Frequently Asked Questions

### How does Cordis prevent memory leaks from disposed plugins?

The registry implements **automatic reference counting** through the `fibers` list. When `Fiber.dispose()` completes, it removes itself from `runtime.fibers`; if no fibers remain, `this.ctx.registry.delete(runtime.callback)` executes immediately. This guarantees that plugin runtimes cannot outlive their active instances, and the private `_internal` map cannot accumulate orphaned entries.

### Can multiple instances of the same plugin run simultaneously?

Yes. Each call to `ctx.registry.plugin()` creates a new `Fiber` appended to the existing runtime's `fibers` list. The runtime persists until **all** fibers are disposed, enabling patterns like plugin reentrancy or scoped sub-instances with independent configurations.

### What happens if I call `registry.delete()` while fibers are still active?

`RegistryService.delete()` synchronously iterates `runtime.fibers` and calls `fiber.dispose()` on each. This triggers the normal disposal sequence for every active instance, ensuring all resources are released before the runtime entry is removed from `_internal`. The method returns `false` if the plugin had no registered runtime.

### Where is the registry counter used, and can it overflow?

The `_counter` field increments via the `counter` getter each time a runtime requests an ID. According to the Cordis source code, this provides sequential identifiers for debugging and ordering purposes. Given JavaScript's `Number.MAX_SAFE_INTEGER` (9,007,199,254,740,991), overflow is practically impossible in real-world usage.