Files
vinihcrosaandClaude Opus 4.8 994fbc999c docs(planning): server-side simulation PRD, techspec, ADRs, tasks
Plan to move physics from the browser (WASM Web Worker) to the server.
Client becomes a thin remote terminal; single engine; foundation for
persistence. Includes 6 ADRs and a 9-task breakdown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:53:51 -03:00

17 KiB
Raw Permalink Blame History

Plano — Migração para Simulação no Servidor

Status: Proposta (nenhum código alterado) Escopo: Restruturação client-server — mover a física do cliente para o servidor Autor: análise assistida por IA Data: 2026-07-03


1. Objetivo

Hoje a física roda localmente no browser (WASM em um Web Worker). Este plano descreve a mudança para um modelo onde a simulação roda no servidor e o cliente apenas:

  1. Envia comandos (criar simulação, play/pause, leme, RPM, ambiente, waypoints).
  2. Recebe snapshots de estado em tempo real e renderiza.

O cliente deixa de ser dono da simulação e passa a ser um terminal remoto de uma simulação autoritativa que vive no servidor.

Boa notícia: a arquitetura alvo já está planejada no repositório. apps/server existe como stub, docs/planning/prds/phases/phase-03-server.md e docs/planning/prds/modules/infrastructure.md já especificam o servidor WebSocket + Protobuf, e docs/architecture/README.md afirma que o Ymir "exposes a WebSocket + Protobuf API to external clients". Este plano ativa esse desenho e religa o cliente web a ele.


2. Estado atual (diagnóstico)

Onde a física roda hoje

apps/web/src/workers/simulation.worker.ts   ← loop de 20 Hz no browser
  ├─ carrega /wasm/ymir.js (Embind) ............ core C++ compilado p/ WASM
  ├─ fallback: mock-wasm.ts (cinemática JS) .... quando ymir.wasm não existe
  └─ postMessage({type:'state'}) → simulationStore (zustand)
  • apps/web/src/stores/simulationStore.ts cria o Worker, faz play/pause/reset/loadScenario/applyEnvironment e recebe state via worker.onmessage.
  • core/src/wasm/YmirBindings.cpp expõe a classe YmirSimulation (addVesselAt, setRudderAngle, setThrusterCommand, step, getState, loadEnvironment, setWaveConditions) — a mesma superfície que o servidor precisará expor.
  • apps/api (Fastify/Node) serve apenas dados (vessels, areas, scenarios, vessel config). Não roda física.
  • apps/server (C++) é um stub INTERFACE que linka Ymir::Simulation + Ymir::Persistence. Vazio — nenhum WebSocket, nenhum loop, nenhum handler.
  • packages/types/src/simulation.ts define SimulationStateDTO, VesselStateDTO, WorkerMessageDTO — hoje o contrato é worker↔UI; vira contrato rede↔UI.

Problemas do modelo atual (motivação)

Problema Consequência
Física por cliente Cada aba/usuário roda sua própria simulação — nada é compartilhado
CPU do cliente é o teto Cenários pesados travam a UI; sem controle de recursos
Sem estado autoritativo Impossível múltiplos observadores da mesma simulação
WASM vs mock divergem Dois motores para manter; risco de comportamento diferente
Sem persistência de run Simulação morre ao fechar a aba

3. Arquitetura alvo

┌─────────────────────┐         WebSocket + Protobuf          ┌──────────────────────────┐
│      apps/web        │  ◄──────────────────────────────────►│       apps/server (C++)    │
│  (React + Three.js)  │                                       │                            │
│                      │  ClientMessage (comandos) ──────────► │  WebSocketServer           │
│  simulationStore     │                                       │    └─ CommandRouter        │
│    └─ ws client      │  ◄────────── ServerMessage (eventos)  │  SessionManager            │
│       (substitui o   │              WorldSnapshot @ N Hz     │    └─ N × World/Simulation │
│        Web Worker)   │                                       │  EventForwarder            │
└─────────────────────┘                                       │  SimulationClock (realtime)│
        │                                                      └──────────────────────────┘
        │ HTTP (CRUD scenarios/vessels/areas)                              │ usa
        ▼                                                                  ▼
