Crear un editor de podcasts multijugador con Automerge
(adamsolove.com)- Hace apenas unos años, la sincronización de datos multijugador en tiempo real era uno de los problemas más difíciles y requería personal especializado e inversiones a nivel empresarial, pero ahora es posible implementar una UI multijugador incluso en proyectos de hobby con una sola ejecución de
npm install - Automerge es una herramienta para construir modelos de datos con prioridad local, seguros para multijugador y con control de versiones; maneja automáticamente persistencia de datos, historial, difusión a colaboradores y resolución de conflictos de una forma similar al patrón
useStatede React, sin que la UI tenga que preocuparse por ello - En el caso de Ducking, un editor de audio multijugador basado en navegador, la clave es diseñar el modelo de datos para que se mapee de forma natural a operaciones CRDT
- En casos que Automerge no garantiza, como el reordenamiento de listas, hay que implementar invariantes más fuertes directamente con código en la capa de aplicación
- La idea central es que la edición colaborativa en tiempo real, que antes parecía magia de nivel industrial, ahora puede aplicarse libremente incluso a apps pequeñas para unos cuantos usuarios
Contexto — el proyecto Ducking
- En los últimos meses, se desarrolló Ducking, un editor de audio multijugador basado en navegador para el podcast de la pareja del autor
- Parecía extraño que la edición de audio siguiera anclada a apps de escritorio para un solo usuario de hace 20 años y al intercambio de archivos
- Mientras una persona edita clips, otra podría corregir la transcripción o ajustar la configuración de EQ; hacía falta un flujo de trabajo colaborativo tipo Google Docs o Figma
- También se necesitaban herramientas modernas de colaboración como comentarios, historial y seguimiento de cambios
- Un artículo anterior trató el diseño de UI poco común y el modelo de layout de audio, que hicieron más eficaz al editor individual, pero lo que realmente se quería era un flujo de trabajo más colaborativo
Cómo funciona Automerge
- Todos los datos de Ducking, excepto los blobs de audio, se almacenan en un documento Automerge
- El patrón central resulta familiar para desarrolladores de React: se obtienen datos con un hook para renderizar, se despachan solicitudes de cambio asíncronas, y cuando los datos cambian el hook dispara un re-render
- Ejemplo con el hook
useDocument:const [doc, changeDoc] = useDocument<Episode>(docUrl)para recibir el documento y actualizarlo al cambiar un input conchangeDoc((d) => { d.title = e.target.value })
- Ejemplo con el hook
- Las operaciones de actualización parecen imperativas, pero no son iguales a objetos o arreglos nativos de JS
- Tienen menos métodos y no mutan de inmediato; internamente interceptan la mutación y la convierten en un elemento de changelist del historial del documento
- Automerge resuelve mucho en los casos simples, pero no es magia; como sus invariantes no siempre coinciden con la semántica deseada, es importante un diseño cuidadoso del modelo de datos
- Lo ideal es que la mayoría de las acciones semánticas del usuario correspondan a una sola operación que Automerge pueda ofrecer
- Las acciones separadas sobre datos relacionados deben resolverse de forma natural desde el punto de vista de las invariantes de esa operación de Automerge
- Conviene separar claramente los datos canónicos almacenados y los datos derivados que se calculan
Modelado de datos para multijugador
- En el modelo de datos de Ducking, un clip es una ventana que reproduce parte de una fuente de audio base inmutable y se encarga del tramo de reproducción, la aplicación de efectos y el espacio ocupado en la línea de tiempo
- El efecto más común es que el clip ajuste el volumen del audio base en el tiempo para hacer crossfade o reducir ruido
- Al principio, cada clip tenía una lista de niveles de volumen indexados por tiempo respecto al inicio del clip, pero eso generaba problemas porque la mayoría de los cambios de volumen pertenecen al audio base, no al clip
- Si se adelanta un poco el inicio del clip, todos los cambios de volumen terminan aplicándose a otra parte del audio
- Escribir código que actualice todos los timestamps de volumen cada vez que cambia el inicio del clip es una mala decisión
- Si dos colaboradores editan al mismo tiempo el inicio de un clip, cada edición agrupa el cambio del inicio con todos los timestamps de automatización de volumen
- Automerge no conoce la relación causal entre esos cambios, así que al fusionarlos puede resolverlos de forma caótica
- Este es el problema típico cuando una acción semántica intenta actualizar varios datos persistentes de una forma causal que el CRDT no entiende
- La solución fue migrar los datos de efectos de audio para que se basaran en el marco temporal del audio base y no en el clip
- Así ya no hace falta actualizar nada cuando cambian el inicio o la duración del clip, y si varios editores modifican inicio, automatización de volumen u otros efectos, esas operaciones quedan más independientes y es más probable que se fusionen bien
- Diferencia entre UI para un solo usuario y UI multijugador
- En una UI de un solo usuario a veces se conserva el modelo existente y se agregan cálculos extra al escribir
- En una UI multijugador es mucho más común migrar el modelo de datos para mantener todos los datos persistentes en un estado ortogonal
- Se termina prefiriendo con fuerza simplificar la escritura y calcular en lectura, para aprovechar al máximo la fusión automática de Automerge
- Consejos sobre migraciones de formato de datos
- Vale la pena asumir que habrá que migrar formatos durante el desarrollo y practicar pronto para no temer la primera gran migración
- Hay distintos patrones, como resolverlo del lado del cliente en lectura o hacer upgrades masivos en el servidor
- Si se encuentra una invariante cómoda para verificar que antes y después son equivalentes, el trabajo se vuelve mucho más fácil
- En Ducking se exportó el audio de todos los proyectos antes y después de cada migración para verificar cambios con un audio fingerprint, lo que permitió desplegar incluso cambios grandes de esquema sin miedo
Implementación del reordenamiento de listas
- A veces hay que escribir invariantes más fuertes en la capa de aplicación para obtener garantías que Automerge no ofrece
- Eso ocurrió al implementar la magnetic timeline de Ducking, una lista ordenada de clips a reproducir
- Automerge ofrece operaciones de arreglo para borrar e insertar por índice, pero no una operación para reordenar de forma atómica un elemento existente
- Existen soluciones conocidas
- Martin Kleppmann publicó un paper sobre una operación atómica de reordenamiento de listas
- También publicó con Liangrun Da el paper "Extending JSON CRDTs with Move Operations"
- Hay incluso un draft PR para agregarlo a Automerge, aunque aún no se fusiona
- Problema del enfoque simple de reordenamiento
- Consiste en borrar el objeto de su índice actual y volver a insertarlo en el índice destino
- Aunque se combinen las invariantes de ambas operaciones, no se garantiza la invariante deseada: que, incluso con muchos reordenamientos concurrentes, el objeto exista exactamente una vez en la lista
- Si hay varias eliminaciones e inserciones concurrentes, el objeto puede quedar en varias posiciones de la lista al mismo tiempo; si Alice y Bob mueven B con delete+insert, las dos eliminaciones se fusionan en un solo tombstone, pero las dos inserciones crean elementos nuevos y sobreviven ambas, así que B aparece dos veces
- Implementación manual en la capa de aplicación de la invariante de “exactamente una vez”
- Cuando un clip se inserta en la línea de tiempo, se le asigna un semantic id
- Al reordenarlo, se disparan las operaciones de borrar e insertar descritas arriba
- En lectura, la aplicación revisa duplicados con el mismo semantic id, elige arbitrariamente el primer elemento no borrado e ignora el resto
- Con eso, el objeto solo existe una vez en la lista y todos los lectores llegan siempre al mismo estado final
- El reordenamiento de listas es la única operación que Automerge no proporcionó en Ducking, y si ese PR se fusiona, esta lógica a nivel aplicación dejaría de ser necesaria
Historial del documento (Document history)
- Una buena UI multijugador necesita herramientas de historial: los colaboradores quieren ver cambios hechos en su ausencia, dejar comentarios sobre diffs, comparar versiones viejas y hacer rollback
- Automerge rastrea el historial de versiones del documento y ofrece excelentes primitivas para trabajar con historial y comparación
- Aun así, corresponde al desarrollador decidir cómo exponer esa información y qué conceptos presentar al usuario
- Se recomienda revisar las Patchwork lab notes de Ink & Switch
- Resulta especialmente interesante el trabajo sobre exponer ramas al usuario y sobre comentarios universales
- El modelo de colaboración e historial, relativamente simple, al que llegó Ducking
- Un historial lineal de versiones con checkpoints nombrados por el usuario, donde cada checkpoint sirve como unidad para agrupar cambios, discutirlos, ver diffs o hacer rollback
- Hilos de comentarios que pueden vincularse a un punto específico del audio, a una zona de la transcripción o a un checkpoint de versión
- Aún no había una razón suficiente para introducir ramas, aunque se menciona que podrían ser útiles más adelante
Texto y marks
- Trabajar con texto enriquecido es especialmente complicado cuando se intenta agregar lógica personalizada sobre texto editable
- Se recomienda el paper Peritext, que explica las dificultades del rich text y del software multijugador en general
- El esquema de rich text de Automerge incluye marks, anotaciones que se aplican a rangos de texto y se mantienen consistentes incluso mientras el texto se edita
- Lo más común es usarlas para formato como negritas o cursivas, pero también es posible crear marks personalizados de la aplicación
- Dos usos de marks personalizados en Ducking
- Rastrear las regiones de transcripción a las que apuntan los hilos de comentarios
- Rastrear timestamps de palabras en la transcripción, permitiendo al mismo tiempo seguir editando el texto
- El servicio de transcripción guarda la transcripción en Automerge como un objeto de rich text con marks que contienen información de tiempo para cada palabra
- Si se corrige solo una palabra por un typo pequeño, el mark se conserva y se mantiene toda la información temporal
- Si se reescribe una oración entera, algunos marks intermedios se pierden, pero los marks del inicio y final permanecen, así que al menos se conserva información temporal aproximada
- Una limitación de los marks es que su datum debe ser un valor simple, por lo general una cadena, y no se fusiona en multijugador
- Para datos pequeños e inmutables como la información temporal de transcripción, se serializa JSON como string
- Para datos más complejos o mutables, como los hilos de comentarios, el mark guarda solo un id y los datos reales se almacenan en otra parte del documento
- Los marks ofrecen una base excelente para construir funcionalidades de aplicación sobre rich text multijugador
Siguiente artículo — estructura de la serie
- Este texto es la parte 2 de una serie de 3 sobre la creación de Ducking
- Parte 1: explicación del diseño de UI poco común del software
- Parte 2 (este artículo): recomendación de revisar Automerge y mostrar que es posible construir proyectos multijugador de hobby
- Parte 3 final, todavía pendiente: retrospectiva sobre la experiencia de crear Ducking
- Comentarios sobre esa parte 3 final
- Se usó apoyo de LLM no para intensificar el trabajo, sino para tener más tiempo de bocetar y descansar en la hamaca
- La alegría de crear software narrowcast que solo necesita satisfacer a unas cuantas personas
Preguntas esperadas
¿Y los datos de audio?
- Todos los datos multijugador se guardan en Automerge, pero los blobs de audio base no se ponen ahí; necesitan un manejo aparte para permitir reproducción rápida
- La meta es que un nuevo colaborador pueda empezar a escuchar y editar en menos de 4 segundos tras cargar la página, más rápido que abrir una app de escritorio y muchísimo más rápido que descargar todo el proyecto
- Un episodio de 1 hora puede depender de aproximadamente 1 gigabyte de audio, sumando 4 horas de grabación de estudio en alta calidad más efectos y música de fondo
- Trabajo que realiza el servicio de audio al subir archivos, para permitir un arranque en frío rápido
- Respaldo del audio original
- Transcripción de la voz para la vista de transcripción
- Generación de waveform para la vista de timeline
- División en ventanas cortas para que, si de una grabación de 40 minutos solo se usa 1 minuto, la mayoría de los clientes descarguen solo uno o dos fragmentos pequeños
- Transcodificación de esos fragmentos a formatos comprimidos para ofrecer una versión lossy reproducible de inmediato mientras el audio de alta calidad se descarga en segundo plano
- La capa de datos de la UI administra la carga de versiones rápidas de los datos necesarios en ese momento y de la versión de alta calidad del audio efectivamente usado, siguiendo la intención del usuario
- La API IndexedDB del navegador resulta útil para caché multinivel y almacenamiento content-addressable, con manejo automático de eviction: si se usa, queda; si no, desaparece
- Una vez terminado todo ese procesamiento y caché local, el resto de la UI puede asumir acceso aleatorio rápido al audio y concentrarse en el flujo de edición
¿Por qué hacer una UI de navegador + servidor y no una app local-first?
- Existe una preferencia por apps local-first al estilo Obsidian, que funcionen por completo sin servidor, especialmente si ofrecen una ruta de salida confiable junto con una experiencia de pago en la nube
- Al principio se empezó con la opción de una app Tauri con almacenamiento en sistema de archivos local y sincronización opcional con servidor
- La UI se construyó sobre una interfaz de datos que pudiera ser provista tanto por servidor como por app local
- Era una protección para que ninguna futura fuente de financiamiento pudiera tentar a volver la app más rentable mediante lock-in
- Después se concluyó que esto no sería un SaaS, sino algo para usar con la pareja y unos pocos amigos
- Al desaparecer el incentivo de manejarlo mal y bajar el costo permanente de operación, se eligió la vía más simple para construirlo
- Una vez que se logró un arranque en frío de unos 3 segundos, nadie quiso perder tiempo descargando e instalando una app nativa
- Se espera que las apps de audio salten desde el mundo actual, centrado en escritorio, directamente a un mundo local-first con opciones de sincronización, evitando 10 o 20 años intermedios de lock-in SaaS
¿Automerge es seguro y web-scale? ¿Debería usarlo una startup?
- La respuesta honesta y alegre es que no se sabe; no como rechazo, sino literalmente porque no se sabe
- Cuando el autor entró a este campo, la edición multijugador en tiempo real sin conflictos parecía magia, y hace 10 años, aunque ya existían soluciones conocidas para ciertos problemas, se necesitaban equipos financiados y experiencia en varias disciplinas
- Hoy basta con instalar una dependencia para, de forma bastante intuitiva, crear una UI y colaborar en tiempo real con amigos
- En seguridad, actualmente Ducking se protege con acceso restringido a la red y una etapa de autorización al crear conexiones websocket al servidor de Automerge
- Los usuarios no pueden descubrir ni editar proyectos a los que no fueron invitados
- La atribución de usuarios en ediciones y comentarios solo es segura en parte y depende de asumir que los amigos no harán cosas maliciosas
- Permisos granulares como poder comentar pero no editar, editar solo parte de un proyecto o controlar la capacidad de descubrimiento requieren trabajo de diseño cuidadoso
- Keyhive, en desarrollo por Ink & Switch, ofrece un modelo de control de acceso basado en capabilities y seguro criptográficamente
- Haría más fácil compartir públicamente apps de Automerge incluso con usuarios no confiables, aunque todavía no está listo
¿Automerge es mejor?
- Existe Yjs como otra solución en este espacio, pero no se puede decidir por otros cuál es la adecuada
- El consejo que no cambia
- Pensar el problema a fondo, hacer cálculos aproximados sobre los límites que podrían aparecer, crear prototipos con varias alternativas y reconocer con honestidad que quizá el problema propio no sea tan difícil como para requerir la solución más nueva o sofisticada
- En el caso de Ducking, un prototipado rápido y la exploración de la documentación mostraron que Automerge es lo bastante maduro y con buen rendimiento para ese uso
- Más importante aún, atrae estéticamente el ecosistema de Ink & Switch
- Automerge no es solo un motor de sincronización y control de versiones, sino parte de una visión mayor para hacer el software más seguro, colaborativo, flexible, disfrutable y personal
- Hay expectativa de que proyectos como Keyhive tengan éxito y de que se multiplique el software pequeño pero mágico hecho para unos pocos
Aún no hay comentarios.