04 — Importação Inteligente de Alunos: o padrão de referência
Ref: 00_INDEX · Esta é a régua contra a qual 03 e 99 comparam.
Contexto
O usuário descreveu o objetivo como "do mesmo jeito que resolvemos a importação de alunos". Este doc destila os 7 elementos desse padrão e demarca exatamente onde cada um vive no código.
Anatomia do padrão (7 elementos)
A1 — Sessão persistente no banco
Tabela importacao_alunos_sessoes (existe — confirmado no inventário Supabase).
- Cada upload cria uma row, com
id(sessionId),dados_importacao(JSON),progresso,status,mensagem. - Source of truth: se a aba fecha, refresh, perde rede — a sessão continua viva no DB.
→ Equivalente para resultados: importacao_resultados_sessoes já existe (mesmo inventário). Reutilizar a mesma forma para recompute.
A2 — Endpoint retorna task_id imediatamente
Padrão visto em gestao-alunos/index.ts (action start_import_session): handler valida payload, cria a row, devolve { success: true, sessionId } e dispara o trabalho real em background.
→ Tempo de Response: dezenas de ms. Nada de esperar processamento.
A3 — Background via EdgeRuntime.waitUntil
_shared/alunos-import.ts é chamado dentro de EdgeRuntime.waitUntil(processarImportacao(...)). Sem void (que o Deno mata pós-Response).
→ Memória obrigatória: ver mem://architecture/edge-function-background-task-standard.
A4 — Batches de tamanho fixo (BATCH_SIZE)
alunos-import.ts:166-235 itera em batchStart += BATCH_SIZE (50). Cada batch faz insert + update separados (não delete-and-recreate), com log de progresso ao final.
Vantagem: cada batch é pequeno o suficiente para caber em uma transação rápida, e dá ponto de retomada se algo falhar.
A5 — Update de progresso na sessão a cada batch
alunos-import.ts:228-231 atualiza progresso + mensagem na row da sessão a cada lote. UI lê isso via polling (ver A6).
A6 — Polling client-side com retomada em refresh
src/hooks/useImportacaoSessao.ts:
localStorage.setItem(IMPORT_SESSION_KEY, newSessionId)(linha 192) — persiste sessionId no navegador.- Ao montar (linha 234): se há
sessionIdno localStorage, busca status no backend → se ainda processando, retoma o polling; se finalizado, limpa. setInterval(..., 3000)ms para polling, com cleanup em unmount.- Detecção de stale (polling sem progresso) cataloga sessão zumbi.
→ Resultado prático: usuário pode fechar a aba, voltar 10min depois, reabrir, e vê a importação ter completado.
A7 — Idempotência por chave canônica
alunos-import.ts:
- Matriculas duplicadas no arquivo são detectadas via
matriculasImportadas.has(matricula.toLowerCase())(linha 125). - Conflito existente no banco resolvido via
onConflictno upsert. - Lookups internos por id (não por nome) — ex.:
inscricaoMap.set(aluno.id, inscricao.id)emresultados-import.ts:159. Padrão correto, vale como referência cruzada com 07.
Tabela de paridade — Alunos vs Resultados
| Elemento | Alunos (import) | Resultados (import) | Resultados (recompute) |
|---|---|---|---|
| A1 Sessão DB | ✅ importacao_alunos_sessoes | ✅ importacao_resultados_sessoes | ❌ ausente |
| A2 task_id imediato | ✅ | ✅ (provável, validar) | ❌ síncrono |
| A3 waitUntil | ✅ | ✅ (provável, validar) | ❌ |
| A4 BATCH_SIZE | ✅ 50 | ✅ via queryInChunks | parcial (só nos reads) |
| A5 progresso | ✅ | ✅ (validar) | ❌ |
| A6 polling + retomada | ✅ useImportacaoSessao | ✅ useImportacaoResultados (presente) | ❌ |
| A7 idempotência por id | ✅ | ✅ inscricaoMap | parcial (upsert por inscricao_id,fase_id) |
→ Recompute é o gap óbvio (5 das 7 colunas vermelhas).
O que extrair para o SSOT (99)
- Toda operação de escrita em massa >50 itens DEVE seguir A1–A7.
- Endpoint nunca espera o processamento. Response em <300ms com
task_id. - UI tem hook genérico de polling com retomada via localStorage.
- Sessões zumbis são detectadas (timeout) e marcadas como
falhadapor job cron.
Próximas etapas
→ Replicar A1–A7 em recompute (Onda B do roadmap). → Hook genérico useBackgroundTask({ taskId, pollIntervalMs }) para qualquer feature consumir — proposto no 99 §6.