wandres.dev
EG-WALKER · guardar operaciones, no metadatos

La idea: guardar la operación original, no el andamio

Eg-walker persiste una lista inmutable y solo-añadir de las operaciones tal como se emitieron, igual que haría la transformación operacional, y trata el estado del documento como algo derivable recorriendo el grafo de eventos.

⏱ 19 min

La lección anterior terminó con una distinción que ahora hay que convertir en estructura de datos: el hecho de que ocurrió una edición es permanente, y el aparato que hizo falta para integrarla es circunstancial. Eg-walker, presentado por Joseph Gentle y Martin Kleppmann en EuroSys 2025, toma esa distinción literalmente y la lleva hasta el final. Lo único que persiste es una lista inmutable y solo-añadir de las operaciones originales, es decir, exactamente lo que el usuario hizo, expresado con posiciones numéricas como lo expresaría un algoritmo de transformación operacional, más una referencia a lo que ese usuario veía en el momento de hacerlo. Nada de identificadores por carácter, nada de punteros a vecinos, nada de lápidas persistidas. El texto no está guardado en ningún sitio: se calcula. Esta lección construye ese modelo de datos pieza a pieza y examina qué se gana, qué se pierde y qué obliga a replantear.

🎯 Al terminar esta lección sabrás
  • Distinguir con precisión qué persiste cada una de las tres escuelas ante la misma sesión de edición.
  • Construir el grafo de eventos como registro causal de operaciones originales con posiciones numéricas.
  • Justificar por qué esa lista puede ser inmutable, solo-añadir y directamente serializable.
  • Entender el estado del documento como función derivada y las consecuencias de tratarlo así.

Lo que persiste cada escuela ante la misma sesión

La comparación más instructiva es la más concreta: alguien escribe la palabra hola, borra la última letra y escribe una i. Tres pulsaciones más dos, y cuatro caracteres visibles al final. Veamos qué queda guardado en cada caso.

Un algoritmo de transformación operacional guarda las operaciones tal como se produjeron, con índices numéricos: inserta en la posición cero, inserta en la uno, borra en la tres. Es una representación compactísima, apenas mayor que el texto, y su debilidad conocida es que esos índices solo son válidos respecto a un estado concreto del documento, de modo que integrar una operación ajena exige transformarla contra todo lo que ocurrió mientras tanto. Cuando dos ramas divergen mucho, ese trabajo crece hasta hacer inviable la fusión de historias largas sin un servidor que imponga un orden común.

Un CRDT de secuencia guarda lo contrario: no las operaciones sino su efecto ya traducido a una estructura con identidades estables, que es precisamente lo que le permite integrar cualquier operación en cualquier orden sin transformar nada. Paga por ello el precio de la lección anterior, que es conservar esa estructura entera y para siempre.

Sesion: escribir hola, borrar la a, escribir la i

  Transformacion operacional
    ins 0 h | ins 1 o | ins 2 l | ins 3 a | del 3 | ins 3 i
    compacto, pero cada indice depende del estado exacto del autor

  CRDT de secuencia
    seis elementos con id, vecino izquierdo, vecino derecho y lapida
    integrable en cualquier orden, pero permanente y voluminoso

  Eg-walker
    las mismas seis operaciones con indices
    mas, en cada una, que operaciones habia visto su autor

La tercera fila es la propuesta entera, y su economía salta a la vista: se parece a la primera en tamaño y a la segunda en capacidad. El único añadido respecto a la representación de la transformación operacional es la referencia causal, y ese añadido es lo que sustituye a toda la maquinaria de identidades de la segunda.

ℹ️
El nombre describe el mecanismo con precisión

Eg-walker abrevia event graph walker, caminante del grafo de eventos. No es una metáfora: el algoritmo literalmente recorre el grafo de operaciones para producir el estado, y ese recorrido es la única vez que existe algo parecido a la estructura interna de un CRDT. Recordar el nombre completo ahorra la mitad de las explicaciones, porque contiene el modelo de datos y el procedimiento en tres palabras.

El grafo de eventos: la operación y lo que su autor veía

La pieza que convierte una lista de operaciones con índices en algo con sentido global es la referencia causal, y conviene entender exactamente qué guarda y por qué basta con eso. Cada evento apunta a sus padres, es decir, al conjunto de eventos que su autor había integrado en el instante de producirlo. Ese conjunto define sin ambigüedad el estado del documento contra el cual los índices de esa operación son correctos.

// Un evento: la operacion original mas su contexto causal
const evento = {
  id: { agente: "a7", seq: 412 },        // identidad del evento, no del caracter
  padres: [{ agente: "b1", seq: 88 }],   // que habia visto el autor al emitirlo
  op: { tipo: "ins", pos: 3, texto: "i" }, // indice, como en transformacion operacional
};

