@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
| Option | Type | Default | Description |
|---|---|---|---|
defaultScope | 'singleton' | 'scoped' | 'transient' | 'singleton' | Lifecycle for providers registered without an explicit scope. |
autoRegister | boolean | true | Fall 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
| Export | Kind |
|---|---|
createContainer | function |
DiPlugin | function |
CircularDetector | class |
ContainerBuilder | class |
ContainerClosedError | class |
ContainerValidationError | class |
DiContainer | class |
ProviderRegistry | class |
RequestScopeUnavailableError | class |
ScopeDisposedError | class |
ScopeRequiredError | class |
ContainerConfig | interface |
DiPluginOptions | interface |
ExternalResolver | interface |
ProviderEntry | interface |
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)
| Export | Kind | Purpose |
|---|---|---|
DiPlugin | function | Plugin factory — registers IContainer under CAPABILITIES.DI_CONTAINER, closes it at the end of bootstrap |
ContainerBuilder | class | Fluent builder for configuring and creating a DiContainer |
createContainer | function | Convenience factory for a standalone IContainer |
DiContainer | class | The IContainer implementation; adds close(), which ends registration and validates the graph |
CircularDetector | class | Cycle detector used by the graph compiler’s walk (exported for testing and advanced use) |
ProviderRegistry | class | Token-keyed provider store with hierarchical lookups |
ContainerValidationError | class | Thrown when a compile or close() finds missing, cyclic or captive dependencies; carries findings |
ScopeRequiredError | class | Thrown when a scoped token, or a transient reaching one, is resolved on the root container |
ScopeDisposedError | class | Thrown by resolve on a disposed scope |
ContainerClosedError | class | Thrown by register on a closed root container |
RequestScopeUnavailableError | class | Thrown 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
| Export | Kind | Purpose |
|---|---|---|
DiPluginOptions | type | Options for DiPlugin() ({ defaultScope?, autoRegister? }) |
ContainerConfig | type | Configuration for DiContainer constructor |
ExternalResolver | type | Subset of IServiceRegistry for auto-registration fallback |
ProviderEntry | type | A provider paired with its resolved scope (internal building block) |
Contract notes:
- Optional: no plugin depends on the DI container. When
DiPluginis not registered,ctx.containerisundefinedand services resolve directly from theServiceRegistry. - Three provider forms:
ClassProvider(constructor injection viainject),FactoryProvider(a factory called with itsinjectdependencies),ValueProvider(pre-built value) — all defined in@setu-ts/common, with the token andinjectrules in “DI container contract (M125)”. - Three lifetimes:
singleton(one instance, shared with every scope),scoped(one instance per scope — a request’s scope comes fromforRequest(ctx)),transient(new instance every resolve, tracked by the scope that built it). Default issingleton. - Open, then closed: a container accepts registrations and resolves while open; a resolve
compiles that token’s subgraph on demand.
DiPlugincloses the container after the last bootstrap hook, the moment the service registry seals; aregisteron the closed root then throwsContainerClosedError. A standalone container stays open until its owner callsclose(). - 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 asingletonreaching ascopedprovider directly or throughtransients — throwing oneContainerValidationErrorthat lists every finding. Validation instantiates nothing. A factory withoutinjectis opaque;DiPluginlists such tokens in onedebugline. - Compiled resolution: no resolve runs the cycle detector or looks up a dependency by token; a
top-level
resolve(token)reads oneMap. Singletons are still created lazily, on first resolve. - Scopes:
createScope()andforRequest(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 (asingletonlifetime there is stored asscoped); 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 inctx.slotunder a key private to the container (a context withoutslotfalls back to a per-containerWeakMap), and registers its disposal withctx.defer. A request that never calls it pays oneundefinedcheck 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.disposeruns fromdispose()— for a request scope when the request finishes, for the root inDiPlugin’sonClosehook. 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 theAggregateErrorand the rest still run, soapp.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, throughDiPlugin’s logger atwarn(ContainerConfig.reportDisposeError), whenever it settles — also after the kernel’s 5000 ms cleanup bound has already reported its own timeout. - Auto-registration (
autoRegister, defaulttruesince 0.9.0): a token not in the container falls back to the kernel’sServiceRegistry, checked withhas()during validation and resolved lazily once, then cached as a singleton;falseconfines 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.*, orprocess.*— it is pure TypeScript and runtime-independent.