wandres.dev
EL PROBLEMA DE SINCRONIZAR · glitches y consistencia

Los glitches: la inconsistencia efímera

Un glitch es un estado intermedio inconsistente: durante la propagación de un cambio, un valor derivado se calcula mezclando entradas ya nuevas con entradas todavía viejas y arroja un resultado que no corresponde a ningún estado real del sistema. No es un error de tu fórmula, sino un artefacto del orden en que se propagó el cambio. Aunque el valor final acabe siendo correcto, el intermedio es observable y puede disparar efectos, renders y peticiones sobre datos que nunca existieron.

⏱ 15 min

Un sistema reactivo promete que, cuando un dato cambia, todo lo que depende de él se pone al día. La promesa esconde una trampa: ponerse al día no es instantáneo, ocurre en pasos, y entre paso y paso el grafo puede quedar a medio actualizar. Si en ese instante alguien lee un valor derivado, puede obtener algo imposible: un número calculado con una parte de los datos ya nuevos y otra parte aún vieja. A esa inconsistencia efímera se le llama glitch. No nace de una lógica incorrecta —tu fórmula es exacta— sino de en qué ORDEN se propagó el cambio. Es el primer síntoma del problema que define todo este nivel: sincronizar no es propagar, es propagar bien.

🎯 Al terminar esta lección sabrás
  • Definir con precisión qué es un glitch y por qué es un estado, no un valor final erróneo.
  • Reconocer que un glitch nace del orden de propagación, nunca de una fórmula equivocada.
  • Ver por qué solo aparece cuando un cambio alcanza a un nodo por más de un camino.
  • Medir el daño real: los efectos que se disparan sobre valores que jamás fueron ciertos.

Anatomía de un glitch

Toma el grafo reactivo más pequeño capaz de fallar: una fuente y dos derivados encadenados.

const a = signal(1)
const b = computed(() => a() + 1)     // b = a + 1
const c = computed(() => a() + b())   // c = a + b, o sea c = 2a + 1

En reposo, el sistema cumple una invariante que se deduce de las fórmulas: c === 2 * a + 1. Con a igual a 1 tenemos b igual a 2 y c igual a 3, y en efecto 3 === 2 * 1 + 1. Si escribimos a igual a 5, el estado consistente final es b igual a 6 y c igual a 11, y de nuevo 11 === 2 * 5 + 1. La invariante se sostiene antes y después. Un glitch es un instante, durante la transición, en el que NO se sostiene: un momento en que c vale algo que no es 2a + 1 para ningún a.

La palabra glitch viene de la electrónica digital, donde nombra un pico espurio en una salida lógica mientras las compuertas se estabilizan. La reactividad la tomó prestada porque el fenómeno es idéntico: una salida que oscila hacia un valor falso mientras sus entradas terminan de propagarse. Que el nombre venga del hardware no es casual —sincronizar salidas que dependen de entradas comunes es el mismo problema en un circuito y en un grafo de signals—.

Conviene tener a mano la distinción operativa, porque cambia cómo se depura:

  • Un bug de lógica produce el valor equivocado en reposo: la fórmula está mal y el estado final es incorrecto.
  • Un glitch produce el valor correcto en reposo pero uno imposible en tránsito: la fórmula está bien y solo falla el camino.
  • Un bug de lógica lo caza un test del estado final; un glitch se le escapa a ese test y solo asoma observando la propagación en curso.

El orden lo es todo

¿Cómo llega c a valer algo imposible? Observa que a alcanza a c por dos caminos: uno directo, porque c lee a, y uno indirecto, porque c lee b, que a su vez lee a. En una propagación ingenua —empuje inmediato, en profundidad— cuando a cambia notifica a sus dependientes en el orden en que se registraron, y cada uno recalcula al instante. Si la ruta directa a c se dispara antes que b, entonces c se recalcula con la a nueva y la b todavía vieja.

sequenceDiagram
participant A as fuente a
participant C as derivado c
participant B as derivado b
A->>C: a paso a 5 recomputa por ruta directa
C->>C: usa a 5 y b viejo 2 y da 7 glitch
A->>B: a paso a 5 recomputa por ruta indirecta
B->>C: b ahora vale 6 y notifica
C->>C: usa a 5 y b 6 y da 11 correcto

