How Cordis Context Manages Plugin Is isolation with Symbols: A Deep Dive into the Registry Architecture
Cordis Context implements plugin isolation through a two-layer symbol registry system that maps service names to unique Symbols at the context level, while the Loader's Realm configuration extends this to support both local and global isolation scopes.
The cordiverse/cordis framework provides a lightweight dependency injection system where multiple plugins share a global runtime without risking state leakage. By leveraging JavaScript's unique Symbol primitives, Cordis ensures that each plugin can receive its own instance of a service—such as a logger or database connection—without interfering with other plugins. This architecture is implemented across the core context module and the loader's configuration system.
The Per-Context Symbol Registry
At the heart of Cordis isolation lies the Context.isolate mechanism, a static Symbol exported from packages/core/src/utils.ts that serves as the key for a private registry.
Context Construction and the Isolate Map
When a new Context is instantiated in packages/core/src/context.ts, the constructor initializes an empty isolation map using Object.create(null):
// packages/core/src/context.ts
this[symbols.isolate] = Object.create(null)
This map stores the relationship between service names (like "logger" or "events") and their corresponding unique Symbols. Because symbols.isolate is a registered Symbol rather than a string property, it remains inaccessible to standard property enumeration, preventing accidental interference.
The isolate() Method Shadowing
Plugins invoke ctx.isolate(name, label?) to create an isolated scope. This method, implemented in packages/core/src/context.ts at lines 65-69, performs three critical operations:
- Creates a shadow object that inherits the current isolate map
- Generates a new
Symbol(name)(or uses the providedlabel) - Returns a new
Contextinstance extended with this shadow map
The new Context inherits all parent services but resolves the isolated service name to a different Symbol, effectively redirecting to a separate instance:
import { Context } from 'cordis'
const root = new Context()
// Plugin A receives Symbol('logger') #1
const pluginA = root.isolate('logger')
pluginA.logger.info('Hello from A')
// Plugin B receives Symbol('logger') #2
const pluginB = root.isolate('logger')
pluginB.logger.info('Hello from B')
Loader-Level Realm Isolation
While the core Context provides manual isolation via isolate(), the Loader automates this through Realms defined in packages/loader/src/config/isolate.ts. This system supports declarative isolation via configuration files.
Local vs Global Realms
The Loader distinguishes between two isolation scopes through the Realm abstraction:
- LocalRealm: Creates entry-specific isolation using the entry ID as a suffix (
#<id>). When a plugin declaresisolate: { foo: true }, the loader generates a Symbol likeSymbol('foo#123'). - GlobalRealm: Creates shared isolation across multiple entries using a user-defined label (
@<label>). When a plugin declaresisolate: { db: "shared-db" }, the loader generatesSymbol('db@shared-db'), allowing multiple plugins to intentionally share the same service instance while remaining isolated from the global default.
Each Realm maintains a store: Dict<symbol> where symbols are generated as Symbol(${key}${suffix}).
The patch-context Hook
During the loader/patch-context phase, the Loader replaces the entry's Context.isolate map with a new map containing the symbols returned by Realm.access. This method, located at lines 71-85 of packages/loader/src/config/isolate.ts, resolves the appropriate realm and returns the correct Symbol for each service name.
The replacement preserves the prototype chain while swapping symbol references, allowing the plugin to use standard property access (ctx.logger) while actually resolving to an isolated instance.
Service Resolution with ReflectService
The actual service lookup occurs in packages/core/src/reflect.ts. The ReflectService queries the isolation map to determine which concrete implementation to return:
// Inside a plugin
export default function (ctx: Context) {
// Retrieve the unique Symbol for this context's logger
const loggerSymbol = ctx[Context.isolate].logger
// Access the concrete service instance from the reflect store
const logger = ctx.reflect.store[loggerSymbol]
logger.info('Running with isolated logger')
}
Because each plugin context may contain different Symbols for the same service name, ctx.logger resolves to different instances across plugins, even when accessed identically in code.
Practical Implementation Examples
Programmatic Isolation
For library authors or dynamic plugin systems, manual isolation provides fine-grained control:
import { Context } from 'cordis'
const root = new Context()
// Create isolated child contexts
const analyticsPlugin = root.isolate('database', 'analytics-db')
const billingPlugin = root.isolate('database', 'billing-db')
// Each receives a distinct database connection
// despite both accessing `ctx.database`
Configuration-Driven Isolation
For application developers using the Cordis Loader, isolation is configured declaratively:
# cordis.yml
plugins:
- name: analytics-worker
isolate:
logger: true # LocalRealm: private logger instance
cache: "shared-cache" # GlobalRealm: shared with other "shared-cache" entries
- name: billing-worker
isolate:
logger: true # Distinct from analytics-worker logger
cache: "shared-cache" # Same cache instance as analytics-worker
Entry Attachment and Lifecycle
The linkage between entries and contexts occurs in packages/loader/src/config/entry.ts, where each Entry attaches to its context via Entry.key. This key allows the Loader to track which isolation realm applies to which running plugin instance, enabling proper garbage collection of GlobalRealms when no entries reference them.
Summary
- Cordis Context plugin isolation relies on a static Symbol registry (
Context.isolate) that maps service names to unique Symbols stored in a private map initialized inpackages/core/src/context.ts. - The
isolate()method creates shadow contexts with new Symbols, allowing distinct service instances while maintaining inheritance from parent contexts. - Loader Realms extend this to configuration-driven isolation, supporting both
LocalRealm(entry-specific, suffix#<id>) andGlobalRealm(label-shared, suffix@<label>) inpackages/loader/src/config/isolate.ts. - Service resolution occurs via
ReflectServiceinpackages/core/src/reflect.ts, which looks up the concrete implementation using the context-specific Symbol from the isolate map. - The
loader/patch-contexthook performs the actual substitution of isolate maps during plugin loading, ensuring isolation is established before the plugin executes.
Frequently Asked Questions
How does Cordis prevent symbol collisions between different plugins?
Cordis prevents collisions by using JavaScript's native Symbol primitive, which guarantees uniqueness even when multiple plugins request isolation for the same service name. When Context.isolate('service') is called, it generates Symbol('service'), and because Symbols are unique by identity rather than by string value, each call produces a distinct key for the internal registry. The Loader further distinguishes contexts through LocalRealm (appending entry IDs) and GlobalRealm (appending user-defined labels), ensuring no two active contexts accidentally share Symbol references unless explicitly configured to do so.
What is the difference between LocalRealm and GlobalRealm in Cordis?
LocalRealm creates strictly entry-private isolation by appending the entry's unique ID to the service name when generating the Symbol (e.g., Symbol('logger#42')), ensuring that even if two plugins both request isolation for logger, they receive separate instances. GlobalRealm appends a user-defined label instead (e.g., Symbol('db@shared-db')), allowing multiple plugin entries to explicitly share the same isolated service instance while remaining isolated from plugins outside that realm. This enables both strict separation and intentional resource sharing as configured in the loader's isolation settings.
Can a plugin access the parent context's services after calling isolate()?
Yes, calling ctx.isolate() returns a new Context that inherits from the original via prototype chain shadowing. The new context contains only the overridden Symbol mappings for the isolated services, while all other service lookups traverse up the prototype chain to the parent context. This means non-isolated services remain shared, allowing plugins to isolate only specific dependencies (like a database or logger) while retaining access to shared event buses or configuration services from the root context.
Where does the actual service instance storage occur in Cordis?
Service instances are stored in the ReflectService internal store, accessible via ctx.reflect.store, which maps the unique Symbols (retrieved from ctx[Context.isolate]) to their concrete implementations. When a plugin accesses ctx.serviceName, the getter resolves the appropriate Symbol from the isolate map, then retrieves the corresponding instance from the reflect store. This separation between the isolation registry (packages/core/src/context.ts) and the service storage (packages/core/src/reflect.ts) allows Cordis to swap service implementations per context without affecting the global service registry.
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 →