Migración
InferDI registra los cambios incompatibles por versión major. La fuente de verdad sigue siendo packages/inferdi/MIGRATION.md, pero aquí se resume la ruta de migración actual.
Migración a 6.0
Este resumen presupone una actualización desde la versión estable 5.0.7. Actualiza todos los paquetes @inferdi/* instalados a 6.0.0; los adaptadores requieren @inferdi/inferdi@^6.0.0.
- Sustituye
RegistrationKindporLifetimeySpec.kindporSpec.lifetime; no queda alias obsoleto. - La versión estable v5 no tenía un overload de
registerFactorycon deps. V6 añaderegisterFactory(key, factory, deps, ...); solo los usuarios de la forma prereleaseregisterFactory(key, deps, factory, ...)deben reordenar los argumentos. Un acompañante de factoría sync exige un lifetime explícito, incluido'singleton'. - Sustituye
{strict: false}de v5 por{fast: true}, y{strict: true}por el valor predeterminado o{fast: false}. La polaridad del booleano se invierte. La opción prereleasemodese eliminó. - Un
Module<TRequirements, TProvides>con nombre acepta grafos reales con registros extra, los conserva, comprueba requisitos exactos y rechaza colisiones de outputs.new Container(parent)deja de ser público; usacreateScope(). - El registro rechaza ahora cualquier tipo de clave que pueda solaparse con una clave principal o lazy existente. Acota las claves amplias o union a un miembro nuevo, o usa
.override()para un reemplazo intencional. - V6 añade inputs de scope solo de tipos mediante
declareScopeInputs<Inputs>()ycreateScope(inputs). Los scopes existentes sin argumentos conservan el comportamiento de v5. - V6 añade
registerAsyncFactory,AsyncSpecygetAsync()para dependencias async declarativas. UnregisterFactoryque devuelve una Promise sigue siendo un servicio síncrono del grafo y se resuelve medianteget(). - El teardown async informa una vez de un objeto de rechazo compartido cuando un fallo de dependencia se propaga por varias Promises en caché. El teardown sync observa el rechazo de una Promise nativa antes de lanzar el error de uso async incorrecto.
Los resolvers genéricos usan claves listas
.get() ahora acepta claves síncronas listas cuyos inputs de scope se hayan proporcionado. Los contenedores concretos sin inputs de scope mantienen el mismo conjunto de claves síncronas. Los helpers genéricos con K extends keyof T deben conservar la preparación y el estado async.
// Before
function resolve<T extends DependenciesMap, K extends keyof T>(
container: Container<T>,
key: K
) {
return container.get(key)
}
// After
function resolve<
T extends DependenciesMap,
K extends Container.SyncReadyKeys<Container<T>>
>(container: Container<T>, key: K) {
return container.get(key)
}Usa Container.ReadyKeys<Container<T>> en helpers genéricos que llamen a getAsync(). Un T extends DependenciesMap genérico puede contener entradas async declarativas o servicios bloqueados por inputs de scope ausentes.
Specs con nombre para acompañantes lazy
LazySpec incorpora un brand privado solo de tipos y v6 añade AsyncLazySpec. Las formas explícitas de Container y Module deben usar estas exportaciones con nombre en vez de reproducir {type, lifetime, lazyOf}. El brand no crea un campo de runtime.
El quinto lazyKey de registerAsyncFactory produce AsyncLazy<T>. Las clases async usan el mismo wrapper; una clase mixed sync/async expone Lazy<T> | AsyncLazy<T>. Los acompañantes de registerFactory Promise-valued siguen siendo Lazy<Promise<T>>. Container.ResolveUnwrapped desenvuelve de forma distributiva todos los modos administrados.
Los conjuntos de claves nuevos se describen en Resumen de la API, Entradas de scope y Dependencias asíncronas.
Migración a 5.0
La release inicial de v5 solo afectó a los adaptadores. El incremento de versión mantiene todos los paquetes publicados en lockstep y alinea los adaptadores de frameworks alrededor de un único contrato de limpieza. Las builds posteriores de v5 también aplican la propiedad del scope hijo y endurecen el contrato {fast: true} descrito a continuación.
Los contratos de los adaptadores ahora comparten estas reglas:
createScope,setupScope,disposeScope,autoDisposeyonDisposeErrorusan el mismo vocabulario.MaybePromise,InferdiScope,InferdiRooteInferdiScopeOfse exportan en todos los adaptadores.- Si
setupScopefalla, el adaptador expone únicamente el error de setup original. - Los fallos de limpieza durante la liberación del setup van a
onDisposeErroro al sink del adaptador. - Una petición fallida libera su scope incluso después de
skipInferdiDispose, salvo por la limitación documentada de Express. - Los hooks de limpieza ven el slot público del scope mientras se ejecutan.
La resolución scoped requiere un scope hijo
Con el valor predeterminado {fast: false}, resolver una clave scoped desde la raíz ahora lanza Scoped "key" cannot be resolved from the root container. Use createScope(). Crea un contenedor hijo con const scope = root.createScope(), llama a scope.get(scopedKey) y libera el hijo en su límite de ciclo de vida. {fast: true} omite este guard en runtime, pero los servicios scoped deben seguir resolviéndose desde scopes hijos.
Contrato de grafo fijo de fast: true
new Container({fast: true}) lee el registro raíz inmutable directamente desde los scopes, evita recorrer los padres y refleja los singletons delegados en la caché del scope. Los scopes strict recorren su cadena exacta de padres en cada fallo local en lugar de conservar instantáneas de búsqueda, por lo que las mutaciones siguen visibles sin contabilidad de invalidación ni metadatos por scope. La deduplicación por identidad de instancias owned se ejecuta durante el disposal en ambos modos. Registra cada clave de runtime una sola vez mediante una cadena fluent lineal, completa el registro antes de la primera resolución o creación de scope, mantén inmutable el árbol activado y elimina los scopes hijos antes que sus ancestros. Usa {fast: false} para hot reload o cualquier árbol que cambie después de activarse.
Notas de los adaptadores
| Paquete | Notas de migración |
|---|---|
@inferdi/fastify | Renombra logDisposeError a onDisposeError; InferdiScope.dispose() puede devolver void o Promise<void>; se añadieron disposeScope, autoDispose, skipInferdiDispose e InferdiScopeOf. |
@inferdi/hono | Los fallos de limpieza después de next() se registran o se envían a onDisposeError; ya no reemplazan una respuesta exitosa. La liberación del setup ya no lanza AggregateError. |
@inferdi/express | onDisposeError es ahora un sink por error para la liberación del setup y la finalización de la respuesta. Express no puede forzar la liberación de un scope omitido ante un error de ruta manejado. |
@inferdi/koa | La liberación del setup expone únicamente el error de setup. Un error aguas abajo libera incluso después de skipInferdiDispose(ctx). |
@inferdi/elysia | La liberación del setup expone únicamente el error de setup. Un fallo de limpieza va a onDisposeError o a console.error. |
Migración a 4.0
v4 endurece la semántica de tiempo de vida de Lazy<T>. Un companion lazy gestionado ahora preserva el tiempo de vida del objetivo. Un singleton solo puede inyectar Lazy<singleton>.
Cambios principales:
AllowedDeps<T, 'singleton'>ya no acepta unLazy<V>arbitrario.LazySpec<V, TargetKind>pasó a ser un tipo público para formas explícitas de contenedores y módulos.- La exención lazy en runtime solo se aplica cuando el tipo del objetivo es
singleton. - Un singleton que inyectaba
Lazy<scoped>oLazy<transient>debe cambiar o el tiempo de vida del objetivo o el del consumidor.
Correcciones comunes:
// 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'>
}Migración a 3.0
v3 traslada la seguridad de los tiempos de vida al sistema de tipos. El comportamiento en runtime se mantiene compatible, y los guards de runtime predeterminados siguen siendo defensa en profundidad.
Cambios principales:
- Las entradas de
DependenciesMappasaron a serSpec<V, Kind>en lugar de tipos de servicio simples. RegistrationKind,Spec<V, K>ySpecMap<M, K>pasaron a ser exportaciones públicas.registerFactoryestrecha su parámetrocpara las factorías singleton.registerClassfiltradepspara los registros singleton.override(key, value)preserva el tipo de tiempo de vida original.new Container({fast: true})puede deshabilitar los guards de ciclo y tiempo de vida en runtime tras una auditoría del grafo.
Correcciones comunes:
// 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) => ...Migración a 2.0
v2 tiene dos cambios incompatibles mecánicos.
Se eliminó container.cradle
Usa .get(key):
// 1.x
const { db, logger } = container.cradle
// 2.x
const db = container.get('db')
const logger = container.get('logger')registerClass(..., lazy: true) pasó a ser lazyKey
Pasa la clave del companion:
// 1.x
.registerClass('clock', Clock, [], 'transient', true)
// 2.x
.registerClass('clock', Clock, [], 'transient', 'clockLazy')v2 también añadió claves de tipo string o symbol a todos los métodos de registro y mejoró los diagnósticos de ancestro liberado.
Lockstep de versiones
Todos los paquetes publicados de InferDI comparten la misma versión:
@inferdi/inferdi@inferdi/fastify@inferdi/hono@inferdi/koa@inferdi/express@inferdi/elysia@inferdi/react
Al actualizar los adaptadores, mantén el paquete del adaptador y @inferdi/inferdi en versiones major coincidentes.
Lista de verificación de actualización
- Lee las notas de migración de cada versión major que atravieses.
- Actualiza
@inferdi/inferdiy todos los adaptadores instalados juntos. - Ejecuta las pruebas de tipos o
tsc --noEmitpara detectar cambios en la forma del grafo. - Ejecuta las pruebas de runtime con el contrato checked predeterminado.
- Revisa la propiedad del scope de petición si usas
skipInferdiDispose,autoDispose: falseo undisposeScopepersonalizado.
Fronteras estables
El paquete core sigue siendo libre de decoradores y sin dependencias. El comportamiento del ciclo de vida de los frameworks vive en los paquetes de adaptadores, no en @inferdi/inferdi.
