Livelymerge aborda el problema fundamental de la persistencia y la colaboración en entornos de programación interactivos, tradicionalmente resuelto con imágenes de sistema (Smalltalk) o sistemas de archivos distribuidos. La tesis es que un Conflict-free Replicated Data Type (CRDT) como Automerge puede servir como el "heap" subyacente de un programa JavaScript, permitiendo que el estado completo de la aplicación sea automáticamente persistente y compartible entre múltiples usuarios en tiempo real. Esto resuelve la complejidad de sincronizar el estado de la aplicación en sistemas distribuidos, delegando la resolución de conflictos a la semántica de Automerge.
La relevancia actual radica en la creciente demanda de aplicaciones colaborativas en tiempo real y la arquitectura "local-first", donde los datos residen primariamente en el cliente y se sincronizan asincrónicamente. Al usar Automerge como el sustrato del heap, Livelymerge elimina la necesidad de una capa de serialización/deserialización explícita y de lógica de sincronización de estado a nivel de aplicación, simplificando drásticamente el desarrollo de sistemas colaborativos complejos. La elección de JavaScript y el navegador como plataforma subraya la ambición de llevar estas capacidades a un entorno ubicuo y accesible.
Arquitectura del Sistema
La arquitectura de Livelymerge se basa en dos componentes principales: un modelo de objetos y un transpiler. El modelo de objetos representa el heap del programa como un documento Automerge, que es una estructura JSON-like. Para manejar referencias cíclicas y la naturaleza de árbol de JSON, el heap se serializa en una objectTable global que mapea UUIDs de objetos a su estado. Cada objeto, array o función tiene una entrada en esta tabla con propiedades especiales ($type, $id, $protoId, $code, $scopes) y propiedades de usuario (@x, @y). Las referencias a otros objetos se serializan como { $type: 'ref', $id: ... }.
El transpiler reescribe el código JavaScript plano para utilizar primitivas de creación de objetos, arrays y funciones que interactúan con este heap serializado. Las referencias a variables globales y las variables capturadas por closures también son reescritas para apuntar a objetos en la objectTable. La interacción con los objetos del heap se realiza a través de proxies. Cuando se lee una propiedad, el proxy deserializa el valor del documento Automerge; cuando se escribe, serializa el valor y lo almacena. Los proxies se cachean por ID de objeto para mantener la identidad de los objetos (===).
Un componente crítico es la función change que envuelve las modificaciones al documento Automerge. Esta función incorpora un recolector de basura (GC) que gestiona los objetos recién creados. En lugar de instalar inmediatamente todos los objetos nuevos en el documento Automerge, los objetos temporales (como bounding boxes en rendering) se mantienen en un "shadow document" local. Solo los objetos que se vuelven alcanzables desde la raíz del heap al final de una transacción change son promovidos al documento Automerge. Este mecanismo de "op economy" evita el inflado del historial del documento con operaciones efímeras. Los objetos que ya han sido persistidos en Automerge nunca son recolectados, respetando la semántica de sistemas local-first donde un colaborador offline podría aún referenciar un objeto.
Flujo de Interacción con Objeto Persistente
- 1 Código JS El usuario escribe o ejecuta código JavaScript (ej. `obj.prop = value;`)
- 2 Transpiler Reescribe el acceso a la propiedad como una operación en un proxy
- 3 Proxy de Objeto Intercepta la operación (lectura/escritura)
- 4 Serializador Serializa el valor a un formato compatible con Automerge (primitivo o `{ $typ...
- 5 Documento Automerge Almacena el valor serializado en la `objectTable`
- 6 Deserializador (en lectura) Recupera el valor serializado y lo convierte de nuevo a un proxy de objeto JS
Flujo de Gestión de Objetos Temporales (GC)
- 1 Inicio de `change` Se invoca la función `change` para ejecutar código LM
- 2 Creación de Objeto Código LM crea un nuevo objeto (ej. bounding box)
- 3 Shadow Document El objeto se almacena inicialmente en un documento local (JS) no persistente
- 4 Fin de `change` Se completa la ejecución del código LM
- 5 GC El recolector de basura identifica objetos alcanzables desde la raíz del heap
- 6 Promoción Objetos alcanzables se mueven del shadow document al documento Automerge
- 7 Reclamación Objetos no alcanzables en el shadow document son descartados sin tocar Automerge
| Capa | Tecnología | Justificación |
|---|---|---|
| storage | Automerge | Proporciona el almacenamiento persistente y colaborativo para el heap del programa, gestionando la replicación y la resolución de conflictos (CRDT). vs CRDTs basados en operaciones (Op-based CRDTs), Bases de datos distribuidas con lógica de sincronización personalizada, Sistemas de archivos distribuidos |
| compute | JavaScript / V8 Engine | Entorno de ejecución para el código del programa Livelymerge. El transpiler y los proxies se integran con el modelo de objetos de JS. vs WebAssembly, Otros lenguajes con runtimes embebibles |
| data-processing | Transpiler (custom) | Reescribe el código JavaScript estándar para interactuar con el modelo de objetos de Livelymerge, manejando la creación de objetos, referencias globales y closures. vs Modificación del runtime JS (ej. V8), Uso de proxies directamente sin reescritura de sintaxis |
| cache | Proxy Cache (custom) | Almacena en caché los proxies de objetos por ID para asegurar la identidad de los objetos (referencias ===) y optimizar el rendimiento de la deserialización. vs No caching (mayor sobrecarga de deserialización), Cachés de objetos genéricos |
Trade-offs
Ganancias
- ▲ Persistencia automática del heap
- ▲ Colaboración multiusuario inherente
- ▲ Simplificación del desarrollo de aplicaciones colaborativas
- ▲ Reducción de operaciones en Automerge para objetos temporales
Costes
- △ Complejidad del modelo de objetos y transpiler
- ▲ Aumento del tamaño del historial de Automerge (objetos persistentes nunca se recolectan)
- ▲ Sobrecarga de rendimiento por serialización/deserialización y proxies
- △ Necesidad de gestionar estado local y efímero fuera del modelo principal
function change(fn) {
let exception;
let returnValue;
docHandle.change((_doc) => {
doc = _doc;
$global = proxify(doc.objectTable.global);
try {
returnValue = fn();
} catch (e) {
exception = e;
} finally {
gc();
}
});
if (exception) {
console.error(exception);
throw exception;
}
return returnValue;
}{
// the names of special properties are prefixed with a $
$type: 'obj',
$id: '84f2…', // id of this object
$protoId: 'd9c0…', // id of the object that this object delegates to
// user properties are prefixed with a @ (this sidesteps an Automerge
// bug involving property names like `toString` that collide with
// Object.prototype)
'@x': 5, // numbers are represented ...
'@y': 6, // ... as numbers
// object references (like the value of the `next` property below)
// are represented as objects with `$type = 'ref'` and the id of
// the referent:
'@next': { $type: 'ref', $id: '52aa…' },
}Fundamentos Teóricos
El concepto de un "heap" persistente y la capacidad de modificar un sistema en ejecución tiene raíces profundas en la computación, notablemente en los sistemas Smalltalk y Lisp. La idea de una "imagen" del sistema que encapsula todo el estado del programa, permitiendo la persistencia y la reanudación exacta del trabajo, fue pionera en Smalltalk. Livelymerge extiende esto al dominio distribuido y colaborativo.
La base teórica de la colaboración multiusuario y la resolución de conflictos se encuentra en los Conflict-free Replicated Data Types (CRDTs). Automerge, la tecnología central de Livelymerge, es una implementación de CRDT que garantiza la convergencia de réplicas sin necesidad de un coordinador central, incluso en presencia de ediciones concurrentes. Este enfoque se alinea con los principios de la computación distribuida tolerante a fallos y la consistencia eventual, donde la disponibilidad y la tolerancia a particiones son priorizadas sobre una consistencia fuerte inmediata, como se describe en el teorema CAP. La gestión de objetos y referencias en un entorno CRDT también se relaciona con los desafíos de la serialización de grafos de objetos y la gestión de la identidad en sistemas distribuidos, un problema explorado en trabajos sobre sistemas de objetos distribuidos y bases de datos orientadas a objetos.