// El grafo completo es una lista solo-anadir de eventos como el anterior
const grafo = [];
function registrar(ev) {
  if (grafo.some((e) => mismoId(e.id, ev.id))) return; // idempotente
  grafo.push(ev);                                       // nunca se modifica lo ya escrito
}

Fíjate en un detalle decisivo: el identificador es del evento, no del carácter. Un evento puede insertar veinte caracteres de golpe y sigue siendo un solo registro. La identidad que la lección anterior repartía carácter a carácter aquí se reparte operación a operación, y las operaciones son órdenes de magnitud menos numerosas que los caracteres cuando la gente escribe de forma normal.

El grafo resultante es un grafo dirigido acíclico exactamente igual al de un sistema de control de versiones distribuido, con la misma semántica: las aristas son la relación de precedencia causal, las ramas son divergencias que aún no se han fusionado y las cabezas son los eventos sin descendientes conocidos. Todo lo que aprendiste en el nivel de relojes lógicos sobre resúmenes por cabezas, cálculo de diferencias y ancestros comunes se aplica aquí sin traducción.

Esa continuidad no es solo pedagógica, también es operativa. El protocolo de sincronización entre dos réplicas es el mismo que ya conoces: intercambiar cabezas, calcular qué le falta a cada una recorriendo el grafo hacia atrás desde las cabezas ajenas, y enviar únicamente esos eventos. No hace falta una sesión, ni un orden acordado, ni que las dos partes estén de acuerdo sobre nada previo. Y como los eventos son inmutables e idempotentes, reenviar de más no rompe nada, lo que permite protocolos de transporte deliberadamente simples.

flowchart LR
R[raiz del documento] --> A1[a7 emite ins 0 h]
A1 --> A2[a7 emite ins 1 o]
A2 --> B1[b1 emite ins 2 mundo]
A2 --> C1[c3 emite del 1]
B1 --> M[fusion: ambas ramas son padres]
C1 --> M
style M fill:#cba6f7,color:#11111b
style B1 fill:#89b4fa,color:#11111b
style C1 fill:#89b4fa,color:#11111b

Una precisión sobre los padres que evita un malentendido habitual: no hace falta enumerar todo lo que el autor había visto, sino únicamente las cabezas de ese conjunto, es decir, los eventos que no eran ancestros de ningún otro evento visto. El resto se deduce recorriendo el grafo hacia atrás. En la enorme mayoría de los casos hay una sola cabeza y la lista de padres tiene un elemento; solo tras integrar una rama ajena aparece un evento con dos.

Las dos ramas azules del diagrama son el caso interesante y el que ocupa la lección siguiente: dos operaciones con índices calculados sobre el mismo estado padre, ninguna de las cuales sabe de la otra. Sus índices son mutuamente incorrectos, y sin embargo el grafo contiene toda la información necesaria para colocarlas bien, porque cada una declara con exactitud contra qué estado se midió.

Por qué la lista puede ser inmutable y solo-añadir

Que el registro sea de solo-añadir no es una elección estética sino una consecuencia de qué se está guardando, y sus efectos prácticos son mayores de lo que parece a primera vista. Un evento describe algo que ya sucedió en un contexto que ya está determinado; ninguna información futura puede alterarlo, porque su verdad no depende del estado global sino del estado local que su autor tenía delante. La transformación operacional clásica, en cambio, reescribe las operaciones al integrarlas, y por eso lo que guarda no es estable.

💾

Serialización trivial

El fichero en disco es la lista de eventos tal cual, sin conversión ni reconstrucción de índices al escribir.

Escritura incremental

Guardar tras una edición es añadir bytes al final del archivo, sin reescribir lo anterior ni compactar nada.

🔁

Idempotencia gratis

Recibir dos veces el mismo evento es detectable por identidad, así que el transporte puede ser tosco sin consecuencias.

🧾

Auditoría real

La historia es lo que se persiste, no un subproducto, de modo que reconstruir cualquier versión anterior es recorrer menos grafo.

La escritura incremental merece un comentario aparte porque es la ventaja que más cambia la experiencia de uso y la que menos se menciona. En un CRDT convencional, persistir el documento implica volcar o actualizar una estructura grande, y las bibliotecas resuelven eso con formatos comprimidos que hay que regenerar cada cierto tiempo. Aquí el guardado es un anexado al final de un archivo, la operación más barata que ofrece cualquier sistema de ficheros, y su coste es proporcional a lo que el usuario acaba de escribir y no a lo que el documento ya contenía.

💡
La codificación por tramos también se aplica aquí, y mejor

Igual que en un CRDT los caracteres consecutivos de un mismo autor se agrupan, aquí se agrupan las operaciones: una ráfaga de tecleo del mismo agente, con contadores consecutivos e índices que avanzan de uno en uno, se representa como un solo tramo con longitud. La diferencia es que este registro no se fragmenta con el uso, porque nadie edita el pasado del grafo, mientras que la estructura de un CRDT sí se fragmenta cada vez que alguien vuelve sobre lo escrito. Es la razón de que la compresión se comporte mejor sobre eventos que sobre elementos.

