Cap'n Web, un nuevo sistema RPC para navegadores y servidores web
(blog.cloudflare.com)- Cap'n Web es un nuevo protocolo RPC implementado en TypeScript, optimizado para entornos web y capaz de funcionar en varios runtimes de JavaScript
- Ofrece serialización basada en JSON y un formato de datos legible para humanos, sin esquemas ni boilerplate engorroso
- Mediante un modelo basado en capacidades de objetos, permite llamadas bidireccionales, paso de referencias a funciones y objetos, pipeline de promesas e implementación de patrones de seguridad
- Soporta distintos entornos de red como WebSocket, HTTP y postMessage, y es un proyecto open source liviano de menos de 10kB
- Además de resolver el problema de waterfall similar a GraphQL, permite modelar RPC de forma natural, como una API normal de JavaScript
Qué es Cap'n Web
- Cap'n Web es un sistema open source de RPC (protocolo) basado en TypeScript desarrollado por Cloudflare
- Está inspirado en Cap'n Proto, pero funciona sin definir esquemas por separado y adopta una forma de serialización amigable para humanos usando JSON
- Se integra con TypeScript para mejorar la experiencia de desarrollo con autocompletado, verificación de tipos, etc.; la validación de tipos en runtime puede manejarse por separado (por ejemplo, con type guards)
- Soporta protocolos de red como HTTP, WebSocket y postMessage, y funciona en navegadores principales, Cloudflare Workers, Node.js y más
- Tiene una estructura liviana sin dependencias y se distribuye con menos de 10kB tras minify + gzip
El modelo basado en capacidades de objetos (OCap) de Cap'n Web
- Adopta un modelo basado en capacidades de objetos (object-capability) que permite expresar más casos que los sistemas RPC tradicionales
- Llamadas bidireccionales: el cliente y el servidor pueden invocar funciones entre sí
- Paso de referencias a funciones y objetos: si una función u objeto se envía por RPC, la contraparte recibe un stub y, al invocarlo, se ejecuta en su ubicación original
- Promise Pipelining: al encadenar varios RPC, puede resolverse con un solo viaje de ida y vuelta por la red
- Patrones de seguridad: permite implementar de forma natural controles de seguridad como autorización y manejo de sesiones
Uso básico
-
Ejemplo de cliente
import { newWebSocketRpcSession } from "capnweb" let api = newWebSocketRpcSession("wss://example.com/api") let result = await api.hello("World") console.log(result) -
Ejemplo de servidor (basado en 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}) } } -
Es fácil agregar métodos a la API, pasar funciones callback del cliente y definir/aplicar interfaces de TypeScript
Qué es RPC y qué distingue a Cap'n Web
- RPC (Remote Procedure Call) es un concepto que permite que dos programas en red se comuniquen como si estuvieran haciendo llamadas a funciones
- A diferencia de los protocolos HTTP/REST tradicionales, RPC permite escribir código con una abstracción de llamada a función que coincide con la forma de pensar del desarrollador
- Cap'n Web encaja bien con el flujo del JavaScript moderno, con soporte para async/await, Promise y Exception
- A diferencia de las controversias históricas sobre RPC (llamadas sincrónicas, errores de red), en el entorno moderno de JS puede usarse de forma más segura y eficiente
Escenarios de uso de Cap'n Web
- Es útil en cualquier entorno donde se necesite comunicación de red entre dos aplicaciones JavaScript
- Cliente-servidor, llamadas entre microservicios, etc.
- Es especialmente adecuado para web apps de colaboración en tiempo real e interacciones que cruzan límites de seguridad complejos
- Está en etapa experimental, por lo que resulta aún más útil para desarrolladores abiertos a adoptar tecnología reciente
Funciones diversas
Modo batch sobre HTTP
-
Cuando no se necesita una conexión persistente, se pueden agrupar varias llamadas RPC de una sola vez con el modo batch de HTTP
import { newHttpBatchRpcSession } from "capnweb" let batch = newHttpBatchRpcSession("https://example.com/api") let result = await batch.hello("World") console.log(result) -
Dentro de un mismo batch, varias llamadas pueden ejecutarse al mismo tiempo y recibir sus resultados en paralelo
let promise1 = batch.hello("Alice") let promise2 = batch.hello("Bob") let [result1, result2] = await Promise.all([promise1, promise2])
Promise Pipelining (llamadas encadenadas)
-
Soporta usar el resultado directamente como argumento de la siguiente llamada sin esperar a que termine la llamada anterior
-
Ejemplo: pasar directamente la Promise del resultado de
getMyName()ahello()para resolverlo en un solo viaje de redlet namePromise = batch.getMyName() let result = await batch.hello(namePromise) -
Las Promise de Cap'n Web funcionan como objetos proxy, de modo que al invocar métodos adicionales pueden encadenarse sin demoras
let sessionPromise = batch.authenticate(apiKey) let name = await sessionPromise.whoami()
Seguridad: autenticación y capacidades de objetos
- Mediante el método
authenticate, al tener éxito se asigna un objeto de permisos (sesión) y luego es posible invocar funciones sin pasos extra de autenticación - A diferencia de otros RPC, el objeto de sesión no puede falsificarse, y no se puede acceder a métodos que requieren permisos sin autenticación
- Supera de forma natural las limitaciones estructurales de WebSocket y garantiza consistencia en la lógica de autenticación
- Al declarar interfaces de API en TypeScript, pueden aplicarse automáticamente entre cliente y servidor, obteniendo autocompletado y seguridad de tipos
Comparación con GraphQL y diferencias de Cap'n Web
-
GraphQL alivia el problema de waterfall de REST, pero requiere introducir un nuevo lenguaje, esquemas y toolchain
-
Cap'n Web resuelve el problema de waterfall usando solo código JavaScript y,
- con soporte para pipeline de promesas y referencias a objetos, permite modelar de forma natural llamadas anidadas o lógica de transacciones compuestas
let user = api.createUser({ name: "Alice" }) let friendRequest = await user.sendFriendRequest("Bob") -
Puede usarse de manera similar a una API de JavaScript, sin la complejidad ni el costo de aprendizaje y administración de GraphQL
Operaciones sobre arreglos (array.map, etc.) y optimización
-
En Cap'n Web es posible hacer operaciones map sobre cada elemento de un arreglo sin viajes adicionales por la red
-
La función callback de
mapse ejecuta una vez en el cliente para registrar la operación (record-replay), se envía al servidor y allí se procesa en bloquelet friendsWithPhotos = friendsPromise.map(friend => { return {friend, photo: api.getUserPhoto(friend.id)} }) let results = await friendsWithPhotos -
A través de un lenguaje específico de dominio (DSL) limitado, se expresa como si fuera una función de JavaScript, pero en realidad usa el protocolo de Cap'n Web para optimizar múltiples llamadas
Estructura interna del protocolo y flujo de comunicación
- Transmite datos estructurados mediante JSON + preprocesamiento especial, con soporte para tipos especiales como arreglos y fechas
- Como protocolo simétrico, permite comunicación bidireccional sin distinción entre cliente y servidor
- Cada parte (por ejemplo, Alice y Bob) administra tablas de export/import y distingue referencias a objetos y funciones mediante IDs
- Mediante mensajes push/pull y asignación de IDs de Promise, es posible reflejar múltiples llamadas en un solo round trip
Estado actual y casos de uso
- Cap'n Web sigue siendo un open source experimental, pero ya se usa en servicios reales como los remote bindings de Cloudflare Wrangler
- Se prevén más publicaciones de blog y varios experimentos de frontend
- Se publica bajo licencia MIT y cualquiera puede adoptarlo libremente
- Ir al repositorio de GitHub
1 comentarios
Comentarios de Hacker News
Tengo dos preguntas
grpc/avro, etc.) intentan resolver este problema de forma directa.Me parece un trabajo realmente innovador
Si tienes un objeto de suscripción (
subscription) con callbacks, la API debe diseñarse para que al iniciar puedas indicar el “último mensaje visto”. Así puedes retomar inmediatamente desde ahí y no perder nada en medio.Creo que valdría la pena armar una serie de posts de blog sobre este tipo de patrones de diseño.
La sección sobre cómo resolvieron el problema de los arreglos es realmente interesante y al mismo tiempo un poco aterradora enlace al blog
En el caso de
.map(), en realidad no se envía código JavaScript directamente al servidor, pero sí se manda algo parecido a “código”, usando un lenguaje específico de dominio (DSL) limitado. Del lado del cliente, se ejecuta una vez el callback con un valor placeholder, se rastrea ese comportamiento con una técnica de record-replay y luego se envía un instruction set al servidor. Del lado del servidor, se reciben esas instrucciones y se ejecutan para cada miembro del arreglo.O sea, la persona desarrolladora solo usó métodos de JS, pero en realidad se aplica el truco de convertir eso a un DSL estrecho. El callback solo puede ejecutarse de forma síncrona y no se puede usar
await. En cambio, solo se permite promise pipelining, para capturar todo ese proceso y enviarlo al servidor, donde puede reejecutarse cuando haga falta.C# tiene expression trees para manejar este tipo de problema. Entity Framework lo aprovecha cuando recibe lambdas y las convierte en consultas SQL. Es decir, se puede usar escaneando o transformando el código sin ejecutarlo.
Por ejemplo,
db.People.Where(p => p.Name == "Joe")hace queWhereno reciba realmente una función predicate, sino una expresión, por lo que puede inspeccionar el código recibido, verificar que el campoNamecoincida con"Joe"y transformarlo en una cláusula SQLWHERE.Como JavaScript no tiene un mecanismo así, lo imitan metiendo valores placeholder y registrando paso a paso cómo se comporta.
Hace poco, al construir el query DSL de Tanstack DB, también usaron este truco de record-replay enlace a la guía. Pasan objetos
RefProxya los callbacks dewhere/select/joiny rastrean qué props u operaciones ocurren sobre esos objetos.Como en JS no se pueden interceptar directamente operadores comunes (
==,>, etc.), crean pequeñas funciones trazables comoeq/gt/not, ejecutan el callback una sola vez para capturar la expresión conectada y la convierten en IR.Curiosamente, también lograron rastrear el operador spread de JS.
Kenton, me pregunto si sería posible agregar este concepto a capnweb con operadores falsos (
eq,gt,in, etc.) para incorporar tracing remoto.Parece que los condicionales están prohibidos (como las reglas de hooks en React), y me pregunto cómo implementan esa restricción.
Este proyecto me parece interesante.
Tiene aspectos muy parecidos a las bibliotecas de compiladores de ML (
TensorFlow 1,JAX jit,PyTorch compile, etc.). Construye un grafo de operaciones por tracing, luego lo compila o lo transforma para ejecutarlo en una VM.En este momento, en vez de definir un DSL nuevo usando un lenguaje dinámico como frontend, esconde la transformación del AST dentro del lenguaje de scripting ya existente.
En ML se retrasa la ejecución de kernels de GPU/álgebra lineal para fusionarlos, y en un RPC como Cap'n Web se pueden retrasar las solicitudes de red para fusionar varias network calls.
Al final, la clave está en separar el plano de instrucciones del plano de datos, y hasta una CPU única a muy pequeña escala tiene una estructura de sistema distribuido (separación entre caché de instrucciones y de datos).
En Cap'n Web, el propio grafo de RPC cumple el papel de instrucción.
Este patrón me parece realmente fascinante, aunque también da la sensación de una estructura de capas que se repite infinitamente (compiler encima de interpreter, interpreter encima de compiler...). Se siente como otra versión del patrón lispy de code is data, data is code. Siento que hay una historia más profunda detrás de todo esto.
Los lenguajes dinámicos ahora se están volviendo el frontend de nuevos DSL, pero sin definir una sintaxis nueva: se incrusta la generación de AST dentro del propio script.
Creo que TypeScript es un game changer aquí. Permite tener al mismo tiempo la flexibilidad en runtime de JavaScript (como el uso ingenioso de
Proxyen Cap'n Web) y seguridad de tipos.Últimamente estoy obsesionado con esta idea en el mundo de los ORM. La mayoría de los ORM son seriales y eager, así que solo puedes manipular cosas justo antes de ejecutar la consulta.
Creo que un ORM realmente composable debería funcionar como un compilador: definir un DSL completamente type-safe sobre SQL en TypeScript, construir un AST de consultas y compilarlo a SQL solo al final.
Lo que estoy desarrollando en Typegres sigue exactamente esa idea. Si te interesa este patrón, puede servirte como referencia.
El problema central de las bibliotecas RPC es que intentan ocultar dónde y cómo ocurren los round-trips.
Incluso viendo
.map()de arreglos en Cap'n Web, es difícil saber dónde ocurren realmente los network round-trips.Creo que eso no es una “feature”, sino más bien un “bug”: al ver el código, deberías poder entender de inmediato cómo se comporta, y ocultarlo no me parece deseable.
Enlace de referencia
await.Con promise pipelining puedes encadenar varios statements sin
await, así que no hay viajes de red extra en medio. Al final haces un soloawaity eso es todo.Si has trabajado con gRPC y la web, sabes lo doloroso que es aplicar Protobuf a la web.
Me encanta la simplicidad de Cap'n Web documentación de capnproto
A diferencia de Cap'n Proto, Cap'n Web no tiene esquema en absoluto. Como casi no hay boilerplate innecesario, se siente mucho como un RPC nativo de JavaScript para Cloudflare Workers.
referencia en github
Vi la nueva biblioteca de kentonv y vine corriendo.
Al mirar el código en GitHub, me sorprendió que el tamaño fuera tan pequeño. Me pregunto si de verdad eso es todo.
En teoría, no parecería tan difícil portar la parte del servidor a otro lenguaje, y me dieron ganas de probarlo con un servidor en Elixir y un frontend JS/TS.
También sería divertido pedirle a un LLM que haga ese port de lenguaje. Me pregunto si en este repo entró código generado por LLM. Hace unos meses vi que kentonv comentó que había hecho un POC generado por IA (revisado por humanos).
En este momento, creo que a un LLM le habría costado crear esta biblioteca. La estructura interna está diseñada como un rompecabezas muy preciso en el que todo encaja.
De hecho, tomó más tiempo pensar el diseño que escribir el código.
Es totalmente distinto de una biblioteca como workers-oauth-provider, que implementa de forma novedosa una spec bien conocida.
La estructura del código podría ser fácil de portar a lenguajes dinámicos como Python, pero creo que sería difícil llevarla a lenguajes con tipos estáticos. Depende mucho de tipos de objetos arbitrarios.
Tiene similitudes y diferencias importantes con OCapN referencia
Ambos soportan capability transfer, promise pipelining y un modelo sin esquema.
Cap'n Web no tiene capabilities fuera de banda como
sturdyrefde OCapN (URIs restaurables). Por eso supongo que necesita autenticación con API key. Unsturdyrefes básicamente un token imposible de adivinar; si lo tienes, obtienes acceso a ese endpoint.Además, Cap'n Web no tiene la función de handoff de tres partes donde Alice presenta a Bob con Carol. Eso es esencial en apps distribuidas, así que Cap'n Web se siente más cercano a un caso de uso cliente-servidor estilo SaaS tradicional, pero con rasgos de ocap.
Creo que
SturdyRefdebería implementarse según cada plataforma, porque la forma de restauración cambia entre una y otra, más que resolverse a nivel del protocolo RPC.Por ejemplo, en Cloudflare Workers pronto será posible persistir capabilities desde el storage de Durable Objects, pero la forma de implementarlo está muy atada a la plataforma de Workers.
Sandstorm también tiene persistent capability, pero limitado a servicios internos.
Por eso en Cap’n Proto se eliminó por completo el concepto de persistent capability, y lo más parecido que existe en los estándares web es OAuth.
Se podría imaginar una definición de
sturdyrefbasada en OAuth refresh tokens, pero no sería una estructura utilizable en cualquier plataforma.Por lo que entendí en una revisión rápida, este sistema parece requerir (o al menos fomentar) que las tablas import/export o el estado de los objetos se guarden de manera stateful del lado del servidor.
En el RPC tradicional, todas las llamadas entran por el nivel superior y cada una lleva su key, etc., así que no importa si las solicitudes se distribuyen entre varios servidores; en Cap’n Web eso no parece ser así.
Me pregunto si sería posible serializar las tablas y guardarlas en una base de datos para seguir distribuyendo el servidor de la misma manera, o si necesariamente exige afinidad con el servidor o una estructura tipo Durable Objects.
El estado solo se conserva dentro de una única sesión RPC.
Si usas WebSocket, el estado vive mientras siga viva la conexión WebSocket.
Si usas transporte por lotes HTTP, la sesión queda limitada a toda la duración de una sola solicitud HTTP y todas las llamadas se procesan de una vez dentro de ella.
Así que Cap’n Web no necesita conservar estado entre múltiples solicitudes o conexiones HTTP.
Aun así, si tu diseño depende de una sesión que al cortarse haga que se pierdan todas las capabilities, entonces es un mal diseño y conviene evitarlo. Debe ser posible restaurar las capabilities en cualquier momento después de reestablecer la conexión.
Leyendo la documentación, parece una estructura que alinea la afinidad mediante WebSocket.
El batching HTTP manda todas las solicitudes de una sola vez y espera la respuesta.
Este enfoque complica el balanceo de carga. Si hay muchos clientes de chat, las conexiones pueden concentrarse en un servidor específico. Eso podría sobrecargar ese servidor.
También vuelve engorroso escalar horizontalmente hacia adentro o hacia afuera. Mantener conexiones largas mientras varias solicitudes se procesan a la vez es muy difícil de administrar.
Otra cosa: si el cliente se dedica a enviar eventos push sin recibir nunca respuestas, el servidor tendría que mantener esas respuestas en memoria, así que me parece que eso facilitaría ataques DDoS.
Según recuerdo de haber leído antes la documentación de Cap'n Proto, el servidor y el cliente pueden intercambiar peer stubs.
Si el servidor C recibe, a través del cliente B, un stub creado por A, entonces C incluso podría invocar a A directamente.
“RPC” originalmente es un paradigma de programación que intenta hacer que una llamada remota se vea indistinguible de una llamada de función local.
En la práctica, para lograr eso se necesitan wire protocols, bibliotecas cliente/servidor, etc.
Últimamente la percepción cambió mucho, y ahora lo más común es una estructura tipo endpoint REST con firmas de función.
Con features del lenguaje como
Future,Optional, etc., se pueden distinguir con claridad propiedades como “esta operación puede demorarse” o “esto puede fallar”.En los RPC del pasado, todas esas propiedades quedaban ocultas.
Me pregunto exactamente a qué se refiere. La programación asíncrona existe en muchos lenguajes. He usado JavaScript, C++, Python, Rust, C# y casi todos tienen algo de eso.
El punto es que los sistemas RPC iniciales bloqueaban el hilo llamador mientras se completaba la solicitud de red, y eso sí era un diseño terrible; por eso hoy se da por hecho que todo debe ser asíncrono.
Me entusiasma mucho que Cap'n Web exista por separado y no esté atado solo a productos de Cloudflare.
Al leer esta parte de la documentación me quedó una duda.
De hecho, creo que Cap'n Web incluso podría adelantarse a worker RPC (de hecho, ya va por delante en funciones de pipeline).
Como la estructura de Cap'n Web es mucho más simple, probablemente primero experimentemos funciones nuevas ahí.