How to Debug Cordis: Fiber-Based Debugging and Logging Strategies
Enable debug logging via ctx.intercept('logger', { level: 3 }), inspect fiber state at ctx.fiber, and wrap effects with labeled disposables to trace plugin lifecycles.
Cordis is a fiber-based plugin framework that isolates contexts and tracks effects through a sophisticated lifecycle system. Learning how to debug Cordis effectively requires understanding its core architecture—specifically how the Context, Fiber, and LoggerService interact to manage plugin states and errors. This guide provides practical strategies for tracing issues within the cordiverse/cordis codebase using actual source implementations from the core and loader packages.
Understanding the Cordis Debugging Architecture
Before setting breakpoints, you need to understand four key components that handle state isolation, lifecycle management, and message routing.
Context and Service Isolation
The Context object in packages/core/src/context.ts (lines 9‑78) serves as the central hub for services including events, logger, reflect, and registry. It creates a proxy via ReflectService.handler that intercepts property access, enabling shadow isolation between plugins. Two critical methods for debugging are:
isolate(name, label?)– Creates a new isolated context where a symbol is attached tosymbols.isolate(lines 65‑69). This allows you to sandbox specific service instances.intercept(name, config)– Clones the intercept map to override service configurations on a per-plugin basis (lines 71‑77). This is the primary mechanism for adjusting log levels without affecting the global application.
Fiber Lifecycle and State Tracking
Each plugin instance runs inside a Fiber, defined in packages/core/src/fiber.ts (lines 78‑166). The fiber manages the plugin lifecycle through state transitions and effect registration:
- Instantiation – When a plugin loads, the fiber constructor (lines 22‑36) initializes a
_runnerobject holding the current epoch and anexecutefunction that invokes the plugin’s callback. - State Machine – The fiber transitions through
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSEDvia_setEpoch(lines 99‑115). These changes are emitted viactx.emit('internal/status', …)(line 60), allowing you to listen for lifecycle events. - Error Capture – Errors are stored in
fiber._errorand propagated to the logger (lines 174‑177). ThecomposeErrorutility (line 30) wraps effect execution and attaches an outer stack trace for easier debugging.
LoggerService and Message Buffering
The LoggerService in packages/core/src/logger.ts (lines 12‑46) creates per-plugin Logger objects that buffer and forward messages to registered exporters. Key implementation details include:
- Configuration Resolution –
_resolveConfig()merges intercept configurations from the current context chain (lines 14‑24), allowing specific plugins to override global log levels. - Message Structure – When you call
logger.debug(), the service builds aMessageobject containing a sequence number (sn), timestamp (ts), and call location (lines 38‑45). - Level Filtering – Exporters iterate through registered handlers, and each exporter’s
levelproperty determines if the message is emitted (lines 41‑44). The service supports standard levels:error,warn,info, anddebug(level 3).
Dynamic Resolution with createResolve
For module-related debugging, packages/loader/src/resolve.ts (lines 31‑63) provides the createResolve helper. This function determines the nearest package.json directory and writes a hidden .cordis/resolve.mjs file that uses import.meta.resolve to handle specifiers relative to the plugin’s scope. This ensures consistent resolution during hot-module-replacement (HMR) cycles.
Practical Debugging Strategies
1. Enable Debug Logging for Specific Plugins
Instead of flooding your console with global debug output, use ctx.intercept to target specific plugins:
// Set debug level (3) for this plugin only
ctx.intercept('logger', { level: 3 })
The LoggerService._resolveConfig() method merges this intercept configuration with the global context chain, ensuring that logger.debug() calls appear in your output while other plugins remain at their default levels.
2. Inspect Fiber State at Runtime
Access the fiber instance through ctx.fiber to examine the internal state machine:
const fiber = ctx.fiber
console.log('Current state:', fiber.state) // FiberState enum value
console.log('Captured error:', fiber._error) // Error object or null
console.log('Active effects:', fiber.getEffects())
The state property reflects the current lifecycle phase (LOADING, ACTIVE, etc.) as updated by _updateState (lines 55‑63). Checking _error immediately after a plugin failure reveals the captured exception before it propagates.
3. Track Effect Lifecycles with Labels
Effects registered via Fiber.effect() (lines 75‑84 and 120‑127) return disposables that include EffectMeta metadata. Wrap critical code with labeled effects to trace hierarchical relationships:
ctx.effect(() => {
// Your initialization code here
return () => {
// Cleanup logic
}
}, 'database-connection')
If an unhandled rejection occurs, the fiber logs the error with the effect label attached, making it easier to identify which disposable failed during the UNLOADING phase.
4. Verify Module Resolution Paths
When debugging import failures or HMR issues, use the resolver directly to confirm paths resolve correctly within the plugin’s scope:
import { createResolve } from '@cordis/loader'
const resolve = await createResolve(import.meta.url)
const resolvedPath = resolve('./config/schema.ts')
console.log('Resolved to:', resolvedPath)
This helper respects the plugin’s package.json boundary and ensures that relative specifiers resolve consistently across HMR cycles.
5. Capture Full Stack Traces from Effect Errors
Errors thrown inside effects are processed by composeError, which calls buildOuterStack from the utilities to append an outer trace. When catching errors, inspect the full error.stack property:
try {
await someEffect()
} catch (err) {
console.error(err.stack) // Contains both inner and outer stack traces
}
This enriched trace shows exactly which effect in the fiber hierarchy triggered the failure.
Debug Code Examples
Enable Verbose Logging for a Single Plugin
import { Context } from 'cordis'
export function apply(ctx: Context) {
// Isolate logger config to debug level for this scope only
ctx.intercept('logger', { name: 'data-processor', level: 3 })
const logger = ctx.logger('data-processor')
logger.debug('Processing batch started')
logger.debug('Configuration loaded:', ctx.config)
}
Inspect Fiber During Plugin Initialization
import { Context } from 'cordis'
export function apply(ctx: Context) {
ctx.effect(() => {
const fiber = ctx.fiber
console.log('--- Fiber Debug Snapshot ---')
console.log('State:', fiber.state) // e.g., FiberState.LOADING
console.log('UID:', fiber.uid)
console.log('Epoch:', fiber._runner?.epoch)
// Return cleanup function
return () => console.log('Effect disposed')
}, 'debug-snapshot')
}
Resolve Modules Within Plugin Scope
import { Context } from 'cordis'
import { createResolve } from '@cordis/loader'
export async function apply(ctx: Context) {
const resolve = await createResolve(import.meta.url)
if (resolve) {
const utilsPath = resolve('./utils/helpers')
ctx.logger.debug('Resolved utils path:', utilsPath)
// Dynamic import using resolved path
const helpers = await import(utilsPath)
}
}
Summary
Debugging Cordis effectively relies on understanding its fiber-based architecture and leveraging its built-in instrumentation:
- Use
ctx.intercept('logger', { level: 3 })to enable debug output for specific plugins without affecting global log levels. - Access
ctx.fiberto inspect the current lifecycle state (LOADING,ACTIVE, etc.) and check_errorfor captured exceptions. - Label your effects with
ctx.effect(callback, 'label')to trace which disposables fail during cleanup. - Utilize
createResolvefrom@cordis/loaderto verify that dynamic imports resolve correctly within the plugin’s package scope. - Inspect
error.stackon caught exceptions to view the full trace including outer stacks added bycomposeError.
Frequently Asked Questions
How do I enable debug logging for only one plugin in Cordis?
Use the ctx.intercept() method to override the logger configuration for that specific context scope. Pass { level: 3 } to set the level to DEBUG (where 0=error, 1=warn, 2=info, 3=debug). The LoggerService._resolveConfig() method in packages/core/src/logger.ts merges this intercept with the global configuration, ensuring only that plugin emits debug messages.
What information does ctx.fiber.state provide?
The state property returns a FiberState enum value indicating the plugin’s current lifecycle phase: PENDING, LOADING, ACTIVE, UNLOADING, or DISPOSED. This state is updated internally by _setEpoch in packages/core/src/fiber.ts and emitted via the internal/status event, allowing you to track exactly when a plugin finishes initialization or begins teardown.
How can I identify which effect is causing a disposal error?
Wrap your effect code with a descriptive label as the second argument to ctx.effect(). If the effect throws during execution or cleanup, Cordis logs the error with this label attached to the EffectMeta metadata. You can also call fiber.getEffects() to retrieve the list of active effect disposables and their associated metadata.
Why are my dynamic imports failing in Cordis plugins?
Dynamic imports may fail if they resolve outside the plugin’s package scope or if the specifier is incorrect relative to the source file. Use createResolve(import.meta.url) from @cordis/loader to obtain a resolver function that respects the nearest package.json boundary. This ensures consistent module resolution during both normal execution and HMR reloads.
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 →