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.
Semaphore funcional y decorador
Sección titulada «Semaphore funcional y decorador»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.
Caché por receiver, solo opt-in
Sección titulada «Caché por receiver, solo opt-in»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.
Capacidad de cachedFor
Sección titulada «Capacidad de cachedFor»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.
Claves JSON y escape hatch
Sección titulada «Claves JSON y escape hatch»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.
CircuitBreaker isFailure
Sección titulada «CircuitBreaker isFailure»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.
Retry con cap y jitter
Sección titulada «Retry con cap y jitter»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.
Observabilidad y medición
Sección titulada «Observabilidad y medición»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:
npm run bench -- --output bench-baseline.jsonnpm run bench -- --compare bench-baseline.json --max-ratio 3 --output bench-current.jsonLa 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.

