How the Cordis Context Proxy Handles Service Lookups: Fiber-Scoped Resolution Explained
The Cordis Context object is wrapped in a JavaScript Proxy that intercepts property access to perform hierarchical service lookups across fiber-scoped stores, combining symbolic isolation with accessor definitions to resolve dependencies lazily.
The Cordis framework implements a sophisticated dependency injection system where the Context class serves as the central registry for services. Understanding how the Context proxy handles service lookups is essential for developers building plugins and managing service lifecycles in Cordis applications. The lookup mechanism combines JavaScript Proxy traps with a fiber-based isolation model to enable dynamic, hierarchical service resolution.
Proxy Implementation in Context Creation
Every Context instance is immediately wrapped in a JavaScript Proxy upon instantiation. In packages/core/src/context.ts (lines 36‑48), the constructor creates the proxy using a handler defined in the ReflectService class. The actual proxy handler implementation resides in packages/core/src/reflect.ts (lines 63‑94), where the get trap defines the core lookup logic.
This architectural choice allows Cordis to intercept every property access on a context instance, enabling runtime resolution of services rather than static property assignment.
The Four-Stage Lookup Process
When you access a property on a Context instance (e.g., ctx.myService), the proxy's get trap executes a prioritized resolution sequence:
Stage 1: Special Property Bypass
The handler first checks if the property key is a symbol, reserved word, numeric string, or starts with an underscore (_). These special properties bypass the service lookup logic entirely and resolve directly via Reflect.get(target, prop), returning the raw value from the Context instance.
Stage 2: Own Property Resolution
If the property exists as an own property on the target Context object, the handler retrieves it and wraps the value using the internal getTraceable function. This ensures that direct context properties maintain proper tracking while avoiding the service registry overhead.
Stage 3: Registered Service Accessors
The ReflectService maintains a map of property definitions (props) that track registered services. If the requested property is declared as an accessor in this map, the handler invokes the accessor's get method directly. This allows for computed service properties that execute custom logic upon access.
Stage 4: Fiber-Scoped Hierarchical Resolution
The most complex stage occurs when the property is not found in the previous stages. The handler checks if the current fiber is active (ctx.fiber.runtime is true):
- Isolation Key Lookup: It reads the isolation key for the requested name from
ctx[symbols.isolate][prop]. - Current Fiber Check: It searches
fiber.store?.[prop]for a registered service implementation (Impl). - Required Validation: If not found, it checks whether the property is required (
prop in fiber.inject). Missing required services in inactive fibers trigger an error. - Parent Traversal: The search climbs to the parent fiber while the isolation key matches, ultimately reaching the root fiber if necessary.
If no fiber is active, the lookup falls back to ReflectService.get, which returns the stored value from the global implementation map via this._getImpl(name, strict)?.value.
Service Registration and Storage
Before services can be looked up, they must be registered through ReflectService.provide (defined in packages/core/src/reflect.ts, lines 150‑190, and exposed via packages/core/src/registry.ts, lines 75‑92). This method:
- Creates a service entry in the
propsmap with{ type: 'service' }. - Ensures a unique symbol key exists in the root isolate map (
this.ctx.root[symbols.isolate][name]). - Stores an
Implrecord in both the globalReflectService.storemap and the current fiber'sstore. - Notifies dependent fibers when the service becomes available.
This dual-storage approach (global and fiber-scoped) enables the hierarchical resolution that respects isolation boundaries while maintaining global visibility.
Practical Usage Examples
import { Context, Inject } from 'cordis'
// Create a new context (automatically proxied)
const ctx = new Context()
// Register a service
ctx.provide('myService', { hello: 'world' })
// Access the service directly – the proxy triggers the lookup logic
console.log(ctx.myService) // → { hello: 'world' }
// Use @Inject to declare a dependency in a plugin
class MyPlugin {
@Inject('myService')
static async apply(ctx: Context) {
// `ctx.myService` resolves via the proxy as above
ctx.logger.info(ctx.myService.hello)
}
}
ctx.plugin(MyPlugin) // registers and runs the plugin
When a plugin runs in a child fiber, the lookup ascends the fiber chain until it locates the service or throws an error if the required service is unavailable.
Summary
- The Cordis
Contextis wrapped in a JavaScriptProxyat instantiation (packages/core/src/context.ts, lines 36‑48) to intercept all property access. - The proxy handler in
packages/core/src/reflect.tsimplements a four-stage lookup: special properties, own properties, registered accessors, and fiber-scoped hierarchy. - Service resolution respects fiber isolation through symbolic keys stored in
ctx[symbols.isolate], climbing the parent fiber chain until finding a match. - Services are registered via
ReflectService.provide, which stores implementations in both global and fiber-scoped maps for hierarchical resolution. - The system supports both direct property access and decorator-based injection (
@Inject), with the same proxy-driven lookup mechanism underlying both approaches.
Frequently Asked Questions
How does the Context proxy determine which fiber to search for services?
The proxy reads the current active fiber from ctx.fiber and checks if ctx.fiber.runtime is true. If active, it retrieves the isolation key for the requested property from ctx[symbols.isolate][prop] and begins searching from the current fiber's store map, climbing to parent fibers while the isolation key matches.
What happens if a service is not found in the fiber hierarchy?
If the service is not found during the fiber traversal, the handler checks whether the property is listed in fiber.inject as a required dependency. For required services, it throws an error indicating the missing dependency. If not required and no fiber is active, it falls back to the global ReflectService.get method.
Where is the proxy handler defined in the Cordis source code?
The proxy handler is defined in packages/core/src/reflect.ts (lines 63‑94 for the get trap, and lines 150‑190 for service registration logic). The Context constructor in packages/core/src/context.ts (lines 36‑48) attaches this handler to the instance immediately upon creation.
How does service isolation work across different fibers?
Service isolation uses symbolic keys stored in the root context's isolate map (ctx.root[symbols.isolate][name]). When a service is provided, it creates a unique symbol key. During lookup, the proxy compares isolation keys while traversing the fiber hierarchy, stopping at fiber boundaries where the isolation key differs, effectively sandboxing services between isolated contexts.
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 →