Creating and Using Context Extensions in Cordis: The Complete Guide

Cordis context extensions create immutable, shallow copies of the runtime Context that isolate state changes while maintaining a shadow chain back to the parent for service resolution.

Cordis relies on an immutable Context object to represent the runtime environment of plugins and services. According to the cordiverse/cordis source code, you never mutate an existing Context instance directly; instead, you use the extend() method to create isolated snapshots that inherit the parent's state. This pattern enables safe plugin isolation and composable service architecture without side effects.

How Context Extensions Work in Cordis

At the core of Cordis is the principle that contexts are immutable. Rather than modifying a context in place, the framework creates a shadow—a shallow copy that inherits from the original via JavaScript’s prototype chain.

The extend Method Implementation

The extension mechanism lives in [packages/core/src/context.ts](https://github.com/cordiverse/cordis/blob/main/packages/core/src/context.ts). The extend method signature is:

extend(meta = {}): this

The implementation creates a traceable shallow copy of the current context while preserving the shadow chain:

extend(meta = {}): this {
  const shadow = Reflect.getOwnPropertyDescriptor(this, symbols.shadow)?.value
  const self = Object.create(getTraceable(this, this))
  for (const prop of Reflect.ownKeys(meta)) {
    Object.defineProperty(self, prop, Reflect.getOwnPropertyDescriptor(meta, prop)!)
  }
  if (!shadow) return self
  return Object.assign(Object.create(self), { [symbols.shadow]: shadow })
}

This implementation performs three critical operations:

  • Creates a traceable prototype chain via Object.create(getTraceable(this, this)), ensuring the new context inherits all existing services and properties from the parent.
  • Defines new properties from the meta object directly onto the new context instance using Reflect.getOwnPropertyDescriptor, allowing you to add or override services and configuration values.
  • Preserves the shadow reference by storing the original context in symbols.shadow, enabling Cordis to resolve services up the hierarchy while tracking the context's origin.

Because extend returns a new Context instance, modifications affect only that extension, leaving the parent context untouched.

Real-World Usage Patterns for Context Extensions

The Cordis loader and fiber systems rely heavily on context extensions to create isolated execution environments.

Isolating Plugin State with Custom Services

When building a plugin, you often need to add services that should not pollute the global context. In [packages/loader/src/config/tree.ts](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/tree.ts), the loader extends the base context with plugin-specific metadata:

// packages/loader/src/config/tree.ts
this.ctx = ctx.extend({ baseUrl: ctx.baseUrl })

You can apply this same pattern to inject custom services:

import { Context } from 'cordis'

export default function plugin(root: Context) {
  // Create an isolated context with a new service
  const pluginCtx = root.extend({ myService: new MyService() })

  // Register handlers using the extended context
  pluginCtx.on('message', (msg) => {
    pluginCtx.myService.handle(msg)
  })

  // The original root context remains clean
  // root.myService // ❌ TypeScript error: Property does not exist
}

Creating Scoped Execution Contexts for Loaders

Each plugin entry in Cordis receives a dedicated context that carries a reference to the entry itself. This pattern appears in [packages/loader/src/config/entry.ts](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/entry.ts):

// packages/loader/src/config/entry.ts
this.ctx = loader.ctx.extend({ [Entry.key]: this })

This ensures that any code running within that context can access the entry metadata without exposing it to sibling plugins.

Fiber-Specific Context Creation

Execution fibers (async execution scopes) extend the parent context to include the fiber instance. The [packages/core/src/fiber.ts](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts) implementation shows:

// packages/core/src/fiber.ts
this.ctx = this.context = parent.extend({ fiber: this })

This creates an isolated scope for the fiber while maintaining access to all parent services.

Using isolate() with Extensions

For temporary, named isolation scopes, Cordis provides the isolate helper, which internally leverages extend. In [packages/core/src/context.ts](https://github.com/cordiverse/cordis/blob/main/packages/core/src/context.ts), isolate ultimately returns an extended context:

import { Context } from 'cordis'

async function runTask(root: Context) {
  // Create an isolated scope named 'task' then extend it
  const taskCtx = root.isolate('task', Symbol('task')).extend({ taskId: 42 })

  // The taskId exists only within this context slice
  for await (const item of someAsyncGenerator(taskCtx)) {
    console.log(taskCtx.taskId, item)
  }
}

Benefits of the Context Extension Pattern

Isolation — Extensions create a fresh sandbox that cannot accidentally corrupt the parent state. Services added to a child context are invisible to siblings and ancestors.

Traceability — The hidden symbols.shadow chain stores the original parent reference, allowing Cordis to resolve services up the prototype hierarchy while maintaining a clear ancestry graph for debugging and dependency injection.

Composability — Multiple extensions can be layered (e.g., a plugin extends a loader which extends the root), building a clear inheritance graph without deep cloning overhead.

Type Safety — The method signature extend(meta = {}): this preserves TypeScript typings, enabling IDE autocomplete for newly added properties while maintaining the base Context interface.

Summary

  • Contexts in Cordis are immutable; use ctx.extend() to create modified copies rather than mutating state directly.
  • The extend method in packages/core/src/context.ts creates a shallow copy using Object.create() and preserves the parent chain via symbols.shadow.
  • Extensions are used throughout the framework for loader configuration, plugin entries, and fiber creation to isolate state while sharing services.
  • Properties passed to extend() become own properties of the new context, inherited services remain accessible via the prototype chain, and the shadow reference enables upstream resolution.

Frequently Asked Questions

How does ctx.extend() differ from direct property assignment in Cordis?

Direct property assignment violates the immutability contract of the Cordis architecture. The extend() method creates a new context instance with the original as its prototype, ensuring that the parent context remains unchanged. This prevents accidental side effects between plugins and maintains predictable service resolution order.

What is the purpose of the symbols.shadow chain in Cordis contexts?

The symbols.shadow property stores a reference to the original context from which an extension was created. This shadow chain enables Cordis to trace service lookups up the hierarchy and provides the framework with metadata about context ancestry, which is essential for debugging and for the getTraceable utility in packages/core/src/utils.ts.

Can I extend a Cordis context multiple times to create nested scopes?

Yes, you can chain extend() calls to create deeply nested context hierarchies. Each call creates a new layer in the prototype chain. The framework efficiently handles this through the shadow mechanism, though for temporary scopes, consider using ctx.isolate() first to create a named boundary before extending.

Does extending a context copy service instances or share them?

Context extensions share existing service instances via JavaScript’s prototype chain rather than deep cloning them. When you call extend(), the new context inherits properties from the parent through Object.create(). Only the new properties defined in the meta parameter become own properties of the extended context. This shallow copy approach ensures memory efficiency while maintaining isolation for new additions.

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 →