Diseña el cliente móvil de una app colaborativa en tiempo real (como Google Docs)
Una inmersión senior en la sincronización móvil basada en CRDT — edición local-first, colas sin conexión, reconciliación por vectores de estado y por qué este problema no se parece en nada a un cliente REST normal.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Diseña el cliente móvil de una app colaborativa en tiempo real (como Google Docs)
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Aclarar los requisitos
Antes de elegir un algoritmo de sincronización, definiría qué tiene que garantizar colaborativo en tiempo real. ¿Varias personas necesitan ver las ediciones de las demás en uno o dos segundos, o basta con consistente eventualmente en unos pocos segundos? ¿La app tiene que seguir funcionando — aceptando ediciones — con el teléfono en modo avión, y fusionar todo limpiamente al reconectar? ¿Necesitamos cursores en vivo y presencia de quién está editando qué, o solo el documento fusionado? ¿El contenido es texto plano, o tiene estructura — encabezados, listas, imágenes incrustadas — que un diff de texto ingenuo destrozaría? ¿Y cuál es la vida útil y el tamaño esperado del documento?, porque un documento que vive años y se edita miles de veces tiene requisitos de almacenamiento muy distintos a los de un documento de vida corta parecido a un chat. Mobile agrega restricciones que un cliente web no tiene que pensar tan a fondo: la conectividad intermitente es el caso normal, no el caso límite, y la app pasa a segundo plano y se cierra de forma mucho más agresiva que una pestaña de navegador.
#OT vs. CRDT: la decisión que le da forma a todo el cliente
Existen dos familias de algoritmos bien establecidas para este problema, y la elección entre ellas define casi todo lo que viene después. Operational Transformation — el modelo que el propio Google Docs popularizó — mantiene a un único servidor como árbitro del orden: cada cliente envía operaciones al servidor, que transforma cada operación entrante contra las operaciones concurrentes que ya aplicó, y transmite el resultado transformado a todos los demás. Funciona bien, pero requiere de manera fundamental un servidor vivo y alcanzable para darle sentido a las ediciones concurrentes, lo cual encaja incómodamente con un cliente móvil que necesita seguir aceptando ediciones sin ninguna conexión. Los CRDT — Conflict-free Replicated Data Types, el modelo detrás de librerías como Yjs y Automerge — toman el enfoque opuesto: cada operación está diseñada para que aplicarla en cualquier orden, en cualquier réplica, converja al mismo resultado sin que una autoridad central resuelva nada. Eso hace que los CRDT sean el encaje natural para un cliente móvil, porque el teléfono puede seguir siendo una réplica completamente funcional del documento incluso con cero conectividad, y la reconciliación al reconectar es una fusión mecánica en lugar de una negociación con un servidor.
#El cliente móvil como almacén CRDT local-first
Dada esa elección, diseñaría el teléfono de modo que el documento CRDT sea la fuente de verdad del cliente, no una caché de algo que el servidor posee. Cada pulsación de tecla muta de inmediato el documento CRDT local, la UI renderiza directamente desde ese estado local — que es lo que da el eco local instantáneo, sin latencia, que los usuarios esperan de un editor de texto — y la operación resultante se agrega a un pequeño log durable en disco antes de que se envíe nada a ningún lado, así una app cerrada a la fuerza no pierde la edición. Una capa de sincronización en segundo plano compara ese log local contra el último estado que envió con éxito, lo agrupa en una actualización binaria compacta (tanto Yjs como Automerge tienen codificaciones binarias eficientes, muy por debajo del tamaño de JSON), y la empuja al servidor por una conexión persistente cuando hay una disponible. Las actualizaciones entrantes de otros clientes pasan exactamente por la misma función de fusión que usaron las ediciones locales, así que no hay un camino de código separado para aplicar un parche remoto que mantener sincronizado con el local — la corrección viene de tener exactamente un único camino de código para fusionar, siempre.
#Presencia y cursores: un canal separado y efímero
Una posición de cursor en vivo o un indicador de que alguien está escribiendo es un tipo de dato completamente distinto al documento en sí — es de alta frecuencia, no tiene valor una vez que queda obsoleto, y nunca debería persistirse ni disputarse en una fusión. Lo mantendría totalmente fuera del documento CRDT y lo correría sobre un canal de presencia separado (el mismo patrón que Yjs llama awareness): cada cliente transmite periódicamente su propio estado efímero — posición del cursor, rango de selección, un color de usuario — por la conexión en vivo, los demás clientes guardan en memoria la última transmisión de cada usuario, y si un cliente se desconecta, su presencia simplemente expira después de un tiempo. Limitar la frecuencia de esas transmisiones (unas pocas veces por segundo, no en cada píxel de movimiento del cursor) importa más en mobile que en web, ya que es tanto un costo de batería como de plan de datos que un cliente de escritorio fijo no tiene que preocuparse tanto.
#Local-first sin conexión y sincronización en segundo plano
El hecho de que las escrituras locales siempre tengan éxito es todo el punto, pero aun así quiero que el documento esté razonablemente actualizado la próxima vez que el usuario abra la app, incluso si nunca la reabrió explícitamente para sincronizar. En iOS registraría una tarea de BGTaskScheduler para intentar una sincronización corta y de mejor esfuerzo mientras la app está suspendida; en Android, WorkManager con restricciones de red cumple el mismo trabajo con una garantía de entrega más fuerte. De cualquier forma, la fusión en sí nunca debería correr en el hilo principal — un documento grande con un historial de edición largo puede implicar una fusión CRDT nada trivial, y hacerla en línea con el hilo de la UI es una fuente garantizada de frames perdidos justo cuando el usuario está a mitad de una pulsación. Enviaría las fusiones a una cola en segundo plano y solo trasladaría el resultado ya renderizado de vuelta al hilo de la UI.
#Evitar que el documento crezca para siempre
Ese crecimiento de tombstones es el mayor costo de largo plazo del enfoque CRDT, y una respuesta senior debería nombrar la mitigación de entrada en lugar de esperar a toparse con ella en producción. La solución estándar es la compactación periódica en el servidor: una vez que todos los clientes conectados confirmaron cierto punto del historial, los tombstones y las operaciones ya superadas antes de ese punto pueden recolectarse, y el documento puede tomarse como snapshot para que un cliente nuevo que se une no tenga que reproducir todo el historial de operaciones desde el momento cero — carga el snapshot y solo transmite las operaciones desde que se tomó. También pondría un límite a qué tan atrás puede quedarse un solo cliente antes de que el servidor insista en una resincronización completa por snapshot en lugar de un delta incremental, para que un teléfono que estuvo sin conexión tres meses no reconstruya tres meses de pulsaciones individuales.
#Tradeoffs que expondría
Los CRDT cambian simplicidad del servidor y resiliencia sin conexión por sobrecarga de metadatos y un orden eventual en lugar de uno arbitrado centralmente por el servidor — lo cual es el intercambio correcto para un producto mobile-first, pero el incorrecto si el requisito real del producto es un orden estricto arbitrado centralmente (los libros contables financieros, por ejemplo, quieren consistencia estilo OT o incluso más fuerte, no convergencia CRDT). Los conflictos estructurales todavía necesitan decisiones a nivel de producto que el algoritmo no puede tomar por ti — si dos personas eliminan concurrentemente el mismo párrafo que una de ellas todavía está editando, el CRDT garantiza que el documento no se corrompa, pero no garantiza que el resultado se sienta sensato para ninguno de los dos usuarios, así que la capa de UI necesita su propio recurso para mostrar que alguien más cambió esto mientras escribías. Y elegir una librería madura (Yjs, Automerge) en lugar de un CRDT hecho a mano es casi siempre la decisión correcta — los casos límite al fusionar texto enriquecido de forma concurrente son del tipo de cosas que toman años en resolverse bien, y este es un caso donde reinventar la rueda es un riesgo real, no solo esfuerzo desperdiciado.