Cap'n Web, un nouveau système RPC pour navigateurs et serveurs web
(blog.cloudflare.com)- Cap'n Web est un nouveau protocole RPC implémenté en TypeScript, optimisé pour l’environnement web et capable de fonctionner sur plusieurs runtimes JavaScript
- Il fournit une sérialisation basée sur JSON et un format de données lisible par un humain, sans schéma ni boilerplate fastidieux
- Grâce à un modèle fondé sur les object-capabilities, il permet les appels bidirectionnels, le passage de références de fonctions et d’objets, le promise pipelining et l’implémentation de patterns de sécurité
- Il prend en charge divers environnements réseau comme WebSocket, HTTP, postMessage, tout en restant un open source léger de moins de 10 kB
- En plus de résoudre le problème de waterfall similaire à GraphQL, il permet une modélisation RPC naturelle, proche des API JavaScript classiques
Qu’est-ce que Cap'n Web ?
- Cap'n Web est un système open source de protocole RPC basé sur TypeScript développé par Cloudflare
- Il s’inspire de Cap'n Proto, mais fonctionne sans définition de schéma séparée et adopte une sérialisation conviviale pour les humains basée sur JSON
- Il est intégré à TypeScript, ce qui améliore l’expérience développeur avec l’autocomplétion et la vérification de types, tandis que la validation de types à l’exécution peut être gérée séparément (type guards, etc.)
- Il prend en charge des protocoles réseau comme HTTP, WebSocket et postMessage et fonctionne dans les principaux navigateurs, sur Cloudflare Workers, Node.js, etc.
- Sa structure légère sans dépendances permet une taille inférieure à 10 kB une fois minifié + gzip
Le modèle object-capability (OCap) de Cap'n Web
- Il adopte un modèle fondé sur les object-capabilities, qui permet une expression plus riche que les systèmes RPC traditionnels
- Appels bidirectionnels : le client et le serveur peuvent appeler les fonctions l’un de l’autre
- Passage de références de fonctions et d’objets : si une fonction ou un objet est transmis via RPC, l’autre partie reçoit un stub qui exécute l’appel à l’emplacement d’origine
- Promise Pipelining : lorsqu’on enchaîne plusieurs RPC, le traitement se fait en un seul aller-retour réseau
- Patterns de sécurité : il devient naturel d’implémenter des contrôles de sécurité comme l’autorisation et la gestion de session
Utilisation de base
-
Exemple côté client
import { newWebSocketRpcSession } from "capnweb" let api = newWebSocketRpcSession("wss://example.com/api") let result = await api.hello("World") console.log(result) -
Exemple côté serveur (basé sur 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}) } } -
Il est facile d’ajouter des méthodes à l’API, de transmettre une fonction de callback du client et de définir puis appliquer des interfaces TypeScript
Qu’est-ce que le RPC, et quelles sont les spécificités de Cap'n Web ?
- Le RPC (Remote Procedure Call) est un concept qui permet à deux programmes sur un réseau de communiquer comme s’il s’agissait d’appels de fonctions
- Contrairement aux protocoles HTTP/REST traditionnels, le RPC repose sur l’abstraction de l’appel de fonction, ce qui permet d’écrire du code aligné sur la façon de penser des développeurs
- Cap'n Web s’accorde bien avec les flux modernes de JavaScript, notamment grâce à la prise en charge de async/await, Promise et Exception
- Contrairement aux controverses historiques autour du RPC (appels synchrones, erreurs réseau), les environnements JS modernes permettent aujourd’hui une utilisation plus sûre et plus efficace
Cas d’usage de Cap'n Web
- Il est utile dans tout environnement nécessitant une communication réseau entre deux applications JavaScript
- appels client-serveur, communication entre microservices, etc.
- il convient particulièrement aux applications web de collaboration en temps réel et aux interactions franchissant des frontières de sécurité complexes
- Encore au stade expérimental, il sera particulièrement utile aux développeurs ouverts à l’adoption de technologies récentes
Différentes fonctionnalités
Mode batch HTTP
-
Lorsqu’une connexion persistante n’est pas nécessaire, le mode batch HTTP permet de regrouper plusieurs appels RPC et de les traiter en une seule fois
import { newHttpBatchRpcSession } from "capnweb" let batch = newHttpBatchRpcSession("https://example.com/api") let result = await batch.hello("World") console.log(result) -
Plusieurs appels peuvent être exécutés simultanément dans un même batch, avec réception des résultats en parallèle
let promise1 = batch.hello("Alice") let promise2 = batch.hello("Bob") let [result1, result2] = await Promise.all([promise1, promise2])
Promise Pipelining (appels chaînés)
-
Il prend en charge l’utilisation immédiate du résultat comme argument de l’appel suivant, sans attendre le résultat de l’appel précédent
-
Exemple : transmettre directement la Promise renvoyée par
getMyName()àhello()pour tout traiter en un seul aller-retour réseaulet namePromise = batch.getMyName() let result = await batch.hello(namePromise) -
Dans Cap'n Web, les Promise fonctionnent comme des objets proxy, ce qui permet d’enchaîner des appels supplémentaires sans délai
let sessionPromise = batch.authenticate(apiKey) let name = await sessionPromise.whoami()
Sécurité : authentification et object-capabilities
- Via la méthode authenticate, un objet de capacité (session) est attribué en cas de succès, permettant ensuite d’appeler des fonctionnalités sans étape d’authentification supplémentaire
- Contrairement aux RPC classiques, il est impossible de falsifier l’objet de session, et l’accès aux méthodes nécessitant des privilèges est impossible sans authentification
- Cela permet de surmonter naturellement les limites structurelles de WebSocket tout en garantissant la cohérence de la logique d’authentification
- Lorsqu’une interface API est déclarée en TypeScript, elle peut être appliquée automatiquement entre client et serveur, avec autocomplétion et sûreté de type
Comparaison avec GraphQL et différenciation de Cap'n Web
-
GraphQL atténue le problème de waterfall de REST, mais nécessite l’introduction d’un nouveau langage, d’un schéma et d’une toolchain
-
Cap'n Web résout le problème de waterfall avec du simple code JavaScript, et
- grâce au promise pipelining et aux références d’objets, il permet de modéliser naturellement des appels imbriqués ou une logique transactionnelle complexe
let user = api.createUser({ name: "Alice" }) let friendRequest = await user.sendFriendRequest("Bob") -
Il peut être utilisé comme une API JavaScript classique, sans la complexité ni le coût d’apprentissage et de gestion de GraphQL
Opérations sur les tableaux (array.map, etc.) et optimisation
-
Dans Cap'n Web, il est possible d’exécuter des opérations map sur chaque élément d’un tableau sans aller-retour réseau supplémentaire
-
La fonction callback de
mapest exécutée une fois côté client pour enregistrer l’opération (record-replay), puis transmise au serveur pour traitement groupélet friendsWithPhotos = friendsPromise.map(friend => { return {friend, photo: api.getUserPhoto(friend.id)} }) let results = await friendsWithPhotos -
Grâce à un DSL spécialisé et limité, cela s’exprime comme une fonction JavaScript tout en utilisant en réalité le protocole Cap'n Web pour optimiser plusieurs appels
Structure interne du protocole et flux de communication
- Transmission de données structurées via JSON + prétraitement spécial, avec prise en charge de types particuliers comme les tableaux et les dates
- En tant que protocole symétrique, il permet une communication bidirectionnelle sans distinction client/serveur
- Chaque partie (par exemple Alice et Bob) gère des tables d’export/import et distingue les références d’objets et de fonctions via des identifiants
- Grâce aux messages push/pull et à l’attribution d’identifiants de Promise, plusieurs appels peuvent être reflétés dans un seul aller-retour
État actuel et cas d’adoption
- Cap'n Web est encore un open source expérimental, déjà utilisé dans des services réels comme les remote bindings de Cloudflare Wrangler
- D’autres billets de blog et diverses expérimentations frontend sont prévus
- Il est publié sous licence MIT et peut être adopté librement par tous
- Accéder au dépôt GitHub
1 commentaires
Commentaires Hacker News
Deux choses m’intriguent
grpc/avro, etc.) essaient justement de résoudre ce problème de frontJe trouve ce travail vraiment innovant
fetchles sous-objets dont ils ont besoinSi vous avez un objet d’abonnement avec callbacks, l’API doit être conçue pour permettre d’indiquer le « dernier message vu » au démarrage. Ainsi, on peut reprendre immédiatement sans perdre les données intermédiaires
Il faudrait probablement que j’écrive une série de billets de blog sur ce genre de patterns de conception
La section expliquant comment ils ont résolu le problème des tableaux est vraiment fascinante, et en même temps un peu inquiétante lien du blog
Dans le cas de
.map(), on n’envoie pas directement du code JavaScript au serveur, mais quelque chose qui ressemble à du « code », via un DSL limité. Côté client, le callback est exécuté une fois avec une valeur placeholder, puis son comportement est suivi en mode record-replay afin d’envoyer un jeu d’instructions au serveur. Le serveur reçoit ensuite ces instructions et les exécute pour chaque élément du tableau.En pratique, le développeur n’a écrit qu’une simple méthode JS, mais derrière il y a une astuce qui la transforme en DSL restreint. Le callback doit rester strictement synchrone et
awaitn’est pas possible. À la place, seul le promise pipelining est autorisé, afin que tout le processus puisse être capturé puis transmis au serveur, où il sera rejoué quand nécessaireEn C#, on a les expression trees pour traiter ce genre de problème. Entity Framework s’en sert quand il reçoit une lambda pour la convertir en requête SQL. Autrement dit, on peut scanner ou transformer le code sans l’exécuter
Par exemple,
db.People.Where(p => p.Name == "Joe")ne passe pas une vraie fonction prédicat àWhere, mais une expression. Le code reçu est donc inspecté pour vérifier que le champNamecorrespond à"Joe", puis converti en clause SQLWHEREJavaScript n’a pas de mécanisme équivalent, donc l’idée est de l’imiter en injectant des valeurs placeholder et en enregistrant pas à pas le comportement
J’ai utilisé ce même trick de record-replay récemment en construisant le DSL de requête de Tanstack DB lien du guide. On passe un objet
RefProxyaux callbackswhere/select/join, puis on trace les propriétés et opérations appliquées à cet objet.Comme on ne peut pas intercepter directement les opérateurs classiques en JS (
==,>, etc.), on crée de petites fonctions traçables commeeq/gt/not, on exécute le callback une seule fois pour capturer l’expression chaînée, puis on la convertit en IRDe façon étonnante, on a même réussi à tracer l’opérateur spread JS
Kenton, tu penses qu’il serait possible d’ajouter aussi ce concept à capnweb avec de faux opérateurs (
eq,gt,in, etc.) pour apporter du tracing distant ?On dirait que les conditionnelles sont interdites (un peu comme les règles des hooks React) ; je me demande comment cette contrainte est implémentée
Ce projet m’intéresse
Il a des points communs avec les bibliothèques de compilation ML (
TensorFlow 1,JAX jit,PyTorch compile, etc.). On construit un graphe d’opérations par traçage, puis on le compile ou on le transforme pour l’exécuter sur une VM donnéeAujourd’hui, au lieu de définir un nouveau DSL avec un langage dynamique comme frontend, on cache une transformation d’AST derrière un langage de script existant
Dans le ML, on retarde l’exécution des kernels GPU/linalg pour pouvoir les fusionner ; dans une RPC comme Cap'n Web, on peut retarder les requêtes réseau pour regrouper plusieurs appels réseau
Au fond, l’idée clé est de séparer instruction plane et data plane, et même un CPU unique à très petite échelle possède une structure de système distribué avec séparation du cache d’instructions et des données
Dans Cap'n Web, c’est le graphe RPC lui-même qui joue le rôle d’instruction
Ce pattern est vraiment fascinant, mais ça donne aussi l’impression d’une pile infinie (compilateur au-dessus d’un interpréteur, interpréteur au-dessus d’un compilateur…). Ça me rappelle une autre variation du motif lispy « code is data, data is code ». J’ai l’impression qu’il y a derrière tout ça une histoire plus profonde
Les langages dynamiques deviennent maintenant le frontend de nouveaux DSL, sans imposer de nouvelle syntaxe, simplement en injectant la génération d’AST dans le script
Je pense que TypeScript change complètement la donne ici. Il permet de combiner la flexibilité à l’exécution de JavaScript (comme l’usage astucieux de
Proxydans Cap'n Web) avec la sûreté de typageEn ce moment, je suis à fond sur cette idée côté ORM. La plupart des ORM sont sériels et eager, donc on ne peut les manipuler qu’au moment juste avant l’exécution de la requête
Un ORM vraiment composable devrait, à mon avis, fonctionner comme un compilateur : définir en TypeScript un DSL totalement type-safe au-dessus de SQL pour construire un AST de requête, puis ne compiler en SQL qu’à la toute fin
Typegres, que je développe, repose exactement sur cette idée. Si ce pattern t’intéresse, ça peut valoir le détour
Le problème fondamental des bibliothèques RPC, c’est qu’elles essaient de masquer où et comment les aller-retours se produisent
Rien qu’avec le
.map()sur les tableaux de Cap'n Web, il est difficile de savoir où se produit réellement le round-trip réseau.Je pense que ce n’est pas une « fonctionnalité », mais plutôt un « bug » — en lisant le code, on devrait pouvoir comprendre immédiatement son comportement, et masquer cela n’est pas souhaitable
lien de référence
awaitLe promise pipelining permet de préparer plusieurs instructions à la suite sans
await, donc sans aller-retour réseau supplémentaire entre elles. À la fin, un seulawaitsuffit, et c’est toutSi vous avez déjà utilisé gRPC sur le web, vous savez à quel point faire entrer Protobuf dans le web peut être pénible
J’aime beaucoup la simplicité de Cap'n Web documentation capnproto
Contrairement à Cap'n Proto, Cap'n Web n’a carrément pas de schéma. Il y a très peu de boilerplate inutile, ce qui donne vraiment une impression de RPC JavaScript native à la Cloudflare Workers
référence github
J’ai découvert la nouvelle bibliothèque de kentonv et j’ai accouru immédiatement
En regardant le code sur GitHub, j’ai été surpris de voir qu’il est étonnamment petit. Je me demande si c’est vraiment tout
En théorie, porter la partie serveur dans un autre langage ne semblerait pas si difficile, et j’aimerais bien l’utiliser avec un serveur Elixir et un frontend JS/TS
Ce serait aussi amusant de confier ce portage de langage à un LLM. Je me demande si ce dépôt contient du code généré par LLM. J’avais vu il y a quelques mois que kentonv avait parlé d’un POC créé par une IA (puis relu par un humain)
À l’heure actuelle, je ne pense pas qu’un LLM aurait pu produire cette bibliothèque. Sa structure interne a été conçue comme un puzzle extrêmement finement ajusté
J’ai passé plus de temps à réfléchir au design qu’à écrire le code lui-même
C’est complètement différent de la bibliothèque
workers-oauth-provider, qui implémente de façon originale une spec bien connueLa structure du code serait probablement facile à porter vers un langage dynamique comme Python, mais difficilement vers un langage à typage statique. Beaucoup d’éléments reposent sur des types d’objets arbitraires
Il y a des ressemblances avec OCapN, mais aussi des différences importantes référence
Les deux prennent en charge le transfert de capabilities, le promise pipelining et un modèle sans schéma
Cap'n Web n’a pas de capability hors bande comme les
sturdyrefd’OCapN (URI restaurables). J’en déduis que c’est pour cela qu’une authentification par clé API est nécessaire. Unsturdyrefest une sorte de jeton impossible à deviner : le posséder donne accès à l’endpoint concernéDe plus, Cap'n Web n’a pas de mécanisme de handoff à trois parties où Alice présente Bob à Carol. C’est indispensable pour les applications distribuées, donc Cap'n Web semble plus proche d’un usage client-serveur de style SaaS traditionnel avec quelques propriétés ocap en plus
Pour
SturdyRef, la façon de restaurer dépend de chaque plateforme, donc je pense qu’il vaut mieux l’implémenter au niveau de la plateforme plutôt qu’au niveau du protocole RPCPar exemple, sur Cloudflare Workers, il sera bientôt possible de persister des capabilities dans le stockage de Durable Object, mais la manière de le faire est spécifique à la plateforme Workers
Sandstorm a aussi des capabilities persistantes, mais limitées à ses services internes
C’est pour cette raison que Cap’n Proto a fini par retirer complètement la notion de capability persistante, et que l’équivalent le plus proche dans les standards du web reste OAuth
On pourrait imaginer définir une sorte de
sturdyrefbasé sur un refresh token OAuth, mais ce ne serait pas une construction utilisable sur toutes les plateformesÀ première vue, ce système semble exiger — ou au moins encourager — que les tables d’import/export ou l’état des objets soient conservés côté serveur de manière stateful
Dans les RPC traditionnelles, tous les appels arrivent au niveau racine, avec une clé ou autre transmise à chaque requête, donc même si les appels sont répartis sur plusieurs serveurs ce n’est pas un problème ; Cap’n Web, lui, ne semble pas fonctionner ainsi
Je me demande s’il est possible de sérialiser ces tables pour les stocker en base et ainsi répartir les serveurs de la même manière, ou si cela impose forcément une affinité serveur ou des structures comme Durable Objects
L’état n’est conservé qu’au sein d’une seule session RPC
Si on utilise WebSocket, l’état reste vivant tant que la connexion WebSocket est maintenue
Si on utilise le transport HTTP batch, la session est limitée à l’ensemble d’une seule requête HTTP, dans laquelle tous les appels sont traités d’un coup
Cap’n Web n’a donc pas besoin de conserver un état à travers plusieurs requêtes ou connexions HTTP
En revanche, si le design fait qu’une session interrompue vous fait perdre toutes les capabilities, alors c’est un mauvais design qu’il faut éviter. Il faut pouvoir réinitialiser la connexion à tout moment puis restaurer les capabilities
En lisant la documentation, on dirait effectivement que l’affinité est assurée via WebSocket
Le batching HTTP consiste à envoyer toutes les requêtes en une fois puis à attendre la réponse
Cette approche complique l’équilibrage de charge. S’il y a beaucoup de clients de chat, par exemple, les connexions risquent de se concentrer sur certains serveurs, avec un risque de surcharge
Le scale in/out des serveurs devient aussi pénible. Avec des connexions longues et plusieurs requêtes traitées simultanément, la gestion devient très compliquée
Autre point : si le client continue à pousser des événements sans jamais recevoir les réponses, le serveur doit garder ces réponses en mémoire, ce qui me semble ouvrir facilement la porte à des attaques DDoS
D’après mes anciens souvenirs de la documentation Cap'n Proto, le serveur et le client peuvent s’échanger des peer stubs
Si le serveur C reçoit, via le client B, un stub créé sur A, alors C peut appeler A directement
À l’origine, « RPC » désigne un paradigme de programmation visant à rendre un appel distant indiscernable d’un appel de fonction interne
En pratique, cela implique un protocole réseau, ainsi que des bibliothèques client et serveur
Aujourd’hui, la perception a beaucoup changé, et le modèle dominant ressemble davantage à des endpoints de type REST avec des signatures de fonctions
Avec l’apparition de fonctionnalités de langage comme
Future,Optional, etc., on peut distinguer explicitement des propriétés comme « cette opération peut être différée » ou « elle peut échouer »Dans les anciens systèmes RPC, tout cela était entièrement masqué
Je vois l’idée, mais la programmation asynchrone existe dans de nombreux langages. J’ai utilisé JavaScript, C++, Python, Rust, C#, etc.
Le point clé, c’est que les premiers systèmes RPC bloquaient le thread appelant pendant toute la durée de la requête réseau, ce qui était une très mauvaise conception ; aujourd’hui, l’asynchrone est devenu la norme
Je suis très enthousiaste de voir que Cap'n Web existe séparément et n’est pas seulement lié aux produits Cloudflare
En lisant cette section de la documentation, je me pose une question
En fait, je pense même que Cap'n Web pourrait prendre de l’avance sur Worker RPC (c’est déjà le cas pour les capacités de pipeline)
La structure de Cap'n Web est beaucoup plus simple, donc les expérimentations de nouvelles fonctionnalités commenceront probablement d’abord dans Cap'n Web