@setu-ts/di-plugin

Optional dependency-injection container. Registers an IContainer under CAPABILITIES.DI_CONTAINER ('di-container').

Supports singleton, scoped, and transient lifetimes, constructor and factory injection, string, typed and class tokens, a request scope, disposal, and an optional fallback to the kernel’s ServiceRegistry. The graph is compiled into direct factories and validated once at boot: a missing dependency, a cycle, or a singleton that reaches a scoped service fails app.start().

The framework does not require this package — the kernel’s service registry is enough for most applications. Reach for the container when you want constructor injection, per-request instances (container.forRequest(ctx)), or something to dispose of what you created.

Installation

import { DiPlugin } from '@setu-ts/di-plugin';

Usage

Register providers while plugins register (or from onInit and onBootstrap); the container closes after the last bootstrap hook, and a register after that throws ContainerClosedError.

import { createApplication } from '@setu-ts/kernel';
import { RuntimePlugin } from '@setu-ts/runtime';
import { DiPlugin } from '@setu-ts/di-plugin';
import { CAPABILITIES, createToken, type IContainer, type IPlugin } from '@setu-ts/common';

class Clock {
  now(): number {
    return 0;
  }
}

class UnitOfWork {
  constructor(readonly clock: Clock) {}
  release(): void {}
}

const UNIT_OF_WORK = createToken<UnitOfWork>('unit-of-work');

const providers: IPlugin = {
  name: 'app-providers',
  version: '1.0.0',
  dependencies: [CAPABILITIES.DI_CONTAINER],
  register(ctx) {
    const container = ctx.services.get<IContainer>(CAPABILITIES.DI_CONTAINER);
    container.register(Clock, { useClass: Clock });
    container.register(
      UNIT_OF_WORK,
      { useClass: UnitOfWork, inject: [Clock] },
      { scope: 'scoped', dispose: (uow) => uow.release() },
    );
  },
};

const app = createApplication({ plugins: [RuntimePlugin(), DiPlugin(), providers] });

app.router.get('/now', (ctx) => {
  // One UnitOfWork per request, released when the response is finished.
  const scope = ctx.services.get<IContainer>(CAPABILITIES.DI_CONTAINER).forRequest(ctx);
  const uow = scope.resolve(UNIT_OF_WORK);
  return ctx.response.json({ now: uow.clock.now() });
});

await app.start({ port: 3000 });

A token is a string, a typed token from createToken<T>(name), or a class; resolve infers the type from the last two. A factory declares its dependencies in inject and receives them as arguments: { useFactory: (clock: Clock) => new UnitOfWork(clock), inject: [Clock] }.

Options

OptionTypeDefaultDescription
defaultScope'singleton' | 'scoped' | 'transient''singleton'Lifecycle for providers registered without an explicit scope.
autoRegisterbooleantrueFall back to the kernel ServiceRegistry for unregistered tokens.

With autoRegister, boot validation checks a fallback token with has(), and the first successful resolve is cached as a singleton. Explicit container registrations always take precedence.

Exports

ExportKind
createContainerfunction
DiPluginfunction
CircularDetectorclass
ContainerBuilderclass
ContainerClosedErrorclass
ContainerValidationErrorclass
DiContainerclass
ProviderRegistryclass
RequestScopeUnavailableErrorclass
ScopeDisposedErrorclass
ScopeRequiredErrorclass
ContainerConfiginterface
DiPluginOptionsinterface
ExternalResolverinterface
ProviderEntryinterface

Generated from the package barrel by pnpm run docs:exports; pnpm run check:docs fails when it drifts.

Full API

Every export and option is documented in PUBLIC_API.md.

API reference

API Reference: @setu-ts/di-plugin

Optional dependency injection container plugin. Registers an IContainer under CAPABILITIES.DI_CONTAINER. The service registry remains the primary resolution mechanism; this container is a convenience layer for constructor injection, lifetimes and disposal. No other plugin depends on it. Implemented in Milestone 8; compiled graph, boot validation and the request scope in Milestone 125. This section is the authoritative export list (AI_GUIDELINES §10.5). All exports carry full JSDoc.

Values (di-plugin exports)

