1

Pitch: 9 de 10 abas restauradas — por que repetir seria um bug

Transparência: sou Danila Pryadko e desenvolvo o Tabwell, uma extensão para
salvar e restaurar sessões do Chrome. Em um texto anterior no
TabNews
,
expliquei por que recriamos primeiro a ordem das abas e só depois os grupos.
Este é um follow-up sobre outro problema: o que fazer quando a restauração já
produziu uma janela, mas termina só parcialmente.

O erro que já produziu um resultado

Imagine um snapshot com dez abas. O Chrome cria a janela, abre nove URLs e
rejeita a décima. A função não terminou como planejado, mas também não voltou ao
estado inicial: uma janela com nove abas já existe.

Se o chamador enxergar apenas uma exceção e aplicar a regra genérica “falhou,
tente de novo”, a segunda execução pode criar mais nove abas corretas. Agora o
usuário tem de separar as duplicatas justamente durante uma recuperação.

Essa é a fronteira que mudou o desenho do fluxo:

antes de existir windowId  → ainda não há janela para duplicar
depois de existir windowId → já houve um efeito externo irreversível

chrome.tabs.create() não é uma operação naturalmente idempotente. Repetir a
mesma entrada não devolve a mesma aba; cria outra. Portanto, a idempotência
precisa estar no protocolo ao redor da API, não na chamada em si.

Um resultado parcial precisa continuar sendo um resultado

O núcleo da restauração devolve uma estrutura explícita, em vez de esconder o
estado útil dentro de um erro genérico:

type RestoreResult = {
  windowId: number;
  requested: number;
  restored: number;
  failed: number;
  failedUrls: string[];
  failedGroups?: number;
};

O loop tenta cada aba isoladamente. Se o Chrome rejeitar uma URL, ou devolver
uma aba sem ID, essa ocorrência entra em failedUrls e as demais continuam.
Esquemas executáveis como javascript:, data: e vbscript: também são
barrados antes de chegar a chrome.tabs.create().

Falhas de agrupamento seguem a mesma ideia. Depois que a janela existe, lançar
uma exceção apagaria do contrato justamente a informação mais importante:
windowId. Por isso, o resultado também carrega failedGroups. A interface
pode distinguir “nada começou” de “uma janela parcial já foi criada”.

O diagrama abaixo resume a fronteira de commit em português. Ele é renderizado
nativamente pelo próprio TabNews, então o texto continua legível mesmo sem uma
imagem externa:

flowchart LR
  A[PENDENTE] --> B[TENTATIVA]
  B --> C{Uma janela foi criada?}
  C -- Não --> D[Repetição segura]
  C -- Sim, 9 de 10 --> E[COMMIT da sessão]
  E --> F[Rollback da janela parcial]
  F -- Removeu --> G[Decisão manual]
  F -- Falhou --> H[Bloquear novo restore]
  E --> I[Repetir somente o cleanup]

O restore não é idempotente. O estado de controle impede que uma nova
tentativa repita as nove criações que já aconteceram.

A máquina de estados fica fora do loop de abas

Para recuperação automática após uma queda do navegador, tratei restauração e
limpeza como fases diferentes. Em pseudocódigo simplificado:

PENDENTE
  └─ persistir tentativa antes de criar a janela
       ├─ COMPLETA  → marcar sucesso → limpar checkpoint
       └─ PARCIAL   → tentar remover a janela parcial
                         ├─ removeu    → preservar snapshot para decisão manual
                         └─ não removeu → marcar COMMIT e bloquear novo restore

O marcador de tentativa vem antes do primeiro efeito externo. Se o service
worker do Manifest V3 for suspenso em um ponto ambíguo, o próximo ciclo não
dispara outra restauração automática. Ele preserva o snapshot e oferece o fluxo
manual.

Quando a restauração termina completamente, a limpeza do checkpoint ainda pode
falhar. Essa falha não invalida a janela restaurada. O ciclo seguinte repete
somente a limpeza; ele não cria outra janela.

Quando a restauração automática é parcial, tentamos rollback removendo a janela
incompleta. Se a remoção funciona, o efeito externo deixa de existir e o usuário
pode decidir como prosseguir. Se a remoção também falha, gravamos um marcador de
commit: há uma janela que não conseguimos desfazer, então outro restore fica
bloqueado.

No fluxo manual, windowId já significa commit

