How the @Inject Decorator Works in Cordis for Dependency Injection

The @Inject decorator in Cordis operates in two distinct modes: at the class level, it defines static dependency requirements that the framework resolves during plugin instantiation; at the method level, it registers initialization hooks that execute the method once specific services become available in the context.

The @Inject decorator is the primary mechanism for declaring dependency injection requirements in the Cordis plugin framework. Implemented in packages/core/src/registry.ts within the cordiverse/cordis repository, it enables both class-level service injection and method-level initialization hooks that resolve automatically when plugins load through the Fiber system.

Class-Level vs. Method-Level Injection Patterns

The decorator adapts its behavior based on whether it is applied to a class or a method, utilizing JavaScript decorator metadata to store injection requirements.

Class-Level Injection (Static Property Declaration)

When applied to a class, @Inject adds a static inject property to the class (or its prototype chain) that records the requested dependency name with optional configuration. This static map is later resolved by Inject.resolve when the class is instantiated as a plugin.

The decorator ensures inheritance by using Object.create(Object.getPrototypeOf(value).inject ?? null), allowing subclasses to inherit injection requirements from their base classes while defining their own.

Method-Level Injection (Runtime Initialization Hooks)

When applied to a method, the decorator stores injection metadata in the method's symbols.metadata.inject object and registers an initialization hook via symbols.initHooks. This hook executes after the plugin's context is created, calling ctx.inject(inject, callback) to create a temporary Fiber that executes the method with resolved services injected as arguments.

Core Implementation in registry.ts

The decorator implementation in packages/core/src/registry.ts handles both usage patterns through a single factory function that inspects the decorator.kind property:

// https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L17-L39
export function Inject<K extends InjectKey>(name: K, config?: …) {
  return function (value, decorator) {
    if (decorator.kind === 'class') {
      // ensure the class has an `inject` map (inherits prototype chain)
      defineProperty(value, 'inject', Object.create(Object.getPrototypeOf(value).inject ?? null));
      value.inject[name] = config;
    } else if (decorator.kind === 'method') {
      const inject = (value[symbols.metadata] ??= {}).inject ??= Object.create(null);
      inject[name] = config;
      decorator.addInitializer(function () {
        const property = this[symbols.tracker]?.property;
        (this[symbols.initHooks] ??= []).push(() => {
          (this.ctx as Context).inject(inject, (ctx) => {
            return value.call(property ? withProps(this, { [property]: ctx }) : this);
          });
        });
      });
    } else {
      throw new Error('@Inject() can only be used on class or class methods');
    }
  };
}

The implementation distinguishes between classes and methods through the decorator.kind check. For classes, it manipulates the constructor's static properties. For methods, it leverages decorator.addInitializer to defer execution until the plugin context is fully established.

Dependency Resolution and Prototype Chain

The Inject.resolve static method (located in the same file) handles the merging of injection requirements across class hierarchies. It walks the prototype chain and consolidates static inject objects, supporting both array-style and object-style specifications:

// https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L42-L60
export namespace Inject {
  export function resolve(inject, result = Object.create(null)) {
    if (!inject) return result;
    if (Array.isArray(inject)) { … }
    else if (Reflect.has(inject, symbols.checkProto)) { … }
    else { … }
    return result;
  }
}

This resolution ensures that subclasses inherit all dependency requirements from their parent classes while allowing overrides and extensions.

Context Integration and Fiber Wiring

The Context surface exposes convenience methods that delegate to the registry for actualplugin loading. The RegistryService.inject method constructs the bridge between the decorator metadata and the Fiber execution system:

// https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L89-L92
export class RegistryService {
  …
  inject(inject: Inject, callback: Plugin.Function<void>) {
    return this.plugin({ inject, apply: callback, name: callback.name });
  }
}

When a plugin is loaded, the RegistryService.plugin method constructs a Fiber with the resolved injection map (Inject.resolve(plugin.inject)). The Fiber then makes the requested services available as part of the plugin's execution context, ensuring dependencies are present before the plugin logic executes.

Practical Code Examples

Class-Level Injection

import { Context, Service, Inject } from 'cordis';

@Service('timer')
class TimerService { /* … */ }

@Inject('timer')
class MyPlugin {
  static inject = {};   // automatically filled by the decorator
  constructor(public readonly ctx: Context) {}

  async start() {
    // The timer service is available via `ctx.timer`
    this.ctx.timer.start();
  }
}

// Register the plugin
ctx.plugin(MyPlugin);

The decorator adds MyPlugin.inject = { timer: undefined }. When MyPlugin is instantiated, Cordis creates a Fiber that resolves ctx.timer and injects it into the plugin's context.

Method-Level Injection

import { Context, Service, Inject } from 'cordis';

@Service('logger')
class LoggerService {
  log(msg: string) { console.log(msg); }
}

class LoggerPlugin {
  constructor(public readonly ctx: Context) {}

  @Inject('logger')
  init(logger: LoggerService) {
    logger.log('LoggerPlugin initialised');
  }
}

// Register the plugin
ctx.plugin(LoggerPlugin);

The method decorator records that init needs the logger service. When the plugin's Fiber is ready, Cordis calls ctx.inject which executes init with the resolved LoggerService instance passed as an argument.

Array-Style Injection

@Inject(['config', 'database'])
class ConfiguredPlugin {
  // `ctx.config` and `ctx.database` are injected automatically
}

Array syntax tells Cordis to inject the listed services with default values if they are not present in the context.

Summary

  • Dual-mode operation: The @Inject decorator handles both class-level dependency declarations (via static inject properties) and method-level initialization hooks (via symbols.initHooks).
  • Prototype chain awareness: Inject.resolve in packages/core/src/registry.ts merges injection requirements across class hierarchies, ensuring proper inheritance of dependencies.
  • Fiber integration: The decorator ultimately wires into Cordis's Fiber system through RegistryService.plugin, creating execution contexts where requested services are guaranteed to be available.
  • Source locations: Core logic resides in packages/core/src/registry.ts, with resolution invoked during plugin loading in packages/loader/src/index.ts.

Frequently Asked Questions

Can @Inject be used on properties or accessors?

No. According to the source implementation in packages/core/src/registry.ts, the @Inject decorator explicitly throws an error if applied to anything other than a class or method: throw new Error('@Inject() can only be used on class or class methods').

How does Cordis handle missing dependencies when using @Inject?

For class-level injection, the Fiber system ensures all requested services are available before the plugin constructor runs, throwing resolution errors if services are missing. For method-level injection, the initialization hook waits until the dependency appears in the context, optionally using default values when specified in the decorator configuration.

Do subclasses inherit injection requirements from parent classes?

Yes. The Inject.resolve function specifically walks the prototype chain using Object.create(Object.getPrototypeOf(value).inject ?? null), allowing subclasses to inherit and extend the injection maps defined by their parent classes without explicit re-declaration.

What is the difference between @Inject on a class versus on a method?

Class-level @Inject declares that the entire plugin requires certain services to be present in its context, resolved when the Fiber is created. Method-level @Inject registers a callback that executes only after the specific requested services become available, allowing for lazy initialization or conditional setup logic within the plugin lifecycle.

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 →