wandres.dev
CASOS DE ESTUDIO · qué hacen los que lo hacen

El patrón del editor colaborativo: contenido convergente, presencia efímera y un servidor que solo presenta

Lo que comparten los editores colaborativos modernos: una estructura convergente para el texto que debe durar, un canal aparte y caducable para la presencia, y un servidor reducido a punto de encuentro.

⏱ 19 min

Si abres cualquier editor colaborativo actual y observas el tráfico, verás algo que sorprende la primera vez: no hay un flujo de datos, hay dos, y viajan por caminos con propiedades opuestas. Por uno circula el contenido, que debe sobrevivir a la desconexión, al cierre del navegador y al paso de los años. Por el otro circulan los cursores, los nombres y los colores, que deben desaparecer en cuanto quien los emitía se marcha, y cuya persistencia sería directamente un defecto. Esa separación no es una curiosidad de implementación: es la decisión estructural que hace posible el resto, y es lo único que comparten sin excepción productos tan distintos como un editor de documentos comercial, una herramienta de notas de empresa y la pila de código abierto sobre la que se construye media industria. Esta lección examina qué está documentado de cada uno y qué patrón emerge cuando se ponen en fila.

🎯 Al terminar esta lección sabrás
  • Separar las tres capas de un editor colaborativo: contenido duradero, presencia efímera y coordinación.
  • Entender por qué el texto acaba en una estructura convergente y las propiedades no siempre.
  • Estudiar un protocolo de presencia real y las decisiones de caducidad que incorpora.
  • Situar el papel del servidor como punto de encuentro, retransmisor y notario de disponibilidad.

Las tres capas y por qué no se mezclan

Merece la pena fijar el vocabulario antes de mirar productos. El contenido es lo que el usuario considera el documento: caracteres, bloques, atributos de formato. Debe converger sin pérdidas, sobrevivir sin conexión y poder reconstruirse desde el disco. La presencia es la información sobre quién está mirando y dónde: posición del cursor, selección, nombre, color, quizá si está escribiendo. Debe llegar en decenas de milisegundos y debe caducar sola. Y la coordinación es todo lo demás: autenticación, permisos, notificación de que un documento cambió, listas de documentos disponibles.

flowchart LR
A[Cliente A] -->|contenido convergente| S[Servidor]
A -->|presencia con caducidad| S
A -->|coordinacion y permisos| S
S -->|retransmision| B[Cliente B]
S --> D[(Persistencia del contenido)]
S -.no persiste.-> P[Presencia]
style D fill:#a6e3a1,color:#11111b
style P fill:#f9e2af,color:#11111b

La razón de no mezclarlas es que sus requisitos son incompatibles punto por punto. El contenido tolera latencia y no tolera pérdida; la presencia tolera pérdida y no tolera latencia. El contenido debe persistirse; la presencia debe olvidarse. El contenido crece de forma monótona; la presencia debe encogerse cuando alguien cierra la pestaña. Meter la posición del cursor dentro del documento colaborativo —que es la tentación obvia, porque el canal ya está montado— produce una estructura que crece con cada movimiento de ratón y que resucita cursores de gente que se marchó hace meses.

El contenido: qué se sabe de la convergencia en productos reales

Aquí hay que ser cuidadoso con las atribuciones. De la generación clásica de editores de documentos en la nube, la técnica documentada públicamente es la transformación operacional, que transporta operaciones expresadas como desplazamientos dentro del texto y las reescribe contra las operaciones concurrentes. Es la familia que popularizaron los editores de documentos en línea; el propio equipo de ingeniería de Figma la describe así al explicar por qué ellos no la eligieron, y esa es una atribución de segunda mano que conviene tratar como tal.

De la generación actual hay un caso reciente y de primera mano. Notion publicó en diciembre de 2025 un artículo firmado por Raymond Xu sobre cómo construyeron el modo sin conexión, que habían lanzado en agosto de ese año tras años como la funcionalidad más pedida. En él afirman dos cosas relevantes para nosotros. La primera es que, durante años, usaron una base de datos local como caché de mejor esfuerzo, sin garantías sobre qué registros estarían disponibles ni durante cuánto tiempo, y que el modo sin conexión exigió convertirla en una capa de almacenamiento persistente con garantías. La segunda es que las páginas marcadas como disponibles sin conexión se migran de forma dinámica a un modelo de datos CRDT para la resolución de conflictos.