A tela de recuperação mantém duas guardas em memória: uma para impedir dois
cliques concorrentes e outra para registrar que uma janela já foi criada. Ao
receber uma resposta com windowId, mesmo que seja 9/10, ela:

  1. mostra o resultado parcial;
  2. desabilita “Restaurar tudo” e “Restaurar grupos selecionados”;
  3. mantém “Pular, apenas descarte” disponível para repetir somente a limpeza.

Captura pública da interface do Tabwell em português do Brasil mostrando um espaço salvo antes e depois da restauração, com nomes, cores, grupos recolhidos e ordem preservados.

Captura de 1280 × 800 da listagem pública pt-BR na Chrome Web Store. Ela usa
dados determinísticos de demonstração e não mostra uma sessão pessoal.

O commit também precisa sobreviver à suspensão do service worker. A referência
ao snapshot pendente fica em armazenamento durável, enquanto a guarda de janela
criada fica no storage.session, que sobrevive aos reinícios do worker dentro
do mesmo processo do Chrome.

Esse escopo é intencional. Se o processo inteiro do navegador cair novamente, a
janela parcial também desaparece; uma nova sessão pode voltar a oferecer a
recuperação. Um marcador permanente confundiria “uma janela ainda existe” com
“uma janela existiu em algum momento”.

Os testes verificam efeitos, não só mensagens

Os casos que protegem esse contrato simulam as bordas onde uma implementação
ingênua costuma duplicar trabalho:

  1. uma das cinco criações volta sem ID; o resultado precisa ser 4/5, manter o
    windowId e apontar a URL que falhou;
  2. um resultado parcial mantém o diálogo aberto, bloqueia as duas ações de
    restore e deixa o descarte habilitado;
  3. reiniciar o service worker na mesma sessão não pode restaurar de novo um
    snapshot já marcado como committed;
  4. depois de sucesso completo, uma falha de cleanup pode ser repetida sem criar
    uma segunda janela;
  5. no auto-restore parcial, o rollback é tentado; se remover a janela falhar, o
    marcador de commit precisa sobreviver ao próximo ciclo do worker.

Também há uma regra simples antes dessa máquina de estados: snapshot vazio ou
seleção vazia são recusados antes de chrome.windows.create(). Sem efeito
externo, não há commit para reconciliar.

Idempotência aqui significa controlar a repetição

Não transformamos chrome.tabs.create() em uma chamada idempotente. O que
fazemos é tornar explícitos três fatos:

  • qual snapshot está pendente;
  • se uma tentativa automática já começou;
  • se uma janela restaurada ainda deve ser tratada como committed.

Isso permite repetir as partes seguras — leitura, apresentação do diálogo e
cleanup — sem repetir a parte que cria recursos no navegador.

Os snapshots, títulos e URLs continuam no IndexedDB do perfil do Chrome; a
recuperação não exige uma conta Tabwell nem uma cópia da sessão em nuvem. Para
quem quiser observar o fluxo na build pública, o Tabwell está na Chrome Web
Store
,
em um link direto e sem parâmetros de rastreamento.

Como vocês modelariam essa fronteira? Usariam um marcador por sessão, um log de
etapas persistente ou uma chave de idempotência própria — e em qual momento
considerariam a criação da janela definitivamente committed?

Carregando publicação patrocinada...
0

Atualização do autor (26/08/2026): sou Danila Pryadko e desenvolvo o Tabwell. A geração de recuperação 1.1 já está pública. A versão 1.1.0 introduziu bundles com várias janelas, o journal durável, o Gentle Restore e os fluxos protegidos de Salvar e fechar e Substituir atual; a 1.1.1 é o hardening de correção e usabilidade guiado por testes externos. Não atribuo toda a arquitetura à correção 1.1.1.

O problema do texto original — “9 de 10 abas foram restauradas; repetir tudo seria outro bug” — fica mais interessante quando o snapshot tem várias janelas. O Chrome não oferece uma transação única entre janelas. Portanto, chamar a restauração de “atômica” esconderia justamente o estado que precisa ser tratado: uma janela pode ter sido reconstruída, outra pode falhar ao receber metadados de grupo e uma terceira ainda nem ter começado.

O contrato que passei a usar é de falha segura, não de atomicidade global:

  1. validar o bundle inteiro antes da primeira mutação;
  2. registrar no journal a operação e o frontier que ela já alcançou;
  3. preservar, por janela, quais alvos foram criados pela operação;
  4. confirmar o resultado somente depois de ordem, abas fixadas, aba ativa e metadados dos grupos passarem pelas verificações previstas;
  5. em uma falha terminal, remover apenas estruturas cuja propriedade ainda possa ser revalidada.
