@btravstack/di
@btravstack/di
Interfaces
Scope
Defined in: port.ts:107
A phantom requirement, not a real service — its shape is never because nothing ever constructs one or reads it out of a Context. A resourceful provider (the acquire/release qualification arm, provider.ts) adds Scope to its Needs, so Module.build — which demands Needs be never — refuses a graph that still owns an un-discharged resource. Only Module.scoped strips Scope back out of Needs before checking for unmet dependencies, because it is the one entry point that actually opens a createScope and guarantees its close. Forgetting to route a resourceful module through Module.scoped is a compile error, not a runtime leak.
Scope being providable — Provider(Scope)({ value: ... }) — is a separate hazard from being unmet, and is deliberately not blocked by the type system: a generic type-level guard keyed on P's id (tried first) turned out to be simultaneously bypassable (any const widened: AnyPort = Scope before the call slips past a conditional that only ever sees the widened structural type) and a false positive on ordinary port-generic helpers (function wrap<P extends AnyPort>(port: P) { return Provider(port) } couldn't typecheck, since the conditional can't reduce for an unresolved P). build.ts's plan() instead rejects a provider registered for Scope's portId as a WiringDefect, the same class of pre-construction wiring bug a dependency cycle or a duplicate provider already is — sound against any type-level alias or widening, because it checks the runtime portId string, not a static type.
Extends
PortInstance<"@di/Scope",never>
Properties
| Property | Modifier | Type | Inherited from | Defined in |
|---|---|---|---|---|
[ID] | readonly | "@di/Scope" | Port("@di/Scope").[ID] | port.ts:10 |
[SERVICE] | readonly | never | Port("@di/Scope").[SERVICE] | port.ts:11 |
Type Aliases
AnyModule
type AnyModule = object;Defined in: module.ts:29
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
exports | readonly | readonly (AnyPort | AnyModule)[] | module.ts:33 |
imports | readonly | readonly AnyModule[] | module.ts:31 |
name | readonly | string | module.ts:30 |
provides | readonly | readonly AnyProvider[] | module.ts:32 |
AnyPort
type AnyPort = object & () => AnyPortInstance;Defined in: port.ts:38
Type Declaration
| Name | Type | Defined in |
|---|---|---|
portId | string | port.ts:39 |
AnyProvider
type AnyProvider = object;Defined in: module.ts:27
Structural bounds, not Provider<any, any, any> / Module<any, any, any> as the brief has it. The phantom channels carry mixed variance by design — capability channels (_port, _exports) contravariant, obligation channels (_error, _needs) covariant, see Module below — and these bounds only need the shape every provider and module has in common regardless of its channels. Listing the concrete (non-phantom) fields structurally keeps the comparison channel-free, so nothing here can trip on a variance rule it does not care about.
The wildcarded bound was originally rejected outright: while _error/_needs were still contravariant, Provider<Env, never, never> failed to satisfy Provider<any, any, any> with "Type 'any' is not assignable to type 'never'". Covariant obligation channels ended that, and the wildcarded form would compile today (measured) — it stays gone because any is a lint error in this repo, and because a bound that compares channels it does not read is a variance bug waiting for the next rule change.
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
deps | readonly | readonly AnyPort[] | module.ts:27 |
port | readonly | AnyPort | module.ts:27 |
Context
type Context<R> = object;Defined in: context.ts:30
_R is load-bearing, not decoration. Context is declared in R, but the in modifier only asserts contravariance — TypeScript still checks the declaration's own structure against it, and get's signature cannot carry that check on its own: get is a generic method whose R appears solely as the bound of its own type parameter (<S extends R>), a position that is not independently contravariant in R. With _R removed — or made optional, which has the same effect on the variance measurement — Context measures as bivariant in R, and Context<Database> starts flowing where a Context<Database | Logger> is required with no error at the call site. The phantom field is what puts R in a genuine parameter position and makes the in annotation something the compiler can actually enforce. Do not remove it, and do not make it optional; there is no signal at the use site if you do. (Ledger note carried over from Task 2.)
Type Parameters
| Type Parameter |
|---|
R |
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
_R | readonly | (r) => void | context.ts:31 |
get | readonly | <S>(port) => ServiceOf<S> | context.ts:39 |
Exportable
type Exportable<I, P> =
| AnyPort & () => Available<I, P>
| AnyProvider & object
| I[number];Defined in: module.ts:146
An export entry is legal only if it is an available port, a provider for one (normalised to provider.port by ModuleDeclaration below), or an imported module (whole-module re-export).
The provider arm exists because the helpers that mint their own port — Config.provider(name)(schema), a starter's HttpRouter(contract)(…) — hand back a provider and no class to name, so exports: [ordersController] is the only spelling that does not go back through .port. It is AnyProvider &, not a bare { port }, so nothing but a provider matches.
The intersection AnyPort & (new () => Available<I, P>) checks a candidate port class's constructor return type — its instance type — against the Available union. Return-type position is covariant, so it is unaffected by the contravariant-field quirk above: AppConfig's instance type is PortInstance<"MAppConfig", Shape>, branded by the literal id, so it is only assignable into Available<I, P> when some available port shares that exact id. That is a genuine check, not a vacuous one — a port that is not available fails it.
This must stay a plain union member, not a generic helper invoked with AnyPort as its argument: instantiating a per-element conditional with the whole AnyPort union (rather than letting each array element be checked against the intersection directly) tests whether every possible port is available, which is never true and rejects legal exports too. Checked directly like this, TypeScript validates each array element against the intersection individually when the exports literal is checked against readonly Exportable<I, P>[].
Type Parameters
| Type Parameter |
|---|
I extends readonly AnyModule[] |
P extends readonly AnyProvider[] |
Module
type Module<Exports, E, Needs> = object;Defined in: module.ts:75
The variance rule, stated once and shared with Provider (see the identical note above Provider's own phantom fields in provider.ts):
Capability channels (
_port,_exports) are contravariant, so you may forget what you have. Obligation channels (_error,_needs) are covariant, so you may not forget what you owe.
Each field's own comment below says why that direction is the right one for that channel; this is the one-line rule they are instances of. It is stated here and in provider.ts because its absence is exactly how Provider came to drift from Module after Task 4 fixed only the latter.
Type Parameters
| Type Parameter |
|---|
Exports |
E |
Needs |
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
_error | readonly | () => E | module.ts:92 |
_exports | readonly | (x) => void | module.ts:76 |
_needs | readonly | () => Needs | module.ts:105 |
exports | readonly | readonly (AnyPort | AnyModule)[] | module.ts:109 |
imports | readonly | readonly AnyModule[] | module.ts:107 |
name | readonly | string | module.ts:106 |
provides | readonly | readonly AnyProvider[] | module.ts:108 |
PortClass()
type PortClass<Id> = PortInstance<Id, Service>;Defined in: port.ts:14
Type Parameters
| Type Parameter |
|---|
Id extends string |
type new PortClass<Service>(): PortInstance<Id, Service>;Returns
PortInstance<Id, Service>
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
portId | readonly | Id | port.ts:16 |
PortClassOf()
type PortClassOf<Id, Service> = PortInstance<Id, Service>;Defined in: port.ts:51
A concrete port class with Id and Service fixed — what a helper that hands a port to a caller (Config.provider("RelayConfig")(schema), which mints one; a starter's HttpRouter(contract)(deps, arm), which targets its own fixed one) returns as the type of provider.port. A class expression (class extends Port(id)<S> {}) has an anonymous type declaration emit cannot name across packages, and a Port(id) left generic and typed per contract has none of its own; this is the nameable spelling of both.
Type Parameters
| Type Parameter |
|---|
Id extends string |
Service |
type new PortClassOf(): PortInstance<Id, Service>;A concrete port class with Id and Service fixed — what a helper that hands a port to a caller (Config.provider("RelayConfig")(schema), which mints one; a starter's HttpRouter(contract)(deps, arm), which targets its own fixed one) returns as the type of provider.port. A class expression (class extends Port(id)<S> {}) has an anonymous type declaration emit cannot name across packages, and a Port(id) left generic and typed per contract has none of its own; this is the nameable spelling of both.
Returns
PortInstance<Id, Service>
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
portId | readonly | Id | port.ts:52 |
PortInstance
type PortInstance<Id, Service> = object;Defined in: port.ts:9
The type that appears in a Needs / Exports union. Identity is the literal Id: two ports declared with different ids have different instance types even when their service shapes are identical.
Extended by
Type Parameters
| Type Parameter |
|---|
Id extends string |
Service |
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
[ID] | readonly | Id | port.ts:10 |
[SERVICE] | readonly | Service | port.ts:11 |
Provider
type Provider<P, E, N> = object;Defined in: provider.ts:237
The package's variance rule, stated here and on Module (module.ts):
Capability channels (
_port,_exports) are contravariant, so you may forget what you have. Obligation channels (_error,_needs) are covariant, so you may not forget what you owe.
With _error/_needs contravariant — as they were until this fix, Task 4 having corrected only Module — an ordinary return-type annotation laundered both, and with them the Scope that ScopeOf puts in Needs, routing a resourceful provider to Module.build (which never closes the scope it opens) and silently dropping its release. See the three "cannot be laundered" tests in provider.test-d.ts.
Type Parameters
| Type Parameter |
|---|
P |
E |
N |
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
_error | readonly | () => E | provider.ts:166 |
_needs | readonly | () => N | provider.ts:167 |
_port | readonly | (p) => void | provider.ts:165 |
construct | readonly | (services) => AsyncResult<unknown, unknown> | provider.ts:175 |
deps | readonly | readonly AnyPort[] | provider.ts:169 |
onStart | readonly | ((service) => void | Promise<void>) | undefined | provider.ts:179 |
onStop | readonly | ((service) => void | Promise<void>) | undefined | provider.ts:180 |
port | readonly | AnyPort | provider.ts:168 |
release | readonly | ((service) => void | Promise<void>) | undefined | provider.ts:176 |
ScopedOptions
type ScopedOptions = object;Defined in: build.ts:192
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
onTeardownError? | readonly | TeardownReporter | build.ts:193 |
ServiceOf
type ServiceOf<T> = T extends PortInstance<string, infer S> ? S : T extends () => PortInstance<string, infer S> ? S : never;Defined in: port.ts:57
Recovers a service shape from either the instance type or the class.
Type Parameters
| Type Parameter |
|---|
T |
Variables
Context
Context: object;Defined in: context.ts:30
Type Declaration
| Name | Type | Defined in |
|---|---|---|
empty() | () => Context<never> | context.ts:72 |
Module
Module: <Name>(name) => <I, P, X>(options) => Module<ResolvedExports<X>, ErrOf<P[number]> | ErrOfModule<I[number]>,
| Exclude<NeedOf<P[number]>, Available<I, P>>
| Exclude<NeedsOfModule<I[number]>, Available<I, P>>> & object;Defined in: module.ts:75
Module.build sorts the tree into dependency-ordered levels, checks for wiring bugs (cycles, duplicate providers) before any factory runs, then constructs level by level. See build.ts for the implementation; this is just the typed entry point, namespaced on Module per the package's convention of hanging operations off the type's constructor.
The rest parameter is the compile-time gate for unmet dependencies: when N (the module's remaining Needs) is never, ..._missing is typed as the empty tuple [], so Module.build(mod) is a normal one-argument call. When N is not never, the tuple has two required elements, so calling with just mod is an arity error — the module's unmet dependency becomes a compile error at the call site, not a runtime surprise.
Type Declaration
| Name | Type | Description | Defined in |
|---|---|---|---|
build() | <X, E, N>(module, ..._missing) => AsyncResult<Context<X>, E> | - | module.ts:212 |
forkScope() | <PParent, X, E, N, A, E2>(parent, module, use, options?, ..._missing) => AsyncResult<A, E | E2> | A short-lived scope layered over an already-built parent Context, for per-request services (a transaction, a request id) that must not outlive the request but do need to read services the parent already constructed (a pool, config). Built from the exact same runScoped Module.scoped uses above — Task 6 gave runScoped a seed parameter precisely so this did not need any new machinery — just seeded with parent instead of Context.empty() and handed a fresh createScope (that happens inside runScoped itself). That fresh scope is exactly what makes the two load-bearing guarantees hold: the parent's own services were never passed through run here (only module's providers are flattened and constructed against this call), so none of the parent's finalisers are registered on this scope — closing it therefore releases only what this fork acquired, and the parent stays up for a second, sibling fork or for whatever the enclosing Module.scoped does after this call returns. The gate is scoped's, with one more exclusion: `Exclude<N, PParent | Scope>rather thanExclude<N, Scope>. PParent— the parentContext's own channel — is subtracted because a request module is allowed to depend on anything the parent already provides (that is the entire point of forking over a *built* parent instead of an empty one); only a need that neither the request module itself nor the parent satisfies must surface as the "UNSATISFIED DEPENDENCIES" arity error, exactly as NeedsMissingdoes infork.test-d.ts`. |
scoped() | <X, E, N, A, E2>(module, use, options?, ..._missing) => AsyncResult<A, E | E2> | Module.build's resourceful counterpart: opens a scope, runs the module, hands the built Context to use, and closes the scope — releasing every already-acquired resource, LIFO — on every path out, whether construction failed, use failed, or use succeeded. See build.ts's runScoped for the unwind itself. The gate mirrors build's — a rest parameter that is the empty tuple only when there is nothing left unmet — except it excludes Scope first: Scope is not a real dependency the caller must supply, it is the phantom marker that routed the module here in the first place, and this is the one entry point that discharges it (by actually opening a createScope, unlike build, which never sees a resourceful module at all — Scope in Needs makes that a compile error). Any other unmet requirement in N still has to surface, so Exclude<N, Scope>, not a blanket bypass, is what the rest parameter is computed from. | module.ts:234 |
Functions
Port()
function Port<Id>(id): PortClass<Id>;Defined in: port.ts:113
A function declaration, not a const, specifically so it hoists: Scope above calls it at module-evaluation time.
Type Parameters
| Type Parameter |
|---|
Id extends string |
Parameters
| Parameter | Type |
|---|---|
id | Id |
Returns
PortClass<Id>
Provider()
function Provider<P, S>(port): {
<D, O> (deps, options): Provider<InstanceType<P>, ErrorOf<O>, InstanceType<D[number]> | ScopeOf<O>> & object;
<O> (options): Provider<InstanceType<P>, ErrorOf<O>, ScopeOf<O>> & object;
};Defined in: provider.ts:237
Type Parameters
| Type Parameter | Default type |
|---|---|
P extends AnyPort | - |
S | ServiceOf<P> |
Parameters
| Parameter | Type |
|---|---|
port | P |
Returns
{ <D, O> (deps, options): Provider<InstanceType<P>, ErrorOf<O>, InstanceType<D[number]> | ScopeOf<O>> & object; <O> (options): Provider<InstanceType<P>, ErrorOf<O>, ScopeOf<O>> & object; }