Migration
InferDI records breaking changes by major version. The source of truth remains packages/inferdi/MIGRATION.md, but the current migration path is summarized here.
Migration to 5.0
The initial v5 release was adapter-only. The version bump keeps all published packages in lockstep and aligns framework adapters around one cleanup contract. Later v5 builds also enforce child-scope ownership and tighten the Fast Mode contract described below.
Adapter contracts now share these rules:
createScope,setupScope,disposeScope,autoDispose, andonDisposeErroruse the same vocabulary.MaybePromise,InferdiScope,InferdiRoot, andInferdiScopeOfare exported across adapters.- If
setupScopefails, the adapter surfaces only the original setup error. - Cleanup failures during setup teardown go to
onDisposeErroror the adapter sink. - A failed request disposes its scope even after
skipInferdiDispose, except for the documented Express limitation. - Cleanup hooks see the public scope slot while they run.
Scoped Resolution Requires a Child Scope
With the default strict: true, resolving a scoped key from the root now throws Scoped "key" cannot be resolved from the root container. Use createScope(). Create a child with const scope = root.createScope(), call scope.get(scopedKey), and dispose the child at its lifecycle boundary. Fast Mode skips this runtime guard. Application code must resolve scoped keys from child scopes.
Fast Mode Immutable Graph Contract
new Container({ strict: false }) now reads the immutable root registry directly from scopes, avoids parent walks, and mirrors delegated singletons into the scope cache. Strict scopes walk their exact parent chain on every local miss instead of retaining parent-lookup snapshots, so mutations stay visible without invalidation bookkeeping or per-scope lookup metadata. Owned-instance identity de-duplication runs during disposal in both modes. Register each runtime key once through one linear fluent chain, complete registration before the first resolve or scope, keep the activated tree immutable, and dispose child scopes before their ancestors. Use strict: true for hot reload or any tree that changes after activation.
Adapter Notes
| Package | Migration notes |
|---|---|
@inferdi/fastify | Rename logDisposeError to onDisposeError; InferdiScope.dispose() may return void or Promise<void>; disposeScope, autoDispose, skipInferdiDispose, and InferdiScopeOf were added. |
@inferdi/hono | Cleanup failures after next() are logged or sent to onDisposeError; they no longer replace a successful response. Setup teardown no longer throws AggregateError. |
@inferdi/express | onDisposeError is now a per-error sink for setup teardown and response completion. Express cannot force-dispose a skipped scope on a handled route error. |
@inferdi/koa | Setup teardown surfaces only the setup error. A downstream error disposes even after skipInferdiDispose(ctx). |
@inferdi/elysia | Setup teardown surfaces only the setup error. Cleanup failure goes to onDisposeError or console.error. |
Migration to 4.0
v4 tightens Lazy<T> lifetime semantics. A managed lazy companion now preserves the target lifetime. A singleton may inject only Lazy<singleton>.
Main changes:
AllowedDeps<T, 'singleton'>no longer accepts arbitraryLazy<V>.LazySpec<V, TargetKind>became a public type for explicit container and module shapes.- The runtime lazy exemption applies only when the target kind is
singleton. - A singleton that injected
Lazy<scoped>orLazy<transient>must change either the target lifetime or the consumer lifetime.
Common fixes:
// v3
.registerClass('req', RequestContext, [], 'scoped', 'reqLazy')
.registerClass('app', AppService, ['reqLazy'], 'singleton')
// v4: make the consumer scoped
.registerClass('req', RequestContext, [], 'scoped', 'reqLazy')
.registerClass('app', AppService, ['reqLazy'], 'scoped')// v3
type Deps = SpecMap<{ clock: Clock }> & {
clockLazy: Spec<Lazy<Clock>, 'transient'>
}
// v4
type Deps = SpecMap<{ clock: Clock }> & {
clockLazy: LazySpec<Clock, 'singleton'>
}Migration to 3.0
v3 moves lifetime safety into the type system. Runtime behavior stays compatible, and strict runtime guards remain defense-in-depth.
Main changes:
DependenciesMapentries becameSpec<V, Kind>instead of bare service types.RegistrationKind,Spec<V, K>, andSpecMap<M, K>became public exports.registerFactorynarrows itscparameter for singleton factories.registerClassfiltersdepsfor singleton registrations.override(key, value)preserves the original lifetime kind.new Container({ strict: false })can disable runtime cycle and lifetime guards after a graph audit.
Common fixes:
// v2
const c = new Container() as Container<{ a: A; b: B }>
// v3
const c = new Container() as Container<SpecMap<{ a: A; b: B }>>// v2
const mod: Module<{ cfg: Config }, { db: Db }> = (c) => ...
// v3
const mod: Module<
SpecMap<{ cfg: Config }>,
SpecMap<{ db: Db }>
> = (c) => ...Migration to 2.0
v2 has two mechanical breaking changes.
container.cradle was removed
Use .get(key):
// 1.x
const { db, logger } = container.cradle
// 2.x
const db = container.get('db')
const logger = container.get('logger')registerClass(..., lazy: true) became lazyKey
Pass the companion key:
// 1.x
.registerClass('clock', Clock, [], 'transient', true)
// 2.x
.registerClass('clock', Clock, [], 'transient', 'clockLazy')v2 also added string or symbol keys to every registration method and improved disposed-ancestor diagnostics.
Version Lockstep
All published InferDI packages share the same version:
When upgrading adapters, keep the adapter package and @inferdi/inferdi on matching major versions.
Upgrade Checklist
- Read the migration notes for every major version crossed.
- Upgrade
@inferdi/inferdiand all installed adapters together. - Run type tests or
tsc --noEmitto catch graph-shape changes. - Run runtime tests in strict mode.
- Review request-scope ownership if you use
skipInferdiDispose,autoDispose: false, or customdisposeScope.
Stable Boundaries
The core package remains decorator-free and zero-dependency. Framework lifecycle behavior lives in adapter packages, not in @inferdi/inferdi.