ExportKindPurpose
DiPluginfunctionPlugin factory — registers IContainer under CAPABILITIES.DI_CONTAINER, closes it at the end of bootstrap
ContainerBuilderclassFluent builder for configuring and creating a DiContainer
createContainerfunctionConvenience factory for a standalone IContainer
DiContainerclassThe IContainer implementation; adds close(), which ends registration and validates the graph
CircularDetectorclassCycle detector used by the graph compiler’s walk (exported for testing and advanced use)
ProviderRegistryclassToken-keyed provider store with hierarchical lookups
ContainerValidationErrorclassThrown when a compile or close() finds missing, cyclic or captive dependencies; carries findings
ScopeRequiredErrorclassThrown when a scoped token, or a transient reaching one, is resolved on the root container
ScopeDisposedErrorclassThrown by resolve on a disposed scope
ContainerClosedErrorclassThrown by register on a closed root container
RequestScopeUnavailableErrorclassThrown by forRequest when the request context has no defer (for example one from createTestContext)

ScopeManager was removed in 0.11.0: the compiled graph keeps singletons on their compiled entries and scoped instances in a per-scope slot array.

Types

ExportKindPurpose
DiPluginOptionstypeOptions for DiPlugin() ({ defaultScope?, autoRegister? })
ContainerConfigtypeConfiguration for DiContainer constructor
ExternalResolvertypeSubset of IServiceRegistry for auto-registration fallback
ProviderEntrytypeA provider paired with its resolved scope (internal building block)

Contract notes:

  • Optional: no plugin depends on the DI container. When DiPlugin is not registered, ctx.container is undefined and services resolve directly from the ServiceRegistry.
  • Three provider forms: ClassProvider (constructor injection via inject), FactoryProvider (a factory called with its inject dependencies), ValueProvider (pre-built value) — all defined in @setu-ts/common, with the token and inject rules in “DI container contract (M125)”.
  • Three lifetimes: singleton (one instance, shared with every scope), scoped (one instance per scope — a request’s scope comes from forRequest(ctx)), transient (new instance every resolve, tracked by the scope that built it). Default is singleton.
  • Open, then closed: a container accepts registrations and resolves while open; a resolve compiles that token’s subgraph on demand. DiPlugin closes the container after the last bootstrap hook, the moment the service registry seals; a register on the closed root then throws ContainerClosedError. A standalone container stays open until its owner calls close().
  • Boot validation: closing compiles every provider into a direct factory and validates the whole graph once — a missing dependency, a cycle (reported A → B → A), and a singleton reaching a scoped provider directly or through transients — throwing one ContainerValidationError that lists every finding. Validation instantiates nothing. A factory without inject is opaque; DiPlugin lists such tokens in one debug line.
  • Compiled resolution: no resolve runs the cycle detector or looks up a dependency by token; a top-level resolve(token) reads one Map. Singletons are still created lazily, on first resolve.
  • Scopes: createScope() and forRequest(ctx) return child containers that share singletons with their parent. A scope may register tokens its parent chain does not have, and they live as long as the scope (a singleton lifetime there is stored as scoped); registering one it has throws, because the parent’s compiled graph cannot be rebound from below.
  • Request scope: forRequest(ctx) creates the scope on first call for that request, keeps it in ctx.slot under a key private to the container (a context without slot falls back to a per-container WeakMap), and registers its disposal with ctx.defer. A request that never calls it pays one undefined check and stays on the kernel’s synchronous path. A scope allocates its own registry only when it registers a token, and instances of its own registrations are held by the scope that resolves them, so request volume never grows anything the root holds.
  • Disposal: ProviderOptions.dispose runs from dispose() — for a request scope when the request finishes, for the root in DiPlugin’s onClose hook. Each asynchronous dispose is bounded at 5000 ms, on the runtime’s timers when a runtime is registered (ContainerConfig.timing); one that never settles is reported in the AggregateError and the rest still run, so app.stop() is never held indefinitely. The bound is per disposal, not a total: k disposals that never settle take about k × 5000 ms, which can exceed the 10 s shutdown drain. A request scope reports each disposal failure itself, through DiPlugin’s logger at warn (ContainerConfig.reportDisposeError), whenever it settles — also after the kernel’s 5000 ms cleanup bound has already reported its own timeout.
  • Auto-registration (autoRegister, default true since 0.9.0): a token not in the container falls back to the kernel’s ServiceRegistry, checked with has() during validation and resolved lazily once, then cached as a singleton; false confines resolution to the container’s own registrations. Explicit DI registrations always take precedence, and framework capability tokens (CAPABILITIES.LOGGER, etc.) work as dependencies without pre-registration.
  • No runtime-specific APIs: the container uses no Date.now(), crypto.*, or process.* — it is pure TypeScript and runtime-independent.

Agent card · TypeDoc API reference