O responsável que não trabalha mais aqui

Product

Os espaços de trabalho do Notez já comportam mais de uma pessoa há algum tempo. As notas têm propriedades tipadas e filtráveis há mais tempo ainda. O passo óbvio entre esses dois recursos é um responsável — uma nota que diz esta é da Marta.

É um recurso pequeno. Uma lista suspensa com as pessoas do espaço de trabalho, um id numa coluna, um chip no cartão. Uma tarde de trabalho.

A parte que não é pequena é a pergunta que ninguém faz até acontecer: a Marta sai. O que acontece com as quarenta notas que têm o nome dela?

Membro é uma propriedade, não um campo

A primeira decisão foi a mais sem graça, e foi sem graça de propósito.

Eu poderia ter adicionado uma coluna assigneeId às notas e feito um seletor para ela. Em vez disso, member virou o décimo primeiro tipo de propriedade personalizada, ao lado de texto, número, data, seleção, relação e os demais.

A diferença aparece em todo lugar, menos no código que guarda o valor. Como membro é uma propriedade:

  • Dá para ter mais de uma. Responsável e Revisor são duas propriedades, não uma mudança de schema.

  • Ela aparece no popover de filtros, no menu de ordenação e no seletor de colunas da visualização em tabela sem que nenhum deles precise aprender o que é uma pessoa.

  • Ela aparece como chip nos cartões, nas células da tabela e na própria nota, pelo mesmo caminho de renderização de todas as outras propriedades.

O recurso inteiro é um caso novo em cerca de uma dúzia de instruções switch, e cada uma se recusa a compilar enquanto o caso não for tratado. O checklist, quem escreveu foi o compilador.

O precedente já existia: relation, o tipo que aponta para outras notas. Vários valores, ordenados, com chave estrangeira para o que referenciam, resolvidos no servidor para que o chip e o filtro nunca discordem sobre o que um valor significa. Uma propriedade de membro é uma propriedade de relação cujo alvo é uma pessoa, e não uma nota. Copiei quase linha por linha.

Onde o id mora

O valor de um membro é um id de usuário. Havia um atalho tentador: os valores de propriedade já têm uma coluna valueText, e um id de usuário é texto. Guardar ali e lançar um dia antes.

Em vez disso, dei a ele uma coluna própria com uma chave estrangeira de verdade, por um motivo que não é capricho.

Contas são excluídas. Quando alguém exclui a conta do Notez, aquela linha some, e toda referência a ela precisa ir junto — senão sobra um valor apontando para um usuário que não existe mais, e toda leitura precisa filtrar fantasmas por precaução. Com uma chave estrangeira e on delete cascade, o banco de dados faz isso sozinho, corretamente, para sempre, numa única linha de schema. Com valueText, isso seria problema meu: uma limpeza manual no hook de exclusão de conta, mais um filtro de ids órfãos em cada caminho de leitura, mais o bug que eu acabaria lançando quando alguém adicionasse um décimo segundo caminho de leitura e esquecesse.

A check constraint que diz exatamente uma coluna de valor não é nula passou de cinco colunas para seis. Esse é o custo todo.

A pergunta de verdade

Agora a parte interessante. A Marta é removida do espaço de trabalho, ou sai. O nome dela está em quarenta notas. Há exatamente três coisas a fazer:

Bloquear a remoção. Recusar a remoção de alguém até que as atribuições dessa pessoa sejam limpas. É a pior opção, e leva uns dez segundos para ver por quê: a Marta não consegue sair de um espaço de trabalho do qual quer sair até que um proprietário edite quarenta notas à mão. Um mecanismo de segurança que faz uma pessoa de refém não é um mecanismo de segurança.

Manter o valor e marcá-lo como morto. Deixar a linha, resolver o nome mesmo assim, exibir em cinza — uma lápide (tombstone). É o que o Notez já faz com valores de relação que apontam para notas na lixeira, então havia precedente, e foi o meu primeiro plano.

Apagar os valores junto com a participação. Remover alguém limpa as atribuições dessa pessoa na mesma transação.

Fiquei com a terceira, e abandonei o plano da lápide enquanto o escrevia.

Por que a lápide perde aqui

Lápides fazem sentido para uma nota excluída. A nota ainda existe, na Lixeira, e pode ser restaurada por trinta dias. A lápide aponta para alguma coisa e diz a verdade: esse alvo está na lixeira.

