Um museu com outras pessoas lá dentro
A versão um era um museu para uma pessoa. A versão dois é o mesmo edifício com outras pessoas a percorrê-lo: quatro caracteres para entrar, até oito visitantes, vozes que se apagam à medida que alguém se afasta — e nem um byte disto guardado num servidor.
Nesta página
Há seis semanas escrevi sobre um museu para tudo o que tenha um URL — o fetch protegido, a cascata de análise, uma planta que nunca se mexe. Funcionava, e era um edifício para exatamente uma pessoa. Podia colar um link, pendurar uma coisa, andar pelas suas próprias salas, e depois a única coisa a fazer com o que tinha construído era descrevê-lo a alguém.
Por isso a versão dois é o mesmo museu com outras pessoas lá dentro. Abre uma sala, lê quatro caracteres em voz alta, e até mais sete visitantes entram no seu edifício — como figuras que vê do outro lado de uma galeria, com vozes que baixam à medida que se afastam para a sala seguinte. O próprio edifício melhorou enquanto eu andava por lá: três tipos de galeria em vez de um, escultura sobre pedestais, um céu que muda conforme o dia, e uma visita guiada que é finalmente um controlador de câmara a sério.
Nada disto fica guardado em lado nenhum. Essa restrição fez mais trabalho de desenho do que qualquer funcionalidade, portanto vem primeiro.
- 8
- visitantes numa sala
- 0
- bytes guardados no servidor
- 246
- textos de interface ×6 línguas
- 303
- testes unitários
A forma de funcionamento do Reliquary v2. O segundo número é aquele com que a v1 já saiu; mantê-lo agora que há mais gente no edifício é o sentido de quase tudo o que se segue.
Uma sala é um canal, não um registo
A maneira óbvia de construir a visita partilhada é uma tabela de salas: uma linha por sala, uma lista de presentes, histórico para o chat. É também o ponto em que o seu museu nunca sai do seu navegador se transforma discretamente em …exceto as partes que agora guardamos. Por isso não há registo de sala em lado nenhum. Uma sala é um topic de canal — vt:reliquary:v1:<CODE> — num relé de difusão em tempo real, e o navegador do anfitrião é a sala. Ela vive enquanto a chave de presença do anfitrião estiver no canal; quando o anfitrião fecha o separador, acabou.
Não ter registo traz uma consequência que eu não tinha previsto: não há forma de perguntar se uma sala existe. Um topic onde não está ninguém subscreve-se exatamente com a mesma boa vontade que um topic cheio. Portanto a pergunta é respondida pela presença e por um prazo — um convidado que entra espera seis segundos que apareça uma chave de anfitrião, e esse tempo limite é a definição de «essa sala não existe». Antes de um anfitrião ficar com um código, a mesma ausência obriga-o a sentar-se 900 ms no topic para ver se já mora ali alguém.
O código tem quatro caracteres, mostrado como REL-7K2M. O alfabeto tem trinta símbolos: sem 0 nem O, sem 1, I ou L, sem U — os dois primeiros pares são as confusões clássicas de quem lê em voz alta, e o U desaparece para que nenhum sorteio soletre algo que um visitante prefira não dizer alto. Trinta à quarta são 810 000 códigos, tirados de crypto.getRandomValues. Isto é privacidade por obscuridade, e diz-se assim mesmo: adivinhar um código é a única porta para o museu de outra pessoa, portanto o anfitrião tem um comando para fechar a sala e cada byte que chega é revalidado na mesma.
Um relé sem autoridade
A credencial que põe um navegador nesse canal é pública por desenho — as páginas de marketing já a entregam a todos os visitantes. A descrição honesta do transporte é um cano burro para onde qualquer pessoa com quatro caracteres pode enviar o que quiser, e tudo o que está a jusante foi construído sobre esse pressuposto.
Cada mensagem é revalidada à chegada, por tipo e por limite, numa única função que é toda a fronteira de confiança desta funcionalidade. As peças que chegam de um par voltam a passar por sanitizeExhibit — a mesma função por onde é filtrado o armazenamento do próprio visitante, importada e nunca copiada, para não se afastarem uma da outra. Um URL recusado a partir do disco é recusado a partir do fio. O id de quem envia é uma afirmação e não uma credencial, e não há deliberadamente camada de assinatura, ao contrário do fio do xadrez 4D, onde uma jogada forjada poderia roubar uma partida. Aqui não há nada para roubar.
Quem envia nunca é quem sanitiza. A montagem de um convidado é um pedido; o anfitrião volta a sanitizá-la, aplica-a através do seu próprio armazenamento e retransmite aquilo que de facto ficou pendurado.
O que foi preciso defender foi a amplificação. Um hello de trinta bytes que compra uma ressincronização de sessenta peças é uma alavanca apontada à sala, portanto todas as mensagens cuja resposta custa mais do que a mensagem transporta têm um piso por remetente: dois segundos entre ressincronizações, 400 ms entre linhas de chat, três segundos entre pedidos de montagem. Escrever é mais lento do que 400 ms; um ciclo não é. A sincronização é partida a 48 KB ou 24 itens, o que vier primeiro — e uma coleção vazia emite mesmo assim um bloco com uma lista vazia, porque um convidado que não recebe nada não consegue distinguir «o anfitrião não tem nada» de «a sincronização ainda vem a caminho».
A posição é a única mensagem que se repete — dez por segundo, por visitante, enquanto a sala estiver aberta — por isso é quantizada ao centímetro e ao milirradiano. E não é só compressão: o quantizador é também o detetor de inatividade, já que duas poses que quantizam para os mesmos inteiros são a mesma pose, e portanto ficar parado interrompe a emissão sem nenhum teste à parte.
Oito pessoas em lã e linho
A especificação da v1 tinha duas linhas logo no topo: nenhuma dependência npm nova, nenhum ficheiro binário. A segunda decidiu o que é um visitante. Sem carregamentos, sem personagens descarregadas — um visitante são quatro inteiros pequenos e um nome, e todo o resultado do vestiário cabe numa carga de presença.
Oito paletas, com nome de tecido e não de cor: ardósia, camelo, ameixa, musgo, ferrugem, tinta, linho, verdete. O vocabulário da galeria é nogueira, cal e latão sob um sol baixo, e cada traje assenta dentro desse intervalo em vez de lutar contra ele. Exatamente uma paleta se aproxima do azul-petróleo, dois degraus abaixo em croma — um visitante de casaco puxado ao azul-petróleo, não uma fonte de luz a atravessar a sala. Aqui, o azul-petróleo continua a ser um bisturi.
As quatro formas de cabeça são uma só curva. Uma superelipse — |y/ry|^n + |x/rx|^n = 1 — é uma elipse com n = 2 e aproxima-se de um retângulo à medida que n cresce; redonda, oval, quadrada e crista são quatro conjuntos de parâmetros dela, a cabeça quadrada em 3.6, um cubo de revolução amaciado em vez de uma caixa com cantos. Uma função emite a silhueta, a figura 3D torneia-a, a pré-visualização plana do vestiário espelha-a num traçado, portanto o seletor não pode prometer uma cabeça que a figura não mantenha. A altura é uma faixa estreita, de 0.9× a 1.1× de uma figura de 1.70 m.
A decisão com que eu contava discutir comigo próprio era a colisão. Os pares são fantasmas: nenhum avatar contribui com um colisor e o controlador de marcha nunca fica a saber que existem. O que desenhamos é uma estimativa — um palpite suavizado por mola sobre onde alguém estava há umas duas centenas de milissegundos — e ser empurrado por uma estimativa é pior do que atravessar uma pessoa que está mesmo ali. E uma porta tem 2.3 m de largura: um único visitante parado nela, sólido, sela uma ala inteira do museu de um desconhecido sem forma nenhuma de lhe pedir que se chegue. Sobrepor-se é um embaraço de dois segundos; ficar preso é uma visita estragada.
Vozes que se apagam quando nos afastamos
O chat de texto foi uma manhã. A voz é o que transforma uma página partilhada numa sala partilhada. O áudio corre de navegador para navegador por ligações diretas entre pares; o canal limita-se a entregar a dois visitantes os blobs opacos de sessão um do outro. É isso que fixa o teto em oito, anfitrião incluído: uma malha completa de oito são 28 ligações e sete fluxos de saída por pessoa, mais ou menos o que aguentam o carregamento de um portátil e o codificador de um telemóvel. Subir o número implicaria um servidor de media, não uma constante maior.
As duas pontas de um par ficam a saber uma da outra pelo mesmo evento de presença, no mesmo instante, portanto ambas tentam abrir a conversa e ambas encravam — o clássico glare. A correção custa uma comparação: o par cujo id ordena primeiro faz a proposta, o outro espera. Os ids de par têm 32 caracteres hexadecimais, portanto a ordenação é total e idêntica dos dois lados. A voz é opcional por visitante, por isso quem responde e não ouviu nada durante 2.5 segundos promove-se e propõe na mesma.
Depois a parte que se ouve mesmo. Uma voz está em ganho total dentro de dois metros — distância de conversa — e a partir daí cai por distância inversa simples. A inversa física, no entanto, nunca cala ninguém: aos catorze metros uma voz continua à volta de 14% de ganho, e sete dessas por baixo do som ambiente da sala dão um murmúrio que soa a avaria e não a museu. Por isso a curva é enjanelada até zero exato no último troço por um smoothstep, plano nas duas pontas para que ninguém estale ao afastar-se. As salas aqui têm uns dez metros, o que coloca o desvanecimento entre o fundo desta sala e duas salas mais à frente.
O balanço é a componente lateral da direção, limitada a ±0.8, e colapsa para o centro abaixo de um metro, para que dois visitantes que se atravessam não lhe chicoteiem da esquerda para a direita nos ouvidos.
A voz corre no seu próprio contexto de áudio em vez de pedir emprestado o do motor de jogos, que se suspende no momento em que o documento fica escondido: correto para passos, errado para uma conversa. Ainda assim obedece ao mesmo silenciar, porque quem silenciou o museu queria dizer fica em silêncio, não que os sinos fiquem em silêncio. O push-to-talk fica em V e corta a faixa do microfone, não a ligação.
O resíduo, nomeado em vez de enterrado: ainda não há servidor de retransmissão. Dois navegadores cujas redes se recusam a encontrar-se diretamente nunca chegam a ligar-se. Esse par fica marcado como indisponível na lista de visitantes e mantém o chat de texto — pior do que voz a funcionar, muito melhor do que uma roda a girar que nunca resolve.
Visitar sem que lhe escrevam por cima
Um convidado está a olhar para a coleção de outra pessoa, e a promessa do despacho de lançamento é que o seu museu é uma chave no seu próprio navegador. Por isso uma visita desenha um mundo inteiramente separado daquele que a sua chave guarda, e o seu armazenamento nunca é escrito enquanto está fora. A troca viaja sob um véu nos dois sentidos, e o véu cai imediatamente, antes de a sincronização em blocos ter chegado — a alternativa era deixar um convidado parado por um instante na sua própria galeria, a perguntar-se de quem são aqueles quadros.
O modo de leitura acabou por ser uma série de juízos pequenos em vez de um interruptor. A gaveta de gestão mantém a lista e o ir-a-pé-até-lá, mas remover e limpar desapareceram. A barra de colagem fica e passa a ser um pedido: a análise continua a acontecer no navegador do convidado, o resultado sanitizado viaja, o anfitrião sanitiza outra vez e responde com um veredicto que chega como aviso. Com a montagem desligada, a barra fica desativada e diz porquê em vez de desaparecer, porque um comando ausente é indistinguível de uma página avariada.
A costura que a versão um deixou aberta
O despacho de lançamento terminava com uma promessa: uma visita guiada, «esboçada atrás da mesma costura de controlo que o modo a pé usa». Durante algum tempo essa visita foi aquilo em que estas coisas costumam tornar-se — uma cadeia de interpolações de câmara de uso único disparadas de dentro do ciclo de desenho, o que significava que a câmara tinha dois donos consoante o minuto.
Agora há uma segunda implementação. O carril é uma lista de paragens pré-calculada: cada obra pela ordem em que está pendurada, cada uma na pose de visão frontal que a caminhada automática já usa. Entre paragens a câmara voa a polilinha de pontos de passagem das portas em vez de interpolar através de uma parede, demora-se 3.6 segundos e no fim volta ao início. Olhar por arrasto durante o percurso acrescenta um desvio limitado por cima da orientação própria do carril — esse desvio decai para o centro enquanto se viaja, portanto a obra seguinte chega sempre centrada, e mantém-se durante a paragem. Tocar nas teclas de movimento é a verdadeira tomada de controlo, e devolve-o à marcha na pose atual. A melhor consequência de um controlador em vez de uma animação: abrir uma obra agora põe a visita em pausa em vez de a terminar.
O minimapa sai do mesmo módulo de planta de onde sai o edifício — entrada no topo, galerias a descer o painel pela ordem em que se percorrem. Clique numa sala ou num ponto e a maquinaria existente de andar-ou-velar leva-o lá. Os outros visitantes aparecem como pontos nas suas próprias cores de acento; o único azul-petróleo do painel é a cunha que mostra para onde está virado.
O próprio edifício melhorou
A v1 tinha uma só receita de galeria: uma sala comprida com as obras a alternar por duas paredes lisas. É uma boa sala, e à peça trinta é a única sala em que alguma vez esteve. Agora há três arquétipos, alternados pelo índice da sala — a sala comprida, um salão (uma sala de aparato mais larga, com pilastras nos cantos sob um óculo no teto) e uma galeria de nichos, onde entre pendurados saem pilares pouco profundos de cada parede lateral, de modo que cada par de obras fica no seu próprio nicho.
O invariante que tornou isto delicado é aquele em que assenta o edifício inteiro: acrescentar a peça N+1 nunca pode mover as peças 1 a N. Portanto tudo o que um arquétipo muda é função pura do índice da sala, e a sala 0 é deliberadamente a sala comprida com os números originais, para que todos os museus construídos em junho reproduzam exatamente a geometria que tinham. As peças sem imagem costumavam ficar penduradas como uma placa na parede; agora erguem-se como uma estela gravada sobre um pedestal, na posição de encaixe idêntica.
E o céu mexeu-se. Três ambientes — hora dourada, uma manhã pálida, um crepúsculo de hora azul — são escolhidos de uma roda ponderada de dez casas por um hash da data local do calendário. A hora dourada fica com cinco das dez porque é o rosto da casa; tem um tempo o dia inteiro, outro amanhã, e duas pessoas no mesmo fuso veem o mesmo museu. Cada ambiente carrega o contrato de iluminação todo, não apenas o céu: o crepúsculo sobe o brilho quente próprio do estuque em vez de o baixar, porque a parede por cima de uma porta não é alcançada nem pelo sol baixo nem pelos focos das obras.
O que saiu sem barulho
Corre agora uma sonda de WebGL antes de o canvas montar, e uma ausência genuína recebe um cartaz localizado em vez de um retângulo em branco — ausência genuína apenas, nunca classe de aparelho: um telemóvel com WebGL a funcionar mantém o museu inteiro, percorrível. A perda de contexto está ligada a um estado de recuperação e a uma remontagem limpa.
As obras são malhas, portanto não há nada a que se chegue com o Tab. O padrão que assenta num canvas de olhar por arrasto é um cursor sobre a ordem em que estão penduradas: [ e ] percorrem as obras, a câmara caminha ou teletransporta-se sob véu até cada uma, e cada uma é anunciada com educação. Enter inspeciona — e sem cursor definido escolhe a obra mais próxima do sítio onde está de pé.
E o museu tem testes, agora: 303 espalhados por 11 ficheiros, a partir de nenhum. Isso é mais arquitetura do que virtude: o protocolo, os códigos de sala, a matemática da voz, a planta e o carril são módulos puros sem React, sem three.js e sem rede lá dentro, e é por isso que porque é que a voz daquela pessoa sai do lado errado é um teste e não uma tarde num navegador. A interface passou de 87 textos para 246, nas seis línguas, com paridade verificada.
| v1 — junho | v2 — hoje | |
|---|---|---|
| Visitantes | 1 | até 8, anfitrião incluído |
| Voz | — | malha por proximidade, direta entre pares |
| Visita guiada | uma cadeia de interpolações de câmara | um controlador de carril; o foco põe-na em pausa |
| Salas de galeria | uma receita de sala | 3 arquétipos, alternados pelo índice da sala |
| Céu | hora dourada | 3 ambientes, um por dia do calendário |
| Lotes sem imagem | uma placa na parede | uma estela sobre um pedestal |
| Orientar-se | andar, ou a lista de gestão | minimapa da planta com pontos dos pares |
| Textos de interface | 87 ×6 línguas | 246 ×6 línguas |
| Testes unitários | 0 | 303 por 11 ficheiros |
| Guardado no servidor | nada | continua nada |
A última linha é a tese. Tudo o que está acima dela é o que é preciso para manter a última linha verdadeira quando há mais do que uma pessoa no edifício.
O modelo de privacidade da versão um era simples de mais para correr mal: sem base de dados, nada a perder. A visita partilhada costuma ser onde essa frase leva uma emenda — é preciso uma sala algures, uma lista de presentes algures, um bocado de histórico para o chat rolar. Afinal não é preciso. É preciso um canal, um código curto o suficiente para se dizer em voz alta, e a disciplina de revalidar cada byte que chega por ele, porque um relé sem autoridade só é seguro se nada a jusante confiar nele.
O que eu não esperava foi quanto o edifício melhorou como efeito secundário. Assim que uma sala pode ter outras pessoas, repara-se que todas as galerias são iguais, que o céu nunca muda, que os lotes só de texto estão a pedir desculpa de si próprios contra uma parede. Sozinho, ninguém se importa. Só se vê isso quando está outra pessoa lá dentro.