El 7 no corresponde a ningún estado del mundo: no es 2 * 1 + 1 ni 2 * 5 + 1. Existió durante una fracción de milisegundo y desapareció. Cambia el orden de registro de los dependientes de a y el glitch se esfuma, o aparece uno distinto. Esa fragilidad ante el orden es la firma del problema.

Piénsalo así: el resultado de tu programa no debería depender de en qué orden registraste los derivados, igual que una suma no depende del orden de sus sumandos. Un sistema que glitchea rompe esa expectativa básica —el mismo grafo, con las mismas fórmulas, recorre trayectorias distintas según un detalle de implementación que ningún programador debería tener que vigilar—. La propagación correcta es, entre otras cosas, la que vuelve ese detalle irrelevante.

De aquí sale una ley que usaremos todo el nivel: una cadena pura no produce glitches. Si a, b y c formaran una línea recta sin atajos, c solo se enteraría de a a través de b, siempre después de que b se actualizara. El glitch exige un camino que se bifurca y vuelve a juntarse: una reconvergencia.

flowchart LR
A[senal a] --> B[derivado b]
A --> C[derivado c]
B --> C
style A fill:#cba6f7,color:#11111b
style C fill:#f9e2af,color:#11111b

La arista directa de a a c es el atajo culpable: permite que c se entere del cambio de a antes de que b lo haya asimilado. Quítala —haz que c dependa solo de b— y no queda ningún camino para el glitch. Esa forma con dos rutas tiene nombre propio, el diamante, y es el tema de la lección siguiente.

El culpable: el empuje ansioso

¿Qué tiene la propagación ingenua que la hace glitchear? Su pecado es ser ansiosa: en cuanto una fuente cambia, recorre a sus suscriptores y los recalcula de inmediato, en profundidad, sin preguntarse si al nodo recalculado le falta enterarse de algo por otra vía.

class Observable {
  subs = new Set()
  set(valor) {
    this.valor = valor
    for (const s of this.subs) s.recompute()   // empuje inmediato y en profundidad
  }
}
// c es suscriptor directo de a Y suscriptor de b:
// el orden del Set de a decide si c ve el glitch

El defecto no está en ninguna línea concreta, sino en que set dispara el recálculo sin comprobar si el suscriptor tiene otras entradas pendientes. El observador trata cada relación como independiente y recorre el grafo como si fuera un árbol; en cuanto hay una reconvergencia ese supuesto es falso, y el fruto es un glitch. Todo el nivel es, en el fondo, la historia de cómo reemplazar este for ingenuo por una propagación que respete la estructura real del grafo.

📝
El empuje no es el enemigo, la avidez lo es

No todo empuje glitchea: el problema no es notificar, sino recalcular en el acto. Los sistemas que estudiaremos conservan una fase de empuje —para saber QUÉ quedó potencialmente obsoleto— pero posponen el cálculo hasta poder hacerlo en el orden correcto. Empujar una marca de “revisar” es barato e inofensivo; empujar un recálculo inmediato es lo que abre la puerta al glitch.

El daño no se cura solo

Es tentador encogerse de hombros: si el sistema converge, ¿a quién le importa un valor que dura un instante? Importa porque los valores derivados no solo se leen: disparan efectos.

Efectos que no se deshacen

Un efecto atado a c correrá con el 7. Si ese efecto lanza una petición, escribe un log o cobra un pago, el glitch ya salió al mundo. La convergencia posterior no cancela lo que ya ocurrió.

🖥️

Parpadeos en la UI

Renderizar el estado intermedio pinta en pantalla una combinación imposible durante un frame: un total que no cuadra, un botón habilitado que debía estar bloqueado.

📊

Datos envenenados

La analítica y las trazas que muestrean el intermedio registran estados que nunca fueron reales, y contaminan cualquier análisis posterior con cifras fantasma.

🌊

Cascadas amplificadas

Si otro derivado depende de c, hereda el glitch y lo propaga. Un solo intermedio malo puede desatar una tormenta de recomputaciones erróneas aguas abajo.

Un ejemplo concreto fija la idea. Imagina que c es la condición “¿el total del carrito supera el límite de crédito?” y que un efecto, cuando c pasa a verdadero, congela la cuenta y envía un correo. Un glitch que ponga c en verdadero durante un instante —porque leyó el total nuevo con un límite viejo— dispara el correo y el bloqueo por una condición que jamás fue real. La posterior corrección de c a falso no deshace ni el correo ni el susto. Esa es la distancia entre un valor que se autocorrige y un efecto que ya salió al mundo.

