MÓDULO 4.1

🔌 RPC e event bus

A coluna vertebral do core. Como cada domínio expõe métodos ao mundo, fala com o renderer sem CORS, e coordena trabalho interno sem virar bola de neve.

6
Tópicos
50
Minutos
Avançado
Nível
Core
Tipo
1

🧩 Controller pattern

Toda funcionalidade do OpenHuman que precisa ser chamada de fora — do React, do CLI, de testes — passa por controllers. A regra é simples e única: cada domínio em src/openhuman/<dom>/ declara handlers em rpc.rs e expõe metadados em schemas.rs, registrados num registry central wired em src/core/all.rs.

Por que registry, não match gigante

Antigamente src/core/jsonrpc.rs tinha um match method { ... } com todos os domínios. Crescia O(n) por feature, gerava merge conflicts, e impedia geração automática de tipos. O controller registry trocou isso por data-driven dispatch.

  • Adicionar método: tocar 2 arquivos do domínio, zero linha no transport.
  • CLI e JSON-RPC compartilham 100% do dispatch — sem desync.
  • Schemas servem para validação, autocomplete e documentação.

Estrutura de um domínio

src/openhuman/cron/
├── mod.rs           // export-only
├── ops.rs           // lógica de negócio
├── store.rs         // persistência
├── types.rs         // tipos do domínio
├── rpc.rs           // handlers async pub fn ...
├── schemas.rs       // ControllerSchema + registro
└── bus.rs           // EventHandler impl
Conceito-chave
Domínio dono do dado
Conceito-chave
mod.rs leve
Conceito-chave
Sem ramos em transport
Conceito-chave
Wire em src/core/all.rs
2

📦 RpcOutcome<T>

É o Result do core. Tipo de retorno padronizado para todos os controllers: carrega o valor de sucesso OU um erro estruturado serializável. Sem ele, cada handler inventa formato próprio e o coreRpcClient do frontend vira um if/else infinito.

O contrato

pub enum RpcOutcome<T> {
    Ok(T),
    Err(RpcError),
}

// No handler do domínio:
pub async fn create_job(params: CreateJobParams) -> RpcOutcome<Job> {
    match store::insert(params).await {
        Ok(job)  => RpcOutcome::Ok(job),
        Err(e)   => RpcOutcome::Err(RpcError::storage(e)),
    }
}

✓ O que FAZER

  • Retornar RpcOutcome::Err com mensagem útil para o usuário
  • Mapear ? do Rust para RpcError via From
  • Logar o erro original (verbose) antes de devolver versão sanitizada

✗ O que NÃO fazer

  • panic!() dentro de handler — derruba o tokio task
  • Devolver Result<Value, String> ad-hoc
  • Vazar stack trace ou secret em RpcError.message
Conceito-chave
Ok/Err uniforme
Conceito-chave
Serializável JSON
Conceito-chave
Erro tipado
Conceito-chave
Propagação até o renderer
3

📋 Schemas e registro

O schemas.rs de cada domínio é onde os metadados viram código. ControllerSchema descreve nome, descrição e campos do método; RegisteredController junta schema com handler. O resultado: o transport não precisa saber NADA sobre o domínio.

Esqueleto do schemas.rs

pub fn all_controller_schemas() -> Vec<ControllerSchema> {
    vec![
        ControllerSchema {
            name: "openhuman.cron_create_job".into(),
            description: "Create a scheduled job".into(),
            fields: vec![
                FieldSchema::required("name", TypeSchema::String),
                FieldSchema::required("cron", TypeSchema::String),
            ],
        },
        // ...
    ]
}

pub fn all_registered_controllers() -> Vec<RegisteredController> {
    vec![RegisteredController::new("openhuman.cron_create_job", handle_create_job)]
}

fn handle_create_job(params: Map<String, Value>) -> ControllerFuture {
    Box::pin(async move { rpc::create_job(serde_json::from_value(params.into())?).await.into() })
}

💡 Dica prática

Naming convention é rígida: openhuman.<namespace>_<function>. Underscore separa namespace de função; ponto separa app de método. Errar isso quebra autocomplete e a busca em about_app.

Conceito-chave
ControllerSchema declarativo
Conceito-chave
FieldSchema required/optional
Conceito-chave
ControllerFuture = Pin<Box>
Conceito-chave
Wire em src/core/all.rs
4

🌉 core_rpc_relay — por que invoke e não fetch

O core escuta em http://127.0.0.1:<port>/rpc com bearer auth. Você poderia usar fetch() direto do React — e vai funcionar em dev. Mas em produção, com headers customizados (Authorization), o browser dispara CORS preflight (OPTIONS) antes da chamada real. Isso quebra silenciosamente.

✗ fetch direto (ERRADO)

fetch('http://127.0.0.1:9001/rpc', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(payload),
});
// → OPTIONS preflight → CORS error

✓ invoke (CORRETO)

import { invoke } from '@tauri-apps/api/core';

const result = await invoke('core_rpc_relay', {
  method: 'openhuman.cron_create_job',
  params: { name, cron },
});
// → IPC nativo → core, sem CORS

Por que IPC nativo é melhor

  • Token OPENHUMAN_CORE_TOKEN nunca aparece em devtools network panel.
  • Tauri injeta a porta do core em runtime — frontend não precisa adivinhar.
  • Sem preflight, sem CORS, sem MIME games.
  • coreRpcClient embrulha tudo isso — você nunca chama core_rpc_relay direto.
Conceito-chave
invoke contorna CORS
Conceito-chave
core_rpc_token expõe bearer
Conceito-chave
Porta dinâmica por launch
Conceito-chave
coreRpcClient é o wrapper
5

📡 Event bus — broadcast tipado

RPC é 1-pra-1, request/response. Mas muito do core é reativo: o cron disparou, agora todo mundo que se interessa precisa saber. Para isso existe o event bus singleton em src/core/event_bus/ — pub/sub tipado sobre tokio::broadcast.

1

Define o evento

src/core/event_bus/events.rs

pub enum DomainEvent {
    CronFired { job_id: String, ts: i64 },
    // ...
}
2

Publica de qualquer lugar

publish_global(DomainEvent::CronFired { job_id, ts }).await;
3

Subscriber implementa EventHandler

impl EventHandler for WebhookRequestSubscriber {
    fn name(&self) -> &str { "webhooks::request" }
    fn domains(&self) -> Option<&[Domain]> { Some(&[Domain::Cron]) }
    async fn handle(&self, ev: &DomainEvent) { /* ... */ }
}
4

Registra no startup, guarda o handle

let _sub: SubscriptionHandle = subscribe_global(handler).await;
// drop = cancela. RAII.

💡 Filtre por domínio

Implementar domains() evita que seu handler seja invocado para TODO evento do sistema. Crons em Memory, mensagens em Channel — o bus despacha milhares por segundo; sem filtro, você processa lixo.

Conceito-chave
DomainEvent #[non_exhaustive]
Conceito-chave
tokio::broadcast por baixo
Conceito-chave
SubscriptionHandle RAII
Conceito-chave
domain() filter obrigatório
6

🎯 NativeRegistry — request/response tipado interno

Tem casos em que dois módulos precisam conversar 1-pra-1, in-process, com handles vivos: passar um mpsc::Sender, um Arc<Mutex<T>>, um oneshot::Sender. JSON-RPC não serve — esses tipos não são Serialize. Para isso existe o NativeRegistry.

Padrão de uso

// 1. Define request/response no domínio. Send + 'static, NÃO Serialize.
pub struct StartScannerReq {
    pub account_id: String,
    pub events_tx: mpsc::Sender<ScanEvent>,
}
pub struct StartScannerResp { pub handle: Arc<ScannerHandle> }

// 2. Registra no startup, keyed por method string
register_native_global(
    "webview_accounts.start_scanner",
    handler,
).await;

// 3. Chama de qualquer outro módulo
let resp: StartScannerResp = request_native_global(
    "webview_accounts.start_scanner",
    StartScannerReq { account_id, events_tx },
).await?;

✓ Use NativeRegistry quando

  • Precisa passar canais Tokio entre domínios
  • Trabalho é estritamente interno (não exposto ao renderer)
  • Quer testes plugáveis (sobrescrever método)

✗ NÃO use quando

  • Quer expor ao TypeScript — use controller registry
  • Muitos subscribers escutando — use event bus broadcast
  • Quer durabilidade ou retry — não tem fila persistente

🧪 Tip: override em testes

Re-registrar o mesmo método via register_native_global substitui o handler antigo. É como você mocka dependências de um domínio em testes sem mexer no código de produção. Para isolamento total, construa um NativeRegistry::new() fresco.

Conceito-chave
Zero serialização
Conceito-chave
Send + 'static
Conceito-chave
Keyed por method string
Conceito-chave
NativeRequestError

Resumo do módulo

Controller pattern — rpc.rs + schemas.rs por domínio, wire em src/core/all.rs.
RpcOutcome<T> — Ok/Err uniforme, sempre serializável.
Schemas declarativos — transport não toca em domínio.
core_rpc_relay — invoke contorna CORS preflight.
Event bus — pub/sub tipado, broadcast desacoplado.
NativeRegistry — 1-pra-1 interno com canais e Arcs.

Próximo módulo:

4.2 — 🖼️ CEF e webview accounts