📝
La frase que más información contiene del artículo de Notion

Que las páginas se migren dinámicamente al modelo convergente cuando se marcan como disponibles sin conexión implica que la aplicación convive con dos representaciones del mismo contenido y sabe pasar de una a otra. Es un detalle que suele omitirse en las explicaciones divulgativas y que revela el coste real de incorporar convergencia a un producto ya en producción: rara vez se reescribe el modelo entero, se abre un carril paralelo y se migran las piezas conforme hacen falta. Lo que ocurra con las propiedades no textuales bajo esa migración —reordenaciones de bloques, cambios en filas de base de datos— no está detallado en el artículo, y no lo voy a suponer.

La presencia: un protocolo con fecha de caducidad incorporada

La capa de presencia sí está documentada con precisión milimétrica en la pila abierta más extendida. En Yjs, la presencia no forma parte del módulo del documento: vive en un paquete de protocolos aparte y la implementan los proveedores de red. Su documentación lo enuncia sin ambigüedad: la información de presencia no se guarda en el documento porque no necesita persistirse entre sesiones.

El diseño del protocolo merece estudiarse porque cada decisión responde a un requisito de la lista anterior. El estado de presencia de cada cliente es un objeto JSON sin esquema acompañado de un reloj creciente; los estados remotos viven en un mapa indexado por identificador de cliente. Cuando cambias tu estado, incrementas tu reloj y lo difundes; cuando recibes uno remoto, sustituyes el que tenías si el recibido es más nuevo. Un estado nulo marca a ese cliente como desconectado. Y la pieza clave: si no llegan noticias de un par durante treinta segundos, se le marca desconectado localmente, lo que obliga a cada cliente a reemitir su estado con regularidad aunque no haya cambiado.

// El patron de presencia, reducido a su esencia
awareness.setLocalStateField("user", { name: "Ada", color: "#89b4fa" });
awareness.on("change", () => pintarCursores(awareness.getStates()));

// Al cerrar, avisar en lugar de esperar al tiempo de espera
window.addEventListener("beforeunload", () => awareness.setLocalState(null));

Dos detalles más completan el cuadro. El protocolo de presencia no usa vectores de estado para intercambiar el mínimo de información, a diferencia del documento, y la propia documentación justifica el atajo diciendo que el impacto es pequeño porque los estados de presencia suelen ser diminutos. Y los campos no están estandarizados: por convención los enlaces con editores usan user para el nombre y el color y cursor para la posición, pero puedes poner lo que quieras. Es un protocolo deliberadamente pobre, y su pobreza es la funcionalidad.

📝

Contenido

Converge sin pérdida, se persiste en disco, sobrevive a la desconexión y crece de forma monótona. Es donde va el esfuerzo algebraico.

👋

Presencia

Reloj creciente, objeto sin esquema, caducidad por silencio y borrado al desconectar. Nunca toca el disco.

🔐

Coordinación

Autenticación, permisos y aviso de que algo cambió. Es la capa que ningún producto ha logrado descentralizar.

🧪

Migración

Convivencia de dos representaciones del contenido y paso de una a otra bajo demanda. El coste real de añadir convergencia a un producto vivo.

El servidor como punto de encuentro

Queda por explicar qué hace el servidor cuando el contenido ya converge solo. La respuesta que se repite en los tres casos es: menos de lo que hacía, pero nada prescindible. En la pila abierta, los proveedores de red son la pieza que conecta el documento con un servidor que retransmite actualizaciones a los demás participantes y, según el proveedor, las persiste. En el caso de Notion, el artículo describe un mecanismo de frescura basado en empuje: cuando se aplica un lote de cambios a una página, el servidor emite un mensaje en un canal asociado a esa página; los clientes se suscriben a los canales de sus páginas disponibles sin conexión y, al recibir el aviso, piden los cambios.

Ese mismo artículo documenta la alternativa que descartaron y el motivo, que es la clase de detalle que rara vez se cuenta: consideraron que los clientes sondearan el servidor periódicamente por cada página disponible sin conexión, funcionó en pruebas tempranas y no escalaba a millones de usuarios y dispositivos. También describen cómo evitan recargar todo al volver de una desconexión: cada cliente guarda la marca temporal de la última descarga de cada página y solo pide aquellas cuya versión en el servidor es posterior.

