Criando um editor de podcast multijogador com Automerge
(adamsolove.com)- Até poucos anos atrás, a sincronização de dados multijogador em tempo real era um dos problemas mais difíceis, exigindo especialistas e investimento em nível empresarial, mas agora dá para implementar uma UI multijogador até em projetos de hobby com um único
npm install - Automerge é uma ferramenta para construir modelos de dados com prioridade local, segurança para multijogador e controle de versões; ela lida automaticamente com persistência de dados, gestão de histórico, broadcast para colaboradores e resolução de conflitos de um jeito parecido com o padrão
useStatedo React, sem que a UI precise se preocupar com isso - No caso do editor de áudio multijogador baseado em navegador Ducking, o ponto-chave é projetar o modelo de dados para que ele se mapeie naturalmente para operações de CRDT
- Em casos que o Automerge não garante, como reordenação de listas, é preciso implementar invariantes mais fortes diretamente com código na camada da aplicação
- O significado central é que a edição colaborativa em tempo real, que antes parecia magia de nível industrial, agora pode ser aplicada livremente até em pequenos apps para poucos usuários
Contexto — projeto Ducking
- Nos últimos meses, foi criado o Ducking, um editor de áudio multijogador baseado em navegador, para o podcast da parceira do autor
- Parecia estranho que a edição de áudio ainda estivesse presa a apps desktop de usuário único com 20 anos de idade e à troca manual de arquivos
- Enquanto uma pessoa edita clipes, outra deveria poder corrigir a transcrição ou ajustar configurações de EQ, em um fluxo de trabalho colaborativo como Google Docs ou Figma
- Ferramentas modernas de colaboração, como comentários, histórico e rastreamento de mudanças, também eram necessárias
- O design de UI peculiar e o modelo de layout de áudio discutidos em um texto anterior tornaram o editor individual mais eficaz, mas o que realmente se queria era um fluxo de trabalho mais colaborativo
Como o Automerge funciona
- Todos os dados do Ducking, exceto os blobs de áudio, são armazenados em um documento Automerge
- O padrão central é familiar para desenvolvedores React: busca-se os dados com um hook, renderiza-se a UI e despacha-se uma solicitação assíncrona de mudança; quando os dados mudam, o hook dispara um rerender
- Exemplo com o hook
useDocument: recebe-se o documento comoconst [doc, changeDoc] = useDocument<Episode>(docUrl)e, ao mudar um campo de entrada, atualiza-se comchangeDoc((d) => { d.title = e.target.value })
- Exemplo com o hook
- As operações de atualização parecem imperativas, mas não funcionam como objetos e arrays nativos de JS
- Há menos métodos, não há mutação imediata, e o sistema intercepta as mudanças para convertê-las em itens de changelist do histórico do documento
- Em usos simples, o Automerge cuida do necessário, mas não é magia; como seus invariantes nem sempre coincidem com o significado desejado, o projeto cuidadoso do modelo de dados é importante
- A maior parte das ações semânticas do usuário deve corresponder a uma única operação oferecida pelo Automerge
- Ações separadas do usuário sobre dados relacionados devem se resolver naturalmente do ponto de vista dos invariantes daquela operação do Automerge
- É importante separar claramente os dados canônicos armazenados dos dados derivados calculados
Modelagem de dados para multijogador
- No modelo de dados do Ducking, um clip é uma janela que reproduz parte de uma fonte de áudio base imutável, sendo responsável pelo trecho reproduzido, pela aplicação de efeitos e pelo espaço ocupado na timeline
- O efeito mais comum é o clip ajustar o volume do áudio base ao longo do tempo para fazer crossfade ou remover ruído
- No início, cada clip tinha uma lista de níveis de volume indexados por tempo relativo ao início do clip, mas isso gerava problemas porque a maioria das mudanças de volume dizia respeito ao áudio base, não ao clip
- Se o início do clip fosse adiantado um pouco, todas as mudanças de volume passavam a ser aplicadas a outra parte do áudio
- Escrever código para atualizar todos os timestamps de volume sempre que o início do clip mudasse seria uma má escolha
- Se dois colaboradores editassem ao mesmo tempo o início do clip, cada edição agruparia a mudança do início com a atualização de todos os timestamps da automação de volume
- O Automerge não conhece a relação causal entre essas mudanças e, ao fazer merge, elas poderiam se resolver de forma caótica
- Esse é um problema típico de quando uma única ação semântica tenta atualizar vários dados persistidos de uma forma causal que o CRDT não entende
- A solução foi migrar os dados de efeitos de áudio para o referencial temporal do áudio base, em vez de deixá-los no clip
- Assim, não é mais necessário atualizar nada ao mudar o início ou a duração do clip, e múltiplos editores podem alterar início, automação de volume e outros efeitos de forma mais independente, com maior chance de merge correto
- Diferença entre UI de usuário único e UI multijogador
- Em uma UI de usuário único, às vezes dá para manter o modelo de dados existente e compensar isso com cálculo extra na hora de gravar
- Em uma UI multijogador, migrar o modelo de dados é muito mais comum para manter todos os dados persistidos em estado ortogonal
- Passa-se a preferir fortemente simplificar a escrita e calcular mais na leitura, maximizando o uso do merge automático do Automerge
- Conselhos sobre migração de formatos de dados
- Vale aceitar que formatos de dados precisarão ser migrados ao longo do desenvolvimento e praticar isso cedo para não ter medo da primeira grande migração
- Existem vários padrões, como tratamento no momento da leitura pelo cliente ou upgrades em lote no servidor
- Se houver um invariante conveniente para verificar que antes e depois da migração são equivalentes, o trabalho fica muito mais fácil
- No Ducking, exportava-se o áudio de todos os projetos antes e depois da migração para verificar mudanças com uma impressão digital de áudio, o que tornou segura até a implantação de grandes mudanças de esquema
Implementando reordenação de listas
- Às vezes, para obter garantias que o Automerge não oferece, é necessário escrever invariantes mais fortes diretamente com código na camada da aplicação
- Isso aconteceu ao implementar a magnetic timeline do Ducking, uma lista ordenada dos clips a reproduzir
- O Automerge oferece operações de array para remover e inserir itens por índice, mas não fornece uma operação para reordenação atômica de itens existentes
- Há soluções conhecidas
- Martin Kleppmann publicou um artigo sobre operação atômica de reordenação de listas
- Também publicou com Liangrun Da o artigo "Extending JSON CRDTs with Move Operations"
- Existe ainda um draft PR para adicionar isso ao Automerge, mas ele ainda não foi mesclado
- Problema da forma simples de reordenar
- Remover o objeto do índice atual e adicioná-lo de novo no índice de destino
- Mesmo combinando os invariantes dessas duas operações, isso não garante o invariante desejado de que, mesmo com muitas reordenações concorrentes, o objeto apareça exatamente uma vez na lista
- Se houver várias remoções e inserções concorrentes, o objeto pode aparecer em vários lugares da lista ao mesmo tempo; se Alice e Bob moverem B com delete+insert, as duas remoções viram um único tombstone, mas as duas inserções criam novos elementos distintos, então B aparece duas vezes
- Implementação própria do invariante de "exatamente uma vez" na camada da aplicação
- Quando um clip é inserido na timeline, recebe um semantic id
- Na reordenação, são disparadas as operações de remoção e inserção descritas acima
- Na leitura, a aplicação procura duplicatas com o mesmo semantic id, escolhe arbitrariamente o primeiro item não removido e ignora os demais
- Assim, o objeto aparece apenas uma vez na lista e todos os leitores chegam sempre ao mesmo estado final
- A reordenação de listas foi a única operação de que o Ducking precisou e que o Automerge não oferecia; quando o PR for mesclado, essa lógica em nível de aplicação poderá deixar de ser necessária
Histórico do documento
- Uma boa UI multijogador precisa de ferramentas de gestão de histórico; colaboradores querem ver o que mudou enquanto estiveram fora, comentar diffs, comparar versões antigas e fazer rollback
- O Automerge rastreia o histórico de versões do documento e oferece excelentes primitivas para lidar com histórico e comparação
- Mas cabe ao desenvolvedor da aplicação decidir como expor isso e quais conceitos apresentar ao usuário
- Recomendação das Patchwork lab notes da Ink & Switch
- Especialmente interessantes são o trabalho de expor branches aos usuários e o de comentários universais
- O Ducking acabou adotando um modelo relativamente simples de colaboração e histórico
- Um histórico linear de versões com checkpoints nomeados pelo usuário, em que o checkpoint é a unidade de agrupamento de mudanças e também a unidade de discussão, diff e rollback
- Threads de comentários que podem ser ligadas a um ponto específico do áudio, a uma região da transcrição ou a um checkpoint de versão
- Ainda não houve motivo suficiente para introduzir branches, mas isso pode ser útil no futuro
Texto e marks
- Trabalhar com texto rico é especialmente complicado quando se tenta adicionar lógica customizada sobre texto editável
- Recomenda-se o artigo Peritext, que explica bem as dificuldades de rich text e de software multijogador em geral
- O esquema de rich text do Automerge inclui marks, anotações aplicadas a intervalos de texto que permanecem consistentes mesmo durante a edição
- O uso mais comum é para formatação como negrito e itálico, mas também é possível criar marks customizados da própria aplicação
- Dois usos de marks customizados no Ducking
- Rastrear regiões da transcrição que são alvo de threads de comentários
- Rastrear timestamps de palavras na transcrição, permitindo edição normal
- O serviço de transcrição armazena a transcrição no Automerge como um objeto rich text com marks de informação temporal em cada palavra
- Se um pequeno erro de digitação for corrigido em apenas uma palavra, o mark é preservado e toda a informação de timing continua intacta
- Se uma frase inteira for reescrita, alguns marks do meio podem se perder, mas os do início e do fim permanecem, garantindo ao menos uma noção aproximada do timing
- Uma limitação dos marks é que seu datum precisa ser um valor simples, normalmente uma string, e ele não faz merge multijogador
- Para dados pequenos e imutáveis, como timing de transcrição, usa-se JSON serializado como string
- Para dados mais complexos ou mutáveis, como threads de comentários, guarda-se apenas o id no mark e os dados reais ficam em outra parte do documento
- Os marks oferecem uma ótima base para construir recursos da aplicação sobre rich text multijogador
Próximo texto — estrutura da série
- Este texto é a parte 2 de uma trilogia sobre a criação do Ducking
- Parte 1: explica o design de UI peculiar do software
- Parte 2 (este texto): recomenda olhar para o Automerge e mostra que é possível construir projetos multijogador de hobby
- Parte 3 final, ainda por vir: retrospectiva da experiência de criar o Ducking
- Menções sobre a parte 3 final
- Uso de suporte de LLM não para intensificar o trabalho, mas para liberar mais tempo de rascunho e descanso na rede
- O prazer de criar software narrowcast, feito para satisfazer apenas um pequeno grupo
Perguntas esperadas
E os dados de áudio?
- Todos os dados multijogador ficam no Automerge, mas os blobs de áudio base não ficam nele; eles precisam ser tratados separadamente para permitir reprodução rápida
- O objetivo é que um novo colaborador consiga começar a ouvir e editar em menos de 4 segundos após carregar a página, mais rápido do que abrir um app desktop e muito mais rápido do que baixar o arquivo inteiro do projeto
- Um episódio de 1 hora pode depender de cerca de 1 GB de áudio, somando 4 horas de gravação de estúdio em alta qualidade, efeitos e música de fundo
- O serviço de áudio faz várias tarefas no upload para garantir cold start rápido
- Backup do áudio original
- Transcrição da fala para a visualização de transcrição
- Geração de waveform para a visualização da timeline
- Divisão em janelas curtas para que, se apenas 1 minuto de uma gravação de 40 minutos for usado, a maioria dos clientes receba só um ou dois pequenos trechos
- Transcodificação dos trechos para formatos comprimidos, oferecendo uma versão com perdas que possa tocar imediatamente enquanto o áudio de alta qualidade é baixado em segundo plano
- A camada de dados da UI gerencia o carregamento da versão rápida do que é necessário naquele momento, seguindo a intenção do usuário, e também da versão de alta qualidade do áudio realmente usado
- A API IndexedDB do navegador é útil para cache em múltiplas camadas e armazenamento content-addressable; com eviction automática, o que é usado fica e o que não é usado desaparece
- Depois de todo esse processamento e cache local, o restante da UI pode assumir acesso aleatório rápido ao áudio e focar no fluxo de edição
Por que fazer uma UI de servidor + navegador em vez de um app local-first?
- Há preferência por apps local-first como o Obsidian, especialmente os que oferecem uma rota de saída confiável junto com uma experiência paga baseada em nuvem
- No início, a opção era começar com um app Tauri com armazenamento no sistema de arquivos local e sincronização opcional com servidor
- A UI foi construída com base em uma interface de dados que poderia ser fornecida tanto pelo servidor quanto por um app local
- Isso servia como proteção para que nenhum financiamento futuro levasse a uma monetização mais agressiva via lock-in
- Depois, concluiu-se que isso não seria um SaaS, mas algo para usar com a parceira e alguns amigos
- Sem incentivo para tratá-lo de forma errada e com baixo custo permanente de operação, decidiu-se fazer do jeito mais simples
- Quando se alcançou um cold start de cerca de 3 segundos, ninguém mais quis perder tempo baixando e instalando um app nativo
- A esperança é que apps de áudio saltem do mundo atual, ainda centrado em desktop, direto para um mundo local-first com opções de sincronização, evitando os 10 a 20 anos intermediários de lock-in de SaaS
O Automerge é seguro e web-scale? Vale usar em startup?
- A resposta sincera é não sei, e isso é dito de forma bem-humorada; não é recusa, é literalmente não saber
- Quando o autor entrou na área, a edição colaborativa em tempo real sem conflitos parecia magia, e há 10 anos existiam soluções conhecidas para certos problemas, mas elas exigiam equipes financiadas e especialização em várias áreas
- Hoje, basta instalar uma dependência e, de forma relativamente intuitiva, criar uma UI em que amigos colaboram em tempo real
- Em termos de segurança, o Ducking hoje é protegido por acesso restrito à rede e uma etapa de autorização ao criar a conexão websocket com o servidor Automerge
- Usuários não conseguem descobrir nem editar projetos para os quais não foram convidados
- A atribuição de autoria a edições e comentários é apenas parcialmente segura e depende da suposição de que os amigos não vão agir de má-fé
- Permissões granulares, como poder comentar mas não editar, editar apenas parte do projeto ou controlar descoberta, exigem trabalho de design cuidadoso
- O Keyhive, em desenvolvimento pela Ink & Switch, oferece um modelo de controle de acesso por capabilities com segurança criptográfica
- Isso deve facilitar compartilhar apps Automerge publicamente até com usuários não confiáveis, mas ainda não está pronto
O Automerge é melhor?
- Existe também o Yjs como outra solução nessa área, mas não dá para decidir pelos outros qual é a mais adequada
- O conselho que não muda
- Pensar profundamente no problema, fazer estimativas aproximadas sobre os limites que podem surgir, criar protótipos com várias alternativas e admitir honestamente que talvez o problema nem seja tão difícil a ponto de exigir a solução mais moderna e sofisticada
- No caso do Ducking, protótipos rápidos e exploração da documentação mostraram que o Automerge era maduro o suficiente e com bom desempenho para esse uso
- Mais importante ainda, o ecossistema da Ink & Switch atrai esteticamente
- O Automerge não é apenas um motor de sincronização e versionamento, mas parte de uma visão maior de tornar o software mais seguro, colaborativo, flexível, prazeroso e pessoal
- Há expectativa de sucesso de projetos como o Keyhive e de uma expansão de softwares pequenos, mágicos e feitos para poucos
Ainda não há comentários.