wandres.dev
EL PROBLEMA DE SINCRONIZAR · glitches y consistencia

El diamante de dependencias

El diamante es la estructura mínima que rompe la propagación ingenua: un valor que depende de dos valores que, a su vez, dependen de una misma fuente común. Cuando la fuente cambia, un sistema ingenuo recalcula el vértice inferior dos veces —trabajo redundante— y la primera de esas veces lo hace con una entrada nueva y otra vieja —un glitch—. Esta lección disecciona por qué el diamante falla por partida doble y por qué su forma, no tu código, es la culpable.

⏱ 16 min

En la lección anterior vimos que un glitch solo aparece cuando un cambio alcanza a un nodo por más de un camino. Esa condición tiene una forma geométrica precisa, y es tan común que merece nombre propio: el diamante. Un valor depende de otros dos, y esos dos comparten una fuente. Cuando la fuente se mueve, el cambio se bifurca por dos ramas y vuelve a juntarse en el vértice inferior. Ahí, en la reconvergencia, es donde la propagación ingenua tropieza —y no una vez, sino dos: recalcula de más y recalcula mal—. Entender el diamante es entender la raíz estructural de todo el nivel.

🎯 Al terminar esta lección sabrás
  • Identificar el diamante como la reconvergencia mínima de un grafo reactivo.
  • Distinguir sus dos patologías: el recálculo redundante y el glitch.
  • Ver por qué el empuje ingenuo trata cada arista por separado y pierde la reconvergencia.
  • Estimar cómo el coste redundante crece de forma exponencial con diamantes apilados.

La forma del diamante

Un diamante tiene cuatro papeles: una fuente arriba, dos derivados intermedios que la leen, y un derivado inferior que combina a los dos. El ejemplo más cotidiano es un cálculo de precio.

const precio = signal(100)
const subtotal = computed(() => precio() * cantidad())  // rama izquierda
const iva = computed(() => precio() * 0.21)             // rama derecha
const total = computed(() => subtotal() + iva())        // reconvergencia
flowchart TD
A[precio fuente] --> B[subtotal rama izquierda]
A --> C[iva rama derecha]
B --> D[total reconvergencia]
C --> D
style A fill:#cba6f7,color:#11111b
style D fill:#f9e2af,color:#11111b

La señal precio está arriba; subtotal e iva cuelgan de ella; total cuelga de ambos. Dibujado, el grafo es un rombo: de ahí el nombre. La propiedad clave no es el número de nodos, sino la topología: hay dos rutas distintas de precio a total, y ambas deben resolverse antes de que total tenga derecho a un valor consistente. El diamante no es un caso rebuscado: aparece en cuanto un selector deriva de otros dos selectores que comparten un dato, o un total agrega dos parciales de la misma raíz. Es el patrón más frecuente de cualquier grafo real.

Falla número uno: recalcular de más

Sigue el empuje ingenuo cuando precio cambia. La señal notifica a sus dos dependientes, subtotal e iva. El nodo subtotal se recalcula y, acto seguido, notifica a total, que se recalcula por primera vez. Después le toca a iva: se recalcula y también notifica a total, que se recalcula por segunda vez. Un solo cambio en precio ha provocado DOS recálculos de total.

// empuje ansioso: cada dependiente se recalcula y propaga al instante
function notificar(nodo) {
  for (const dep of nodo.dependientes) {
    dep.recalcular()   // usa los valores que haya AHORA, viejos o nuevos
    notificar(dep)     // y sigue en profundidad
  }
}
// precio.dependientes = [subtotal, iva]
// subtotal propaga a total ANTES de que iva se toque: total corre dos veces

El desperdicio parece menor con un diamante suelto, pero la forma se apila. Si la salida de un diamante es la fuente de otro, cada nivel duplica el número de recálculos del vértice final: dos diamantes en cascada dan cuatro, tres dan ocho. El coste del último nodo crece como dos elevado a la profundidad, cuando lo correcto sería exactamente uno. Un grafo con reconvergencias anchas y profundas puede pasar de miles de recomputaciones donde bastaba una.

