Cap'n Web, um novo sistema de RPC para navegadores e servidores web
(blog.cloudflare.com)- 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 parahello(), resolvendo tudo em uma única ida e volta de redelet 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
mapem 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 servidorlet 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
Comentários do Hacker News
Tenho duas dúvidas
grpc/avroetc.) tentam resolver isso de forma mais diretaAcho que é um trabalho realmente inovador
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 meioAcho 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
awaitnã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árioEm 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 queWherenão receba a função predicate em si, mas uma expression, então ele pode inspecionar o código recebido, verificar se o campoNamecorresponde a"Joe"e convertê-lo em uma cláusula SQLWHEREComo 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
RefProxypara os callbacks dewhere/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 comoeq/gt/not, que são executadas só uma vez no callback para capturar a expressão encadeada e transformá-la em IRCuriosamente, 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,inetc.) para incluir recursos de rastreamento remotoParece 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 compileetc.). Elas constroem um grafo de operações via tracing, depois compilam isso ou transformam para executar em uma VMA 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
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
Proxyno Cap'n Web) com segurança de tiposUltimamente 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
awaitPromise 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 únicoawaitresolve tudoSe 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)
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
sturdyrefdo OCapN (uma URI restaurável). Por isso, imagino que autenticação por chave de API seja necessária. Umsturdyrefé basicamente um token impossível de adivinhar; quem o possui ganha acesso ao endpoint correspondenteAlé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
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 RPCPor 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
sturdyrefbaseado em refresh token do OAuth, mas isso não seria uma estrutura utilizável em qualquer plataformaPelo 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” 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,Optionale 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
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