El estado del documento como función derivada

La consecuencia conceptual de este modelo de datos es la que hay que interiorizar antes de pasar al recorrido: en Eg-walker el documento no es un objeto almacenado que se actualiza, sino el resultado de aplicar una función al grafo. El texto es una vista materializada de la historia, y la estructura con identidades y lápidas que la lección anterior describía como el documento pasa a ser un intermedio temporal que se fabrica cuando hace falta calcular esa vista y se destruye después.

// El texto no se guarda: se deriva del grafo
function textoDe(grafo, hastaVersion) {
  const orden = ordenTopologico(grafo, hastaVersion);
  let estadoInterno = crearEstadoVacio();   // el andamio, efimero
  for (const ev of orden) estadoInterno = integrar(estadoInterno, ev);
  return proyectar(estadoInterno);          // el texto visible
}
// Terminada la llamada, estadoInterno queda sin referencias y se recolecta

Esto no es gratis, y conviene decir con claridad qué se paga. Derivar el estado desde cero para un documento con historia larga es caro, así que un sistema práctico mantiene el texto visible en memoria y le aplica las operaciones nuevas de forma incremental, reservando el recorrido completo para los casos en que de verdad hace falta. La lección siguiente muestra que esos casos son menos y más acotados de lo que se teme, y la cuarta muestra cuánto cuestan cuando se miden en trazas reales.

Hay una segunda contrapartida que conviene anticipar porque cambia el diseño del producto y no solo el del algoritmo: el registro crece de forma monótona y nada lo reduce por sí solo. En un CRDT esa acumulación estaba escondida dentro de la estructura y se percibía como memoria; aquí está explícita en un archivo cuyo tamaño se puede mirar. Es más honesta y también más incómoda, porque obliga a decidir de forma consciente cuánta historia se conserva, dónde se corta si se corta y qué se pierde al hacerlo. Esa decisión existía antes igual, solo que nadie la tomaba.

Es la separación entre el libro de asientos y el balance, aplicada a la edición de texto

Hay un patrón que la ingeniería de datos lleva medio siglo redescubriendo bajo nombres distintos, y reconocerlo aquí convierte a Eg-walker de truco ingenioso en instancia de algo mucho más general. En contabilidad se distingue entre el libro de asientos, que es inmutable, solo-añadir y donde vive la verdad, y el balance, que es una agregación derivada, cara de recomputar desde cero y que se mantiene al día de forma incremental. En bases de datos esa misma distinción aparece como registro de transacciones frente a tablas materializadas; en arquitectura de aplicaciones, como abastecimiento por eventos frente a proyecciones. En todos los casos la moraleja es idéntica y contraintuitiva: guardar la historia y derivar el estado sale más barato que guardar el estado, siempre que la historia se pueda escribir sin reescribirla. Lo que distingue este resultado es que el texto colaborativo parecía el peor candidato imaginable para ese patrón. Un CRDT de secuencia es, mirado con esta lente, un balance que se ha vuelto más grande que el libro de asientos que lo generó, porque cada línea del libro deja en el balance un residuo permanente que ya no se puede agregar con nada. El error no estaba en materializar, estaba en que la materialización no era una agregación: era una acumulación. Y la razón profunda de que Eg-walker funcione es que el andamio de la fusión resulta ser agregable en un sentido en que las lápidas no lo eran, porque el orden relativo entre dos caracteres, una vez que todas las réplicas han visto ambos, deja de ser una hipótesis que haya que poder revisar y pasa a ser un hecho consolidado que el texto visible ya expresa por sí mismo. Ahí está la palanca completa. La consecuencia de diseño que conviene llevarse más allá de este algoritmo es una pregunta que se puede hacer a cualquier estructura replicada que resulte cara: qué parte de lo que estoy guardando es un hecho, qué parte es andamio de una integración que ya terminó, y si el andamio es reconstruible a partir de los hechos. Cuando la respuesta a lo tercero es que sí, guardarlo es una decisión, no una necesidad, y es exactamente la decisión que hay que revisar.

⚔️ Modela tu documento como grafo de eventos
  1. Toma una sesión de edición corta y escríbela a mano como lista de eventos con identidad, padres y operación con índice.
  2. Introduce a propósito una rama concurrente y comprueba que los índices de las dos ramas son mutuamente incorrectos.
  3. Serializa tu lista a un archivo y verifica que añadir una edición nueva solo requiere anexar bytes al final.
  4. Compara el tamaño de esa serialización con el volcado de la misma sesión en la biblioteca de CRDT que uses.
  5. Escribe la función que recibe dos eventos y responde si uno precede al otro, si son iguales o si son concurrentes.
  6. Anota qué pregunta de tu producto necesita el grafo entero y cuál se responde solo con el texto derivado.