# Cordis Dispose Mechanism for Fibers and Plugins: Complete Technical Guide

> Explore the Cordis dispose mechanism for fibers and plugins. Learn how this hierarchical system automatically cleans up resources and awaits async work on unload.

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

---

**Cordis implements a hierarchical disposal system where every plugin runs inside a lightweight Fiber that automatically cleans up resources, removes registry entries, and awaits pending async work when unloaded via `ctx.unload()`.**

In the `cordiverse/cordis` framework, robust plugin lifecycle management depends on a sophisticated **dispose mechanism** that treats every loaded plugin as a fiber—a lightweight execution context owning its own `Context`, configuration, and disposables. This architecture ensures that plugins can be dynamically loaded and unloaded at runtime without dangling references or memory leaks.

## How Fibers Model Plugin Execution Contexts

Every plugin registered in Cordis is encapsulated within a `Fiber` instance defined in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts). This fiber acts as the plugin’s runtime boundary, tracking its state, managing injection, and maintaining a `DisposableList` of cleanup functions. By isolating plugins into fibers, Cordis creates a tree-like hierarchy where parent fibers control the lifecycles of their children.

## Creating the Dispose Effect in the Fiber Constructor

When a plugin is registered, the `Fiber` constructor (lines 22-33) immediately establishes the disposal logic by calling `parent.fiber.effect()`. This attaches the disposal routine to the parent fiber’s lifecycle, ensuring that when a parent disposes, all child fibers clean up automatically.

The constructor registers the effect using this pattern:

```typescript
this.dispose = parent.fiber.effect(() => {
  // Cleanup logic executes here
}, 'ctx.plugin()')

```

This assignment at lines 70-100 creates a disposable effect that Cordis invokes when the plugin unloads or the parent fiber terminates.

## The Three-Stage Disposal Execution Process

When `ctx.unload()` triggers or a parent fiber disposes, the effect stored in `Fiber.dispose` executes a precise three-stage cleanup sequence:

### 1. Removing the Fiber from the Runtime List

The disposal effect first detaches the fiber from the runtime’s active tracking list. During construction, the fiber stores a removal callback: `const remove = runtime.fibers.push(this)`. When disposal triggers, calling `remove()` (lines 81-82) eliminates the fiber from `runtime.fibers` without leaving stale references.

### 2. Cleaning Up the Plugin Registry

If the disposed fiber represents the last active instance of its plugin, the mechanism cleans the registry entry. As implemented in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts) (lines 84-86), the code checks whether the fiber list is empty and then executes `this.ctx.registry.delete(runtime.callback)`. This step, defined in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts), ensures that plugin symbols become unreachable once all associated fibers terminate.

### 3. Resetting State and Awaiting Pending Work

Finally, the fiber resets its internal state by clearing the `uid`, emitting an `internal/plugin` event, and setting the epoch to the `INACTIVE` placeholder. Crucially, the disposal awaits any pending **inertia** promises (lines 88-98) to guarantee that all asynchronous operations complete before the fiber terminates, preventing mid-cleanup interruptions.

## Hierarchical Resource Cleanup

Because Cordis attaches the dispose effect to the **parent** fiber’s disposable list, the framework supports recursive teardown. Disposing a parent fiber automatically invokes the disposal effects of all descendant fibers, creating a stack-like cleanup pattern that propagates from the root context down to nested plugins.

All disposables registered via `ctx.effect()`, `ctx.once()`, or pushed directly onto `Fiber._disposables` are collected in a `DisposableList`. When the fiber disposes, Cordis invokes these functions in reverse order, mirroring the construction sequence and ensuring that dependent resources release before their consumers.

## Working with Disposables in Plugin Code

Plugins interact with the dispose mechanism by registering cleanup hooks. The framework supports two primary patterns: implementing the `symbols.dispose` method for class-based plugins, or using the `ctx.effect()` API to register functional cleanup routines.

Example implementation demonstrating both approaches:

```typescript
import { Context, symbols } from 'cordis'

export default class MyPlugin {
  [symbols.dispose] = () => {
    console.log('Class-level cleanup executed')
  }

  constructor(public ctx: Context) {
    ctx.effect(() => {
      const timer = setInterval(() => console.log('tick'), 1000)
      const connection = openDatabaseConnection()
      
      // Return disposal function
      return () => {
        clearInterval(timer)
        connection.close()
      }
    })
  }
}

```

The function returned to `ctx.effect()` automatically enters the fiber’s `DisposableList` and executes during the stage-three cleanup.

## Unloading Plugins at Runtime

To programmatically trigger the dispose mechanism, Cordis exposes `ctx.unload()` in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts). This method locates the plugin’s fiber and initiates the full disposal chain, including the three-stage process and hierarchical propagation.

Example usage:

```typescript
// Register the plugin
const fiber = ctx.plugin(MyPlugin, { option: true })

// Later, trigger complete cleanup
await ctx.unload(MyPlugin)  // Waits for inertia and all disposables

```

Calling `await ctx.unload()` ensures that the fiber’s disposal effect completes, including awaiting any pending inertia promises before returning control to the caller.

## Summary

- Every Cordis plugin executes inside a **Fiber** that functions as a disposable execution context with its own state and resource tracking.
- The `Fiber` constructor registers disposal effects via `parent.fiber.effect()`, linking child lifecycles to parent fibers for automatic cascading cleanup.
- Disposal executes three defined stages: removal from `runtime.fibers`, registry cleanup via `this.ctx.registry.delete()`, and state reset with inertia awaiting.
- The **DisposableList** manages cleanup functions in reverse order, ensuring that resources release in the correct dependency order.
- Hierarchical disposal propagates automatically through the fiber tree, allowing nested plugins to unload without manual intervention.

## Frequently Asked Questions

### What triggers the Cordis fiber dispose mechanism?

The dispose mechanism triggers when `ctx.unload()` is called on a plugin, when a parent fiber disposes (cascading to children), or during application shutdown. The effect registered in the `Fiber` constructor (lines 70-100 in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts)) runs automatically, executing the three-stage cleanup process.

### How does Cordis handle asynchronous cleanup during disposal?

The disposal process explicitly awaits pending **inertia** promises (lines 94-98 in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts)) before marking the fiber as inactive. This ensures that asynchronous operations like database writes or network requests complete before the fiber’s `uid` clears and the epoch sets to `INACTIVE`.

### Where does the plugin registry cleanup happen in the source code?

Registry cleanup occurs in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts) at lines 84-86. When the last fiber for a given runtime is removed, the code executes `this.ctx.registry.delete(runtime.callback)`, which removes the plugin’s callback from the registry defined in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts).

### Can a plugin register multiple cleanup hooks?

Yes. Plugins can define the `[symbols.dispose]` property for class-based cleanup, use `ctx.effect()` to register functional cleanup with automatic disposal, or push functions directly onto the fiber’s internal `_disposables` list. All mechanisms integrate into the same `DisposableList` that Cordis executes in reverse order during fiber teardown.