Um membro que saiu não está numa lixeira. Não há nada para restaurar. E duas coisas específicas dão errado:

Ninguém mais consegue encontrar essas notas. O filtro de membro lista as pessoas que estão no espaço de trabalho agora — é isso que faz dele um seletor útil, e não uma lista que só cresce com todo mundo que já passou pelo espaço. Então a Marta não está nela. Quarenta notas agora carregam um valor que nenhum filtro seleciona. Elas não são encontráveis; só dá para tropeçar nelas por acaso.

As notas ficam silenciosamente hostis à edição. Cada gravação de uma propriedade de vários valores envia o array inteiro. A validação de escrita confere se cada id é de um membro atual. Então uma nota atribuída à Marta e a alguém que continua no espaço recusaria a próxima gravação — você teria de perceber o chip morto da Marta e removê-lo antes de conseguir adicionar qualquer pessoa. Eu poderia abrir uma exceção para ids que já estão gravados, e essa exceção é exatamente o tipo de regra sutil que vira bug dois recursos depois.

Apagar tem um custo real, e vale dizer com todas as letras: convidar a pessoa de novo não traz as atribuições de volta. É informação perdida de verdade. Acho que é a troca melhor, porque a alternativa é uma informação presente mas inalcançável, o que é pior do que ausente — parece dado e se comporta como lixo.

O precedente para apagar estava ali mesmo, no mesmo arquivo. Excluir uma opção de seleção já remove em cascata os valores dela de todas as notas. A caixa de confirmação para remover um membro já diz essa pessoa perderá o acesso a todas as notas deste espaço de trabalho. Limpar a atribuição é coerente com as duas coisas.

O caso que escapou

Há um quarto cenário que a transação não cobre, e vale ser honesto sobre ele.

Um membro removido entre a validação e o insert — uma janela de milissegundos — deixa uma linha que a limpeza nunca viu. O usuário ainda existe, então o chip continua aparecendo. É um valor desatualizado, e a próxima edição dessa propriedade é recusada até você desmarcá-lo.

Daria para fechar essa brecha com um lock. Escolhi não fechar: a janela é de milissegundos, a falha é um chip confuso e não dados perdidos, e como o seletor mantém as linhas selecionadas fixas no topo, sempre dá para desmarcar. Algumas condições de corrida merecem um comentário, não um lock.

O que ficou de fora de propósito

  • Atribuir pelo chat de IA. Quando isto foi lançado, o chat conseguia ler quem está numa nota, mas não definir: nenhuma ferramenta entregava a ele a lista de pessoas do seu espaço de trabalho, então ele só poderia chutar um id, e um chute com cara de plausível é o pior modo de falha possível numa escrita. Hoje o chat recebe todos os membros do espaço de trabalho com os respectivos ids e define uma propriedade de membro como qualquer outro valor.

  • Quadros agrupados por responsável. O quadro está ligado à sua própria propriedade de coluna. Agrupar por uma propriedade qualquer é uma mudança maior do que esta.

  • "Atribuídas a mim". Um filtro de um clique pelo próprio nome era o próximo passo óbvio, e já foi lançado: ative Mostrar na barra lateral numa propriedade de membro, e a barra lateral lista todo mundo do espaço de trabalho com a contagem de notas de cada um. Um clique no seu nome filtra as suas.

  • Notificações. Ser atribuído a uma nota não gera aviso. Avise o seu colega.

Quando escrevi isto, dois desses quatro eram difíceis e dois simplesmente ainda não tinham sido feitos. A atribuição pelo chat e o filtro foram lançados desde então; os quadros agrupados por responsável e as notificações, não. A diferença importa, e prefiro dizer qual é qual a deixar uma lista de recursos dar a entender que tudo está para sair.

A lição

Adicionar um campo é fácil. O que transforma um campo num recurso é decidir o que ele faz nos dias que fogem do caminho feliz — quando a pessoa sai, quando a conta é excluída, quando duas escritas disputam corrida.

Gastei talvez um quinto do tempo no seletor e no chip. A maior parte foi para o parágrafo do artigo de ajuda que explica o que acontece quando alguém sai — porque foi escrever essa frase que provou que o primeiro plano estava errado.

Continue lendo