How Cordis Interceptors Modify Plugin Behavior at Runtime
Cordis interceptors alter plugin behavior at runtime by creating prototype-based configuration chains that services traverse to merge settings, allowing dynamic overrides without modifying source code.
Cordis, the modular plugin framework maintained in the cordiverse/cordis repository, provides a sophisticated interceptor mechanism that enables runtime modification of service configurations. This system allows developers to customize plugin behavior—such as logging levels or reflection settings—on a per-context basis without altering the underlying plugin code. Understanding how Cordis interceptors work requires examining the prototype-chain architecture in the core context implementation and the service-level configuration merging logic.
The Core Mechanism Behind Context.intercept
At the heart of Cordis interceptor functionality lies the Context.intercept method defined in packages/core/src/context.ts (lines 71-77). When invoked as ctx.intercept(name, config), the system creates a new intercept map that prototypes the existing this[Context.intercept] object.
Prototype Chain Architecture
The implementation uses Object.setPrototypeOf (or equivalent prototype linkage) to chain configuration objects. When you call intercept(name, config), Cordis:
- Creates a new object inheriting from the current intercept map
- Stores the supplied
configunder the specifiednamekey - Attaches this new map to a fresh
Contextinstance via theextendmethod
This produces a shadow context where the interceptor chain follows the pattern: newIntercept → oldIntercept → …. Services later traverse this chain to aggregate configurations, with each link in the chain representing a layer of potential overrides.
Runtime Service Resolution
Services that support interception walk the prototype chain at runtime to resolve their final configuration. For each service name, implementations look for matching entries on ctx[Context.intercept] and merge intercepted configurations into their default options.
LoggerService Configuration Merging
The LoggerService in packages/core/src/logger.ts (lines 215-221) demonstrates this traversal explicitly. The service iterates while the 'logger' key exists in the intercept map, collecting each configuration object into a configs array. After collecting all entries from the prototype chain, the service applies these configurations in order, ensuring that the most recent interceptor (closest to the execution context) takes precedence over earlier values.
ReflectService Pattern
Similarly, ReflectService in packages/core/src/reflect.ts (lines 52-58) follows an identical pattern for the 'reflect' key. This enables dynamic tweaks to reflection behavior without restarting the application or modifying the service instantiation code. Any custom service can implement equivalent logic by traversing ctx[Context.intercept] to detect and apply configuration overrides.
Hierarchical Context Isolation
The interceptor chain plays a crucial role in plugin isolation within the Cordis loader. In packages/loader/src/config/isolate.ts (lines 88-122), the loader creates fresh intercept objects for child plugin entries and explicitly links them to parent interceptors via Object.setPrototypeOf. This wiring ensures that isolated plugins inherit their parent's interceptor configurations while maintaining the ability to add or override their own settings, creating a hierarchical configuration tree that mirrors the plugin dependency structure.
Practical Cordis Interceptor Implementation
Developers interact with interceptors through the Context API, applying configurations that affect all subsequent service operations within that context scope.
Basic Runtime Interception
To modify a logger's name for a specific execution context:
import { Context } from 'cordis'
const ctx = new Context()
ctx.intercept('logger', { name: 'my-plugin' })
export default function myPlugin(ctx: Context) {
// The logger now uses the intercepted name 'my-plugin'
ctx.logger.info('plugin started')
}
Layered Contexts
Child contexts inherit parent interceptors but can override them:
const parent = new Context()
parent.intercept('logger', { name: 'parent' })
const child = parent.extend()
child.intercept('logger', { name: 'child' })
// Logging behavior demonstrates precedence
child.logger.info('from child') // → [child] from child
parent.logger.info('from parent') // → [parent] from parent
Loader-Level Configuration
Interceptors can be applied globally to all loaded plugins:
import { Loader } from '@cordis/loader'
const loader = new Loader({
intercept: { logger: { level: 'debug' } }
})
await loader.load('my-plugin')
Summary
- Cordis interceptors store hierarchical configurations using prototype chains initiated by
Context.interceptinpackages/core/src/context.ts(lines 71-77). - Services like
LoggerServicetraverse these chains at runtime to aggregate settings, with later interceptors taking precedence over earlier ones. - The loader leverages
Object.setPrototypeOfinpackages/loader/src/config/isolate.ts(lines 88-122) to ensure isolated plugins inherit parent interceptors while maintaining override capabilities. - This architecture enables hot-reloading, test isolation, and feature toggles without requiring modifications to plugin source code.
Frequently Asked Questions
What is the primary purpose of Cordis interceptors?
Cordis interceptors allow developers to modify service configurations (such as logger names or reflection settings) at runtime without changing plugin source code. They create a prototype chain of configuration objects that services traverse to determine final behavior.
How do Cordis interceptors handle configuration precedence?
When multiple interceptors target the same service, Cordis merges configurations by walking the prototype chain from the context outward. Configurations applied closer to the execution context override earlier values, as verified in packages/core/tests/logger.spec.ts (lines 91-103).
Can interceptors be used across parent and child contexts?
Yes. When extending a context via ctx.extend(), child contexts inherit the parent's interceptor chain through prototype linkage. The loader's isolation logic in packages/loader/src/config/isolate.ts (lines 88-122) explicitly wires child interceptors to their parents using Object.setPrototypeOf, ensuring hierarchical configuration inheritance.
Which Cordis services support interception out of the box?
The core framework provides interception support for LoggerService (packages/core/src/logger.ts) and ReflectService (packages/core/src/reflect.ts). Custom services can implement similar logic by traversing ctx[Context.intercept] to check for configuration overrides.
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 →