El recorrido del grafo: qué gana Loro por adoptarlo
Loro toma del recorrido del grafo de eventos la idea de guardar índices simples y reconstruir el andamio solo al fusionar, y separa el registro de operaciones del estado del documento para pagar poco en memoria y en carga.
El nivel 35 presentó el recorrido del grafo de eventos como una tercera posición entre la transformación operacional y los CRDT clásicos, y lo dejó como algoritmo: elegante sobre el papel, con dos implementaciones citadas y una promesa de mejor comportamiento en memoria y en carga. Este nivel cierra ese arco mostrando qué ocurre cuando esa idea se lleva a una biblioteca de producción con compatibilidad hacia atrás, texto enriquecido, árboles con movimiento y viaje en el tiempo. La respuesta no es una adopción literal, y la propia documentación se cuida de aclararlo: Loro no es una implementación estricta del algoritmo, sino una biblioteca fuertemente inspirada por su filosofía de diseño que incorpora sus intuiciones clave. Esta lección precisa qué toma y qué no, y traduce esa decisión arquitectónica a las dos magnitudes que el usuario final acaba notando.
- Precisar en qué sentido Loro adopta el recorrido del grafo y en cuál se aparta de él.
- Entender la separación entre el registro de operaciones y el estado del documento, y por qué existe.
- Explicar de dónde sale el ahorro de memoria y qué parte de él es condicional.
- Interpretar correctamente el tiempo de carga, incluida la advertencia que la propia documentación publica.
Inspirado, no idéntico
La documentación de Loro dedica un aviso destacado a este punto, y conviene tomarlo en serio en lugar de leerlo como falsa modestia. Loro no implementa el algoritmo tal cual: adopta dos de sus intuiciones y las combina con una tercera pieza que no venía en el paquete.
La primera intuición adoptada es la representación. En lugar de guardar por cada carácter las identidades de sus vecinos —lo que exige el algoritmo de secuencia clásico, o lo que exige Fugue en su formulación directa— se registra la operación con el índice simple que tenía en el momento de ejecutarse. La segunda es la reproducción selectiva: cuando llegan operaciones remotas, se reconstruye la estructura de posiciones únicamente para el tramo divergente de la historia, no para el documento entero.
De donde viene cada propiedad
que se persiste ............ del recorrido del grafo
operaciones con indices simples mas contexto causal
como se coloca lo concurrente de Fugue
identidades con vecino izquierdo y derecho, sin entrelazar
como se expresa el formato ... aportacion propia de Loro
anclas emparejadas integradas por la secuencia
la correccion de la fusion la aporta la segunda capa,
no la primera, y confundirlo genera casi todos los malentendidos
La tercera pieza es la que rompe la identidad con el algoritmo original: para colocar lo concurrente, Loro usa Fugue, el algoritmo del nivel 34 que minimiza el entrelazado. Es decir, la garantía de calidad de fusión no procede del recorrido sino de la secuencia que el recorrido reconstruye temporalmente. Y en el caso del texto enriquecido esa combinación fue exactamente el problema: la formulación original de Peritext no se podía montar sobre el recorrido, porque asume que una misma operación produce siempre el mismo efecto independientemente del estado, y eso es justo lo que la reproducción no puede garantizar. De ahí nació el mecanismo de anclas de la lección anterior.
Es fácil mezclar los niveles al hablar de esto. La representación de lo que se persiste procede del recorrido del grafo: operaciones con índices y su contexto causal. El criterio de colocación de lo concurrente procede de Fugue. Y la semántica de texto enriquecido es una capa propia de Loro que se apoya en las dos anteriores sin depender de ninguna en particular. Al leer comparativas, casi todos los desacuerdos vienen de atribuir a una capa una propiedad que pertenece a otra.
Registro de operaciones y estado del documento
La consecuencia arquitectónica de adoptar esa representación es una separación tajante que atraviesa toda la biblioteca: por un lado un registro dedicado a la historia, por otro un estado que solo contiene el documento actual y ninguna información histórica. Cuando llegan actualizaciones remotas, un calculador de diferencias toma del registro las operaciones relevantes, computa la diferencia y la aplica al estado.
flowchart LR N[operaciones remotas] --> O[registro de operaciones] O --> C[calculador de diferencias] C --> S[estado del documento] S --> V[vista de la aplicacion] O --> T[viaje en el tiempo a cualquier version] T --> S style O fill:#cba6f7,color:#11111b style S fill:#a6e3a1,color:#11111b
Esta separación explica varias cosas de golpe y por eso vale la pena fijarla. Explica por qué el viaje en el tiempo es barato de implementar: si el estado es derivable y la historia está completa, moverse a una versión anterior es recalcular el estado, no deshacer nada. Explica por qué existen dos modos de exportación distintos —actualización e instantánea— porque son literalmente las dos mitades. Y explica por qué el estado en reposo no arrastra el peso de la historia: son estructuras separadas con ciclos de vida separados.
La separación también aclara un punto de la lección anterior que quedaba suelto. La lógica de texto enriquecido vive casi entera en el lado del estado, mientras que el cálculo de diferencias reutiliza sin cambios la misma maquinaria de secuencia que las listas. Es la razón técnica de que las anclas puedan tratarse como elementos ordinarios: el componente que las coloca no sabe que son estilos, y el componente que sabe que son estilos no participa en la fusión.
// Las dos mitades se exportan por separado y sirven para cosas distintas
const cambios = doc.export({ mode: "update" }); // historia incremental
const completo = doc.export({ mode: "snapshot" }); // estado mas historia
const otro = new LoroDoc();
otro.import(completo);
otro.checkout(version); // el estado se recalcula, no se deshace
Lo que se gana en memoria
El ahorro tiene tres fuentes distintas y conviene separarlas porque no son igual de sólidas.
La primera es la ausencia de metadatos permanentes por elemento. Un CRDT de secuencia convencional guarda para siempre, junto a cada carácter, las identidades que permiten recolocar cualquier inserción futura; aquí esas identidades solo existen mientras dura una fusión y se descartan al terminar. En reposo, lo que hay en memoria es el texto y una lista de operaciones comprimida.
Conviene precisar el alcance de ese ahorro, porque es la afirmación que más se exagera al citarla. No dice que la memoria sea baja en todo momento: dice que es baja en reposo. Durante una fusión con concurrencia real el andamio existe y ocupa, y su tamaño depende del tramo divergente que haya que atravesar. Lo que cambia es que ese pico es transitorio y acotado por la concurrencia, en lugar de ser permanente y acotado por la longitud del documento.
La segunda es la compresión de la propia lista. Las operaciones consecutivas del mismo autor sobre posiciones contiguas se fusionan en memoria mediante codificación de longitud de secuencia, de modo que teclear un párrafo entero no genera cientos de registros sino unos pocos rangos. Es la ventaja de representación de la lección primera aplicada aquí.
Que hay en memoria segun el momento
en reposo, sin fusionar nada
el estado del documento
la lista de operaciones comprimida por rangos
durante una fusion de dos ramas
lo anterior mas el andamio de identidades
su tamano depende del tramo concurrente, no del documento
despues de la fusion
el andamio se descarta y se vuelve al primer caso
La tercera es la recolección de lápidas, y es la condicional. La tesis es que una operación sincronizada con todos los extremos ya no puede ser concurrente con nada nuevo, y por tanto puede eliminarse de la historia sin perder corrección. La condición es saber que todos los extremos la tienen, lo cual en una topología abierta es precisamente lo que nadie sabe. Loro ofrece para esto instantáneas superficiales, que descartan la historia anterior a un punto a cambio de perder la capacidad de viajar más atrás de él.
Aquí hay que ser honesto con el balance. Junto con Automerge, Loro conserva el grafo completo del historial de edición pulsación a pulsación; la otra biblioteca del bloque no lo hace, y por eso sus documentos exportados son más compactos en el caso simple. Lo que la documentación de Loro señala a cambio es que esa comparación es incompleta: para poder viajar en el tiempo, la alternativa necesita almacenar por cada versión guardada un vector de versión y un conjunto de borrados, y ese coste no aparece en el tamaño del documento que se reporta. La comparación justa no es documento contra documento, sino capacidad contra capacidad.
Lo que se gana al cargar
Aquí está el resultado más vistoso y también el más fácil de malinterpretar, así que vale la pena enunciarlo con la precisión que la propia documentación emplea.
Cargar un documento exportado como instantánea no exige reproducir la historia. La instantánea contiene el estado ya materializado; la historia viaja al lado, comprimida, y solo se toca cuando hay que fusionar algo concurrente o cuando alguien pide una versión antigua. Frente a un diseño donde el estado se reconstruye aplicando todas las operaciones, la diferencia no es de porcentaje sino de orden: se pasa de un trabajo proporcional a la longitud de la historia a una lectura proporcional al tamaño del estado.
Estado materializado
La instantánea trae el documento listo, de modo que abrir un archivo con años de historia no obliga a recorrer esa historia.
Historia comprimida al lado
El registro de operaciones se codifica aparte y con compresión de rangos, y permanece inerte mientras nadie fusione ni viaje en el tiempo.
Versiones compactas
Identificar en qué punto está una réplica no requiere enumerar borrados: basta con las cabezas del grafo, que son unos pocos identificadores.
Instantáneas superficiales
Cuando la historia deja de hacer falta, se puede recortar por debajo de un punto y quedarse solo con lo posterior.
Y ahora la advertencia que la propia documentación publica junto a sus mediciones, porque omitirla sería vender humo: el tiempo de análisis es significativamente mayor tanto en Loro como en Automerge cuando el documento inicial no está vacío, por ejemplo al sincronizar contenido desde un servidor remoto. Es decir, el caso brillante es abrir un documento ya escrito; el caso menos brillante es recibir contenido sobre un documento que ya tenía algo. Cualquier medición propia debería reproducir los dos.
Vale la pena traducir eso a una recomendación operativa, porque tiene consecuencias en el diseño del arranque de la aplicación. Si la sesión empieza cargando una instantánea local y después recibe del servidor un lote de actualizaciones acumuladas, estás cayendo justo en el caso desfavorable. La alternativa habitual es invertir el orden cuando se pueda —resolver primero lo remoto sobre un documento vacío y aplicar después lo local— o bien agrupar la importación en un solo lote en lugar de aplicar los cambios uno a uno, que es lo que la biblioteca ofrece con su modo de importación por lotes.
Conviene no derivar de esta lección la conclusión de que hay que conservarlo todo para siempre. La historia completa es lo que habilita el viaje en el tiempo, la auditoría y la fusión de ramas muy divergentes; si tu aplicación no usa ninguna de esas tres capacidades, estás pagando almacenamiento por una opción que nunca ejerces. La respuesta de la biblioteca a ese caso son las instantáneas superficiales, y la decisión de usarlas debería tomarse explícitamente y no por omisión.
La decisión que sostiene todo lo anterior no es un algoritmo sino una línea trazada dentro del sistema, y esa línea es la lección exportable. Loro divide su interior en dos objetos con contratos opuestos: uno que es la verdad y solo crece —la historia, inmutable, sin la cual no se puede reconstruir nada— y otro que es una conveniencia y es desechable —el estado, derivable en cualquier momento a partir del primero—. Todo lo demás se sigue de haber trazado esa línea y de haberla respetado sin excepciones. El viaje en el tiempo se vuelve barato porque el estado ya era recalculable y nadie tuvo que inventar un mecanismo de deshacer. El andamio de identidades se puede destruir al acabar cada fusión porque no era verdad, era conveniencia. La instantánea puede incluir el estado sin que eso sea redundancia peligrosa, porque es una caché legítima de algo derivable. Y las instantáneas superficiales se vuelven expresables porque recortar la verdad es una operación con semántica clara —renuncias a lo anterior a un punto— mientras que recortar un estado materializado no significaría nada. Observa lo que ocurre cuando esa línea no se traza: en un sistema donde el estado integrado es la única representación, cada capacidad nueva —deshacer, historial, auditoría, fusión de ramas largas— hay que construirla como mecanismo aparte, y cada mecanismo aparte trae su propio almacenamiento y sus propios modos de fallo. La pregunta que hay que hacerle a cualquier sistema con estado es cuál de sus estructuras es la fuente de verdad y cuál es una caché, y si el sistema no puede responderla en una frase, todas sus capacidades futuras van a ser caras.
- Construye un documento con historia larga y expórtalo en los dos modos, actualización e instantánea.
- Mide el tiempo de importar cada uno sobre un documento vacío y anota la diferencia.
- Repite la importación de la actualización sobre un documento que ya tenga contenido y compara con el caso vacío.
- Mide la memoria en reposo tras cargar y compárala con la memoria durante una fusión de dos ramas divergentes.
- Recorta la historia con una instantánea superficial y comprueba qué operaciones dejan de estar disponibles.
- Escribe en dos frases cuál de las dos estructuras es la fuente de verdad en tu propia aplicación.