How the Cordis Shadow Configuration System Works: Complete Technical Guide
The Cordis shadow configuration system tracks caller context through a symbol-based shadow mechanism that wraps service access in transparent proxies, enabling isolated service state with preserved caller metadata.
Cordis implements a sophisticated shadow configuration system to maintain caller context isolation while allowing services to trace their origin. This architecture, found in the cordiverse/cordis repository, solves the fundamental problem of context leakage in dependency injection frameworks. Unlike simple context passing, Cordis uses symbol-based shadows and traceable proxies to ensure that services always know who created them without exposing their internal state.
What Is the Cordis Shadow System?
The shadow system consists of two interconnected mechanisms working together in packages/core/src/utils.ts. First, the shadow symbol acts as a hidden property key for storing caller references. Second, the createTraceable function builds proxies that temporarily inject this shadow during property access.
The core symbol is defined as a shared symbol to ensure cross-realm compatibility:
// packages/core/src/utils.ts
export const symbols = {
shadow: Symbol.for('cordis.shadow'),
// ... other symbols
}
This design choice means Symbol.for('cordis.shadow') returns identical values even across different module boundaries, critical for plugin ecosystems.
How Shadows Store Caller Metadata
When a service instantiates, its constructor receives a caller context. Cordis captures this relationship by storing the caller as a shadow on the service's own context object. The createShadow function performs this operation:
// packages/core/src/utils.ts#L41-L46
export function createShadow<T extends object>(origin: T, ctx: Context) {
return ctx.extend({
[symbols.shadow]: origin,
}) as T
}
The createShadow function extends the provided context with the symbols.shadow property pointing to the original caller. This allows services to access their creator through this.ctx[symbols.shadow] while keeping their own context tree separate.
Transparent Proxying with createTraceable
Property access on traceable objects triggers temporary shadow creation. The createTraceable proxy handler in packages/core/src/utils.ts#L57-L90 intercepts gets and sets to inject the correct caller context:
// Simplified flow from createTraceable implementation
const handler = {
get(target, prop, receiver) {
// Check if property has its own tracker
const value = Reflect.get(target, prop, receiver)
// Create temporary shadow with caller context preserved
const shadow = createShadow(ctx[symbols.shadow] ?? ctx, ctx)
// Return wrapped value that uses shadow for nested calls
return wrapWithShadow(value, shadow)
}
}
This proxy ensures that when you access service.someMethod(), the this context inside someMethod sees the original caller, not the service's internal shadow. Functions receive special treatment through createShadowMethod, which binds this to the shadow object.
Context Extension Preserves Shadows
The Context.extend method in packages/core/src/context.ts#L55-L63 maintains shadow continuity when creating derived contexts:
// From context.ts extend implementation
extend(meta: Context.Meta) {
const self = getTraceable(this, this)
const shadow = this[symbols.shadow]
const ctx = Object.assign(Object.create(self), {
[symbols.shadow]: shadow,
// ... other meta
})
return ctx
}
This preservation is essential for plugin composition. When a plugin extends a context, any existing shadow from upstream callers travels with it, maintaining the full ancestry chain.
Practical Service Implementation
A standard service automatically participates in the shadow system:
import { Context, Service, symbols } from '@cordis/core'
class Greeter extends Service {
constructor(ctx: Context) {
super(ctx, 'greeter')
}
hello() {
// Access the caller context through the shadow symbol
const caller = this.ctx[symbols.shadow] as Context
return `Hello from ${caller.config.name}`
}
}
// Usage: caller context is automatically preserved
const root = new Context({ name: 'main-app' })
const greeter = root.provide('greeter', Greeter)
greeter.hello() // "Hello from main-app"
The Service base class handles shadow registration during construction, making the pattern transparent to implementers.
Opting Out with noShadow Services
Services can disable shadow tracking by setting noShadow: true in their tracker configuration. This is useful for lightweight utilities that don't need caller tracing:
import { Context, Service, symbols } from '@cordis/core'
class Simple extends Service {
static inject = []
[Service.tracker] = {
property: 'ctx',
noShadow: true // Disables shadow creation
}
constructor(public ctx: Context) {}
checkShadow() {
// this.ctx[symbols.shadow] is undefined
return this.ctx[symbols.shadow]
}
}
The test suite in packages/core/tests/shadow.spec.ts#L54-L71 validates this behavior, confirming that noShadow services bypass the wrapping logic entirely.
Fiber Resolution Uses Shadows
The reflective fiber system in packages/core/src/reflect.ts#L82-L84 leverages shadows to determine execution context:
// From reflect.ts fiber resolution
const fiber = ctx[symbols.shadow] ?? ctx
This fallback pattern—shadow first, then direct context—ensures that fiber-bound operations always associate with the caller's runtime state, not the service's internal fiber tree. The shadow becomes the bridge between static service definition and dynamic execution context.
Shadow System Architecture Summary
| Component | Location | Responsibility |
|---|---|---|
symbols.shadow |
utils.ts#L49-L50 |
Global symbol for shadow property key |
createShadow |
utils.ts#L41-L46 |
Builds shadow-wrapped contexts |
createTraceable |
utils.ts#L57-L90 |
Proxy factory for transparent shadow injection |
Context.extend |
context.ts#L55-L63 |
Preserves shadows during context derivation |
| Fiber resolution | reflect.ts#L82-L84 |
Uses shadow for runtime context selection |
Summary
-
Cordis shadow configuration uses
Symbol.for('cordis.shadow')to store caller references without polluting the public context API. -
The
createShadowfunction inpackages/core/src/utils.tswraps contexts with caller metadata, enabling services to trace their origin. -
createTraceableproxies inject temporary shadows during property access, ensuring nested calls maintain correct caller context. -
noShadow: trueinService.trackerconfiguration opts out of shadow wrapping for lightweight services. -
Fiber resolution prefers shadows over direct contexts, binding runtime operations to caller state rather than service internals.
Frequently Asked Questions
What problem does the Cordis shadow system solve?
The shadow system prevents context leakage in dependency injection frameworks. Without shadows, services would expose their internal context tree to consumers, making it impossible to distinguish between the caller's state and the service's own configuration. Shadows maintain this boundary while still allowing services to access caller metadata when needed.
How do I access the caller context in my service?
Access this.ctx[symbols.shadow] where symbols is imported from @cordis/core. The shadow contains the context that originally instantiated or injected your service. Note that this returns undefined for services with noShadow: true or when accessed from root-level contexts without parents.
Can I disable shadow tracking for performance?
Yes. Set [Service.tracker] = { property: 'ctx', noShadow: true } in your service class. This bypasses createShadow and createTraceable wrapping, returning raw values directly. Use this only for services that never need caller introspection, as it breaks the symbols.shadow access pattern.
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 →