┌─────────────────────┐                                       ┌──────────────────────────┐
│   apps/api (Node)    │  (inalterado — só dados)              │ libs/{simulation,world,     │
└─────────────────────┘                                       │  physics,vessel,persistence}│
                                                               └──────────────────────────┘

Dois canais no cliente, com papéis distintos:

  • HTTP → apps/api: CRUD de dados de projeto (scenarios, vessels, areas). Não muda.
  • WebSocket → apps/server: ciclo de vida e controle da simulação em tempo real. Novo.

4. Decisão-chave: qual servidor roda a física?

Há duas opções. O repositório já escolheu a Opção A (docs + stub apps/server); este plano segue essa escolha e registra o trade-off.

Opção A — Servidor C++ nativo (apps/server) — recomendada

O cliente fala WebSocket + Protobuf direto com um processo C++ que linka libs/simulation, libs/world, etc.

  • ✅ Alinhada com docs existentes (phase-03-server, infrastructure, architecture README)
  • ✅ Física roda nativa (CVODE completo, sem limites de WASM)
  • ✅ Reusa libs/* sem reescrever nada de física
  • ✅ Múltiplas simulações isoladas por thread (já previsto em data-flow.md)
  • ⚠️ Requer escolher lib WebSocket C++ (uWebSockets / libwebsockets / Boost.Beast) — decisão aberta
  • ⚠️ Introduz Protobuf no build C++ e um gerador de tipos TS a partir do .proto

Opção B — Estender apps/api (Node) como gateway

Node mantém o WebSocket com o browser e chama a física via FFI/child-process (WASM em Node, ou binário C++ via IPC/N-API).

  • ✅ Um só ponto de rede (Node), auth/CRUD/realtime juntos
  • ✅ Reusa TypeBox/JSON — sem Protobuf
  • ❌ Contradiz a arquitetura documentada
  • ❌ Overhead de ponte Node↔C++; WASM em Node teria o mesmo teto de performance de hoje
  • ❌ Fica difícil isolar simulações por thread/processo com garantias de tempo real

Recomendação: Opção A. Manter apps/api para dados; apps/server C++ para simulação. A decisão final (e a escolha da lib WS + Protobuf) deve virar um ADR antes de codar (ver docs/planning/prds/phases/phase-03-server.md — "Decisões Abertas para TechSpec").


5. Contrato de protocolo

O protocolo já está esboçado em phase-03-server.md. Consolidação para esta migração:

Comandos (cliente → servidor) — ClientMessage

Mensagem Mapeia o que o cliente faz hoje
CreateSimulation{scenario_json} loadScenario() + boot do worker
StopSimulation{sim_id} reset()
PauseSimulation / ResumeSimulation pause() / play()
SetSpeed{factor} (novo) fast-time multiplier
SetRudder{vessel_id, angle_deg[]} setActuator(rudder)
SetRPM{vessel_id, rpm[]} setActuator(thruster) — hoje setThrusterCommand(power%, azimuth)
SetEnvironment{update} applyEnvironment() / loadEnvironment() + setWaveConditions()
SetWaypoints{vessel_id, waypoints[]} (novo — depende de controlador de waypoint)

Eventos (servidor → cliente) — ServerMessage

Evento Substitui
WorldSnapshot{entities[]} @ N Hz WorkerMessageDTO{type:'state'}
SimulationStatus{running/paused/ended} estados internos do store
EntityCreated / EntityRemoved (novo)
EnvironmentChanged feedback de applyEnvironment
erro WorkerMessageDTO{type:'error'}

Nota sobre serialização

O plano-fonte usa Protobuf. Alternativa mais leve: JSON sobre WebSocket reusando os DTOs TypeBox atuais (SimulationStateDTO) — elimina toolchain Protobuf e mantém o contrato TS existente, ao custo de payloads maiores. Recomendação: começar com JSON (menor risco, reusa packages/types) e migrar para Protobuf se o bandwidth do snapshot virar gargalo. Registrar em ADR.


6. Componentes do servidor (a construir em apps/server)

Todos já especificados em phase-03-server.md; resumo do que precisa existir:

  1. WebSocketServer — aceita conexões, identifica Manager vs Viewer, (de)serializa mensagens.
  2. SessionManager — mapeia conexão ↔ sim_id; cria/encerra World; limite de simulações simultâneas.
  3. CommandRouter — despacha ClientMessage para operações na simulação correta.
  4. EventForwarder — assina o EventBus/ExternalEventSink, serializa eventos, envia aos clientes.
  5. SimulationClock — loop real-time (1× wall clock, dt_target 50 ms / 20 Hz), suporta fast-time.
  6. Bootstrap — main() que sobe o servidor, lê porta de config/env, instancia os componentes.

Pré-requisitos de física que precisam ser verificados antes (o PRD assume que existem):

  • World::snapshot() retornando estado completo
  • EventBus com ExternalEventSink
  • API de mutação do Environment
  • Superfície de comando por corpo (leme, RPM) equivalente ao que YmirBindings.cpp já expõe

A superfície de física necessária já existe hoje no wrapper WASM (YmirBindings.cpp). O servidor pode espelhar exatamente essa fachada YmirSimulation, extraída para uma classe reusável em libs/simulation ou em apps/server, evitando divergência entre WASM e servidor.


7. Componentes do cliente (a alterar em apps/web)

O ponto central: substituir o Web Worker WASM por um cliente WebSocket. A superfície pública do simulationStore (play/pause/reset/loadScenario/applyEnvironment/setActuator) permanece igual — só a implementação interna troca de postMessage para ws.send.

Arquivo Mudança
stores/simulationStore.ts Trocar criação de Worker por conexão WS; postMessage→ws.send; onmessage passa a decodificar ServerMessage
workers/simulation.worker.ts Removido (ou mantido atrás de flag "modo offline" — ver §9)
workers/mock-wasm.ts Removido ou reaproveitado como servidor de dev local
packages/types/src/simulation.ts WorkerMessageDTO → ServerMessage/ClientMessage; DTOs viram contrato de rede (ou gerados do .proto)
(novo) lib/simulation-socket.ts Cliente WS: conectar, reconectar, (de)serializar, fila de comandos
.env / config VITE_SIM_WS_URL (default ws://localhost:8080)

Preservado sem mudança: toda a renderização (Ocean, Area3DView, VesselMarker, geo/geo3d), os stores de UI (vesselPanelStore, environmentStore), e o CRUD via apps/api.

Diferenças de comportamento a tratar no cliente

  • Latência de rede: hoje o estado é síncrono no worker; via rede há delay. Considerar interpolação/extrapolação client-side para render suave a 60 fps sobre snapshots a 20 Hz.
  • Reconexão: definir se retoma o snapshot atual do servidor ou reinicia (decisão aberta no PRD).
  • Autoridade: o cliente não muta mais estado local — só envia comando e espera o snapshot. Actuadores no vesselPanelStore viram "comando pendente" até o servidor confirmar.

8. Faseamento (migração incremental, sem big-bang)

Fase 0 — Fundação e decisões (bloqueia o resto)

  • ADR: Opção A vs B; lib WebSocket; Protobuf vs JSON; auth (nenhuma nesta fase?).
  • Verificar que libs/{simulation,world} expõem snapshot + comandos + EventBus necessários.
  • Extrair a fachada de YmirBindings.cpp para uma classe reusável (fonte única de verdade).

Fase 1 — Servidor mínimo (echo do WASM atual)

  • apps/server: WebSocketServer + SessionManager + SimulationClock básicos.
  • Suportar: CreateSimulation, Start/Stop/Pause/Resume, SetRudder, SetRPM, SetEnvironment.
  • Emitir WorldSnapshot a 20 Hz. Paridade funcional com o worker atual.
  • Teste de integração: cliente mock → comando → snapshot de volta (< 100 ms wall, critério do PRD).

Fase 2 — Cliente WebSocket atrás de flag

  • Novo lib/simulation-socket.ts + simulationStore com backend selecionável (wasm | remote).
  • Rodar os dois lado a lado; validar que a UI se comporta igual contra o servidor.
  • E2E Playwright existentes (simulation-run.spec.ts) devem passar em ambos os modos.

Fase 3 — Corte e limpeza

  • Tornar remote o padrão.
  • Remover (ou esconder atrás de flag "offline") o Worker WASM e o mock-wasm.
  • Atualizar docs de arquitetura e data-flow; rodar graphify update ..

Fase 4 — Além da paridade (opcional, futuro)

  • Múltiplos viewers da mesma simulação.
  • SetSpeed / fast-time, waypoints, persistência de run (SQLite — Fase 6 do roadmap).

9. Decisão: manter WASM como modo offline?

Duas posturas possíveis — decidir no ADR:

  • Remover WASM: um só motor (servidor). Menos manutenção, mas exige servidor sempre online; perde o "abrir e rodar" sem backend.
  • Manter WASM atrás de flag: simulationStore com dois backends (remote default, wasm offline). Mantém demo/local sem servidor, ao custo de dois motores para manter sincronizados.

Recomendação: manter o backend selecionável durante a migração (necessário para a Fase 2 de qualquer forma) e decidir a remoção definitiva só depois da paridade comprovada.


10. Impacto por área (resumo)

Área Impacto Muda física?
libs/physics, libs/world, libs/vessel Nenhum (talvez expor snapshot/comandos se faltar) Não
core/src/wasm/YmirBindings.cpp Extrair fachada para classe reusável Não
apps/server Construção completa (WS, router, sessão, clock, forwarder) Não
apps/api (Node) Nenhum — continua servindo dados Não
apps/web stores/workers simulationStore religado; worker removido/flag Não
packages/types Contrato worker→rede; possivelmente gerado do .proto Não
proto/ (novo, se Protobuf) Schemas + geração TS/C++ Não
docs/architecture, data-flow Atualizar para refletir cliente remoto Não

11. Riscos e decisões abertas

  • Lib WebSocket C++: uWebSockets vs libwebsockets vs Boost.Beast (peso × maturidade).
  • Protobuf vs JSON: toolchain × tamanho de payload. Recomendação: JSON primeiro.
  • Thread-safety: como CommandRouter acessa NavalSimulation com o SimulationClock avançando o tick? (lock por simulação / fila de comandos aplicada no início do tick).
  • Frequência de snapshot: 20 Hz para todos ou configurável por cliente/viewer.
  • Reconexão: retomar snapshot atual vs reiniciar.
  • Autenticação: nenhuma nesta fase (LAN confiável) — confirmar.
  • Interpolação no cliente: necessária para render suave; define o quão "esperto" o cliente fica.
  • Determinismo WASM↔servidor: se manter os dois, garantir que produzem a mesma trajetória (mesmos parâmetros VLCC, mesmo integrador) ou documentar a divergência.

12. Critérios de aceite (herdados do PRD phase-03-server)

  1. Cliente conecta via WS, envia CreateSimulation, recebe sim_id.
  2. SetRPM → tick seguinte usa novo RPM; SetEnvironment(wind) reflete no próximo tick.
  3. Cliente recebe WorldSnapshot a cada tick com posição correta.
  4. PauseSimulation emite SimulationStatus(paused).
  5. Duas simulações simultâneas, estados independentes.
  6. Cliente desconecta — simulação continua no servidor.
  7. Roundtrip comando → evento < 100 ms (wall) em teste de integração.
  8. UI web (apps/web) renderiza contra o servidor com paridade ao modo WASM atual.
  9. Cobertura de testes ≥ 80% (regra global do projeto).

13. Não-metas

  • Persistência de runs (SQLite) — Fase 6 do roadmap.
  • apps/fast-time — fora deste plano.
  • Colisões, mooring, anchoring — fases próprias.
  • Multi-tenant / auth robusta — LAN confiável por ora.

14. Próximos passos

  1. Aprovar este plano e a Opção A (servidor C++).
  2. Escrever ADR das decisões abertas (§11) e um TechSpec de apps/server.
  3. Verificar/expor a superfície de física necessária (§6) em libs/simulation/libs/world.
  4. Executar Fases 1→3 do §8.

Todo o código permanece inalterado até a aprovação. Este documento é somente o plano.