flowchart TD
A[fuente] --> B[b]
A --> C[c]
B --> D[d]
C --> D
D --> E[e]
D --> F[f]
E --> G[g final]
F --> G
style A fill:#cba6f7,color:#11111b
style G fill:#f38ba8,color:#11111b

Falla número dos: recalcular mal

La redundancia solo cuesta tiempo; la segunda patología cuesta corrección. Vuelve al primer recálculo de total, el que dispara subtotal. En ese instante subtotal ya refleja el precio nuevo, pero iva todavía no se ha tocado: sigue con su valor viejo. Así que total se calcula con una rama nueva y otra vieja. El resultado es un glitch: un total que no corresponde a ningún precio, ni al viejo ni al nuevo.

Pongámosle números, con cantidad igual a 2 y precio pasando de 100 a 150.

  1. Estado en reposo: subtotal vale 200, iva vale 21, total vale 221.
  2. precio pasa a 150. Se recalcula subtotal, que ahora vale 300.
  3. subtotal notifica a total, que se recalcula: 300 + 21 da 321. Este es el glitch.
  4. Se recalcula iva, que ahora vale 31.5.
  5. iva notifica a total, que se recalcula: 300 + 31.5 da 331.5. Ahora sí, correcto.

El 321 es imposible: para que subtotal valga 300 el precio ha de ser 150, pero para que iva valga 21 el precio ha de ser 100. Ningún precio produce ese par. El vértice del diamante exhibió un total que jamás fue real.

ℹ️
El diamante explica el glitch de la lección 1

El ejemplo a, b, c del capítulo anterior era un diamante disfrazado: a arriba, b en una rama, la lectura directa de a desde c en la otra, y c como reconvergencia. Por eso surgía el 7. Todo glitch esconde un diamante; sin reconvergencia, un cambio llega a cada nodo por una sola ruta y el orden nunca puede mezclar lo viejo con lo nuevo. El diamante es, literalmente, la condición necesaria del glitch.

Las dos fallas comparten una raíz: el vértice inferior se recalcula tantas veces como aristas de entrada tiene, y cada recálculo intermedio ocurre antes de que todas sus entradas estén al día. Redundancia y glitch son las dos caras de la misma moneda: propagar por aristas sueltas.

Por qué el empuje ingenuo no puede evitarlo

🔗

Razona por arista, no por nodo

El empuje ingenuo sigue cada arista en cuanto puede. Las aristas de subtotal y de iva hacia total son, para él, dos eventos independientes. No sabe que apuntan al mismo nodo ni que deberían fundirse en una sola actualización.

🙈

No ve la reconvergencia

Le falta la información global de que total tiene dos rutas hasta precio. Sin esa visión, no puede esperar a que ambas ramas terminen antes de tocar total.

🔁

No memoiza el disparo

Cada notificación que llega a total lo recalcula. No hay memoria de que ese nodo ya está en la cola para esta ronda, así que la segunda notificación no se descarta.

⏱️

Confunde llegar con estar listo

Trata el hecho de ser notificado como permiso para recalcular, cuando lo correcto es recalcular solo cuando TODAS sus entradas estén al día. Esa distinción es justo lo que arreglará el orden topológico.

La solución no puede ser local: ningún ajuste a la arista entre subtotal y total arregla el diamante, porque el problema es la relación entre DOS aristas que el empuje mira por separado. Hace falta una propiedad global del grafo —saber que total debe esperar a sus dos ramas— y esa propiedad tiene nombre matemático: el orden topológico, tema de la próxima lección.

Antes de ver esa cura en la próxima lección, conviene aterrizar el diamante en la práctica, porque aparece muchísimo más de lo que su aire teórico sugiere. Suena abstracto hasta que reconoces cuántas veces lo has creado sin querer: aparece siempre que dos derivados comparten una raíz y un tercero los combina, y ese patrón está por todo el ecosistema.

🧮

Selectores memoizados

