# How Cordis Plugin Runtime Manages Fibers and Disposal: A Deep Dive into the Lifecycle

> Discover how Cordis plugin runtime manages fibers and disposal using an internal effect system. Learn about cleanup routines, error isolation, and preventing resource leaks.

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

---

**Cordis orchestrates plugin lifecycles through Fiber instances that collect disposables via an internal effect system, executing cleanup routines in reverse order during unload or hot-module-replacement while isolating errors to prevent resource leaks.**

Cordis is a modern plugin framework where every plugin executes within an isolated runtime context called a **fiber**. Understanding how the Cordis plugin runtime manages fibers and disposal reveals the architectural patterns that enable hot reloading, deterministic cleanup, and robust error isolation. This analysis examines the actual source implementation to explain how fibers are created, how they track resources, and how they guarantee cleanup even during complex reload scenarios.

## Fiber Creation and Plugin Registration

The lifecycle begins in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts) when `RegistryService.plugin` registers a new plugin. This method serves as the primary entry point for fiber instantiation.

```ts
// packages/core/src/registry.ts
const fiber = new Fiber(this.ctx, config, Inject.resolve(plugin.inject), runtime, getOuterStack)

```

The constructor resolves the plugin's injection dependencies and creates a `Fiber` instance that immediately registers itself within its parent runtime. Inside [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts), the constructor establishes the disposal mechanism:

```ts
// packages/core/src/fiber.ts
this.dispose = parent.fiber.effect(() => {
    const remove = runtime.fibers.push(this)
    // ...
    return async () => { /* cleanup logic */ }
}, 'ctx.plugin()')

```

This effect adds the fiber to `runtime.fibers`—a `DisposableList<Fiber>`—and returns an async disposal function that will later handle teardown. Each fiber maintains its own **child context** (`ctx`) that inherits from the parent, creating an isolated execution environment for the plugin's callback.

## The Effect System and Disposable Collection

Cordis implements disposal through an **effect system** where plugins declare resources via `ctx.effect()`. Inside `Fiber.effect` in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts), the framework wraps each callback in a runner that captures returned disposables.

```ts
// packages/core/src/fiber.ts
const disposables: Disposable[] = []
const runner: EffectRunner<boolean> = {
  execute,
  epoch: true,
  collect: (dispose) => {
    disposables.push(dispose)
    this._disposables.delete(dispose)
    if (dispose[symbols.effect]) meta.children.push(dispose[symbols.effect])
  },
  // ...
}
let task = this._execute(runner)

```

The `_execute` method interprets the callback's return value dynamically. If the callback returns a **function**, Cordis treats it as a disposal routine. If it returns an **iterable** or **async iterable**, each yielded value is collected as a disposable. This flexible design supports multiple return shapes—`Disposable`, `Iterable<Disposable>`, or `Promise<Disposable>`—while storing everything in `this._disposables` for later cleanup.

## Unloading and Disposal Lifecycle

When a fiber stops—whether through manual disposal, deletion, or hot-module-replacement—the disposal sequence activates. The fiber first waits for any pending operations via the `inertia` property, ensuring no race conditions during teardown.

The `dispose` effect created in the constructor triggers `_unload`, defined in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts):

```ts
// packages/core/src/fiber.ts (lines 39-60)
private async _unload() {
  await Promise.all(this._disposables.clear().map(async (dispose) => {
    try {
      await composeError(async (info) => {
        await Promise.resolve()
        info.error = new Error()
        await dispose()
      }, this._runner.getOuterStack)
    } catch (reason) {
      this.ctx.logger.error(reason)
    }
  }))
  this.store = undefined
  // ...
}

```

**Disposables execute in reverse order** (LIFO) because `this._disposables.clear()` returns them in reverse insertion order. Each disposal runs through `composeError` from [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts), which enriches thrown errors with merged stack traces for debugging. Importantly, errors during disposal are **logged but not thrown**, ensuring that one failing cleanup routine does not prevent others from executing.

