Propagar cambios: del Worker a todas las pestañas
Avisar de que algo cambió y enviar el cambio son dos arquitecturas distintas con costes, garantías y modos de fallo distintos, y elegir mal es lo que produce la tormenta de actualizaciones.
Una escritura no termina cuando la transacción confirma: termina cuando todas las proyecciones que dependían de lo que acaba de cambiar lo saben. Ese último tramo es la mitad menos estudiada del patrón y la que más aplicaciones estropea, porque parece un detalle de fontanería —hay que avisar, se avisa— y en realidad esconde una decisión arquitectónica de primer orden con dos respuestas incompatibles. Puedes decir que algo cambió y dejar que cada interesado vuelva a preguntar, o puedes enviar directamente lo que cambió y que cada interesado lo aplique. Las dos funcionan, las dos son defendibles, y tienen costes, garantías y formas de romperse tan distintas que elegir una u otra por inercia es exactamente el error que produce esa cascada de repintados que todo el mundo acaba llamando la tormenta.
- Distinguir la propagación por invalidación de la propagación por delta y saber cuándo conviene cada una.
- Numerar los avisos para que un receptor pueda detectar que se ha perdido alguno y resincronizarse.
- Elegir el canal de transporte adecuado y entender qué garantiza y qué no cada uno.
- Apagar la tormenta de actualizaciones con agrupación, deduplicación y suscripciones con granularidad.
Invalidar o enviar el delta
En el modelo de invalidación, el escritor emite un aviso mínimo del estilo de que la colección de notas ha cambiado, y cada pestaña interesada vuelve a pedir lo que necesitaba. El aviso no lleva datos, es diminuto, es idempotente —recibirlo dos veces no hace daño— y tiene una propiedad valiosísima: el receptor termina siempre con un estado consistente porque lo obtiene de la única fuente autoritativa que existe. A cambio, cada aviso desencadena tantos cruces de frontera como pestañas interesadas haya, y una escritura barata puede provocar mucho trabajo caro.
En el modelo de delta, el escritor emite qué ha cambiado exactamente: las filas insertadas, las modificadas y las claves borradas. Cada pestaña aplica el cambio sobre su proyección sin volver a preguntar nada. Un solo mensaje, ningún cruce adicional, latencia mínima. Pero has firmado tres obligaciones nuevas y no siempre reversibles: que cada receptor aplique los deltas en el mismo orden, que ningún receptor se pierda ninguno, y que aplicar un delta sobre la proyección produzca exactamente el mismo resultado que habría dado consultar. Ese tercer punto es más exigente de lo que parece en cuanto la proyección contiene agregados, ordenaciones o campos derivados.
flowchart TB
W[Escritor unico] --> M{Modelo de propagacion}
M --> I[Invalidacion: solo el que cambio]
M --> D[Delta: las filas que cambiaron]
I --> I2[Cada pestana vuelve a consultar]
I2 --> I3[Coste en cruces, siempre consistente]
D --> D2[Cada pestana aplica el cambio]
D2 --> D3[Coste minimo, exige orden y version]
style I3 fill:#a6e3a1,color:#11111b
style D3 fill:#f9e2af,color:#11111bLa recomendación honesta es empezar por invalidación y migrar a delta solo donde la medida lo justifique. No por prudencia genérica, sino porque la invalidación es autorreparable: un receptor que se pierde un aviso se recupera solo en cuanto le llegue el siguiente, porque cada aviso provoca una relectura completa de lo afectado. El modelo de delta no tiene esa propiedad; un delta perdido deja la proyección corrupta de forma permanente y silenciosa, y nada en el sistema lo detecta salvo que tú lo detectes.
Por eso el modelo de delta obliga a lo que la invalidación te regala. Cada aviso lleva un número de secuencia estrictamente creciente que el escritor asigna al confirmar cada transacción. El receptor guarda el último que aplicó y comprueba la continuidad: si le llega el número que esperaba, aplica; si le llega uno mayor, sabe que hay un hueco y pide una resincronización completa; si le llega uno menor o igual, lo descarta por duplicado.
// Receptor: aplicar solo si la secuencia es contigua
let ultimaVersion = 0;
function recibirAviso(aviso) {
if (aviso.version <= ultimaVersion) return; // duplicado o tardio
if (aviso.version !== ultimaVersion + 1) { // hueco detectado
return resincronizarCompleto(); // la unica salida honesta
}
aplicarDelta(aviso.delta);
ultimaVersion = aviso.version;
}
Esas nueve líneas son la diferencia entre un sistema que se degrada de forma detectable y uno que muestra datos falsos con total confianza. La ruta de resincronización completa no es opcional ni un caso raro: es la red de seguridad de todo el modelo, y hay que ejercitarla con regularidad porque el código que solo se ejecuta en el fallo es exactamente el código que estará roto cuando llegue el fallo.
Por dónde viaja el aviso
El transporte no es neutral y cada opción tiene una peculiaridad que hay que conocer antes de elegir.
- Canal de difusión por origen. Es el ajuste natural: cualquier contexto del origen publica y todos los demás reciben. Su peculiaridad decisiva es que el emisor no recibe su propio mensaje, lo cual resulta cómodo para evitar ecos y es una trampa cuando la lógica de aplicar el cambio vive en el receptor, porque la pestaña que escribió se queda sin actualizar. No garantiza entrega: quien no esté escuchando en ese instante no se entera, y ahí es donde la numeración se vuelve indispensable.
- Puertos dedicados desde el líder. El líder mantiene un puerto por pestaña seguidora. Cuesta llevar un registro de suscriptores y limpiarlo cuando alguien desaparece, y a cambio permite enviar avisos distintos a receptores distintos, que es la base de las suscripciones con granularidad.
- Worker compartido. Donde exista, es el transporte más limpio: una lista de puertos dentro del propio Worker, un solo salto y ninguna diferencia entre pestaña líder y seguidora.
- Eventos de almacenamiento. Funcionan entre pestañas y son un recurso de compatibilidad, pero son síncronos respecto del hilo que escribe, tienen un límite de tamaño muy bajo y arrastran el coste de serializar a texto. Sirven como señal, nunca como canal de datos.
Es tentador reutilizar el mismo canal del RPC para los avisos, y funciona hasta que deja de hacerlo. Un aviso no tiene identificador de petición, no resuelve ninguna promesa y puede llegar cuando no hay nada en vuelo; mezclarlo con las respuestas obliga a que un mismo manejador discrimine entre dos protocolos con reglas opuestas. Sepáralos desde el principio, aunque compartan tubería física: dos manejadores, dos vocabularios de mensaje, dos formas de fallar. El día que quieras que una pestaña reciba avisos sin haber hecho ninguna petición, ya estará hecho.
La tormenta de actualizaciones
La tormenta es lo que ocurre cuando el número de repintados crece con el producto de tres factores que nadie mira juntos: las escrituras por segundo, las pestañas abiertas y los componentes suscritos en cada pestaña. Cada factor parece pequeño por separado. Una importación de cinco mil filas, tres pestañas abiertas y cuarenta componentes suscritos son seiscientas mil notificaciones si el aviso se emite por fila, y ninguna máquina sobrevive a eso con la interfaz respondiendo.
Las cuatro palancas que la apagan son independientes y se aplican en este orden.
Avisar por transacción, nunca por fila. El nivel correcto de granularidad del aviso es la unidad de atomicidad, no la unidad de modificación. Una importación masiva es un aviso, no cinco mil. Esta sola regla elimina la mayor parte del problema y es la que más se incumple, porque el sitio natural donde poner la emisión —justo al lado de la escritura— es precisamente el sitio equivocado.
Agrupar en la ventana del fotograma. Del lado del receptor, los avisos que llegan dentro de un mismo intervalo de pintado se funden en una sola actualización. No hay ninguna razón para repintar dos veces antes de que el usuario pueda ver el resultado intermedio.
// Receptor: fundir todo lo que llegue antes del proximo pintado
const sucios = new Set();
let programado = false;
function marcar(tema) {
sucios.add(tema);
if (programado) return;
programado = true;
requestAnimationFrame(() => {
programado = false;
const lote = new Set(sucios);
sucios.clear();
refrescar(lote); // una sola pasada por todos los temas afectados
});
}
Deduplicar por tema. Veinte avisos sobre la misma colección dentro de la ventana son un solo refresco. El conjunto del ejemplo anterior lo hace de forma natural, y es la razón de usar un conjunto y no una lista.
No refrescar lo que nadie está mirando. Una pestaña oculta no necesita repintar nada. Basta con anotar qué temas quedaron sucios y refrescarlos cuando vuelva a ser visible. En una aplicación que el usuario deja abierta en cuatro pestañas durante toda la jornada, esta única regla es la diferencia entre un consumo de batería aceptable y uno que le hará cerrar la aplicación.
La escritura optimista y el aviso se solapan, y hay que decidir cómo. El patrón que mejor envejece es aplicar el cambio en la proyección local en cuanto se emite la intención, marcarlo como no confirmado, y usar la respuesta del escritor para confirmarlo o revertirlo. El aviso general, entonces, no debe volver a aplicar nada en esa pestaña: llega con una versión que ella ya tiene. Numerar los avisos vuelve a resolver el problema sin ninguna lógica especial de detección de ecos, que es siempre frágil.
Granularidad y suscripciones
Queda decidir sobre qué se avisa, y la respuesta correcta casi nunca es la tabla. Avisar por tabla es tan grueso que cualquier escritura despierta a toda la aplicación; avisar por fila es tan fino que el volumen de avisos se vuelve el problema. El punto útil está en un concepto intermedio que hay que nombrar explícitamente y que llamaremos tema: una porción del estado con sentido para la interfaz, del estilo de las notas de un libro concreto o el resumen del panel principal.
Los temas se derivan de las vistas, no del esquema. Cada operación de escritura declara qué temas invalida y cada componente declara a qué temas se suscribe, y el escritor solo emite avisos de los temas que alguien está observando. Esa última condición es la que convierte el sistema en escalable: un registro de suscripciones en el líder permite no enviar absolutamente nada cuando cambia algo que ninguna pestaña abierta está mostrando, que en una aplicación real es la mayoría de lo que cambia.
// En el escritor: declarar los temas afectados junto a la operacion
const TEMAS = {
crearNota: (args) => [`libro:${args.libro}`, 'panel'],
moverNota: (args) => [`libro:${args.origen}`, `libro:${args.destino}`, 'panel'],
};
function tras(metodo, args, version) {
const afectados = TEMAS[metodo](args).filter((t) => suscritos.has(t));
if (afectados.length) difundir({ tipo: 'aviso', version, temas: afectados });
}
Observa que el mapa de temas está declarado junto a la operación y no dentro de ella. Esa separación permite razonar sobre la propagación leyendo una sola tabla, en lugar de auditar el cuerpo de cada escritura buscando emisiones sueltas, y hace evidente el error más común de esta capa: una operación que modifica dos colecciones y solo declara una, que produce el fallo clásico de una pantalla que no se entera hasta que se recarga. Cuando aparezca ese síntoma, el mapa de temas es el primer sitio donde mirar y casi siempre el único.
Deriva los temas de las vistas
Si un tema no corresponde a algo que alguna pantalla muestra, sobra. La granularidad la dicta la interfaz, no las tablas.
Numera siempre, aunque invalides
La versión permite detectar huecos, descartar duplicados y decidir si un aviso es anterior a lo que ya tienes. Cuesta un entero por transacción.
Limpia los suscriptores muertos
Una pestaña cerrada deja su puerto colgado en el registro del líder. Sin poda, el escritor acaba emitiendo a destinatarios que ya no existen.
Ejercita la resincronización
La ruta de recuperación completa solo se ejecuta cuando algo va mal, así que dispárala a propósito en desarrollo o estará rota el día que haga falta.
Detente en la elección de esta lección, porque no es una decisión de ingeniería local: es la primera aparición, en su forma más sencilla, de la dicotomía que estructura todo lo que viene después. Propagar por invalidación significa transmitir la señal de que el estado cambió y dejar que el receptor obtenga el estado; propagar por delta significa transmitir la operación y dejar que el receptor la aplique. Esa es exactamente la distinción entre replicación basada en estado y replicación basada en operaciones, que es la línea divisoria alrededor de la cual está construida la teoría entera de los tipos de datos replicados sin conflicto que ocupa quince niveles de este track. Y las propiedades que has descubierto aquí en pequeño son literalmente las mismas que allí se demuestran en grande: el enfoque de estado tolera pérdidas, duplicados y reordenaciones porque cada transmisión reconstruye la verdad completa, y paga por ello en volumen; el enfoque de operaciones es mínimo en volumen y a cambio exige un canal que no pierda nada, que no duplique y que respete la causalidad, garantías que alguien tiene que proporcionar. Fíjate en lo que has hecho para conseguirlas: un número de secuencia por transacción, la detección de huecos y una ruta de resincronización. Eso es un contador de versión, un mecanismo de entrega causal y una recuperación por transferencia de estado, es decir, los tres ingredientes con los que se construye cualquier motor de sincronización real. La diferencia entre este nivel y los que vienen es únicamente que aquí existe un escritor único que puede asignar esos números por autoridad propia, sin discutirlos con nadie, porque vive en un solo hilo y ve todas las escrituras en un orden total que él mismo determina. En cuanto haya dos dispositivos que escriben sin verse, ese lujo desaparece: nadie puede asignar el siguiente número porque no hay un siguiente único, el orden total se rompe en un orden parcial y toda la maquinaria de relojes lógicos, causalidad y convergencia algebraica existe precisamente para reemplazar lo que aquí resolvía un simple contador incremental. Cuando llegues a ese punto, reconocerás el problema porque acabas de resolver su versión de juguete.
- Instrumenta cuántas actualizaciones recibe cada componente durante una importación masiva. Anota el número antes de tocar nada.
- Mueve la emisión del aviso de la escritura a la confirmación de la transacción y vuelve a medir.
- Implementa la fusión por fotograma con deduplicación por tema y compara los repintados con las tres mediciones anteriores.
- Añade número de secuencia a los avisos y provoca un hueco a propósito descartando un mensaje. Comprueba que se dispara la resincronización.
- Define el mapa de temas de una pantalla real y haz que el escritor no emita nada cuando ninguna pestaña esté suscrita a lo que cambió.
- Deja cuatro pestañas abiertas una hora con escrituras periódicas y mide el consumo con y sin la regla de no refrescar lo oculto.