💡
Cómo se delata un glitch en la práctica

Si un efecto corre más veces de las que esperas, o si un log intermedio muestra un valor que “no puede ser”, sospecha de un glitch. La prueba diagnóstica es contar ejecuciones: en un grafo sano, un cambio de una fuente produce exactamente una ejecución por efecto afectado, con el valor final. Dos ejecuciones, o una con un valor imposible, delatan una reconvergencia mal propagada.

⚠️
Convergir no es ser correcto

La reactividad ingenua confunde dos garantías muy distintas: “el sistema termina en el valor correcto” y “el sistema nunca exhibe un valor incorrecto”. La primera habla del punto fijo; la segunda, de cada instante del camino hacia él. Un glitch cumple la primera y viola la segunda. Cualquier lógica que observe el grafo mientras se propaga —un efecto, un render, un log— depende de la segunda, y por eso los glitches son bugs reales aunque el estado final sea impecable.

Detectar un glitch a mano es difícil justamente porque es fugaz: un depurador que pausa la ejecución nunca lo ve, porque para cuando miras el grafo ya convergió. Las técnicas que funcionan son indirectas, y todas se apoyan en instrumentar el grafo para que él mismo delate la inconsistencia cuando ocurra.

🔢

Contar ejecuciones

Un efecto que corre más de una vez por cambio de fuente es sospechoso. En un grafo sano, un cambio produce una ejecución por efecto, con el valor final.

🧪

Afirmar la invariante

Mete la comprobación, por ejemplo c === 2a + 1, dentro del propio efecto. Si alguna vez falla, tienes el glitch capturado con su valor exacto.

🧵

Trazar los intermedios

Registra cada valor que pasa por el nodo y busca a posteriori los que no corresponden a ninguna configuración de las fuentes.

Conviene saber que este no es un problema nuevo ni exótico. El término glitch en reactividad se acuñó en la investigación de FRP —programación reactiva funcional— a mediados de los 2000, y la cura que allí se propuso —propagar en orden topológico por altura de nodo— es la misma que hoy incorporan las librerías de signals. No estás ante una rareza de tu framework, sino ante un problema clásico, bien entendido y ya resuelto, siempre que uses un sistema que lo resuelva.

La consistencia es una propiedad de cada instante, no solo del reposo

El salto mental de este nivel es dejar de pensar la reactividad como una función que, dadas las entradas, produce las salidas, y empezar a pensarla como un proceso que atraviesa estados intermedios que también son observables. En el modelo ingenuo solo razonamos sobre el punto fijo: si al detenerse todo cuadra, damos el sistema por correcto. Pero un grafo reactivo no vive en su punto fijo; vive propagándose, y cada estado intermedio por el que pasa es tan real como el final para quien lo observe en ese microsegundo. Un glitch es la prueba de que “eventualmente consistente” y “siempre consistente” son promesas diferentes, y de que la reactividad seria exige la segunda. Toda la maquinaria que estudiaremos —orden topológico, batching, propagación push-pull— existe para una sola meta: garantizar que ningún observador vea jamás un estado que no corresponda a una configuración real de las fuentes. No se trata de llegar bien, sino de que en ningún momento del trayecto se pueda leer una mentira. Quien interioriza esto deja de depurar parpadeos misteriosos y empieza a exigirle a su sistema de estado una garantía formal de consistencia.

⚔️ Caza tu primer glitch
  1. Escribe a, b igual a a + 1 y c igual a a + b con tu librería de signals favorita y añade un efecto que registre cada valor de c.
  2. Cambia a de 1 a 5 y cuenta cuántas veces corre el efecto y con qué valores. ¿Aparece el 7?
  3. Explica por qué c recibe el cambio de a por dos caminos y dibuja los dos en papel.
  4. Convierte el grafo en una cadena pura, haz que c dependa solo de b, y comprueba que el glitch desaparece.
  5. Reescribe el observador ingenuo de arriba y provoca el glitch a voluntad cambiando el orden de suscripción.
  6. Argumenta en dos frases por qué “el valor final es correcto” no basta como criterio de corrección.