Core Concepts

Dependency injection

Compose runtime modules and adapt services without changing business expression code.

What it means

RS-X is designed to be adaptable. To accomplish that it uses the composition pattern so services can be extended or adapted through dependency injection. The current dependency-injection implementation is based on Inversify and exposed through InjectionContainer.

Practical value

You can replace or extend runtime behavior at composition boundaries instead of editing core logic. This keeps app code stable while letting infrastructure concerns like logging, equality, observers, and metadata evolve independently.

Key points

How composition and DI work together

Composition in RS-X means each subsystem exposes clear extension points (tokens, module bindings, and service lists) instead of hard-coding dependencies inside feature logic.

Dependency injection wires those extension points at startup. Business code consumes stable contracts, while implementations stay swappable.

In practice this lets you adapt runtime behavior per app or environment without forking RS-X internals.

rs-x uses Inversify

rs-x uses Inversify as its dependency injection implementation.

The core container API (`Container`, `ContainerModule`, `Inject`, `Injectable`, `MultiInject`) is re-exported from `@rs-x/core`, so rs-x modules and your app modules use one DI style.

`InjectionContainer` is the shared runtime container instance. Module load order defines how bindings are composed and when overrides are applied.

This design keeps DI mechanics centralized while still allowing package-level modules (`@rs-x/core`, `@rs-x/state-manager`, `@rs-x/expression-parser`) to remain independently maintainable.

Two extension patterns you will use most

Single-service override: unbind a token and rebind it to your implementation (for example custom `IErrorLog` or custom equality behavior).

Multi-service extension: register ordered service lists (for accessors, observers, metadata) with `registerMultiInjectServices` or replace list order with `overrideMultiInjectServices`.

Together these cover most adaptation cases: behavior replacement and ordered pipeline composition.

Common DI mistakes to avoid

Rebinding a token without unbinding first can leave multiple active bindings and produce unexpected resolution behavior.

Changing a broad multi-service list can affect unrelated runtime behavior. Prefer small, explicit list changes and verify service order.

Because `InjectionContainer` is shared, bindings can leak between test runs or hot-reload sessions if modules are not unloaded.

Example

import {
  ContainerModule,
  Inject,
  Injectable,
  InjectionContainer,
  type IError,
  type IErrorLog,
  printValue,
  RsXCoreInjectionTokens,
} from '@rs-x/core';
import {
  type IStateChange,
  type IStateManager,
  RsXStateManagerInjectionTokens,
  RsXStateManagerModule,
} from '@rs-x/state-manager';
import { Observable, Subject } from 'rxjs';

@Injectable()
class PrefixedErrorLog implements IErrorLog {
  private readonly _error = new Subject<IError>();