How Cordis Handles Callable Service Instances: A Complete Guide to Service.invoke

Cordis treats a service as callable when the service class defines the special symbol Service.invoke, wrapping the instance in a proxy that forwards function calls to the service's invoke method while preserving context-aware tracing.

Cordis, the dependency injection framework powering the Cordiverse ecosystem, enables developers to treat service instances as regular functions through a sophisticated proxy mechanism. This capability allows callable service instances to behave like ordinary functions while maintaining full integration with Cordis' context isolation, shadowing, and lifecycle management systems.

What Makes a Service Callable in Cordis

A service becomes callable based on the presence of a specific symbol rather than inheritance or interface implementation. This design keeps the system extensible without forcing base class complexity.

The Service.invoke Symbol

The foundation of callable services resides in packages/core/src/service.ts, where Service.invoke is defined as a unique symbol. When a service class implements this symbol, it signals to the Cordis runtime that instances should support function-like invocation. The constructor logic explicitly checks for this marker using self[symbols.invoke], determining whether proxy wrapping is necessary.

How Cordis Creates Callable Service Instances

The instantiation process involves detection, proxy creation, and trap installation to seamlessly blend object-oriented services with functional interfaces.

Constructor Detection and Proxy Wrapping

During construction in packages/core/src/service.ts (lines 26-28), the base Service class examines the newly created instance. If self[symbols.invoke] exists, the constructor replaces self with a proxy returned by createCallable(name, …, tracker). This replacement happens before the instance is fully initialized, ensuring all external references obtain the callable proxy rather than the raw service object.

The createCallable Implementation

The createCallable function, implemented in packages/core/src/utils.ts (lines 19-26), constructs the actual function object that wraps the service. This function:

  1. Calls createTraceable to obtain a traceable proxy of the service for context tracking
  2. Executes the original function with supplied arguments when invoked
  3. Sets the function's name property for debugging purposes

The result is a callable object that maintains the service's prototype chain while behaving like a native function.

Invocation Handling via Proxy Traps

When the callable proxy is invoked, the apply trap defined in packages/core/src/utils.ts (lines 215-217) intercepts the call and delegates to applyTraceable. If the target possesses symbols.invoke, the proxy redirects the invocation to the service's Service.invoke implementation rather than executing the service object directly. This redirection preserves the this context and ensures proper error-stack composition through Cordis' tracing system.

Extending Callable Services

Callable services support inheritance and extension through the Service.extend method (lines 41-49 in packages/core/src/service.ts). When extending a callable service, Cordis recreates the callable proxy while preserving the original prototype chain. This mechanism allows derived services to inherit callable behavior while overriding or augmenting the Service.invoke implementation.

The extension process ensures that shadowed services and interceptors work correctly across the inheritance hierarchy, maintaining the context-aware tracing that makes Cordis services predictable in complex plugin ecosystems.

Practical Implementation Example

The following example demonstrates defining and using a callable service with configuration merging and extension capabilities:

// packages/core/src/service.ts pattern implementation
class Foo extends Service {
  constructor(ctx: Context, public config: Config) {
    super(ctx, 'foo')
  }

  // The core of the callable behavior
  protected [Service.invoke](init?: Config) {
    const result = { ...this.config }
    let intercept = this.ctx[Context.intercept]
    while (intercept) {
      Object.assign(result, intercept.foo)
      intercept = Object.getPrototypeOf(intercept)
    }
    return Object.assign(result, init)
  }

  // Expose a convenient method that forwards to the callable proxy
  invoke() {
    return this()
  }

  // Create an extension that retains callability
  extend(config?: Config) {
    return this[Service.extend]({ config: { ...this.config, ...config } })
  }
}

// Usage in application code
const ctx = new Context()
await ctx.plugin(Foo, { a: 1 })

// Direct call resolves to Service.invoke
ctx.foo()                     // → { a: 1 }
ctx.foo.extend({ b: 2 })()   // → { a: 1, b: 2 }

Callable services integrate seamlessly with dependency injection. Other services can inject and invoke them as functions:

class Outer extends Service {
  static inject = ['callable']  // Inject another callable service
  
  call() {
    const callable = this.ctx['callable'] as Callable
    // Both original and extended versions support function syntax
    return [callable(), callable.extend()()]
  }
}

Summary

  • Symbol-based activation: Services become callable by defining the Service.invoke symbol in packages/core/src/service.ts, not through interface implementation.
  • Automatic proxy wrapping: The constructor detects the symbol and wraps instances using createCallable from packages/core/src/utils.ts.
  • Transparent invocation: The proxy's apply trap (lines 215-217) forwards calls to Service.invoke while maintaining tracing and context isolation.
  • First-class extension: Service.extend recreates callable proxies that preserve prototype chains and interceptors.
  • Traceable integration: Every call automatically respects Cordis' shadowing and error-stack composition mechanisms through the createTraceable integration.

Frequently Asked Questions

How do I make a service callable in Cordis?

Define the Service.invoke symbol as a method in your service class. According to the source code in packages/core/src/service.ts, Cordis automatically detects this symbol during construction and wraps the instance with a callable proxy. No additional configuration or inheritance beyond extending the base Service class is required.

What is the difference between Service.invoke and a regular method?

Service.invoke is a unique symbol that transforms the entire service instance into a function. While regular methods require explicit calls like service.method(), Service.invoke enables syntax like service() where the instance itself is callable. The implementation in packages/core/src/utils.ts routes these function calls through the proxy's apply trap to the invoke implementation.

How does callable service extension work?

When you call this[Service.extend]() (implemented in packages/core/src/service.ts, lines 41-49), Cordis creates a new service instance with merged configuration and recreates the callable proxy wrapper. This ensures that extended services maintain their callable nature while properly inheriting the parent service's prototype and interceptors.

Where is the callable proxy logic implemented?

The core proxy creation logic resides in packages/core/src/utils.ts within the createCallable function (lines 19-26). The invocation handling occurs in the same file through the apply trap (lines 215-217), which delegates to applyTraceable to maintain Cordis' context-tracking capabilities.

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 →