6 points par GN⁺ 2025-09-23 | 1 commentaires | Partager sur WhatsApp
  • 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éseau

    let 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 map est 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

 
GN⁺ 2025-09-23
Commentaires Hacker News
  • Deux choses m’intriguent

    1. Je me demande comment il faut gérer le déploiement d’applications dont la sémantique RPC évolue. Autrement dit, comment garantir que le client et le serveur utilisent bien la même version de la RPC ? Les protocol buffers (grpc/avro, etc.) essaient justement de résoudre ce problème de front
    2. Je me demande aussi comment il vaut mieux gérer les connexions réseau instables. Les tables d’export/import étant directement liées à une connexion WebSocket avec état, j’imagine qu’on perd l’état si la connexion tombe. En théorie, le client et le serveur pourraient mettre cet état en cache puis le restaurer à la reconnexion, mais comme la table peut contenir des closures, ça semble difficile à sérialiser et potentiellement problématique en mémoire. Je serais curieux de savoir comment l’équipe a réfléchi à ça
      Je trouve ce travail vraiment innovant
      1. On peut voir ça comme une mise à jour d’API JavaScript sans casser les appelants existants. Tant qu’on respecte les règles de compatibilité de base attendues pour des appels de fonctions locaux, on peut ajouter de nouvelles méthodes, des arguments optionnels, etc.
      2. Si la connexion tombe, il faut se reconnecter puis reconstruire les objets depuis zéro. Dans une vraie app React, on passe le stub RPC principal en argument au composant racine. Ce composant crée plusieurs objets enfants et les transmet à ses enfants. Si la connexion coupe, on crée un nouveau stub et on le redonne au composant racine. Cela déclenche alors un nouveau rendu, comme n’importe quel autre changement d’état, et tous les enfants vont à nouveau fetch les sous-objets dont ils ont besoin
        Si 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 await n’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écessaire

    • En 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 champ Name correspond à "Joe", puis converti en clause SQL WHERE
      JavaScript 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 RefProxy aux callbacks where/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 comme eq/gt/not, on exécute le callback une seule fois pour capturer l’expression chaînée, puis on la convertit en IR
      De 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ée
    Aujourd’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

    • Tout à fait d’accord — la vision de ça comme abstraction universelle est excellente
      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 Proxy dans Cap'n Web) avec la sûreté de typage
      En 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

    • Le round-trip se produit quand on utilise await
      Le 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 seul await suffit, et c’est tout
  • Si 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)

    • Certains tests ont été générés par LLM, mais pas la bibliothèque elle-même
      À 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 connue
      La 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 sturdyref d’OCapN (URI restaurables). J’en déduis que c’est pour cela qu’une authentification par clé API est nécessaire. Un sturdyref est 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

    • J’aimerais ajouter le support du 3PH plus tard, mais pour cette première release, la priorité était avant tout la communication navigateur <-> serveur web
      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 RPC
      Par 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 sturdyref basé 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

  • 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. Ce passage m’a embrouillé. Si le postulat central de la RPC dépend fortement d’un langage particulier ou d’un modèle de concurrence spécifique, comment cela peut-il être un protocole ?

  • À 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

    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. Une fois qu’il y aura parité fonctionnelle entre les deux, avez-vous l’intention de continuer à les garder synchronisés, ou bien Cap'n Web finira-t-il par prendre du retard sur Cloudflare Workers ? Je me demande aussi quel écart vous anticipez

    • Pour l’essentiel des fonctionnalités réellement pertinentes, nous prévoyons de garder les deux produits presque toujours synchronisés
      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