wandres.dev
ESTADO COLABORATIVO · tiempo real y presencia

Presencia y awareness: el estado que no se guarda

En un producto colaborativo hay dos estados que viajan por el mismo cable y obedecen leyes opuestas. El primero es el documento: duradero, convergente, auditable, y por él pagamos toda la maquinaria de las lecciones anteriores. El segundo es la presencia —quién está conectado, dónde tiene el cursor, qué ha seleccionado, qué está mirando, si está escribiendo ahora mismo—, y es exactamente lo contrario: efímero, sin historia, sin valor pasados dos segundos y, sobre todo, sin conflictos posibles, porque cada participante es el único autor legítimo de su propia presencia. Esta lección demuestra por qué para el estado de awareness la política del último que escribe gana no solo es aceptable sino correcta, cómo se ancla un cursor a la identidad de un átomo o se transforma como una operación más para que sobreviva a las ediciones ajenas, y cómo se diseña el protocolo efímero con latidos, expiración por tiempo de vida y limitación de frecuencia para que sesenta movimientos de ratón por segundo no arruinen la red. Termina con lo que el diseño olvida y el usuario nota: la presencia es la capa social que resuelve los conflictos semánticos que ningún algoritmo puede resolver, y también una fuga de privacidad si se persiste.

⏱ 18 min

Si observas a dos personas trabajando en un documento compartido, descubrirás que la mayor parte de la coordinación no la hace el algoritmo. La hacen ellas, mirándose. Ana ve que el cursor de Bruno está en el tercer párrafo y se va al quinto; Bruno ve que Ana tiene seleccionada una frase y espera antes de tocarla; los dos ven que Carla acaba de entrar y saben que conviene avisarla. Ninguna de esas negociaciones aparece en el modelo CCI ni en ninguna función de transformación, y sin embargo son las que evitan la mayoría de las colisiones reales. Esa capa se llama presencia, o awareness, y es el estado efímero de la colaboración: quién hay, dónde está, qué mira, qué toca. Técnicamente es el hermano pobre del documento y recibe una fracción de la atención en la literatura, pero en producto es la mitad de la experiencia, y además tiene una propiedad que la hace fascinante de estudiar justo después de dos lecciones agotadoras sobre convergencia: la presencia no tiene conflictos. Ninguno. Y no porque hayamos encontrado un algoritmo mejor, sino porque su estructura de propiedad lo hace imposible. Entender por qué es entender que la dificultad de la colaboración nunca estuvo en la concurrencia como tal, sino en la escritura concurrente sobre lo mismo.

🎯 Al terminar esta lección sabrás
  • Separar los dos canales del producto colaborativo: estado duradero del documento y estado efímero de presencia.
  • Justificar por qué el último que escribe gana es la política correcta para awareness y catastrófica para el documento.
  • Anclar cursores y selecciones para que sobrevivan a las ediciones concurrentes de otros.
  • Diseñar el protocolo efímero: latidos, expiración por tiempo de vida y limitación de frecuencia.

Dos canales, dos leyes

La decisión de arquitectura más importante de un producto colaborativo es reconocer que hay dos flujos de estado con requisitos contrapuestos y no mezclarlos jamás en la misma estructura. El estado del documento debe persistir, converger, sobrevivir a caídas y admitir auditoría; cada byte suyo puede ser trabajo de alguien y perderlo es inaceptable. El estado de presencia debe ser rápido y desechable; guardarlo no solo es innecesario, es un error, porque un cursor almacenado en la base de datos es un dato falso desde el instante en que su dueño cierra la pestaña.

📄

Estado del documento

Duradero, convergente y con historia. Cada operación importa, se persiste y se puede auditar. Es el territorio de OT y de los CRDT, y su coste está justificado porque el dato es el producto.

👥

Estado de presencia

Efímero, de alta frecuencia y sin historia. Solo importa el valor actual, caduca en segundos, no se persiste nunca y se puede perder sin consecuencias. Su coste debe ser mínimo porque su valor es momentáneo.

La razón profunda de que la presencia sea fácil no es que sea poco importante, sino que su estructura de propiedad elimina la posibilidad misma del conflicto. En el documento, dos personas escriben sobre la misma región y hay que decidir qué significa eso. En la presencia, cada participante escribe únicamente en su propia entrada del mapa: Ana solo actualiza la clave de Ana, Bruno solo la de Bruno. Nadie escribe nunca donde escribe otro, y donde no hay escrituras concurrentes sobre el mismo objeto no hay nada que reconciliar. Por eso el último que escribe gana, que en la primera lección era la causa del desastre, es aquí la política correcta y no un atajo: el último valor de mi cursor es, literalmente, dónde está mi cursor. Este mapa de clave por cliente con escritura exclusiva del propietario es, en el vocabulario de la lección anterior, un CRDT trivial, y es la razón por la que la presencia se implementa en una tarde mientras el documento cuesta meses.

// El mapa de presencia: una entrada por cliente, escrita solo por su dueno
type Presencia = {
  cliente: string
  nombre: string
  color: string
  cursor: Ancla | null       // ver siguiente seccion
  seleccion: [Ancla, Ancla] | null
  visto: number              // marca de tiempo del ultimo latido
}

