Core Concepts
Dependency injection
Compose runtime modules and adapt services without changing business expression code.
Practical value
Key points
- RS-X runtime modules are container modules that register implementations under contract identifiers (tokens).
- Services are resolved by token, so consumers depend on interfaces/contracts rather than concrete classes.
- You can add or replace services by loading your own module after the base module.
- Multi-service pipelines use list registration helpers for ordered extension points.
- This keeps runtime customization explicit, testable, and easy to reason about.
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>();