Ir al contenido

Migración a 2.0

La versión 2.0 mantiene Node 20+ y los decoradores estándar de TypeScript, pero corrige comportamientos observables de 1.x. Esta guía cubre los cambios incompatibles y enlaza las pruebas que definen cada contrato.

En 1.x, @TransitionTo cambiaba el estado justo después de invocar el método, aun cuando éste devolviera una promesa pendiente o finalmente rechazara. En 2.0, un Promise o PromiseLike sólo confirma la transición cuando se resuelve; un rechazo conserva el estado anterior y se propaga sin envolver la razón.

Antes (1.x):

@When('draft')
@TransitionTo('review')
async submit(): Promise<void> {
await saveDraft();
}
const result = doc.submit();
doc.state; // 'review' antes de que result se resuelva

Después (2.0):

const result = doc.submit();
doc.state; // 'draft' mientras result está pendiente
await result;
doc.state; // 'review'

Si await result rechaza, doc.state continúa siendo 'draft'. Mantén @When encima de @TransitionTo para que la guarda de estado se ejecute primero. Contrato verificable: tests/state-machine-async.test.ts y tests/state-machine.test.ts.

@Timeout(ms) sigue rechazando la promesa devuelta cuando vence el límite y limpia su temporizador al resolver, rechazar o agotar el plazo. No inyecta un AbortSignal ni puede cancelar de forma segura una operación que no coopera: el trabajo original puede seguir ejecutándose y su resultado se descarta.

Antes y después, usa el resultado decorado como límite de espera:

class Api {
@Timeout(500)
async fetchUser(id: string): Promise<User> {
return requestUser(id);
}
}
await new Api().fetchUser('42'); // rechaza tras 500 ms si no termina

Si necesitas cancelación, haz que tu propia operación acepte y observe una señal; 2.0 no añade argumentos ni una cancelación implícita. Contrato verificable: tests/timeout.test.ts.

En 1.x, una base decorada podía compartir por accidente su instancia o cache con una derivada no decorada. En 2.0, el cache se particiona por el constructor efectivo (newTarget): base y derivada son distintas incluso con el mismo argumento, y cada clase conserva su propia reutilización.

Antes (1.x):

@Singleton
class Base {}
class Derived extends Base {}
new Base() === new Derived(); // podía ser true

Después (2.0):

const base = new Base();
const derived = new Derived();
base === derived; // false
new Base() === base; // true
new Derived() === derived; // true

El mismo aislamiento aplica a @Flyweight: new Base('x') y new Derived('x') no comparten entrada, pero cada clase reutiliza su propia entrada para 'x'.

Hay una limitación de JavaScript para @Singleton: si la derivada no está decorada, su constructor se ejecuta en cada new Derived(...) después de super(). Puede mutar campos propios con argumentos posteriores aunque reutilice la misma instancia derivada. Decora la derivada si necesitas controlar también esa inicialización. Contrato verificable: tests/singleton.test.ts, tests/flyweight.test.ts y tests/characterization/inheritance.test.ts.

Container ahora detecta un ciclo durante la resolución síncrona y lanza el error público CircularDependencyError. El error expone tokenPath y un mensaje legible, por ejemplo A -> B -> A.

Antes (1.x):

container.resolve(A); // un ciclo A -> B -> A agotaba la pila

Después (2.0):

try {
container.resolve(A);
} catch (error) {
if (error instanceof CircularDependencyError) {
console.log(error.tokenPath); // ['A', 'B', 'A']
}
}

El contenedor permanece deliberadamente pequeño: providers sólo síncronos (useValue, useClass, useFactory), scopes singleton y transient; no hay providers async, scopes adicionales, contenedores hijo, disposal ni unregister. Contrato verificable: tests/injection.test.ts y tests/characterization/injection-contract.test.ts.

2.0 añade una API funcional (/operators, /pipe, /run, /policy), observabilidad (/observability), stores async (/store) y contexto de request para Node (/node). Son subpaths nuevos y opt-in: el código que usa solo decoradores no tiene que cambiar nada por ellos. La librería continúa siendo dependency-free en runtime y requiere Node 20+.