Yjs y Automerge
La lección anterior explicó por qué los CRDTs convergen; esta presenta las dos librerías que los llevan a producción y dominan el ecosistema de colaboración en tiempo real. Yjs apuesta por el rendimiento: implementa el algoritmo YATA sobre tipos compartidos mutables —Y.Map, Y.Array, Y.Text—, produce actualizaciones binarias diminutas, sincroniza con vectores de estado para enviar solo lo que falta y se enchufa a cualquier editor a través de bindings maduros y a cualquier transporte a través de providers desacoplados. Automerge apuesta por la fidelidad del modelo de datos: ofrece un documento con forma de JSON, una API inmutable al estilo de un change funcional, y sobre todo una historia completa e inspeccionable —un Git para tus datos— donde cada cambio es un nodo en un grafo acíclico dirigido. La lección diseca ambos modelos, muestra su código mínimo, explica por qué el protocolo awareness separa el estado efímero del documento persistente, y contrasta las dos filosofías para que el lector sepa cuál elegir según valore el rendimiento y la ergonomía del editor o la historia y la pureza del modelo de datos.
Saber que un CRDT converge es una cosa; construir uno que edite un documento de cien mil palabras a sesenta cuadros por segundo, sincronice por una red intermitente enviando kilobytes y se integre con el editor que ya usas es otra muy distinta. Entre la teoría del semirretículo y el editor colaborativo real hay años de ingeniería, y esa ingeniería está encarnada hoy en dos librerías que se reparten casi todo el terreno: Yjs y Automerge. No son competidoras en el sentido trivial de hacer lo mismo peor o mejor; son dos filosofías sobre qué debe optimizar un CRDT en producción. Yjs pregunta cómo hacer que fundir sea tan rápido y tan compacto que puedas colaborar en tiempo real sobre cualquier estructura, y sacrifica sin remordimiento lo que haga falta —la historia, por ejemplo— en el altar del rendimiento. Automerge pregunta cómo hacer que tus datos tengan la forma limpia de un JSON y arrastren su historia completa como un repositorio de Git, y acepta pagar por ello en peso y velocidad. Esta lección te da el modelo mental de cada una, su código esencial y, sobre todo, el criterio para elegir: porque la pregunta correcta no es cuál es mejor, sino qué estás dispuesto a sacrificar.
- Comprender el modelo de Yjs: tipos compartidos, actualizaciones binarias y sincronización por vectores de estado.
- Comprender el modelo de Automerge: documento JSON, API inmutable e historia como grafo de cambios.
- Distinguir el estado persistente del documento del estado efímero que gestiona el protocolo awareness.
- Contrastar ambas filosofías —rendimiento frente a historia— y elegir según el caso de uso.
Yjs: rendimiento y tipos compartidos
Yjs, obra de Kevin Jahns, es el CRDT más usado en producción para colaboración en tiempo real, y su seña de identidad es la velocidad. En su núcleo late YATA —Yet Another Transformation Approach—, un CRDT de secuencia optimizado hasta el extremo para que insertar, borrar y fundir sean baratísimos incluso en documentos enormes. Por encima de ese núcleo, Yjs expone un puñado de tipos compartidos que se comportan como estructuras normales pero son CRDTs por dentro: Y.Map para objetos, Y.Array para listas y Y.Text para texto rico. Todos cuelgan de un Y.Doc, la raíz del documento. Los editas con una API mutable y familiar, y Yjs se encarga de que cada cambio genere una actualización binaria diminuta —un delta— que representa solo lo que cambió, no el documento entero.
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
const doc = new Y.Doc()
const provider = new WebsocketProvider('wss://demo', 'sala-1', doc)
const texto = doc.getText('contenido')
texto.insert(0, 'Hola') // edicion local mutable; se propaga sola
doc.on('update', (delta: Uint8Array) => {
// delta binario y compacto: viaja solo el cambio, nunca el documento entero
})
Dos piezas hacen a Yjs desplegable en el mundo real. La primera son los providers: Yjs no sabe nada de red, y delega el transporte en módulos intercambiables —y-websocket para un servidor central, y-webrtc para conexiones entre pares, y-indexeddb para persistir en el navegador—. Puedes apilar varios a la vez y cambiarlos sin tocar la lógica de datos. La segunda es la sincronización por vectores de estado: antes de enviar nada, dos réplicas intercambian un resumen compacto de qué operaciones ya conocen, y así cada una manda al otro únicamente lo que le falta, sin retransmitir lo que ya tiene. Es la diferencia entre sincronizar un documento grande en unos bytes o en megabytes.
Hay un tipo de estado que nunca debería vivir en el Y.Doc: el efímero. La posición del cursor, la selección, el color y el nombre de cada colaborador cambian decenas de veces por segundo y no tiene sentido persistirlos ni fundirlos como parte del documento. Yjs lo resuelve con un canal aparte, el protocolo awareness, que propaga presencia sin ensuciar el CRDT.
import { WebsocketProvider } from 'y-websocket'
provider.awareness.setLocalState({ usuario: 'Ada', cursor: 42 })
provider.awareness.on('change', () => {
const presentes = provider.awareness.getStates() // quien esta y donde mira
})
Automerge: un documento con historia
Automerge, ligado al trabajo de Martin Kleppmann, hace la apuesta contraria: prioriza que tus datos tengan la forma de un documento JSON limpio y que conserven su historia completa. Un documento Automerge es un objeto anidado —mapas, listas, texto, números— que editas a través de una API inmutable: no mutas el documento, sino que llamas a change con una función que describe la modificación, y recibes un documento nuevo. Por dentro, cada llamada a change produce un cambio identificado por un hash, y esos cambios se enlazan en un grafo acíclico dirigido exactamente como los commits de Git, con lo que puedes inspeccionar la historia, atribuir cada edición a su autor y viajar a cualquier estado pasado.
import { next as Automerge } from '@automerge/automerge'
let doc = Automerge.init<{ items: string[] }>()
doc = Automerge.change(doc, (d) => { d.items = [] }) // API inmutable
doc = Automerge.change(doc, (d) => { d.items.push('leche') })
const historia = Automerge.getHistory(doc) // cada cambio queda registrado
const binario = Automerge.save(doc) // codificacion columnar compacta
El núcleo de Automerge se reescribió en Rust y se ejecuta en el navegador vía WebAssembly, lo que cerró buena parte de la brecha de rendimiento con Yjs sin renunciar a su modelo. La fusión de dos réplicas divergentes es explícita y limpia, y devuelve un documento nuevo que ambas partes comparten, con el grafo de cambios reconstruido a partir de los hashes que cada una aportó.
// dos replicas editaron por separado; merge las reconcilia sin arbitro
let miDoc = Automerge.change(base, (d) => { d.items.push('pan') })
let suDoc = Automerge.change(base, (d) => { d.items.push('huevos') })
miDoc = Automerge.merge(miDoc, suDoc) // ahora contiene pan y huevos, converge
La historia no es un adorno: es la propiedad que define a la librería. Donde Yjs, por defecto, descarta el pasado para ir ligero, Automerge lo guarda todo, y esa decisión habilita casos —auditoría, atribución, deshacer profundo, ramas y fusiones al estilo de Git— que en Yjs habría que construir aparte. Cada réplica se identifica con un actor único, cada cambio conoce a sus predecesores, y el conjunto forma un grafo que puedes recorrer, comparar y bifurcar. El precio es un documento más pesado y una biblioteca más grande, la contrapartida honesta de guardar cada paso del camino.
Un error común es meter en el CRDT cosas que no deberían persistir: la posición del cursor, la selección, quién está en línea, el color de cada colaborador. Ese estado es efímero —cambia cien veces por segundo y no tiene sentido guardarlo en la historia del documento—. Yjs lo resuelve con un protocolo separado, awareness, que propaga presencia y cursores por un canal aparte, sin ensuciar el documento ni engordar su historial. La regla es limpia: lo que debe sobrevivir a la sesión va en el CRDT; lo que solo importa mientras hay alguien conectado va en awareness.
Dos filosofías del mismo problema
Ambas librerías resuelven el mismo problema —fundir ediciones concurrentes sin árbitro— y ambas comparten rasgos: transporte agnóstico, protocolos de sincronización binarios y eficientes, y bindings o integraciones para editores. La diferencia está en qué optimizan, y esa diferencia debe guiar tu elección.
Yjs: velocidad y ecosistema
Óptimo cuando el rendimiento manda: documentos grandes, muchos colaboradores, edición a sesenta cuadros por segundo. Su ecosistema de bindings —ProseMirror, TipTap, CodeMirror, Monaco— es el más maduro para editores. Descarta la historia por defecto.
Automerge: modelo e historia
Óptimo cuando importan la forma de los datos y su pasado: JSON limpio, historia completa, atribución, ramas al estilo de Git. Ideal para datos de aplicación estructurados, no solo texto. Más pesado a cambio de recordarlo todo.
El flujo de sincronización, en cambio, es conceptualmente idéntico en las dos, y merece la pena visualizarlo porque es el corazón de la colaboración en tiempo real: cada réplica anuncia qué conoce, recibe solo lo que le falta y funde localmente sin pedir permiso.
sequenceDiagram participant A as Replica A participant B as Replica B A->>B: vector de estado, esto ya conozco B->>A: vector de estado, esto conozco yo B->>A: delta binario con lo que a A le falta A->>B: delta binario con lo que a B le falta Note over A,B: cada una funde local, convergen sin arbitro
La elección práctica, entonces, se reduce a una pregunta sobre tus prioridades. Si construyes un editor de texto colaborativo de alto rendimiento y quieres enchufarte al ecosistema de editores más rico, Yjs es la elección de defecto. Si construyes una aplicación cuyos datos son documentos estructurados que quieres versionar, auditar y ramificar —una app de notas con historial, una herramienta con deshacer profundo, un sistema donde importa quién cambió qué y cuándo—, Automerge encaja mejor. Ninguna es universalmente superior; cada una hizo un sacrificio distinto, y elegir bien es reconocer cuál de esos sacrificios puedes permitirte.
Un rasgo compartido que conviene interiorizar es la estricta separación de capas. El CRDT —el Y.Doc o el documento Automerge— no sabe nada de red ni de interfaz; es dato puro que sabe fundirse. El transporte —providers en Yjs, automerge-repo en Automerge— es una capa aparte y sustituible. Y la interfaz —los bindings de editor— es otra más. Esa separación es lo que te permite empezar sobre WebSocket y migrar a conexiones entre pares sin tocar tu modelo de datos, o probar tu lógica de fusión sin abrir un navegador. El CRDT es el núcleo estable alrededor del cual todo lo demás es intercambiable.
Conviene dar un paso atrás y ver qué clase de objeto son realmente Yjs y Automerge, porque no encajan en ninguna categoría de las que traías del track. No son un store como Redux ni un signal ni un caché de servidor: son máquinas para reconciliar el tiempo. Todo gestor de estado que estudiaste hasta ahora asumía una línea temporal única —una secuencia de acciones, un orden de renders, un reloj—, y su trabajo era mantener coherente esa línea. Yjs y Automerge trabajan en un mundo sin línea temporal única: cada réplica tiene su propio tiempo, sus propias ediciones ocurren en paralelo a las de otros, y no existe un ahora global que las ordene. Lo que estas librerías empaquetan es la matemática de la lección anterior convertida en una herramienta que fusiona esos tiempos divergentes en un presente compartido y consistente, sin un reloj central que los sincronice. Por eso sus decisiones de diseño se leen mejor como respuestas a la pregunta qué hacemos con el pasado: Yjs contesta que el pasado es lastre y lo tira para volar ligero en el presente; Automerge contesta que el pasado es el activo más valioso y lo conserva entero, aunque pese. Esa divergencia no es un detalle de implementación, es una postura filosófica sobre la naturaleza de los datos colaborativos, y entenderla te da algo más duradero que saber invocar sus APIs: te da el criterio para juzgar la próxima librería CRDT que aparezca. No preguntes qué features tiene; pregunta qué hace con el tiempo y con la historia, y sabrás para qué sirve.
- Levanta un
Y.Doccony-websockety ábrelo en dos pestañas. Edita unY.Texten una y observa la propagación instantánea a la otra. - Inspecciona en la consola el
Uint8Arrayque emite el eventoupdatede Yjs y comprueba lo compacto que es un delta frente al documento completo. - Desconecta la red, edita en ambas pestañas a la vez y reconecta. Verifica que las dos convergen al mismo texto sin perder ninguna edición.
- Repite el ejercicio con Automerge usando
changeymerge, y llama agetHistorypara ver la lista de cambios que Yjs no te habría dado. - Añade presencia con el protocolo
awarenessde Yjs y confirma que la posición del cursor viaja por un canal distinto y no queda en la historia del documento. - Escribe en tres frases, para un proyecto tuyo concreto, si elegirías Yjs o Automerge y qué sacrificio estás aceptando al hacerlo.