Implementación de comentarios por selección de texto en documentos mediante algoritmos OT y CRDT

Introducción al sistema de comentarios por selección de texto

En plataformas de documentos colaborativos, la capacidad de comentar fragmentos seleccionados del texto es fundamental. Este mecanismo permite a los usuarios proporcionar retroalimentación precisa sobre contenido específico, mejorando así la calidad del documento. Aunque puede parecer una funcionalidad sencilla desde el punto de vista de interfaz, su implementación correcta requiere manejar con cuidado la sincronización de posiciones cuando el contenido cambia dinámicamente.

Este artículo explora dos enfoques técnicos para mantener la coherencia de los comentarios: el uso de OT (Operational Transform) y CRDT (Conflict-free Replicated Data Type). Ambos permiten que los comentarios se mantengan en la posición correcta incluso cuando el documento evoluciona, ya sea por ediciones locales o cambios remotos.

Desafíos clave en la persistencia de posiciones de comentario

La principal dificultad radica en que las posiciones de selección no son estáticas. Si un usuario marca un rango de texto [2, 6] y posteriormente se inserta contenido al inicio del documento, esa misma selección ahora apuntaría a una parte diferente del texto. Sin un mecanismo adecuado, el comentario quedará desincronizado.

Una solución simple sería almacenar el camino DOM completo hasta el nodo textual, pero esta aproximación tiene limitaciones en rendimiento y mantenibilidad. Una alternativa más eficiente consiste en usar identificadores únicos por nodo o indices relativos, aunque esto implica cambios significativos en la estructura de datos subyacente.

Implementación basada en OT: transformación de posiciones

En entornos donde se utiliza OT, cada operación de edición se representa como un delta. Al aplicar estas operaciones, las posiciones de los comentarios deben actualizarse mediante transformación.

const COMMENT_LIST = [
  { index: 12, length: 2 },
  { index: 17, length: 4 }
];

editor.on("text-change", delta => {
  for (const item of COMMENT_LIST) {
    const start = delta.transformPosition(item.index);
    const end = delta.transformPosition(item.index + item.length, true);
    item.index = start;
    item.length = end - start;
  }
  editor.renderLayer(COMMENT_LIST);
});

El método transformPosition toma en cuenta el orden de las operaciones. El segundo parámetro indica si la operación actual se considera previa (true) o posterior (false) a otras, lo cual es crucial para resolver conflictos.

Para sincronizar comentarios entre versiones distintas del documento (por ejemplo, versión A vs B), se pueden aplicar dos estrategias:

  • Registro de ops: Almacenar todas las operaciones entre versiones. Esto permite reconstruir el cambio exacto y aplicar transformaciones precisas.
  • Diferenciación de contenidos (diff): Comparar el texto original con el nuevo para determinar las diferencias. Es menos costoso en infraestructura, pero puede perder semántica si hay múltiples bloques idénticos.

Ejemplo de transformación usando diff:

const online = new Delta().insert("yyy");
const draft = new Delta().insert("xxxyyy");
const comment = { index: 0, length: 3 };
const commentDelta = new Delta().retain(0).retain(3, { id: "comment-1" });
const diff = online.diff(draft); // [{ insert: "xxx" }]
const nextCommentDelta = diff.transform(commentDelta);
// Resultado: [{ retain: 3 }, { retain: 3, attributes: { id: "comment-1" } }]

Implementación basada en CRDT: posicionamiento relativo

Con CRDT, en lugar de almacenar índices absolutos, se almacenan posiciones relativas dentro del documento. Estas posiciones son independientes del estado del documento y se resuelven automáticamente cuando se renderiza.

Usando yjs, se define una instancia de Y.Doc y se vincula con el editor mediante QuillBinding:

const ydoc = new Y.Doc();
new IndexeddbPersistence("y-indexeddb", ydoc);
const ytext = ydoc.getText("quill");
new QuillBinding(ytext, editor);

Cuando se crea un comentario, se convierte la selección en una posición relativa:

applyComment.onclick = () => {
  const selection = editor.getSelection();
  if (selection) {
    const start = Y.createRelativePositionFromTypeIndex(ytext, selection.index);
    const end = Y.createRelativePositionFromTypeIndex(ytext, selection.index + selection.length);
    COMMENT_LIST.push([JSON.stringify(start), JSON.stringify(end)]);
    renderAllCommentWithRelativePosition();
    editor.setSelection(selection.index + selection.length);
  }
};

Al actaulizar el contenido, simplemente se vuelve a renderizar todos los comentarios utilizando sus posiciones relativas:

editor.on("text-change", () => {
  renderAllCommentWithRelativePosition();
});

Esta aproximación elimina necesidades de transformación manual, ya que el sistema CRDT garantiza que las posiciones se ajusten automáticamente ante cambios.

Renderizado del panel lateral de comentarios

El panel de comentarios debe mantenerse sincronizado con la posición visual del texto seleccionado. Para obtener coordenadas precisas, se recomienda usar Range.getClientRects() en lugar de solo getBoundingClientRect(), especialmente en textos con saltos de línea.

const range = new Range();
range.setStart(textNode, startIndex);
range.setEnd(textNode, endIndex);
const rect = range.getBoundingClientRect();
const editorRect = editor.container.getBoundingClientRect();
const topOffset = rect.top - editorRect.top;

Estos valores pueden usarse directamente con transform: translateY() en CSS para posicionar tarjetas de comentarios de forma fluida.

Consideraciones finales

La elección entre OT y CRDT depende del contexto:

  • OT es más adecuado cuando se necesita control granular sobre operaciones y se dispone de registro de cambios.
  • CRDT ofrece mayor simplicidad en sistemas distribuidos, sin necesidad de transformaciones manuales.

En ambos casos, el uso de capas virtuales para mostrar comentarios permite una separación limpia entre lógica de datos y presentación, facilitando pruebas y mantenimiento.

Etiquetas: OT CRDT yjs quill collaborative editing

Publicado el 10-15 23:13