← Voltar ao arquivo

Local Player: progresso atômico que viaja com o HD

Como writeFileAtomic + fila serial + .bak + .corrupt-<ts> eliminaram perda de posição mesmo matando o servidor no meio do save.

Perder onde eu parei no vídeo era o bug que mais me irritava. Então eu tratei progresso como dado crítico.

O contexto

Na parte 1 eu contei a arquitetura geral do Local Player. Aqui eu vou contar do subsistema que me fez perder mais sono: salvar a posição da aula em server/progress/store.js.

Eu decidi que o progresso ia morar junto da biblioteca, em <lib>/.courseplayer/progress.json. Assim ele viaja com o HD ou pendrive — eu desplugo, levo pra outra máquina, e continuo de onde parei. Cada entrada é {position, duration, completed, updatedAt} numa chave libId\0rel.

O problema que eu queria resolver

Era sempre a mesma história: eu fechava o servidor no meio do save, faltava luz, ou o progress.json corrompia — e eu perdia tudo. Ou pior: eu limpava o cache de transcode e junto ia o progresso.

Minha regra virou: save normal nunca apaga chave, nunca tira um completed, nunca perde posição válida. O frontend também tinha que colaborar, com throttle e umas travas contra regressão.

A decisão mais importante

Três camadas simples no backend:

  1. escrita atômica (writeFileAtomic: escreve num tmp, dá fsync, renomeia, dá fsync no diretório);
  2. fila serial (updateProgress) — um save por vez, nunca dois juntos;
  3. backup duplo (.bak e .bak.1; se o principal corromper, preserva como .corrupt-<ts> e restaura do backup).
save → tmp exclusivo → fsync arquivo → rename → fsync dir
corrompido → preserva .corrupt-<ts> → tenta .bak → tenta .bak.1

Eu pensei em usar SQLite com WAL, mas ia puxar dependência nativa e complicar em FAT/exFAT. JSON atômico com backup cobre meu volume real e qualquer um consegue abrir e auditar.

flowchart TD
  Player["timeupdate / pause / ended"] --> Fila["updateProgress (serial)"]
  Fila --> Atomica["writeFileAtomic"]
  Atomica --> Disco["progress.json"]
  Disco --> Backup[".bak / .bak.1"]

Como usei IA no desenvolvimento

Ela foi ótima pra gerar a matriz de testes (progress-invariance, progress-persistence, progress-forensic, library-progress) — regressão de posição, completed preservado, isolamento por biblioteca.

Mas tentou me convencer a fazer escrita paralela com debounce agressivo no backend pra “otimizar”. Eu tive que travar a regra no AGENTS.md: fila serial, sem exceção.

E a validação eu fiz na marra: escrevi progresso, matei o servidor no meio, reiniciei e conferi se voltou. Corrompi o JSON de propósito pra ver o .corrupt-<ts> nascer e o restore funcionar.

O que eu descobri implementando

  • Limpar é cirúrgico: por prefixo <curso>/ ou global. E o POST /api/transcode/clear limpa transcode e cancela jobs, mas nunca encosta no progress.json — tem teste só pra isso.
  • Rescan nunca apaga progresso.
  • No frontend (public/js/player.js): timeupdate com throttle de 5s, pause/ended/beforeunload com sendBeacon; acima de 95% marca concluído; reassistir não desmarca; posição 0 não apaga; sem metadata não grava; retomada só se 3 < position < duration - 2.
  • Remover biblioteca é só config (com job ativo dá 409); a padrão não sai (403).

O que funcionou

  • LP_DATA_DIR pra rodar os testes com servidor real num diretório temporário, sem encostar nos meus dados.
  • LP_PROGRESS_FORENSIC=1 quando eu preciso de log verboso.
  • Chave por biblioteca acabou com vazamento de progresso entre HD interno e pendrive.

O que não funcionou

  • O primeiro fs.writeFile direto: kill no meio gerava JSON truncado e perda total. O backup só prestou depois da atomicidade.
  • Tentei persistir pastas abertas da sidebar junto e desisti — a árvore re-renderiza muito e brigava com o salvo.

O que eu faria diferente hoje

Ter escrito as travas (“save normal nunca…”) como teste antes do código, não depois. Muito bug da IA teria caído no primeiro node --test.

Resultado

Hoje passa: kill no meio preserva, corrompido vira .corrupt-<ts> e restaura, clear de transcode não afeta progresso, rescan preserva tudo.

Falta: compactar o progress.json pra biblioteca gigante — hoje reescreve o arquivo todo a cada save.

O que aprendi

1. Atomicidade é barata

Tmp + fsync + rename resolve 90% sem dependência nova.

2. “Não regredir” é especificação

Tem que ser teste, não comentário.

3. Formato segue portabilidade

JSON no .courseplayer viaja com o disco; banco local prenderia meu dado à máquina.

Próximos passos

Conclusão

Progresso confiável não precisa de stack nova. Precisa de ordem total nas escritas, fsync no lugar certo e backup testado com kill de verdade.

Referências

  • server/progress/store.js, server/core/fs-atomic.js, server/progress/routes.js
  • test/progress-invariance.test.js, test/progress-persistence.test.js, test/progress-forensic.test.js
  • docs/VALIDATION.md seção 4
Diagrama ampliado
100%
↑↓ para navegar · Enter para abrir 8 itens no índice