bundle validado
  └─ journal durável (operationId + frontier)
      ├─ Janela A — confirmada
      ├─ Janela B — falha parcial
      │   └─ reler grupo e abas
      │       ├─ ownership confirmado → remover só a casca criada
      │       └─ ownership ambíguo → preservar e relatar parcial
      └─ Janela C — não iniciada

Mapa textual conceitual: uma restauração com várias janelas pode terminar em sucesso, resultado parcial ou rollback limitado aos alvos que ainda pertencem à operação.

O quinto item é a fronteira destrutiva. Um ID de grupo retornado pelo Chrome é evidência de que um grupo existiu; não é uma autorização permanente para apagá-lo. Entre a criação e o rollback, o usuário pode mover uma aba, renomear o grupo ou reaproveitar aquela janela. Antes de remover uma “casca” de grupo depois de uma atualização de metadados esgotada, a operação precisa conferir de novo o alvo, sua relação com a operação e as abas que ainda estão dentro dele. Se a propriedade ficou ambígua, o resultado correto é deixar o estado visível e reportar a falha — não adivinhar e apagar.

Em pseudocódigo, a diferença é esta:

const shell = await readGroup(targetGroupId);

if (!shell || !stillOwnedBy(operation, shell)) {
  return { kind: 'partial', cleanup: 'skipped-ambiguous-owner' };
}

await removeOnlyOwnedShell(shell.id);
return { kind: 'partial', cleanup: 'confirmed' };

Isso não transforma uma sequência de chamadas da API do Chrome em transação. A vantagem é mais estreita e verificável: uma falha tardia não autoriza automaticamente a extensão a desfazer trabalho que talvez já não seja só dela.

O journal também muda o significado de “tentar de novo”. Depois de uma interrupção do service worker ou do perfil inteiro, a pergunta não é “qual botão o usuário apertou por último?”, mas “qual operação durável existe, qual frontier foi confirmado e quais efeitos podem ser retomados sem duplicação?”. Uma nova tentativa cega abriria janelas e abas paralelas. Retomar a mesma operação permite continuar do checkpoint; se o estado externo não satisfaz mais as invariantes, o fluxo deve parar com um resultado parcial explícito.

Na 1.1.1, um caso concreto desse princípio foi o rollback de grupos: após esgotar a atualização de metadados, a limpeza passou a remover somente a casca exatamente revalidada como pertencente à operação. Outro ajuste evitou que o Substituir atual abortasse por um pendingUrl transitório com a mesma URL, mantendo o bloqueio quando existe divergência real. São correções do hardening 1.1.1 sobre a arquitetura lançada na 1.1.0.

O teste útil não é apenas “todas as abas abriram”. Eu separaria pelo menos estes casos:

  • falha antes de qualquer janela nova;
  • primeira janela concluída e segunda interrompida;
  • grupo criado, mas atualização de nome/cor esgotada;
  • usuário altera o grupo antes do rollback;
  • service worker reinicia entre criação e confirmação;
  • mesmo pendingUrl transitório versus URL realmente diferente;
  • retomada do mesmo journal sem duplicar o que já foi confirmado.

O resultado pode continuar sendo 9 de 10, mas agora há respostas diferentes para três perguntas: o que foi processado, o que ficou confirmado e o que ainda pertence à operação e pode ser revertido com segurança. Misturar essas respostas em um único booleano é que tornaria a recuperação destrutiva.

Usei IA para apoiar a estrutura e a revisão em português. Conferi as afirmações técnicas no changelog, no código e nos testes congelados da versão pública 1.1.1. Esta atualização é um registro de implementação do próprio autor, não uma avaliação independente nem uma promessa de recuperação sem perda.

0

Atualização do autor (16/08/2026): sou Danila Pryadko e desenvolvo o Tabwell. A versão 1.0.8 já está pública e fecha uma lacuna prática do fluxo descrito acima.

Em restaurações com 10 ou mais abas, a interface agora mostra progresso apenas por contagens — processadas, solicitadas, restauradas e com falha — sem levar títulos ou URLs para o evento. Uma aba que falha faz processadas avançar, mas não restauradas. Depois que todas as abas foram tentadas, o fluxo entra em uma etapa separada de finalização enquanto reconstrói os grupos; o resultado final continua sendo a fonte de verdade para sucesso completo ou parcial.

Também há uma guarda síncrona contra clique repetido: se uma restauração já está em andamento nessa interface, a segunda chamada é recusada antes de iniciar a tarefa, então ela não abre outra janela. Redigi esta atualização com apoio de IA e conferi cada afirmação no código imutável da versão 1.0.8.