Ir al contenido

Migración a 2.3

2.3.0 conserva Node 20+, TypeScript 5.0+, decoradores TC39, las subrutas ESM/CJS y cero dependencias de runtime. Las APIs experimentales no se promueven automáticamente por publicar una versión estable del paquete. Las pruebas de integración no certifican proveedores remotos ni coordinación distribuida.

Funcionalidad Estado en 2.3 Importación
rateLimit, memoize({ maxEntries }) Estable /operators
executionMethod Estable /run
TransitionTo(..., { concurrency: 'reject-stale' }) Estable, opt-in Raíz o /behavioral/state-machine
Handles de breaker, keyed lock y semaphore Estable /operators
Adaptadores de eventos Estable /observability
cancelWhenUnused Experimental, opt-in Opciones de caché/overlap
observeOperation, asyncCachedFor, CAS Experimental /experimental

Las nuevas opciones no se activan por defecto. Los operadores existentes conservan sus resultados síncronos y su comportamiento anterior de promesas sin señal cuando no se activa cancelación compartida. Las uniones públicas de eventos, errores y capas de políticas no se amplían.

import { memoize, rateLimit } from 'annotations-design-patterns/operators';
import { pipe } from 'annotations-design-patterns/pipe';
const cachedLength = pipe(
(value: string) => value.length,
memoize<object, readonly [string], number>({ maxEntries: 100 }),
);
const admitted = pipe(() => 'ok', rateLimit({ limit: 20, windowMs: 1_000 }));

rateLimit cuenta admisiones en una ventana deslizante monotónica, por método aplicado. Un exceso devuelve una promesa rechazada con RateLimitExceededError y retryAfterMs; una admisión síncrona conserva su resultado síncrono. Los fallos consumen cuota. Una señal preabortada no la consume. No se modifica el decorador @RateLimit anterior.

maxEntries es un entero seguro positivo. La política es FIFO e incluye entradas pendientes; los aciertos no reordenan. Con perReceiver, el límite es por receptor. Expulsar una entrada no detiene su ejecución física.