// Sin conflictos posibles: nadie escribe en la clave de otro
function alRecibirPresencia(mapa: Map<string, Presencia>, p: Presencia) {
  mapa.set(p.cliente, p)     // el ultimo valor propio es el valor verdadero
}
flowchart LR
E[editor local] --> D[canal de documento]
E --> P[canal de presencia]
D --> S1[persistencia y convergencia]
P --> S2[difusion efimera con expiracion]
S1 --> B[base de datos]
S2 --> X[se descarta al desconectar]
style S1 fill:#89b4fa,color:#11111b
style S2 fill:#f9e2af,color:#11111b
style X fill:#f38ba8,color:#11111b

Anclar el cursor: una posición que sobrevive al texto

Un cursor no es un número, aunque el navegador te lo ofrezca como tal. Si Bruno tiene el cursor en la posición 40 y Ana inserta un párrafo al principio, el cursor de Bruno debe seguir señalando la misma palabra, no el mismo número. Y aquí reaparece con una simetría muy elegante el problema central de todo el nivel: la posición de un cursor es una coordenada en un marco de referencia, exactamente igual que el índice de una operación, y sufre exactamente la misma corrupción. Por eso cada familia resuelve el anclaje con su propia herramienta, y la solución es la misma que ya empleó para el documento.

En un sistema OT el cursor se transforma. Cada vez que llega una operación remota se aplica al documento y, con la misma función de transformación, se ajusta la posición de todos los cursores conocidos. Un cursor es, para estos efectos, una operación degenerada de longitud cero. En un sistema CRDT el cursor no se transforma porque no hace falta: se guarda como una referencia a la identidad del átomo vecino, y esa identidad no cambia nunca. Yjs llama a esto posiciones relativas y ofrece dos funciones para convertir entre la posición absoluta que necesita el navegador para pintar y la referencia estable que se transmite por la red.

// En un CRDT el ancla es una identidad, no un numero
type Ancla = { atomo: string; sesgo: 'antes' | 'despues' }

// Se resuelve a indice solo en el momento de pintar
function anclaAIndice(doc: Atomo[], a: Ancla): number {
  const i = doc.findIndex((x) => x.id === a.atomo)
  return a.sesgo === 'antes' ? i : i + 1
}

// En OT, en cambio, el cursor se transforma como una operacion mas
function transformarCursor(pos: number, op: Op): number {
  if (op.tipo === 'insertar') return op.pos <= pos ? pos + 1 : pos
  return op.pos < pos ? pos - 1 : pos
}

El campo de sesgo del ancla, que parece un detalle, es la diferencia entre un cursor que se comporta bien y uno que enfurece. Si tienes el cursor justo detrás de una palabra y otra persona inserta texto exactamente en ese punto, tu cursor debe quedarse antes del texto nuevo o después según lo que estuvieras haciendo, y ninguna de las dos respuestas es universalmente correcta. La convención habitual es que el cursor se pegue al carácter anterior, de modo que el texto ajeno crezca a tu derecha y tú no te veas empujado. Los detalles de este tipo son los que separan un editor colaborativo que se siente sólido de uno que se siente embrujado.

El protocolo efímero: latidos, caducidad y frecuencia

La presencia se difunde por un canal aparte con tres mecanismos propios. El primero es el latido: cada cliente reenvía su estado periódicamente aunque no haya cambiado, típicamente cada pocos segundos. El segundo es la expiración: toda entrada del mapa lleva una marca de tiempo y se elimina si supera su tiempo de vida, lo que resuelve elegantemente el caso más incómodo de la presencia, que es el usuario cuyo portátil se cierra sin despedirse. Sin caducidad, un mapa de presencia acaba lleno de fantasmas que nunca se van, y la lista de participantes deja de ser creíble. El tercero es la limitación de frecuencia: un ratón moviéndose genera decenas de eventos por segundo y ninguna persona necesita ver un cursor ajeno actualizado sesenta veces por segundo. Enviar cada veinte o cincuenta milisegundos, quedándose siempre con el último valor y descartando los intermedios, reduce el tráfico en un orden de magnitud sin que nadie note la diferencia.

// Limitacion de frecuencia con ultimo valor: descarta los intermedios
let pendiente: Presencia | null = null
let programado = false

function publicar(p: Presencia) {
  pendiente = p                     // solo importa el mas reciente
  if (programado) return
  programado = true
  setTimeout(() => {
    enviar({ ...pendiente!, visto: Date.now() })
    programado = false
  }, 40)
}

// Expiracion: los ausentes desaparecen solos, sin mensaje de despedida
setInterval(() => {
  const ahora = Date.now()
  for (const [id, p] of mapa) if (ahora - p.visto > 15000) mapa.delete(id)
}, 5000)
⚠️
La presencia es un canal de privacidad, no solo de UX

