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:
- mostra o resultado parcial;
- desabilita “Restaurar tudo” e “Restaurar grupos selecionados”;
- mantém “Pular, apenas descarte” disponível para repetir somente a limpeza.
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:
- uma das cinco criações volta sem ID; o resultado precisa ser
4/5, manter o
windowIde apontar a URL que falhou; - um resultado parcial mantém o diálogo aberto, bloqueia as duas ações de
restore e deixa o descarte habilitado; - reiniciar o service worker na mesma sessão não pode restaurar de novo um
snapshot já marcado como committed; - depois de sucesso completo, uma falha de cleanup pode ser repetida sem criar
uma segunda janela; - 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?