import { executionMethod, run } from 'annotations-design-patterns/run';
import { memoize } from 'annotations-design-patterns/operators';
import { pipe } from 'annotations-design-patterns/pipe';
const load = pipe(
executionMethod(async ({ signal }, id: string) => {
const response = await fetch(`/records/${encodeURIComponent(id)}`, { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.text();
}),
memoize<object, readonly [string], string>({
key: (id) => id,
maxEntries: 100,
cancelWhenUnused: true,
}),
);
const caller = new AbortController();
const result = run({}, ['record-1'], load, { signal: caller.signal });

executionMethod recibe la señal efectiva derivada por los operadores; una llamada directa no tiene señal contextual. Usa pipe/run: .apply() y los wrappers opacos son fronteras de contexto, sin propagación ambiental.

cancelWhenUnused se ofrece en memoize, cachedFor y overlap modo share. Cada consumidor con señal puede retirarse de su espera. Uno sin señal permanece hasta settlement. La última retirada elimina la reserva visible antes de abortar la raíz compartida: una llamada reentrante puede crear un reemplazo y el settlement antiguo no lo elimina. El backend solo se detiene si coopera.

Al activar esta opción, no se conserva como contrato la identidad anterior de las promesas. El iniciador síncrono sigue siendo síncrono; un consumidor reentrante de una reserva recibe una promesa. Evicción, TTL e invalidación no retiran consumidores. Los ciclos que esperan su propia reserva no se soportan.

import { StateMachine, TransitionTo, When } from 'annotations-design-patterns';
class Document extends StateMachine<'draft' | 'published'> {
@When('draft')
@TransitionTo('published', { concurrency: 'reject-stale' })
async publish(): Promise<void> {
// Persistir aquí los efectos que puedan convivir con un posible conflicto.
}
}

Una transición manual, A-B-A o al mismo estado invalida el trabajo pendiente. Su finalización rechaza con StateTransitionConflictError; no revierte efectos externos ni reintenta. El commit optimista es interno y no invoca el override virtual de transitionTo. Los overrides usados para cambios manuales deben delegar a super.transitionTo. Sin la opción se conserva el despacho virtual.

circuitBreakerWithHandle, keyedLockWithHandle y semaphoreWithHandle devuelven { operator, handle }. Comparten estado entre las aplicaciones de ese operador de fábrica; un operador normal conserva estado por aplicación. inspect devuelve snapshots inmutables. El reset del breaker avanza su generación: el trabajo y las sondas anteriores ya no cambian el estado nuevo. No aborta trabajo ni revoca permisos. Lock y semaphore no ofrecen un reset.

createCounterSink, createSpanEventSink y combineEventSinks aíslan throws y rechazos accidentales de callbacks. Los operadores desconocidos usan la etiqueta custom; los IDs y argumentos no se convierten en etiquetas.

Los sinks pasados directamente a run también aíslan un rechazo asíncrono devuelto accidentalmente. emit(): void sigue sin esperar el callback ni añadir backpressure, y el resultado de negocio conserva su canal síncrono o asíncrono.

observeOperation mide el resultado visible en su posición de la composición, no el final físico de un backend. La procedencia de cancelación distingue cancelled de timeout o un error llamado AbortError. Un preaborto rechazado por run antes de entrar en la capa no genera observación. Los datos de invocación requieren includeInvocation: true.

Caché externa y CAS: contratos experimentales

Sección titulada «Caché externa y CAS: contratos experimentales»

asyncCachedFor siempre es asíncrono. Exige proveedor con TTL real declarado, namespace, versión de esquema, clave explícita, codec síncrono, deadline finito de I/O y política storeFailure. bypass tolera cualquier fallo de get/set, incluidos permisos/configuración; los errores de clave, sobre y codec rechazan. onDiagnostic permite observar fallos del proveedor sin cambiar el resultado.

Un deadline limita la espera; una lectura o escritura ya enviada puede terminar después. No se inicia negocio ni una escritura nueva tras cancelar. No hay deduplicación distribuida ni garantía contra escrituras tardías obsoletas.

VersionedMemoryStore es una referencia CAS local. Los tokens pertenecen a su almacén, no se reutilizan dentro de la instancia y distinguen ausencia de un valor undefined. Las versiones de ausencia pueden invalidarse al escribir otra clave, produciendo conflictos conservadores. El fallback sin Web Crypto no garantiza unicidad entre instancias. No certifica locks, breakers ni semáforos distribuidos; hacen falta proveedores y pruebas de red específicos.

Los snapshots congelan solo su estructura. Los valores se guardan y devuelven por referencia, sin clonación ni congelación profunda: deben tratarse como inmutables. Una mutación externa de un objeto no avanza su versión CAS.

Cambio de temporización en AsyncMemoryStore

Sección titulada «Cambio de temporización en AsyncMemoryStore»

En 2.2 un set, delete o clear podía adelantarse a un update pendiente y ser sobrescrito por su finalización. Ahora las mutaciones se admiten en orden por clave; clear y cleanup son barreras para las mutaciones. Las lecturas get/has conservan el acceso sin espera al último valor confirmado, incluso dentro de un updater.

Antes de adoptar 2.3, revisa los updaters que esperan otra mutación de su propia clave o una barrera: crearían una dependencia circular. Devuelve el nuevo valor desde el updater en lugar de escribirlo dentro. La cola no tiene backpressure y el TTL empieza al confirmar la escritura.

El consumidor browser minificado con memoize pasa de 1.664 a 2.800 bytes gzip en la medición local de 2.2 frente al runtime de 2.3, por la ruta opcional compartida alcanzable desde su fábrica. No es una medición de Bundlephobia ni del paquete completo. Se revisó su presupuesto explícitamente; no se elevaron todos los límites de consumidores.

La publicación estable usa un tag versionado y el workflow de release: valida el tarball exacto antes de publicarlo con procedencia de npm. Los resultados de integración y CI no convierten las APIs experimentales en estables.