// Reconstruccion del patron de reincorporacion descrito por Notion
const aRefrescar = paginasSinConexion.filter(
  (p) => servidor.ultimaModificacion[p.id] > p.ultimaDescarga
);
💡
El servidor de un editor colaborativo tiene cuatro trabajos y ninguno es fusionar

Puestos en fila, los papeles que le quedan al servidor son: retransmitir lo que unos escriben a los que están mirando, persistir el contenido para quien llegue después, autorizar quién puede leer y escribir cada documento, y avisar de que algo cambió a quien no está mirando. Fíjate en que fusionar no está en la lista: eso lo hace la estructura de datos en cada cliente. Es una reducción enorme de responsabilidad respecto al modelo clásico, y aun así ninguno de esos cuatro trabajos ha desaparecido en ningún producto real que haya podido examinar.

Persistencia y frescura son dimensiones opuestas, y todo el patrón sale de tomárselo en serio

Hay una manera de mirar este patrón que lo convierte de catálogo de técnicas en principio de diseño, y consiste en advertir que contenido y presencia no son dos tipos de datos sino dos posiciones extremas en un mismo eje, el eje que va de lo que debe durar para siempre a lo que debe morir en cuanto deje de refrescarse. Casi todas las decisiones que hemos ido enumerando son consecuencias mecánicas de la posición en ese eje, y una vez lo ves puedes derivarlas en lugar de memorizarlas. ¿El dato debe durar? Entonces necesita una estructura que converja sin perder nada, necesita tocar el disco, necesita metadatos causales que crezcan con la historia y necesita, tarde o temprano, una estrategia de compactación. ¿El dato debe morir? Entonces no necesita nada de eso —ni convergencia demostrable, ni disco, ni vectores de estado, ni compactación— y en cambio necesita justo lo contrario: un mecanismo activo de olvido, porque en un sistema distribuido nadie te avisa de que alguien se ha ido, solo deja de hablar. De ahí sale el detalle que más llama la atención del protocolo de presencia y que resulta ser su núcleo: el tiempo de espera de treinta segundos y la obligación de reemitir aunque no cambie nada. Eso no es una optimización, es la única forma de implementar olvido cuando la desconexión es indistinguible del silencio. Y de ahí sale también la advertencia práctica más rentable de toda la lección: la patología más común en los editores colaborativos que fallan no es un algoritmo de fusión incorrecto, sino haber colocado un dato en el lado equivocado del eje. Guardar la posición del cursor dentro del documento convergente produce un documento que crece sin límite y que muestra los fantasmas de quienes ya no están. Enviar el contenido por el canal efímero produce pérdidas silenciosas que nadie detecta hasta que alguien reclama un párrafo desaparecido. Y el caso intermedio, que es el genuinamente difícil, aparece con los datos que parecen efímeros y no lo son: quién está editando qué para bloquear una sección, el estado escribiendo ahora, el indicador de que alguien tiene el documento abierto para no borrarlo. Cada uno de ellos hay que colocarlo a mano en el eje, preguntando una sola cosa —qué pasa si este dato sobrevive a quien lo emitió— y el patrón que estudias aquí no es más que la respuesta sistemática a esa pregunta, aplicada capa por capa hasta que ninguna quedó en el lado incorrecto.

⚔️ Coloca cada dato de tu editor en el eje de la caducidad
  1. Haz el inventario completo de lo que viaja entre clientes en tu aplicación colaborativa, incluidos los indicadores de interfaz que no consideras datos.
  2. Para cada elemento, responde por escrito qué ocurre si sobrevive a quien lo emitió, y clasifícalo como duradero o efímero según esa respuesta.
  3. Comprueba si algún dato efímero está viajando hoy por el canal persistente y estima cuánto ha crecido tu documento por su culpa.
  4. Implementa el olvido activo de la capa efímera con un tiempo de espera explícito y verifica que un cierre abrupto de pestaña limpia el estado en los demás clientes.
  5. Separa el aviso de cambio de la descarga de cambios y mide cuántas peticiones ahorra usar marcas temporales de última descarga frente a recargar entero.
  6. Enumera los cuatro trabajos que le quedan a tu servidor y comprueba si alguno de ellos está haciendo, además, fusión de contenido sin que te hubieras dado cuenta.