State Machines (XState) em Fluxos Médicos
Para fluxos de negócio que possuem múltiplos estados, transições complexas e regras de decisão baseadas em eventos, utilizamos o XState para criar Máquinas de Estado. No contexto de saúde, a precisão do fluxo é uma questão de segurança do paciente.
A Necessidade de Máquinas de Estado na Saúde
Enquanto o Redux gerencia os dados (o que é o prontuário), o XState gerencia o comportamento (o que pode ser feito com o prontuário agora).
Por que XState e não apenas Booleanos?
Em um atendimento de telemedicina, usar múltiplos if (isLoading && isStarted && !isFinished) leva ao "Código Espaguete" e a estados impossíveis. O XState resolve isso ao:
- Prevenir Erros de Fluxo: Um médico não pode "Finalizar" uma consulta que ainda não foi "Iniciada".
- Garantir Protocolos: Podemos forçar que um "Termo de Consentimento" seja assinado antes que a câmera seja ligada.
- Visualização Clínica: O fluxo definido no código é exatamente o mesmo que o protocolo médico desenhado pelos analistas de negócio.
Caso de Uso: Ciclo de Vida da Teleconsulta
O módulo de consultas (packages/modules/consultation) utiliza o XState para gerenciar o progresso rigoroso de um atendimento.
Diagrama de Estados da Consulta
[*] -> AGENDADA
AGENDADA -> SALA_DE_ESPERA : PACIENTE_CONECTOU
SALA_DE_ESPERA -> EM_ATENDIMENTO : MEDICO_INICIOU
EM_ATENDIMENTO -> FINALIZADA : MEDICO_CONCLUIU [TERMO_ASSINADO]
EM_ATENDIMENTO -> CANCELADA : ERRO_TECNICO_GRAVE
AGENDADA -> CANCELADA : PACIENTE_DESISTIUExemplo de Protocolo: Termo de Consentimento
Um dos usos mais poderosos de Guards em máquinas de estado na saúde é a imposição de protocolos legais. Por exemplo, a câmera do médico ou do paciente não deve ser ativada antes que o Termo de Consentimento de Teleconsulta seja assinado eletronicamente.
Implementação do Guard de Protocolo
Abaixo, detalhamos como a máquina de estado bloqueia a transição para o estado de vídeo se o contrato (contexto) não indicar que o termo foi aceito.
// packages/modules/consultation/machines/sessionMachine.ts
const teleconsultationMachine = createMachine({
id: 'teleconsultation',
initial: 'aguardandoTermo',
context: {
isTermOfUseAccepted: false, // Inicia como falso
},
states: {
aguardandoTermo: {
on: {
// Evento disparado quando o usuário clica em "Aceito os Termos"
ACCEPT_TERMS: {
actions: assign({ isTermOfUseAccepted: true }),
target: 'preparandoEquipamento'
}
}
},
preparandoEquipamento: {
initial: 'verificandoCamera',
states: {
verificandoCamera: {
on: {
START_VIDEO: {
target: 'cameraAtiva',
// GUARD CRÍTICO: Impede a ativação se o termo não estiver assinado
cond: 'hasAcceptedTerms'
}
}
},
cameraAtiva: {
// Lógica de stream de vídeo
}
}
}
}
}, {
guards: {
// Implementação da lógica do Guard
hasAcceptedTerms: (context) => context.isTermOfUseAccepted === true
}
});Por que esta abordagem é superior à lógica tradicional?
- Impossibilidade de Bypass: Como a transição
START_VIDEOexige o guardhasAcceptedTerms, não existe nenhum caminho no grafo da aplicação que permita chegar ao estadocameraAtivasem passar pelo eventoACCEPT_TERMS. - Auditabilidade: O estado da máquina pode ser persistido, servindo como prova técnica de que o software seguiu o protocolo legal antes de abrir o stream de vídeo.
- Interface Reativa: A UI pode usar o seletor
state.can('START_VIDEO')para desabilitar visualmente o botão de câmera, fornecendo feedback instantâneo ao usuário sobre a pendência do termo.
Máquinas de Estado Paralelas: O Desafio da Telemedicina
Em uma teleconsulta, múltiplos sistemas precisam operar de forma independente, mas coordenada. O médico precisa enviar mensagens no chat enquanto o vídeo está em fase de reconexão. O XState gerencia isso através de Estados Paralelos (Orthogonal States).
Caso de Uso: Sessão Ativa (Vídeo + Chat + Prontuário)
Durante o estado EM_ATENDIMENTO, a aplicação entra em uma sub-máquina paralela onde cada "região" mantém seu próprio status.
Diagrama de Estados Paralelos
SESSAO_ATIVA (Estado Paralelo)
├── REGIAO VÍDEO (WebRTC) ────────────────────────┐
│ [*] -> AGUARDANDO_PERMISSAO │
│ AGUARDANDO_PERMISSAO -> CONECTANDO [HAS_CAM] │
│ CONECTANDO -> EM_CHAMADA │
│ EM_CHAMADA -> RECONECTANDO (se cair o sinal) │
└─────────────────────────────────────────────────┘
├── REGIAO CHAT (WebSocket) ──────────────────────┐
│ [*] -> SINCRONIZANDO_HISTORICO │
│ SINCRONIZANDO_HISTORICO -> PRONTO │
│ PRONTO -> ENVIANDO_MENSAGEM │
└─────────────────────────────────────────────────┘
├── REGIAO PRONTUARIO (Auto-save) ────────────────┐
│ [*] -> OCIOSO │
│ OCIOSO -> EDITANDO │
│ EDITANDO -> SALVANDO (Debounce) │
└─────────────────────────────────────────────────┘Implementação com Guards e Contexto
Os Guards garantem que transições só ocorram se critérios clínicos ou técnicos forem atendidos.
import { createMachine } from 'xstate';
export const sessionMachine = createMachine({
id: 'session',
type: 'parallel',
context: {
hasCameraPermission: false,
isTermOfUseAccepted: false
},
states: {
video: {
initial: 'aguardandoPermissao',
states: {
aguardandoPermissao: {
on: {
GRANT_PERMISSION: {
target: 'conectando',
// Transição condicional (Guard)
cond: (context) => context.isTermOfUseAccepted
}
}
},
conectando: { /* ... */ },
emChamada: { /* ... */ }
}
},
chat: {
initial: 'sincronizando',
states: {
sincronizando: { on: { SYNC_SUCCESS: 'pronto' } },
pronto: { on: { SEND_MESSAGE: 'enviando' } },
enviando: { on: { SENT: 'pronto' } }
}
}
}
});Implementação Detalhada: Máquina de Sessão Ativa
Uma implementação de máquina de estado em XState deve ser robusta para lidar com os desafios técnicos de uma teleconsulta.
import { createMachine, assign } from 'xstate';
interface SessionContext {
hasCameraPermission: boolean;
isTermOfUseAccepted: boolean;
retryCount: number;
}
export const sessionMachine = createMachine({
id: 'session',
type: 'parallel',
context: {
hasCameraPermission: false,
isTermOfUseAccepted: false,
retryCount: 0
} as SessionContext,
states: {
video: {
initial: 'aguardandoPermissao',
states: {
aguardandoPermissao: {
on: {
GRANT_PERMISSION: {
target: 'conectando',
// Guard: Só avança se o termo foi aceito
cond: (context) => context.isTermOfUseAccepted
},
REJECT_PERMISSION: 'semAcesso'
}
},
conectando: {
invoke: {
src: 'connectToWebRTC',
onDone: 'emChamada',
onError: {
target: 'reconectando',
actions: assign({ retryCount: (ctx) => ctx.retryCount + 1 })
}
}
},
emChamada: {
on: { DISCONNECT: 'finalizado' }
},
reconectando: {
after: {
3000: { target: 'conectando' } // Tenta reconectar após 3 segundos
}
},
semAcesso: {},
finalizado: { type: 'final' }
}
},
chat: {
initial: 'sincronizando',
states: {
sincronizando: {
on: { SYNC_SUCCESS: 'pronto' }
},
pronto: {
on: { SEND_MESSAGE: 'enviando' }
},
enviando: {
on: { SENT: 'pronto', ERROR: 'pronto' }
}
}
}
}
});Integração com a Arquitetura Global
As máquinas de estado não substituem o Redux; elas atuam como o "Cérebro" da lógica.
- Redux: Armazena os dados brutos (nome do médico, especialidade, texto da prescrição).
- XState: Decide qual botão deve estar habilitado ou qual tela deve aparecer.
- Sagas: Escutam as transições da máquina para disparar efeitos colaterais (ex: iniciar o sinal de vídeo ao entrar no estado
conectando).