Anatomía de un glitch
Qué es exactamente un estado inconsistente, la definición formal de consistencia glitch-free, y el catálogo de daños reales que produce un valor imposible cuando llega a un efecto.
Un glitch es un valor que el sistema calcula y que no corresponde a ningún estado real de sus entradas. No es un valor viejo ni uno nuevo: es una mezcla imposible. Esta lección lo define con precisión, muestra por qué la definición importa, y recorre el catálogo de daños que produce cuando esa mezcla imposible atraviesa la frontera del sistema y llega al mundo.
- Definir glitch y consistencia glitch-free con precisión.
- Distinguir un valor obsoleto de un valor inconsistente.
- Enumerar los daños reales según el tipo de efecto afectado.
- Escribir un detector de glitches para un motor propio.
La definición
Sea S el conjunto de valores de todas las fuentes del grafo, y sea f la función que define una derivación. La derivación es consistente si su valor es igual a f aplicada a los valores actuales de sus entradas.
Un glitch es un instante en el que una derivación se evalúa con entradas que no pertenecen todas al mismo estado global. En el rombo de la lección anterior, con a pasando de 1 a 2, el efecto se evaluó con b calculado desde a = 2 y c calculado desde a = 1. El resultado no es el de a = 1 ni el de a = 2: es de un estado que nunca existió.
Consistencia glitch-free es la garantía de que ningún observador ve jamás uno de esos estados. Formalmente: entre dos estados globales consecutivos, no hay ningún instante en el que un nodo evaluado refleje una mezcla de ambos.
La distinción es importante. Un valor obsoleto corresponde a un estado global anterior, y es perfectamente coherente: si lees un nodo antes de que se haya propagado nada, ves el mundo de antes, que es un mundo válido. Un valor inconsistente no corresponde a ningún estado. Los sistemas asíncronos producen valores obsoletos continuamente y no pasa nada; producir valores inconsistentes es otra cosa. Todos los motores modernos garantizan lo segundo; algunos ni siquiera intentan lo primero, y hacen bien.
Por qué la definición importa
Podría parecer un problema teórico: al fin y al cabo, el valor incorrecto se corrige en cuanto la propagación termina. La cuestión es qué ocurre en el intervalo, y en un sistema con efectos el intervalo tiene consecuencias.
Efectos que escriben en el DOM. El valor imposible se pinta y se sobrescribe. Si ocurre dentro de un mismo fotograma, el usuario no lo ve. Si la propagación cruza un límite de fotograma —cosa que ocurre en grafos grandes o con efectos costosos—, hay un parpadeo real.
Efectos que hacen peticiones. Una petición con parámetros inconsistentes se envía. No hay forma de recogerla. El servidor recibe una consulta con un filtro nuevo y un identificador viejo, devuelve datos que no tienen sentido, y el bug se manifiesta a kilómetros del origen.
Efectos que escriben en almacenamiento. Se persiste un estado que nunca existió. Al recargar, la aplicación arranca desde ese estado imposible. Este es el peor caso, porque el glitch sobrevive a la sesión.
Efectos que registran o miden. Los datos analíticos quedan contaminados con eventos que no corresponden a ninguna interacción real. Es un daño silencioso que nadie detecta hasta que alguien analiza los datos meses después.
Efectos que disparan animaciones. Una transición arranca hacia un valor imposible y se reinicia hacia el correcto. El resultado visual es un tirón.
flowchart TB G[valor inconsistente calculado] --> D[llega a un efecto] D --> A[escribe en el DOM parpadeo] D --> B[peticion de red datos incoherentes] D --> C[almacenamiento estado imposible persistido] D --> E[analitica datos contaminados] D --> F[animacion tiron visual] style G fill:#f38ba8,color:#11111b style D fill:#f9e2af,color:#11111b style A fill:#fab387,color:#11111b style B fill:#fab387,color:#11111b style C fill:#f38ba8,color:#11111b style E fill:#fab387,color:#11111b style F fill:#fab387,color:#11111b
Un detector de glitches
Detectar glitches es sorprendentemente fácil si el motor lleva un contador de época que se incrementa una vez por transacción de propagación. Un nodo que se evalúa dos veces en la misma época ha visto, casi con seguridad, un estado intermedio.
let Epoca = 0;
function detectorDeGlitches(motor) {
const vecesPorEpoca = new Map();
motor.alEjecutar = (nodo) => {
const clave = `${nodo.nombre}:${Epoca}`;
const n = (vecesPorEpoca.get(clave) ?? 0) + 1;
vecesPorEpoca.set(clave, n);
if (n > 1) console.warn(`glitch: ${nodo.nombre} evaluado ${n} veces en la epoca ${Epoca}`);
};
}
La heurística no es perfecta —un nodo puede ejecutarse legítimamente dos veces si un efecto escribe una fuente que lo afecta, lo que es una cascada y no un glitch— pero atrapa el caso clásico. Para el rombo, señala el problema exactamente.
Un detector más estricto compara los valores observados contra los que se obtendrían recalculando el grafo entero desde las fuentes al final de la transacción. Es caro y solo sirve en pruebas, pero es exacto.
function verificarConsistencia(nodos) {
for (const n of nodos) {
if (!n.fn) continue;
const recalculado = evaluarDesdeCero(n);
if (!n.iguales(n.valor, recalculado)) {
throw new Error(`inconsistencia en ${n.nombre}: cache ${n.valor}, real ${recalculado}`);
}
}
}
Dónde se esconden y cómo se resuelven
Dónde suelen esconderse
Tres sitios concretos donde los glitches sobreviven incluso en motores que prometen consistencia.
En la frontera con código no reactivo. Un manejador de eventos que lee tres señales y las combina a mano no participa del grafo, así que ninguna garantía lo cubre. Si lee entre dos escrituras, verá un estado intermedio. La solución es agrupar las escrituras en un lote, que es el nivel 8.
Entre dos motores. Ya lo vimos en el nivel 0: la consistencia no compone entre grafos separados.
En efectos asíncronos. Un efecto que empieza a leer, se suspende en un await y sigue leyendo después ha construido su resultado con valores de dos momentos distintos. Ninguna garantía del motor lo cubre, porque el motor solo controla el tramo síncrono.
Vale la pena entender qué se está comprando exactamente, porque suena a detalle de implementación y es una propiedad fundacional. Sin ella, no puedes razonar localmente sobre tu código. Miras un efecto, ves que lee tres derivaciones, y no puedes concluir nada sobre qué combinaciones de valores va a observar: depende del orden en que el motor recorra sus listas, que depende del orden en que se crearon las aristas, que depende del orden en que se ejecutó el código de inicialización. Un cambio inocente en otro fichero puede alterar ese orden y hacer aparecer un bug que no tiene ninguna relación aparente con el cambio. Con la garantía, la afirmación es sencilla y comprobable: este efecto solo verá combinaciones que corresponden a estados reales del sistema. Eso convierte el razonamiento sobre un sistema reactivo grande en algo tratable, y es exactamente la misma clase de propiedad que la atomicidad en una transacción de base de datos: no puedes ver la mitad de una transferencia. Nadie discute que una base de datos deba ofrecer atomicidad, y sin embargo la garantía equivalente en un motor reactivo se presenta a veces como una optimización. No lo es: es la diferencia entre un sistema sobre el que se puede razonar y uno sobre el que hay que rezar.
La solución, adelantada
La receta es de una simplicidad que sorprende después de tanto preámbulo: no evaluar nada hasta haber marcado todo. Si la fase de marcado recorre el subgrafo entero antes de que se evalúe el primer nodo, entonces cuando el nodo de reunión se evalúe encontrará sus dos fuentes marcadas y las resolverá a ambas antes de calcularse.
Eso es el marcado en dos fases, y es la lección siguiente.
- Construye un rombo con un efecto en el punto de reunión, usando un motor con push puro.
- Haz que el efecto escriba en
localStoragey comprueba qué queda guardado tras una escritura. - Haz que el efecto lance una petición simulada y registra los parámetros de cada llamada.
- Repite con un motor de marcado en dos fases y verifica que solo hay una llamada por escritura, con los parámetros correctos.