Mostrar quién está mirando qué es una función social útil y, a la vez, una emisión continua de datos de comportamiento. Un cursor revela en qué párrafo dudas y cuánto tiempo pasas en la cláusula de indemnización; un indicador de escritura revela que empezaste una respuesta y la borraste. Persistir esa información —guardar el historial de presencia en la base de datos porque total, ya la tenemos— convierte un canal efímero en un registro de vigilancia que nadie aceptó. La regla de diseño es doble y no negociable: no persistir nunca la presencia, y ofrecer un modo de trabajo discreto en los productos donde la observación tenga consecuencias. Lo que se evapora no se puede filtrar.

La escalera de señales: de estar a seguir

No toda la presencia vale lo mismo ni cuesta lo mismo. Conviene pensarla como una escalera de señales ordenada por intensidad, porque cada peldaño añade valor social y también ancho de banda, ruido visual y exposición, y casi ningún producto necesita subir hasta arriba.

🟢

Estar y mirar

La lista de participantes y, como mucho, en qué sección está cada uno. Es la señal más barata, la que más colisiones evita por unidad de coste y la primera que debe existir.

✏️

Cursor, selección y escritura

La posición exacta y el rango seleccionado, más el indicador de que alguien está tecleando ahora. Multiplica el tráfico y exige anclaje y suavizado, pero es lo que convierte el documento en un espacio habitado.

👣

Seguir a alguien

El modo en que tu vista se ata al desplazamiento de otra persona. Es presencia convertida en control remoto consentido, útil en revisiones y presentaciones, y conviene que sea explícito y reversible en un gesto.

Al pintar cursores ajenos aparece además un detalle de percepción que decide si el producto se siente vivo o roto. Como el estado remoto llega limitado en frecuencia y con latencia variable, un cursor ajeno dibujado en su posición exacta salta a tirones. La corrección es interpolar el movimiento entre el valor anterior y el nuevo durante el intervalo de actualización, de modo que el ojo perciba desplazamiento continuo aunque los datos lleguen a saltos. Es exactamente la misma técnica que usan los juegos en red para las entidades remotas, y por la misma razón: el usuario no juzga la fidelidad de los datos, juzga la plausibilidad del movimiento.

💡
Suaviza el movimiento, no el hecho

La interpolación es legítima para posiciones continuas —dónde está un cursor— y engañosa para hechos discretos. Nunca suavices la entrada o la salida de un participante ni el indicador de escritura: si alguien deja de estar, debe dejar de estar de golpe, porque un fantasma que se desvanece lentamente comunica una información falsa durante todo el tiempo que tarda en irse. La regla es sencilla y se aplica a cualquier interfaz en tiempo real: interpola magnitudes, nunca interpoles predicados.

La presencia no adorna el algoritmo: resuelve exactamente lo que el algoritmo no puede

Es tentador clasificar la presencia como una capa cosmética, un rato de trabajo de interfaz sobre la parte seria del sistema. Esa lectura invierte la realidad, y la primera lección de este nivel ya dejó preparada la refutación. Allí vimos que hay concurrencias sin solución algorítmica posible: si Ana reescribe una frase y Bruno la borra a la vez, no existe documento que respete ambas intenciones, y ningún avance en OT ni en teoría de CRDT cambiará eso, porque el problema no es de índices sino de significado. La preservación de la intención, el tercer criterio del modelo CCI, es el único de los tres que no admite definición formal general, y por tanto el único que ninguna máquina puede garantizar. Lo que la presencia hace es evitar que ese caso llegue a plantearse: Bruno ve el cursor de Ana dentro de la frase y no la borra, o pregunta antes. Es decir, el estado efímero no compite con el algoritmo de fusión, cubre justo el hueco que el algoritmo demostró no poder cubrir, y lo hace trasladando la coordinación a las personas, que sí disponen de la información que le falta a la máquina: el propósito. Esto tiene una moraleja de diseño que se aplica mucho más allá de los editores. Cuando un sistema tiene un límite teórico duro, la solución rara vez consiste en empujar el límite; consiste en construir el canal por el que un humano aporta lo que al sistema le falta. Que ese canal cueste una tarde de trabajo y el algoritmo cueste meses no lo hace secundario: lo hace la mejor relación entre esfuerzo y corrección de todo el producto. Y explica un fenómeno que cualquiera puede comprobar en su propia experiencia, que los editores colaborativos que se sienten mágicos casi nunca lo son por su fusión, sino porque en ellos siempre sabes dónde están los demás.

⚔️ Implementa la capa de presencia completa
  1. Diseña el mapa de presencia con una entrada por cliente y comprueba explícitamente que ninguna ruta de tu código escribe en la entrada de otro.
  2. Implementa latido, marca de tiempo y expiración, y verifica que un cliente que cierra la pestaña sin avisar desaparece de la lista en el plazo previsto.
  3. Añade limitación de frecuencia con último valor y mide cuántos mensajes por segundo ahorras moviendo el ratón de forma continua durante diez segundos.
  4. Implementa el anclaje del cursor y demuestra que sobrevive a una inserción ajena por encima. Prueba las dos convenciones de sesgo y decide cuál se siente mejor.
  5. Añade selección además de cursor y comprueba qué ocurre cuando otra persona borra un fragmento situado dentro de tu selección.
  6. Enumera qué datos de tu canal de presencia serían inaceptables si se persistieran, y verifica que ninguno llega a tu base de datos.