Un selector de Reselect o de Redux Toolkit que deriva de otros dos selectores que leen la misma porción del store: raíz común, reconvergencia asegurada.

🟢

Computed encadenados

Un computed de Vue que depende de otros dos computed colgados de un mismo ref, o un useMemo de React que lee dos memos hermanos del mismo estado.

🔶

Derived stores

Un derived store de Svelte construido a partir de dos stores que, a su vez, derivan del mismo origen. El diamante está dibujado sin que lo hayas visto.

⚙️

Cualquier agregado

Un total, un resumen o un flag que combina dos parciales de la misma raíz. En cuanto agregas, reconverges.

En todos, el motor de reactividad decide si sufres las dos patologías o no. Con un sistema glitch-free ni te enteras; con uno ingenuo, verás el vértice recalcularse de más y, de vez en cuando, un parpadeo inexplicable que resulta ser el glitch del diamante. Y ojo con el falso alivio de las pruebas: la redundancia se paga siempre —dos recálculos, ocurra lo que ocurra—, pero el glitch visible solo aparece si algo observa el vértice entre el primer recálculo y el segundo. Un diamante puede parecer inofensivo en tests, donde nadie miró en el microsegundo malo, y fallar en producción, donde un efecto sí escuchaba.

💡
Cómo reconocer un diamante en tu grafo

Busca cualquier nodo con dos o más entradas y pregúntate: ¿esas entradas comparten, más arriba, alguna dependencia común? Si la respuesta es sí, tienes un diamante y estás a merced de cómo tu motor ordene la propagación. Si es no —cada entrada cuelga de fuentes disjuntas— ese nodo está a salvo: sin raíz compartida no hay reconvergencia, y sin reconvergencia no hay glitch.

La moraleja invierte la intuición: el diamante no es un patrón a evitar diseñando grafos en árbol. Los grafos reales reconvergen por todas partes, y está bien que lo hagan —expresan que varios resultados comparten un origen—. Lo que no puede fallar es el motor que los recorre.

El diamante es la prueba de que propagar es más que recorrer aristas

El diamante enseña que un grafo reactivo no es la suma de sus aristas, sino una estructura con reconvergencias que hay que respetar como un todo. La propagación ingenua es, en el fondo, un recorrido codicioso: en cuanto una arista puede dispararse, se dispara. Ese algoritmo es correcto para un árbol o una cadena, donde cada nodo tiene una única ruta hasta cada ancestro, pero se rompe en cuanto dos rutas se reencuentran. La reconvergencia introduce una obligación que ninguna arista conoce por sí sola: el nodo inferior no debe recalcularse hasta que TODAS sus entradas, por todas las rutas, estén al día. Formular esa obligación exige mirar el grafo entero, no una arista cada vez, y ahí está el giro conceptual: la corrección de la propagación es una propiedad global, emergente de la topología, que el empuje local jamás podrá garantizar por muy afinado que esté. El diamante no es un caso raro que haya que parchear; es el caso mínimo que revela que la reactividad seria necesita un planificador con visión de conjunto. Cada mecanismo del resto del nivel —altura de nodos, colas de prioridad, marcas de sucio, arrastre perezoso— es una manera de dotar al sistema de esa visión global que el diamante demuestra imprescindible.

⚔️ Desarma el diamante
  1. Construye precio, subtotal, iva y total y cuenta cuántas veces corre total al cambiar precio una sola vez con propagación ingenua.
  2. Registra el valor de total en cada recálculo, reproduce el 321 del ejemplo e identifícalo como el glitch.
  3. Apila un segundo diamante sobre la salida de total y predice cuántas veces se recalculará el vértice final; luego mídelo.
  4. Explica por qué arreglar una sola arista no elimina el problema.
  5. Formula en una frase la obligación que el vértice inferior debería respetar y que el empuje ingenuo ignora.
  6. Localiza un diamante real en una app tuya —dos derivados con una raíz común y un tercero que los une— y decide si tu motor lo propaga glitch-free.