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:

  1. Creates a shadow object that inherits the current isolate map
  2. Generates a new Symbol(name) (or uses the provided label)
  3. Returns a new Context instance 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 declares isolate: { foo: true }, the loader generates a Symbol like Symbol('foo#123').
  • GlobalRealm: Creates shared isolation across multiple entries using a user-defined label (@<label>). When a plugin declares isolate: { db: "shared-db" }, the loader generates Symbol('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 in packages/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>) and GlobalRealm (label-shared, suffix @<label>) in packages/loader/src/config/isolate.ts.
  • Service resolution occurs via ReflectService in packages/core/src/reflect.ts, which looks up the concrete implementation using the context-specific Symbol from the isolate map.
  • The loader/patch-context hook 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →