Ir al contenido

Migración a 2.2

2.2.0 amplía la base de correctness de 2.1 mediante opciones explícitas y un operador funcional. Es la referencia publicada para la migración a 2.3. Mantiene Node 20+, TypeScript estricto, decoradores TC39, ESM/CJS, subpaths, tree-shaking y cero dependencias de runtime.

import { semaphore } from 'annotations-design-patterns/operators';
import { pipe } from 'annotations-design-patterns/pipe';
const limited = pipe<object, readonly [tenantId: string], string>(
async (tenantId) => tenantId,
semaphore({
permits: 4,
key: ({ args }) => args[0],
maxQueue: 100,
}),
);
await limited.call({}, 'tenant-a');

semaphore() siempre devuelve Promise. El trabajo admitido comienza de forma síncrona y el permiso se libera cuando termina la ejecución interna, tanto en resolve como en reject o throw. Los permisos son enteros seguros positivos; maxQueue, si se configura, es un entero seguro no negativo. La cola por defecto no tiene límite configurado.

Sin key, una pipeline aplicada comparte un pool entre receivers. Con key, cada clave tiene su pool; el resolver recibe el contexto readonly usado por overlap y keyedLock. Una key explícita de run() tiene prioridad sobre el resolver.

La admisión es FIFO y una llegada nueva no adelanta a un waiter ya admitido. Una señal preabortada impide ejecutar; un waiter abortado se retira de la cola. El aborto del holder rechaza a su llamante, pero el trabajo no cooperativo conserva el permiso hasta terminar. Al llenarse la cola se rechaza con SemaphoreQueueFullError, cuyo código es el existente POLICY_ERROR y cuya propiedad key identifica la partición si existe.

@Semaphore(n) mantiene pools por instancia y su canal de errores mediante Promise; ahora adapta el mismo operador y PermitQueue. Se reutilizan semaphore.queued, semaphore.acquired y semaphore.released sin ampliar la unión PolicyEvent.

import { cachedFor, memoize } from 'annotations-design-patterns/operators';
const memoized = memoize<object, readonly [id: string], string>({
perReceiver: true,
}).apply((id) => id);
const cached = cachedFor<object, readonly [id: string], string>(60_000, {
perReceiver: true,
maxEntries: 1_000,
}).apply((id) => id);

Con perReceiver true, objetos y funciones diferentes tienen cachés distintas; el mismo receiver con los mismos argumentos comparte entrada. La identidad se almacena en WeakMap. Undefined, null y todos los primitivos, incluidos symbols, producen un TypeError explícito. La opción definida debe ser booleana.

Sin la opción, el comportamiento no cambia: la caché funcional se comparte entre receivers de un método aplicado. Los decoradores continúan aislando por instancia.

memoizeWithHandle() y cachedForWithHandle() no admiten perReceiver en sus opciones de objeto literal y rechazan activarlo en runtime. Una variable de opciones estructuralmente compatible puede compilar, pero no habilita un modo sin contrato de invalidación. Los handles siguen ofreciendo solo invalidate(key: string): void y clear(): void, sin size ni enumeración de particiones débiles.

Las opciones nuevas perReceiver/maxEntries se capturan al crear la fábrica. Mutar el objeto original después no cambia particiones ni capacidad. El resolver key existente conserva su captura al aplicar el operador.

maxEntries es un entero seguro positivo; Infinity explícito, cero y fracciones son inválidos. Omitirlo conserva la capacidad ilimitada actual. El límite se aplica por partición de receiver o por handle compartido entre aplicaciones de ese mismo operador.

La expulsión sigue el orden de inserción: un hit no rejuvenece la entrada. Las promesas pendientes cuentan; expulsarlas no cancela a quienes ya las esperan. Un rechazo antiguo no puede borrar una entrada nueva de la misma clave.

Solo las cachés limitadas limpian entradas expiradas de forma perezosa en un miss. Esa limpieza por TTL no emite invalidaciones; la expulsión por capacidad emite el kind existente cache.invalidated con la clave expulsada. El límite se restablece antes de avisar al sink, también si este reentra. No hay timers permanentes, sweeper global ni política LRU.

TTL sigue empezando al invocar, también con promesas pendientes. TTL Infinity significa sin caducidad; cachedFor(0) funcional conserva caché desactivada.

El default sigue siendo JSON.stringify(args). No se introduce un serializador universal ni se altera la identidad de entradas existentes.

Entrada Límite del default
undefined, null, NaN, infinitos, Symbols o funciones como argumento Pueden convertirse en [null] y colisionar
-0 y 0 Misma representación
Propiedades undefined/Symbol/función y claves Symbol Se omiten; pueden colisionar con un objeto vacío
Objetos El orden de inserción de propiedades puede distinguir claves; no es identidad de referencia
Arrays Se conserva el orden; huecos, undefined y null pueden colisionar
Date Usa su ISO; puede colisionar con el string equivalente; una Date inválida se convierte en null
Map y Set Sin propiedades serializables propias suelen equivaler a {}
BigInt u objetos cíclicos Lanzan TypeError antes de ejecutar el método
toJSON personalizado o getters Se ejecutan y pueden cambiar la clave o lanzar
Receiver No entra en la clave; usa perReceiver si necesitas separar identidades

Usa key: (...args) => string para claves del dominio, incluyendo casos con BigInt, ciclos o identidad. @MemoizeByRef sigue disponible como decorador con semántica por referencia distinta; no cambia el default funcional.

El predicado síncrono isFailure(error, info) está disponible en el operador, @CircuitBreaker y definePolicy. info ofrece receiver, copia readonly de args, key, operationId y metadata, sin signal, sink o controles mutables de ejecución. La protección es superficial: el receiver y los objetos contenidos siguen siendo los de la aplicación.

true cuenta el fallo. false lo ignora: no incrementa ni resetea fallos y no equivale a éxito. El error original sigue rechazando. En HALF_OPEN, ignored libera la probe vigente conservando el historial OPEN y sin cerrar el circuito. Una excepción del predicado rechaza con ese error de decisión, sin contar el error original, como ocurre con shouldRetry. La cancelación del llamante y las generaciones obsoletas se resuelven antes de invocar el predicado.

maxDelayMs admite valores finitos de 0 a 2_147_483_647, incluidas fracciones. El clamp técnico del timer sigue aplicándose aunque no configures esa opción.

Orden: delay base → backoff exponencial → mínimo del cap técnico y maxDelayMs si existe → jitter.

jitter Espera a partir del delay limitado cap
none, default cap; no consulta Math.random
full random × cap
equal cap / 2 + random × cap / 2

Un resultado cero usa el camino de reintento por microtask cancelable. No se añade una fuente aleatoria pública. @Retry y definePolicy comparten las opciones; describe solo añade maxDelayMs/jitter cuando se configuran explícitamente.

Tipos, diagnostics y límites que se conservan

Sección titulada «Tipos, diagnostics y límites que se conservan»

Los mapas de eventos Observer ahora pueden declararse como interfaces nombradas, manteniendo la relación evento/payload de On y Emits y el escape hatch de Subject sin genérico con payload unknown. Las comprobaciones de estados cerrados de When/TransitionTo no cambian.

Los decoradores que necesitan identidad de instancia dan un error claro al invocarse desacoplados o con receiver primitivo, incluido Symbol. Se conservan objetos/funciones válidos y el canal síncrono o Promise que ya usaba cada API.

La cancelación compartida sigue aislada por consumidor: abandonar todos no aborta automáticamente el trabajo. rateLimit funcional, políticas nuevas para la carrera async de StateMachine, integración distribuida de stores/locks, adapters de vendor y registros globales de testing siguen fuera del runtime. Los diseños y decisiones están en POST-2.1-RFCS.md del repositorio; no son APIs publicadas.

La guía de observabilidad usa los exports y eventos reales y muestra recetas externas con callbacks. Ningún SDK de tracing o métricas se añade al paquete.

npm run bench mide diez caminos con warmup y varias muestras: llamada directa, operador, decorador, pipe, policy, memoize hit/miss, éxito de retry, breaker CLOSED y semaphore sin contención. Para comparar dos ejecuciones manualmente:

Ventana de terminal
npm run bench -- --output bench-baseline.json
npm run bench -- --compare bench-baseline.json --max-ratio 3 --output bench-current.json

La comparación requiere la misma versión de Node, plataforma, arquitectura y parámetros de muestreo; el límite 3× detecta regresiones grandes y no sustituye mediciones de producción. Los percentiles son de medias de muestras, no de latencias individuales. npm run bundle:check mantiene presupuestos de tamaño reproducibles; los cambios se revisan con deltas medidos, sin rebasing automático.