## Hot Reload and Epoch Management

Cordis supports dynamic configuration changes without full restarts through the `_reload` mechanism. When configuration updates occur, `Fiber._setEpoch` triggers `_reload`:

```ts
// packages/core/src/fiber.ts (lines 17-33)
private async _reload() {
  this.store = { ...this._store }
  const oldEpoch = this._runner.epoch
  try {
    await Promise.resolve()
    await this._execute(this._runner)
  } catch (reason) {
    this.ctx.logger.error(reason)
    this._error = reason
    this._runner.epoch = INACTIVE
  }
}

```

The method snapshots the current store, then re-executes the effect runner to create fresh disposables. If reload fails, the fiber transitions to a `FAILED` state and stores the error for later retrieval via `await fiber`. During hot-module-replacement (HMR), Cordis simply calls `fiber.restart()`, which sets the epoch to `INACTIVE` and triggers this reload flow, preserving the fiber's identity while replacing its implementation.

## Practical API for Plugin Developers

Plugin authors interact with this system through the public `Context` API. To register a plugin with automatic disposal:

```ts
// packages/create/src/index.ts example pattern
import { Context } from 'cordis'

export default function myPlugin(ctx: Context, config: { greeting: string }) {
  ctx.effect(() => {
    const timer = setInterval(() => console.log(config.greeting), 1000)
    return () => clearInterval(timer)  // disposable cleanup
  }, 'greeting-timer')
}

```

`ctx.plugin(myPlugin, { greeting: 'Hello' })` creates a new fiber and returns a promise-like object resolving to the fiber instance. To manually trigger disposal:

```ts
const fiber = await ctx.plugin(myPlugin, { greeting: 'Hi' })
await fiber.dispose()  // triggers unload, runs all disposables

```

The `dispose` method waits for any ongoing reload (checking `this.inertia`) before finalizing, ensuring consistent state transitions.

## Summary

- **Fiber instantiation** occurs through `RegistryService.plugin` in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts), which creates a Fiber that registers itself in `runtime.fibers`.
- **Effect collection** happens via `Fiber.effect` in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts), capturing disposables in `this._disposables` regardless of whether they return as functions, iterables, or promises.
- **Disposal order** is strictly LIFO (reverse insertion), executed by `_unload`, with each disposable wrapped in `composeError` for enhanced stack traces.
- **Error isolation** ensures disposal failures are logged but do not abort the cleanup sequence, preventing resource leaks.
- **Hot reloading** uses `_reload` to snapshot state and recreate disposables without destroying the fiber identity, enabling HMR workflows.

## Frequently Asked Questions

### How does Cordis ensure resources are cleaned up when a plugin unloads?

Cordis tracks every resource through the `Fiber.effect` system, which stores disposables in an internal `_disposables` list. When the fiber unloads, the `_unload` method clears this list and invokes each disposable in reverse order. The disposal logic waits for pending work to complete via the `inertia` property, ensuring that asynchronous operations finish before cleanup begins.

### What happens to disposables when a plugin is reloaded?

During reload, triggered by `_reload` or `fiber.restart()`, the fiber snapshots its current state and re-executes the effect runner. This creates an entirely new set of disposables while the old ones are cleared and invoked. The fiber maintains its identity and context, but all resources are freshly allocated according to the new configuration or code changes.

### How does error handling work during fiber disposal?

Each disposable runs inside a `composeError` wrapper from [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts), which captures stack traces and merges them with the original execution context. If a disposable throws, Cordis catches the error, logs it via `this.ctx.logger.error`, and continues executing remaining disposables. This guarantees that one failing cleanup routine does not prevent others from running.

### Can plugin authors manually trigger fiber disposal?

Yes. The `fiber.dispose` method, which is the same effect created in the Fiber constructor, can be awaited to trigger full teardown. This method waits for any ongoing reload operations (checking `this.inertia`) before invoking `_unload`, making it safe to call even during active plugin transitions.