Understanding the Epoch Mechanism in Cordis Fibers: How It Enables Plugin Composability
The epoch mechanism in Cordis Fibers is a lightweight generation-tracking system that assigns every active effect a unique identifier (string | boolean), automatically aborting stale asynchronous work and restarting effects when dependencies change, which enables safe, composable plugin architectures.
The cordiverse/cordis framework models each plugin as a lightweight execution unit called a fiber that can be started, stopped, and reloaded independently. Understanding the epoch mechanism in Cordis Fibers is essential for building reactive applications that manage complex dependency trees without race conditions or memory leaks. This mechanism operates through the EffectRunner class and provides deterministic lifecycle management for async effects.
What Is the Epoch Mechanism in Cordis Fibers?
At its core, the epoch mechanism functions as a version-tracking system for asynchronous operations. It provides a way to distinguish between different generations of a fiber's active effects, ensuring that only the current generation continues executing while previous iterations are safely discarded.
The Epoch as a Generation Identifier
An epoch is a simple identifier with the type string | boolean that is stored on the fiber's internal EffectRunner. According to the source code in packages/core/src/fiber.ts, the epoch property is declared at lines 72-76 within the EffectRunner class. This property records the current generation of the fiber's active effect, allowing the system to detect when a fiber transitions between active and inactive states or when its dependencies change.
When the epoch value changes, any previously started asynchronous work is considered obsolete. The previous generation's operations receive a signal to stop early, preventing them from interfering with the new execution context.
The INACTIVE State
The mechanism relies on a special constant called INACTIVE (defined at lines 146-148 in packages/core/src/fiber.ts). This constant represents the epoch value assigned to fibers that are not currently running. When a fiber is first created or when it is fully stopped, its runner initializes with INACTIVE as the epoch value, effectively marking it as dormant until explicitly activated.
How Epochs Drive Fiber Lifecycle Transitions
The transition between fiber states—whether starting fresh, reloading due to dependency changes, or shutting down—is governed by epoch manipulation. The system uses specific methods to compute and assign these values, triggering appropriate lifecycle hooks based on the transition type.
Initializing and Updating Epochs via _setEpoch
The _setEpoch method in packages/core/src/fiber.ts (lines 386-396) computes the appropriate epoch value for a fiber based on its current state and dependencies. This method evaluates three conditions:
- If the fiber is starting cleanly, it assigns an empty string
"" - If the fiber has dependencies, it generates a colon-separated list of dependent fiber IDs
- If a required dependency is missing, it assigns the
INACTIVEconstant
After computation, lines 399-402 assign this value to runner.epoch. The method then examines the transition at lines 405-411, triggering _reload if moving from INACTIVE to an active state, or _unload if transitioning to INACTIVE.
Epoch-Aware Async Execution
The central _execute method (lines 263-267) implements the actual safety check that makes epochs useful. Before invoking an effect, _execute captures the current epoch value as oldEpoch. Throughout the async iteration of the effect—whether using generators or promises—the runner repeatedly verifies that runner.epoch !== oldEpoch. If the epoch has changed during execution, the iteration aborts immediately.
This check guarantees that once a fiber's epoch updates (for example, because a dependency changed or restart() was called), any in-flight async work from the previous epoch is safely discarded before the new effect begins.
How the Epoch Mechanism Enables Composability
The epoch system directly supports Cordis's composable architecture by allowing fibers to depend on one another while maintaining isolated execution contexts. This prevents the "spaghetti state" common in plugin systems where cascading changes create unpredictable side effects.
Dependency Injection and Epoch Propagation
Fibers declare dependencies through the inject map in their configuration. When a dependency's epoch changes, the dependent fiber automatically receives a new epoch via the _refresh method. This propagation causes the dependent fiber's async effects to restart, picking up the new state from its dependencies.
Because the epoch change triggers a complete teardown of the old effect (via the _execute abort check) before starting the new one, the system ensures that fibers never operate with stale data from previous configurations.
Isolated Effect Lifecycles
Each effect executes within a scope tied to a distinct epoch identifier. This isolation means that nested or parallel effects within the same fiber do not interfere with each other—their lifecycles are cleanly separated by the epoch boundary. When composing multiple fibers, each reacts to configuration changes independently, and the epoch mechanism guarantees that these reactions occur in a deterministic order without race conditions.
Practical Implementation: Epoch-Aware Plugins
Below is a practical example demonstrating how fibers use epochs to manage long-running async operations. The ticker plugin runs a periodic loop that automatically stops when its epoch changes, while the controller plugin demonstrates how dependencies trigger epoch refreshes.
import { createApp } from '@cordis/create'
// Simple plugin that logs a message every second
export const ticker = {
name: 'ticker',
inject: {},
async callback(ctx) {
// Effect runs a periodic async loop scoped to the current epoch
ctx.fiber.effect(async function* () {
while (true) {
await new Promise(r => setTimeout(r, 1000))
ctx.logger.info('tick')
// The loop stops automatically if the fiber's epoch changes
// because _execute checks runner.epoch against the captured oldEpoch
}
})
},
}
// Plugin that depends on `ticker` and triggers epoch changes
export const controller = {
name: 'controller',
inject: { ticker: true },
async callback(ctx) {
ctx.fiber.effect(() => {
// When configuration changes, restart updates the epoch,
// causing ticker's async loop to abort and recreate
ctx.ticker?.on('configChanged', () => ctx.fiber.restart())
})
},
}
// Initialize the application
createApp({
plugins: [ticker, controller],
}).then(app => app.start())
In this implementation, the ticker plugin creates an async generator effect that yielding control back to the runner. The internal _execute method in packages/core/src/fiber.ts handles the epoch comparison on every iteration. When controller detects a configuration change and calls ctx.fiber.restart(), Cordis invokes _setEpoch with a new value, causing the old generator to abort at the next iteration check and a fresh effect to start with the updated context.
Summary
- The epoch is a
string | booleanidentifier stored in theEffectRunnerclass (packages/core/src/fiber.ts), tracking the generation of active effects. - The
_setEpochmethod computes new epoch values based on dependency states (empty string for clean starts, colon-separated IDs for dependencies, orINACTIVEfor missing requirements) and triggers_reloador_unloadtransitions. - The
_executemethod captures the epoch before invoking effects and continuously checksrunner.epoch !== oldEpoch, aborting stale asynchronous work immediately upon mismatch. - The
injectdependency map works with_refreshto propagate epoch changes through the fiber tree, enabling composable architectures where plugins restart automatically when their dependencies change. - Each effect runs in isolation tied to its specific epoch, preventing race conditions and ensuring deterministic lifecycle management across complex plugin compositions.
Frequently Asked Questions
What is an epoch in Cordis?
An epoch in Cordis is a generation identifier with the type string | boolean stored in the EffectRunner class at packages/core/src/fiber.ts (lines 72-76). It functions as a version tag for active effects, distinguishing between different execution generations of a fiber to ensure obsolete async work can be identified and terminated.
How does Cordis detect when to restart a fiber?
Cordis detects restart conditions through the _setEpoch method in packages/core/src/fiber.ts. When dependencies change via the inject map or when ctx.fiber.restart() is called, _setEpoch computes a new epoch value (lines 386-396) and compares it to the previous state. If the fiber was INACTIVE and becomes active, or vice versa, the system triggers _reload or _unload respectively (lines 405-411).
Why does the epoch prevent stale async operations?
The epoch prevents stale operations through the check implemented in the _execute method (lines 263-267). Before running an effect, _execute stores the current epoch as oldEpoch. During every step of async iteration, the runner verifies that runner.epoch !== oldEpoch. If the values differ—meaning a new epoch has started—the iteration aborts immediately, guaranteeing that only the current generation's code continues executing.
How does the epoch mechanism support plugin composition?
The epoch mechanism supports composition by allowing fibers to declare dependencies via the inject map. When a dependency's epoch changes, Cordis calls _refresh on dependent fibers, updating their epochs and triggering restarts. Because each effect is scoped to a specific epoch, nested and parallel effects do not interfere with each other, enabling developers to safely compose complex plugin trees where changes propagate deterministically without manual cleanup or race condition management.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →