6 pontos por GN⁺ 2025-09-23 | 1 comentários | Compartilhar no WhatsApp
  • Cap'n Web é um novo protocolo de RPC implementado em TypeScript, otimizado para o ambiente web e capaz de rodar em vários runtimes JavaScript
  • Sem schemas nem boilerplate incômodo, oferece serialização baseada em JSON e um formato de dados legível por humanos
  • Com um modelo baseado em object-capability, permite chamadas bidirecionais, passagem de referências de funções e objetos, promise pipelining e implementação de padrões de segurança
  • Suporta diversos ambientes de rede, como WebSocket, HTTP e postMessage, e é open source leve, com menos de 10kB
  • Além de resolver o problema de waterfall semelhante ao GraphQL, também permite modelar RPC de forma natural, como em APIs JavaScript comuns

O que é o Cap'n Web

  • Cap'n Web é um sistema open source de RPC (protocolo) baseado em TypeScript desenvolvido pela Cloudflare
  • Foi inspirado no Cap'n Proto, mas funciona sem definição de schema separada e adota uma forma de serialização amigável para humanos usando JSON
  • É integrado ao TypeScript, melhorando a experiência do desenvolvedor com autocomplete e checagem de tipos; a validação de tipos em runtime pode ser tratada separadamente, como com type guards
  • Suporta protocolos de rede como HTTP, WebSocket e postMessage e roda nos principais navegadores, Cloudflare Workers, Node.js e outros
  • Tem estrutura leve, sem dependências, e fica com menos de 10kB após minify + gzip

O modelo object-capability (OCap) do Cap'n Web

  • Adota um modelo baseado em object-capability, permitindo expressar mais coisas do que sistemas de RPC tradicionais
    • Chamadas bidirecionais: cliente e servidor podem chamar funções um do outro
    • Passagem de referências de funções e objetos: ao enviar uma função ou objeto via RPC, o outro lado recebe um stub e, ao chamar, a execução acontece no local de origem
    • Promise Pipelining: ao encadear vários RPCs, tudo pode ser processado com uma única ida e volta de rede
    • Padrões de segurança: é possível implementar naturalmente controles de segurança como autorização e gerenciamento de sessão

Uso básico

  • Exemplo de cliente

    import { newWebSocketRpcSession } from "capnweb"  
    let api = newWebSocketRpcSession("wss://example.com/api")  
    let result = await api.hello("World")  
    console.log(result)  
    
  • Exemplo de servidor (baseado em Cloudflare Worker)

    import { RpcTarget, newWorkersRpcResponse } from "capnweb"  
    class MyApiServer extends RpcTarget {  
      hello(name) {  
        return `Hello, ${name}!`  
      }  
    }  
    export default {  
      fetch(request, env, ctx) {  
        let url = new URL(request.url)  
        if (url.pathname === "/api") {  
          return newWorkersRpcResponse(request, new MyApiServer())  
        }  
        return new Response("Not found", {status: 404})  
      }  
    }  
    
  • É simples adicionar métodos à API, passar funções de callback do cliente e definir/aplicar interfaces TypeScript

O que é RPC e quais são as características do Cap'n Web

  • RPC (Remote Procedure Call) é um conceito que permite a dois programas em rede se comunicarem como se estivessem fazendo chamadas de função
  • Diferente dos protocolos HTTP/REST tradicionais, RPC oferece uma abstração de chamada de função, permitindo escrever código alinhado à forma como os desenvolvedores pensam
  • O Cap'n Web combina bem com o fluxo do JavaScript moderno, incluindo async/await, Promise e suporte a Exception
  • Diferente das controvérsias históricas sobre RPC (chamadas síncronas, erros de rede), nos ambientes JS modernos ele pode ser usado de forma mais segura e eficiente

Cenários de uso do Cap'n Web

  • É útil em qualquer ambiente que exija comunicação de rede entre duas aplicações JavaScript
    • Cliente-servidor, chamadas entre microsserviços etc.
    • É especialmente adequado para webapps de colaboração em tempo real e interações que atravessam fronteiras de segurança complexas
  • Ainda está em fase experimental, sendo mais útil para desenvolvedores abertos à adoção de tecnologias recentes

Vários recursos

Modo de lote HTTP

  • Quando não é necessário manter uma conexão persistente, é possível usar o modo de lote (batch) HTTP para agrupar várias chamadas RPC de uma vez

    import { newHttpBatchRpcSession } from "capnweb"  
    let batch = newHttpBatchRpcSession("https://example.com/api";)  
    let result = await batch.hello("World")  
    console.log(result)  
    
  • Dentro de um mesmo lote, várias chamadas podem ser executadas ao mesmo tempo, com os resultados recebidos em paralelo

    let promise1 = batch.hello("Alice")  
    let promise2 = batch.hello("Bob")  
    let [result1, result2] = await Promise.all([promise1, promise2])  
    

Promise Pipelining (chamadas encadeadas)

  • Suporta o uso do resultado da chamada anterior diretamente como argumento da próxima, sem esperar o resultado anterior

  • Exemplo) a Promise retornada por getMyName() pode ser passada direto para hello(), resolvendo tudo em uma única ida e volta de rede

    let namePromise = batch.getMyName()  
    let result = await batch.hello(namePromise)  
    
  • As Promises do Cap'n Web funcionam como objetos proxy, permitindo encadeamento adicional de métodos sem atraso

    let sessionPromise = batch.authenticate(apiKey)  
    let name = await sessionPromise.whoami()  
    

Segurança: autenticação e object-capability

  • Pelo método authenticate, em caso de sucesso é atribuído um objeto de permissão (sessão), e depois disso as funções podem ser chamadas sem etapas extras de autenticação
  • Diferente de RPCs tradicionais, não é possível falsificar o objeto de sessão, e métodos que exigem permissão não podem ser acessados sem autenticação
  • Isso supera naturalmente limitações estruturais do WebSocket e garante consistência na lógica de autenticação
  • Ao declarar interfaces da API em TypeScript, elas podem ser aplicadas automaticamente entre cliente e servidor, garantindo autocomplete e segurança de tipos

Comparação com GraphQL e diferenciais do Cap'n Web

  • O GraphQL alivia o problema de waterfall do REST, mas exige a adoção de uma nova linguagem, schema e toolchain

  • O Cap'n Web resolve o problema de waterfall apenas com código JavaScript e,

    • com suporte a promise pipelining e referências de objetos, permite modelar de forma natural chamadas aninhadas ou lógica de transações complexas
    let user = api.createUser({ name: "Alice" })  
    let friendRequest = await user.sendFriendRequest("Bob")  
    
  • Pode ser usado de forma parecida com APIs JavaScript, sem a complexidade nem o custo de aprendizado e manutenção do GraphQL

Operações com arrays (array.map etc.) e otimização

  • No Cap'n Web, é possível fazer operações map em cada elemento de um array sem rodadas extras de rede

  • A função de callback do map é executada uma vez no cliente para registrar a operação (record-replay), enviada ao servidor e então processada em lote no lado do servidor

    let friendsWithPhotos = friendsPromise.map(friend => {  
      return {friend, photo: api.getUserPhoto(friend.id)}  
    })  
    let results = await friendsWithPhotos  
    
  • Por meio de uma linguagem específica de domínio (DSL) limitada, a expressão parece uma função JavaScript, mas na prática usa o protocolo do Cap'n Web para otimizar várias chamadas

Estrutura interna do protocolo e fluxo de comunicação

  • Transmite dados estruturados com JSON + pré-processamento especial, com suporte a tipos especiais como arrays e datas
  • Como é um protocolo simétrico, permite comunicação bidirecional sem distinção entre cliente e servidor
  • Cada parte (por exemplo, Alice e Bob) gerencia tabelas de export/import e distingue referências de objetos e funções por ID
  • Por meio de mensagens push/pull e atribuição de IDs de Promise, é possível refletir várias chamadas em uma única rodada de comunicação

Status atual e casos de uso

  • O Cap'n Web ainda é um open source experimental, mas já está sendo usado em serviços reais, como remote bindings do Cloudflare Wrangler
  • Estão previstos mais posts de blog e vários experimentos de frontend
  • É publicado sob licença MIT e qualquer pessoa pode adotá-lo livremente
  • Ir para o repositório no GitHub

1 comentários

 
GN⁺ 2025-09-23
Comentários do Hacker News
  • Tenho duas dúvidas

    1. Tenho curiosidade sobre qual seria a melhor forma de lidar com a implantação de apps em que a semântica de RPC é atualizada. Ou seja, como garantir que cliente e servidor estejam usando a mesma versão do RPC. Protocol Buffers (grpc/avro etc.) tentam resolver isso de forma mais direta
    2. Também tenho curiosidade sobre como é melhor lidar com conexões de rede instáveis. Como a tabela de export/import fica diretamente vinculada a uma conexão WebSocket com estado, imagino que, se a conexão cair, o estado será perdido. Em teoria, cliente/servidor até poderiam fazer cache do estado e restaurá-lo ao reconectar, mas como a tabela pode incluir closures, acho que isso seria difícil de serializar e poderia causar problemas de memória. Queria saber como a equipe pensou sobre isso
      Acho que é um trabalho realmente inovador
      1. É melhor pensar nisso como atualizar uma API JavaScript sem quebrar os chamadores existentes. Desde que você siga as mesmas regras básicas de compatibilidade que valem para chamadas de função locais, pode adicionar novos métodos, argumentos opcionais etc.
      2. Se a conexão cair, é preciso reconectar e reconstruir os objetos desde o início. Em um app React real, o stub RPC principal é passado como argumento para o componente de topo. Esse componente cria vários objetos derivados e os passa para os filhos. Se a conexão cair, um novo stub é criado e repassado ao componente de topo. Isso dispara um re-render, como qualquer outra mudança de estado, e todos os filhos buscam novamente os objetos derivados de que precisam
        Se houver um objeto de assinatura (subscription) com callback, a API precisa ser desenhada para permitir indicar a “última mensagem vista” no momento da inicialização. Assim, os dados podem continuar a partir dali sem perder nada no meio
        Acho que seria bom organizar uma série de posts de blog sobre esse tipo de padrão de design
  • A seção sobre como resolveram o problema dos arrays é realmente interessante e ao mesmo tempo um pouco assustadora link do blog
    No caso de .map(), não se envia código JavaScript diretamente para o servidor, mas algo parecido com “código”, usando uma linguagem de domínio específico (DSL) limitada. Do lado do cliente, o callback é executado uma vez com um valor placeholder, e seu comportamento é rastreado no estilo record-replay para então enviar um conjunto de instruções ao servidor. No servidor, essas instruções são recebidas e executadas para cada membro do array.
    Ou seja, o desenvolvedor simplesmente usou métodos JS, mas por trás entra um truque que converte isso para uma DSL restrita. O callback só pode funcionar de forma síncrona, e await não é possível. Em vez disso, só é permitido promise pipelining, para que todo o processo seja capturado e enviado ao servidor, que pode reexecutá-lo quando necessário

    • Em C# existem expression trees para lidar com esse tipo de problema. O Entity Framework usa isso ao receber lambdas e convertê-las em consultas SQL. Ou seja, dá para escanear ou transformar o código sem executá-lo
      Por exemplo, db.People.Where(p => p.Name == "Joe") faz com que Where não receba a função predicate em si, mas uma expression, então ele pode inspecionar o código recebido, verificar se o campo Name corresponde a "Joe" e convertê-lo em uma cláusula SQL WHERE
      Como JavaScript não tem esse mecanismo, a solução é imitar isso inserindo valores placeholder e registrando passo a passo como o código se comporta

    • Recentemente, ao criar a DSL de consultas do Tanstack DB, também usei esse truque de record-replay link do guia. Passa-se um objeto RefProxy para os callbacks de where/select/join, e rastreia-se quais props/operações acontecem nesse objeto.
      Como em JS não dá para interceptar diretamente operadores comuns (==, > etc.), criam-se pequenas funções rastreáveis como eq/gt/not, que são executadas só uma vez no callback para capturar a expressão encadeada e transformá-la em IR
      Curiosamente, também conseguiram rastrear o operador spread do JS
      Kenton, fico curioso se esse conceito também poderia ser adicionado ao capnweb com operadores falsos (eq, gt, in etc.) para incluir recursos de rastreamento remoto

    • Parece que condicionais são proibidos (como as regras de hooks do React), e fiquei curioso sobre como implementam esse tipo de restrição

  • Esse projeto é interessante
    Tem aspectos muito parecidos com bibliotecas de compiladores de ML (TensorFlow 1, JAX jit, PyTorch compile etc.). Elas constroem um grafo de operações via tracing, depois compilam isso ou transformam para executar em uma VM
    A ideia agora é usar uma linguagem dinâmica como frontend, escondendo a transformação de AST na linguagem de script existente em vez de definir uma nova DSL
    Em ML, atrasa-se a execução de kernels de GPU/linalg para fundi-los; em um RPC como Cap'n Web, dá para adiar requisições de rede para juntar várias network calls
    No fim, a chave é separar instruction plane e data plane, e até uma CPU única em escala minúscula já tem uma estrutura de sistema distribuído (separação entre cache de instruções e cache de dados)
    No Cap'n Web, o próprio grafo de RPC faz o papel de instrução
    Acho esse padrão realmente fascinante, embora também dê a sensação de uma pilha infinita (compiler em cima de interpreter, interpreter em cima de compiler...). Parece outra versão do padrão lispy de code is data, data is code. Dá a impressão de que existe uma história ainda mais profunda por trás disso

    • Concordo totalmente — é muito elegante enxergar isso como uma abstração universal
      Linguagens dinâmicas agora estão virando o frontend de novas DSLs, mas sem definir nova sintaxe; em vez disso, a geração de AST é incorporada ao script
      Acho que o TypeScript é um divisor de águas aqui. Ele permite combinar a flexibilidade de runtime do JavaScript (como o uso esperto de Proxy no Cap'n Web) com segurança de tipos
      Ultimamente estou obcecado com esse conceito no mundo de ORM. A maioria dos ORMs é serial e eager, então só dá para mexer imediatamente antes de executar a query
      Acho que um ORM realmente composable precisa funcionar como um compilador: definir em TypeScript uma DSL totalmente type-safe sobre SQL, construir a AST da query e só no final compilar para SQL
      O Typegres que estou desenvolvendo segue exatamente essa ideia. Se esse padrão te interessa, vale a pena dar uma olhada
  • O problema central de bibliotecas RPC é que elas tentam esconder onde e como os round-trips acontecem
    Basta olhar para o .map() de array do Cap'n Web para ver como fica difícil saber onde de fato ocorre um round-trip de rede.
    Acho que isso não é uma “funcionalidade”, mas sim um “bug” — ao ler o código, o comportamento deveria ficar imediatamente claro, e esconder isso não parece desejável
    link de referência

    • O round-trip acontece quando se usa await
      Promise pipelining permite encadear várias instruções sem await, então não há ida e volta extra pela rede no meio. No final, um único await resolve tudo
  • Se você já usou gRPC e web, sabe o quanto é doloroso aplicar Protobuf na web
    A simplicidade do Cap'n Web é realmente ótima documentação do capnproto
    Ao contrário do Cap'n Proto, o Cap'n Web não tem schema nenhum. Há pouquíssimo boilerplate desnecessário, então a sensação é bem parecida com o RPC nativo em JavaScript do Cloudflare Workers
    referência no github

  • Vi a nova biblioteca do kentonv e vim correndo
    Olhando o código no GitHub, me surpreendi com o fato de ele ser inesperadamente pequeno. Fiquei me perguntando se isso é realmente tudo
    Em teoria, não parece que seria tão difícil portar o lado do servidor para outras linguagens, e isso me deu vontade de usar com um servidor Elixir e um frontend JS/TS
    Também parece divertido pedir para um LLM fazer esse tipo de port entre linguagens. Fiquei curioso se este repositório inclui código gerado por LLM. Alguns meses atrás vi que o kentonv tinha comentado sobre um POC criado por IA (e revisado por humanos)

    • Alguns testes usaram geração por LLM, mas a biblioteca em si, de jeito nenhum
      No estado atual, acho que seria difícil para um LLM construir essa biblioteca. A estrutura interna foi desenhada como um quebra-cabeça muito refinado, em que tudo se encaixa
      Levei mais tempo pensando no design do que escrevendo o código em si
      Isso é completamente diferente da biblioteca workers-oauth-provider, que implementa de forma original uma spec já bem conhecida
      A estrutura do código talvez seja fácil de portar para linguagens dinâmicas, como Python, mas acho difícil em linguagens de tipagem estática. Há muita dependência de tipos arbitrários de objeto
  • Também há semelhanças com OCapN, junto com diferenças importantes referência
    Ambos suportam transferências de capability, promise pipelining e um modelo sem schema
    O Cap'n Web não tem capabilities out-of-band como o sturdyref do OCapN (uma URI restaurável). Por isso, imagino que autenticação por chave de API seja necessária. Um sturdyref é basicamente um token impossível de adivinhar; quem o possui ganha acesso ao endpoint correspondente
    Além disso, o Cap'n Web não tem a função de handoff em três partes, em que Alice apresenta Bob para Carol. Isso é essencial para apps distribuídos, então o Cap'n Web parece mais próximo de um uso cliente-servidor tradicional no estilo SaaS, só que aproveitando algumas características de ocap

    • Eu gostaria de adicionar suporte a 3PH mais tarde, mas nesta versão inicial a prioridade maior foi manter o foco na comunicação entre navegador e servidor web
      Quanto a SturdyRef, como a forma de restauração varia conforme a plataforma, acho que faz mais sentido implementá-lo por plataforma, e não no nível do protocolo RPC
      Por exemplo, no Cloudflare Workers em breve será possível persistir capabilities no storage do Durable Object, mas a forma de implementação é específica da plataforma de workers
      O Sandstorm também tem capability persistente, mas limitada a serviços internos
      Por isso o Cap’n Proto removeu completamente a noção de capability persistente, e o conceito mais próximo nos padrões web acaba sendo o OAuth
      Dá até para imaginar um sturdyref baseado em refresh token do OAuth, mas isso não seria uma estrutura utilizável em qualquer plataforma
  • Pelo que vi rapidamente, esse sistema parece exigir (ou incentivar) armazenar de forma stateful no servidor a tabela de import/export ou o estado dos objetos
    Em um RPC tradicional, todas as chamadas entram pelo topo, passando key e afins em cada request, então não há problema mesmo que as requisições sejam distribuídas entre vários servidores. No Cap’n Web, parece não ser assim
    Fiquei curioso se é possível serializar a tabela e armazená-la em um banco para permitir a mesma distribuição entre servidores, ou se isso necessariamente exige afinidade com o servidor ou uma estrutura como Durable Objects

    • O estado só é mantido dentro de uma única sessão RPC
      Se você usar WebSocket, o estado continua vivo enquanto a conexão WebSocket permanecer ativa
      Se usar transporte por lote HTTP, a sessão fica limitada à duração de uma única requisição HTTP inteira, e todas as chamadas são processadas de uma vez dentro dela
      Portanto, o Cap’n Web não precisa manter estado ao longo de várias requisições/conexões HTTP
      Ainda assim, se o design fizer com que perder a sessão signifique perder todas as capabilities, isso é um problema e deve ser evitado. É preciso conseguir restaurar as capabilities a qualquer momento depois de restabelecer a conexão

    • Pelo que li na documentação, parece ser uma arquitetura com afinidade via websocket
      O batching por HTTP envia todas as requisições de uma vez e espera a resposta
      Esse tipo de abordagem dificulta o balanceamento de carga. Se houver muitos clientes de chat, por exemplo, as conexões podem acabar se concentrando em um servidor específico, o que pode sobrecarregá-lo
      Escalar para cima ou para baixo também fica trabalhoso. Manter conexões longas ao mesmo tempo em que várias requisições estão sendo processadas em paralelo é algo bem difícil de administrar
      Outra questão é que, se o cliente continuar só enviando eventos push sem nunca consumir as respostas, o servidor terá de manter essas respostas em memória, então me parece que isso facilitaria ataques de DDoS

    • Pelo que lembro de ter lido na documentação do Cap'n Proto, servidor e cliente podem trocar peer stubs
      Se o servidor C receber, via cliente B, um stub criado em A, então C também pode chamar A diretamente

  • RPC is often accused of committing many of the fallacies of distributed computing.

But this reputation is outdated. When RPC was first invented some 40 years ago, async programming barely existed. We did not have Promises, much less async and await. Essa parte me deixou confuso. Se a premissa central do RPC depende tanto de uma linguagem específica ou de um modelo de concorrência, como isso pode ser considerado um protocolo?

  • “RPC” originalmente era um paradigma de programação que tentava fazer uma chamada remota parecer indistinguível de uma chamada de função interna
    Na prática, isso exige wire protocol, bibliotecas cliente/servidor etc.
    Mais recentemente, essa percepção mudou bastante, e hoje domina um modelo parecido com endpoints REST, mas com assinaturas de função
    Com recursos de linguagem como Future, Optional e outros, ficou possível distinguir com clareza características como “essa operação pode atrasar” ou “isso pode falhar”
    Nos RPCs antigos, tudo isso ficava escondido

  • Fiquei na dúvida sobre o que isso quer dizer. Programação assíncrona existe em várias linguagens. Já usei JavaScript, C++, Python, Rust, C# e quase todas têm isso
    A questão é que os primeiros sistemas de RPC bloqueavam a thread de chamada enquanto a requisição de rede estava em andamento, e isso era um design realmente ruim; hoje em dia, por isso, o assíncrono virou algo natural

  • Estou bem animado por ver que o Cap'n Web existe por conta própria e não está preso só a produtos da Cloudflare
    Lendo esta parte da documentação, fiquei com uma dúvida

    as of this writing, the feature set is not exactly the same between the two. We aim to fix this over time, by adding missing features to both sides until they match. Quando os dois lados chegarem à paridade de funcionalidades, a intenção é continuar mantendo essa sincronização depois disso, ou o Cap'n Web eventualmente vai ficar atrás do Cloudflare Workers? Também tenho curiosidade sobre qual seria esse intervalo

    • A ideia é manter os dois produtos sincronizados em praticamente tudo que seja uma funcionalidade relevante em comum
      Na verdade, acho que o Cap'n Web pode até avançar mais rápido que o worker RPC (de fato, o recurso de pipeline já está na frente)
      Como a estrutura do Cap'n Web é muito mais simples, novos experimentos de funcionalidades provavelmente acontecerão primeiro nele