Sistem RPC baru untuk browser dan server web: Cap'n Web
(blog.cloudflare.com)- Cap'n Web adalah protokol RPC baru yang diimplementasikan dengan TypeScript, dioptimalkan untuk lingkungan web dan berjalan di berbagai runtime JavaScript
- Tanpa skema atau boilerplate yang merepotkan, ia menyediakan serialisasi berbasis JSON serta format data yang mudah dibaca manusia
- Melalui model berbasis object-capability, dimungkinkan pemanggilan dua arah, pengiriman referensi fungsi·objek, promise pipelining, dan implementasi pola keamanan
- Mendukung berbagai lingkungan jaringan seperti WebSocket, HTTP, postMessage, serta merupakan open source ringan berukuran di bawah 10kB
- Selain menyelesaikan masalah waterfall yang mirip dengan GraphQL, ia juga memungkinkan pemodelan RPC yang alami seperti API JavaScript biasa
Apa itu Cap'n Web
- Cap'n Web adalah sistem RPC open source berbasis TypeScript yang dikembangkan oleh Cloudflare
- Terinspirasi oleh Cap'n Proto, tetapi bekerja tanpa definisi skema terpisah dan mengadopsi metode serialisasi yang ramah manusia dengan memanfaatkan JSON
- Terintegrasi dengan TypeScript untuk meningkatkan pengalaman developer seperti autocomplete dan type checking, sementara validasi tipe saat runtime dapat ditangani secara terpisah (misalnya dengan type guard)
- Mendukung protokol jaringan seperti HTTP, WebSocket, dan postMessage, serta berjalan di browser utama, Cloudflare Workers, Node.js, dan lainnya
- Dengan struktur ringan tanpa dependensi, ukurannya kurang dari 10kB setelah minify + gzip
Model berbasis object-capability (OCap) di Cap'n Web
- Mengadopsi model berbasis object-capability, sehingga mampu mengekspresikan lebih banyak hal dibanding sistem RPC tradisional
- Pemanggilan dua arah: klien dan server dapat saling memanggil fungsi
- Pengiriman referensi fungsi·objek: ketika fungsi atau objek dikirim lewat RPC, pihak lawan menerima stub dan eksekusi terjadi di sisi asal saat dipanggil
- Promise Pipelining: saat beberapa RPC dirangkai dalam chain, semuanya dapat diproses dalam satu round trip jaringan
- Pola keamanan: kontrol keamanan seperti otorisasi dan manajemen sesi dapat diimplementasikan secara alami
Cara penggunaan dasar
-
Contoh klien
import { newWebSocketRpcSession } from "capnweb" let api = newWebSocketRpcSession("wss://example.com/api") let result = await api.hello("World") console.log(result) -
Contoh server (berbasis 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}) } } -
Menambahkan method ke API, mengirim fungsi callback dari klien, serta mendefinisikan dan menerapkan interface TypeScript dapat dilakukan dengan mudah
Apa itu RPC dan karakteristiknya di Cap'n Web
- RPC (Remote Procedure Call) adalah konsep yang memungkinkan dua program di jaringan berkomunikasi seolah-olah sedang melakukan pemanggilan fungsi
- Berbeda dari protokol HTTP/REST tradisional, RPC menggunakan abstraksi pemanggilan fungsi sehingga memungkinkan penulisan kode yang selaras dengan cara berpikir developer
- Cap'n Web sangat cocok dengan alur JavaScript modern, termasuk dukungan async/await, Promise, dan Exception
- Berbeda dari kontroversi historis RPC (pemanggilan sinkron, error jaringan), di lingkungan JS modern ia bisa digunakan dengan lebih aman dan efisien
Skenario penggunaan Cap'n Web
- Cocok untuk semua lingkungan yang membutuhkan komunikasi jaringan antar dua aplikasi JavaScript
- Seperti klien-server, pemanggilan antar-microservice, dan lain-lain
- Sangat cocok khususnya untuk web app kolaborasi real-time dan interaksi yang melintasi batas keamanan yang kompleks
- Masih berada pada tahap eksperimental, sehingga lebih bermanfaat bagi developer yang terbuka pada adopsi teknologi terbaru
Berbagai fitur
Mode batch HTTP
-
Saat koneksi persisten tidak diperlukan, beberapa pemanggilan RPC dapat digabung dan diproses sekaligus dengan mode batch HTTP
import { newHttpBatchRpcSession } from "capnweb" let batch = newHttpBatchRpcSession("https://example.com/api") let result = await batch.hello("World") console.log(result) -
Dalam satu batch, beberapa pemanggilan dapat dijalankan bersamaan dan hasilnya diterima secara paralel
let promise1 = batch.hello("Alice") let promise2 = batch.hello("Bob") let [result1, result2] = await Promise.all([promise1, promise2])
Promise Pipelining (pemanggilan berantai)
-
Mendukung cara menggunakan hasil panggilan sebelumnya langsung sebagai argumen untuk panggilan berikutnya tanpa menunggu hasil sebelumnya selesai
-
Contoh) Promise hasil
getMyName()langsung diberikan kehello()sehingga diproses dalam satu round trip jaringanlet namePromise = batch.getMyName() let result = await batch.hello(namePromise) -
Promise di Cap'n Web bekerja sebagai objek proxy, sehingga pemanggilan method tambahan dapat dirangkai tanpa penundaan
let sessionPromise = batch.authenticate(apiKey) let name = await sessionPromise.whoami()
Keamanan: autentikasi dan object-capability
- Melalui method authenticate, objek hak akses (sesi) diberikan saat berhasil, dan setelah itu fitur dapat dipanggil tanpa tahap autentikasi tambahan
- Berbeda dari RPC tradisional, objek sesi tidak dapat dipalsukan, dan method yang membutuhkan hak akses tidak bisa diakses tanpa autentikasi
- Secara alami mengatasi keterbatasan struktural WebSocket dan menjaga konsistensi logika autentikasi
- Saat mendeklarasikan interface API dengan TypeScript, penerapannya bisa otomatis ke sisi klien~server, sambil memperoleh autocomplete dan type safety
Perbandingan dengan GraphQL dan pembeda Cap'n Web
-
GraphQL meredakan masalah waterfall bertingkat pada REST, tetapi memerlukan pengenalan bahasa, skema, dan toolchain baru
-
Cap'n Web menyelesaikan masalah waterfall hanya dengan kode JavaScript,
- Dengan dukungan promise pipelining/referensi objek, pemanggilan bertingkat maupun logika transaksi kompleks dapat dimodelkan secara alami
let user = api.createUser({ name: "Alice" }) let friendRequest = await user.sendFriendRequest("Bob") -
Dapat digunakan mirip API JavaScript tanpa kompleksitas serta biaya belajar·pengelolaan ala GraphQL
Operasi array (array.map dan sebagainya) serta optimisasi
-
Di Cap'n Web, operasi map pada setiap elemen array dapat dilakukan tanpa round trip jaringan tambahan
-
Fungsi callback map dijalankan sekali di klien untuk merekam isi operasinya (record-replay), lalu dikirim ke server agar diproses secara massal di sisi server
let friendsWithPhotos = friendsPromise.map(friend => { return {friend, photo: api.getUserPhoto(friend.id)} }) let results = await friendsWithPhotos -
Melalui bahasa spesifik domain (DSL) yang terbatas, ekspresinya tetap seperti fungsi JavaScript, tetapi di balik layar Cap'n Web mengoptimalkan banyak pemanggilan lewat protokolnya
Struktur protokol internal dan alur komunikasi
- Mengirim data terstruktur melalui JSON + prapemrosesan khusus, dengan dukungan tipe khusus seperti array dan tanggal
- Sebagai protokol simetris, ia memungkinkan komunikasi dua arah tanpa pembedaan klien·server
- Setiap pihak (misalnya Alice dan Bob) mengelola tabel export/import dan membedakan referensi objek·fungsi dengan ID
- Melalui pesan push/pull dan alokasi Promise ID, banyak pemanggilan dapat direfleksikan dalam satu round trip
Status saat ini dan contoh penerapan
- Cap'n Web masih merupakan open source eksperimental, tetapi sudah digunakan dalam layanan nyata seperti remote bindings di Cloudflare Wrangler
- Direncanakan akan ada posting blog tambahan dan berbagai eksperimen frontend
- Dirilis dengan lisensi MIT, sehingga siapa pun dapat menggunakannya dengan bebas
- Langsung ke repositori GitHub
1 komentar
Komentar Hacker News
Ada dua hal yang saya penasaran
Menurut saya ini pekerjaan yang benar-benar inovatif
Jika ada objek subscription yang memiliki callback, API perlu dirancang agar saat memulai bisa menentukan “pesan terakhir yang sudah dilihat”. Dengan begitu data bisa langsung dilanjutkan tanpa ada yang terlewat di tengah jalan
Sepertinya saya perlu merangkum seri design pattern seperti ini dalam sebuah blog post
Bagian tentang bagaimana mereka menyelesaikan masalah array itu sangat menarik sekaligus agak menakutkan tautan blog
Dalam kasus
.map(), mereka memang tidak langsung mengirim kode JavaScript ke server, tetapi mengirim sesuatu yang mirip “kode”, menggunakan domain-specific language (DSL) yang terbatas. Di sisi klien, callback dijalankan sekali dengan menyisipkan nilai placeholder, lalu perilakunya dilacak dengan cara record-replay sehingga instruction set bisa dikirim ke server. Di server, instruction itu diterima lalu dijalankan untuk setiap anggota array.Jadi pengembang hanya menulis method js biasa, tetapi di balik layar diterapkan trik yang mengubahnya menjadi DSL yang sempit. Callback hanya boleh berjalan secara sinkron,
awaittidak dimungkinkan. Sebagai gantinya hanya promise pipelining yang diizinkan, sehingga seluruh proses bisa ditangkap lalu dikirim ke server, dan di server dijalankan ulang sesuai kebutuhanC# punya expression tree untuk menangani masalah seperti ini. Entity Framework memanfaatkannya saat menerima lambda expression lalu mengubahnya menjadi SQL query. Jadi kode bisa digunakan dengan cara dipindai atau ditransformasikan tanpa dieksekusi
Misalnya,
db.People.Where(p => p.Name == "Joe")bukan berarti Where menerima fungsi predicate sungguhan, melainkan expression, sehingga kode yang diterima bisa dipindai untuk memeriksa bahwa field Name cocok dengan "Joe" lalu diubah menjadi klausa SQL WHEREJavaScript tidak punya mekanisme seperti ini, jadi pendekatannya meniru dengan menyisipkan nilai placeholder lalu merekam satu per satu bagaimana perilakunya
Baru-baru ini kami juga memakai trik record-replay ini saat membuat query DSL untuk Tanstack DB tautan panduan. Objek RefProxy diberikan ke callback where/select/join, lalu dilacak prop/operasi apa yang terjadi pada objek itu.
Karena di js operator biasa (
==,>, dll.) tidak bisa dicegat secara langsung, kami membuat fungsi-fungsi kecil yang bisa dilacak seperti eq/gt/not, menjalankan callback sekali untuk menangkap expression yang terhubung, lalu membuat IR darinyaMenariknya, operator spread js juga berhasil kami lacak
Kenton, saya penasaran apakah konsep ini juga bisa ditambahkan ke capnweb dalam bentuk operator palsu (
eq,gt,in, dll.) untuk memberi kemampuan remote tracingSepertinya percabangan kondisi dilarang (mirip seperti aturan hook di React), jadi saya penasaran bagaimana pembatasan seperti itu diimplementasikan
Proyek ini menarik
Ada sisi yang mirip dengan pustaka kompiler ML (TensorFlow 1, JAX jit, PyTorch compile, dll.). Dengan pendekatan tracing, ia membuat operation graph, lalu meng-compile atau mentransformasikannya agar berjalan sesuai VM
Saat ini, alih-alih mendefinisikan DSL baru dengan bahasa dinamis sebagai frontend, AST transformation disembunyikan di dalam bahasa scripting yang sudah ada
Di ML, eksekusi GPU/linalg kernel ditunda agar kernel bisa digabungkan, sedangkan pada RPC seperti Cap'n Web, network request dapat ditunda agar beberapa network call bisa digabung
Pada akhirnya kuncinya adalah memisahkan instruction plane dan data plane, dan bahkan CPU tunggal dalam skala sangat kecil pun memiliki struktur distributed system (pemisahan cache perintah/data)
Di Cap'n Web, graph RPC itu sendiri berperan sebagai instruction
Pola seperti ini sangat menarik, tetapi juga terasa seperti struktur stack (compiler di atas interpreter, interpreter di atas compiler...) yang berulang tanpa akhir. Rasanya seperti versi lain dari pola Lispy code is data, data is code. Sepertinya ada cerita yang secara fundamental lebih dalam di sini
Bahasa dinamis sekarang menjadi frontend untuk DSL baru, tetapi tanpa menetapkan sintaks baru; pembuatan AST justru dilebur ke dalam script
Menurut saya TypeScript adalah game changer di sini. Karena kita bisa sekaligus mendapatkan fleksibilitas runtime JavaScript (seperti Cap'n Web yang memanfaatkan Proxy dengan cerdik) dan type safety
Belakangan ini saya sangat tertarik pada konsep ini di dunia ORM. Kebanyakan ORM bersifat serial dan eager, jadi hanya bisa dimanipulasi tepat sebelum query dieksekusi
Menurut saya ORM yang benar-benar composable harus bekerja seperti compiler: mendefinisikan DSL yang sepenuhnya type-safe di atas SQL dengan TypeScript untuk membuat query AST, lalu baru meng-compile ke SQL di tahap akhir
Typegres yang sedang saya kembangkan juga persis memakai ide ini. Jika pola ini menarik bagi Anda, mungkin layak dilihat
Masalah inti pustaka RPC adalah kecenderungannya menyembunyikan di mana dan bagaimana round-trip terjadi
Hanya dengan melihat
.map()array di Cap'n Web pun sulit mengetahui di mana network round-trip sebenarnya terjadi.Menurut saya ini bukan “fitur”, melainkan “bug”—saat membaca kode, kita seharusnya bisa langsung memahami perilakunya, dan menyamarkan hal ini tidaklah ideal
tautan referensi
awaitpromise pipelining memungkinkan beberapa statement disiapkan berurutan tanpa
await, sehingga tidak ada tambahan network round-trip di tengah. Ketika akhirnyaawaitdipanggil sekali, itulah keseluruhannyaJika pernah memakai gRPC dan web, Anda pasti tahu betapa menyakitkannya menerapkan Protobuf ke web
Saya sangat menyukai kesederhanaan Cap'n Web dokumentasi capnproto
Berbeda dari Cap'n Proto, Cap'n Web sama sekali tidak punya schema. Karena hampir tidak ada boilerplate yang tidak perlu, rasanya sangat seperti RPC native JavaScript untuk Cloudflare Workers
referensi github
Saya langsung datang begitu menemukan pustaka baru dari kentonv
Setelah melihat kode di GitHub, saya kaget karena ukurannya ternyata sangat kecil. Saya penasaran apakah memang hanya segitu semuanya
Secara teori, sepertinya porting sisi server ke bahasa lain juga tidak akan terlalu sulit, dan saya jadi ingin memakainya dengan server Elixir dan frontend JS/TS
Menyuruh LLM melakukan porting bahasa seperti ini juga terdengar menarik. Saya penasaran apakah ada kode berbasis LLM yang masuk ke repo ini. Beberapa bulan lalu saya sempat melihat kentonv bercerita tentang POC buatan AI (yang ditinjau manusia)
Pada titik saat ini, sepertinya akan sulit bagi LLM untuk membuat pustaka ini. Struktur internalnya dirancang seperti puzzle yang sangat presisi dan saling terkait
Waktu yang dihabiskan untuk memikirkan desain lebih banyak daripada waktu untuk menulis kode sebenarnya
Ini sangat berbeda dari pustaka workers-oauth-provider yang mengimplementasikan well-known spec dengan cara baru
Struktur kodenya mungkin cukup mudah dipindahkan ke bahasa dinamis seperti Python, tetapi menurut saya akan sulit untuk bahasa bertipe statis. Ada banyak bagian yang bergantung pada tipe objek arbitrer
Ada kemiripan dan juga perbedaan penting dengan OCapN referensi
Keduanya mendukung capability transfer, promise pipelining, dan model schemaless
Cap'n Web tidak memiliki capability out-of-band seperti sturdyref (URI yang bisa dipulihkan) milik OCapN. Karena itu saya menduga autentikasi dengan API key menjadi perlu. sturdyref adalah semacam token yang tidak bisa ditebak; jika memilikinya, Anda mendapatkan hak akses ke endpoint tersebut
Selain itu, Cap'n Web tidak memiliki kemampuan handoff tiga pihak di mana Alice memperkenalkan Bob kepada Carol. Ini penting untuk aplikasi terdistribusi, jadi Cap'n Web terasa lebih dekat ke layanan bergaya client-server SaaS tradisional yang hanya mengambil sebagian karakteristik ocap
Untuk SturdyRef, cara pemulihannya berbeda-beda di tiap platform, jadi menurut saya lebih tepat diimplementasikan sesuai platform masing-masing daripada di level protokol RPC
Misalnya di Cloudflare Workers, capability persistence dari Durable Object storage akan segera dimungkinkan, tetapi cara implementasinya spesifik pada platform worker
Sandstorm juga punya persistent capability, tetapi terbatas pada layanan internal
Karena itu konsep persistent capability sengaja dihilangkan dari Cap’n Proto, dan konsep yang paling mirip dalam standar web adalah OAuth
Kita bisa membayangkan definisi sturdyref berbasis OAuth refresh token, tetapi itu bukan struktur yang bisa dipakai di semua platform
Dari tinjauan cepat saya, sistem ini tampaknya mengharuskan (atau mendorong) penyimpanan stateful atas tabel import/export atau state objek di sisi server
Dalam RPC tradisional, semua pemanggilan masuk ke level teratas dan setiap pemanggilan membawa key dan sebagainya, sehingga tetap tidak masalah walau request tersebar ke banyak server, tetapi Cap’n Web tidak seperti itu
Saya penasaran apakah tabel tersebut bisa diserialisasi lalu disimpan ke DB sehingga distribusi server tetap dimungkinkan dengan cara yang sama, atau apakah ia benar-benar membutuhkan server affinity atau struktur seperti Durable Objects
State hanya dipertahankan di dalam satu sesi RPC
Jika menggunakan WebSocket, state akan tetap hidup selama koneksi WebSocket itu hidup
Jika menggunakan pengiriman batch HTTP, sesi dibatasi pada keseluruhan satu request HTTP, dan semua pemanggilan di dalamnya diproses sekaligus
Jadi Cap’n Web tidak perlu mempertahankan state lintas banyak request/koneksi HTTP
Namun, jika desain Anda membuat semua capability hilang ketika sesi terputus di tengah jalan, maka desain seperti itu sebaiknya dihindari. Capability harus bisa dipulihkan kapan saja setelah koneksi di-reset
Setelah membaca dokumentasinya, sepertinya strukturnya mengandalkan affinity melalui websocket
HTTP batching berarti semua request dikirim sekaligus lalu menunggu respons
Pendekatan seperti ini membuat load balancing menjadi rumit. Jika klien chat sangat banyak, koneksi bisa menumpuk ke server tertentu. Kalau begitu ada risiko server tersebut kelebihan beban
Scale in/out server juga jadi merepotkan. Mempertahankan koneksi jangka panjang sambil banyak request diproses bersamaan membuat pengelolaannya sangat sulit
Satu hal lagi, jika klien terus mengirim push event tanpa pernah menerima respons, server harus terus menyimpan respons itu di memori, jadi menurut saya serangan DDOS akan mudah dilakukan
Dari dokumentasi Cap'n Proto yang pernah saya baca dulu, server dan klien bisa saling bertukar peer stub
Jika server C menerima stub yang dibuat di A melalui klien B, maka C pun bisa langsung memanggil A
“RPC” pada dasarnya adalah paradigma pemrograman yang membuat pemanggilan jarak jauh tampak tidak bisa dibedakan dari pemanggilan fungsi internal
Tentu saja, untuk itu dibutuhkan wire protocol, pustaka klien/server, dan sebagainya
Belakangan ini pemahamannya banyak berubah, dan struktur yang mirip REST endpoint tetapi memiliki function signature menjadi arus utama
Dengan adanya fitur bahasa pemrograman seperti Future, Optional, dan lain-lain, karakteristik seperti “operasi ini bisa tertunda” atau “bisa gagal” dapat dibedakan dengan jelas
Pada RPC lama, semua sifat itu disembunyikan
Saya penasaran apa maksudnya. Pemrograman asinkron ada di banyak bahasa. Saya punya pengalaman memakai JavaScript, C++, Python, Rust, C#, dan hampir semuanya
Intinya, sistem RPC awal bekerja dengan memblokir thread pemanggil selama request jaringan berlangsung, dan itu benar-benar desain yang buruk; itulah sebabnya sekarang pendekatan asinkron menjadi hal yang wajar
Saya sangat antusias karena Cap'n Web ada sebagai proyek terpisah dan tidak hanya terikat pada produk Cloudflare
Setelah membaca bagian ini di dokumentasi, saya punya pertanyaan
Bahkan saya pikir Cap'n Web bisa melampaui worker RPC (faktanya kemampuan pipeline sudah lebih maju)
Struktur Cap'n Web jauh lebih sederhana, jadi eksperimen fitur baru juga kemungkinan besar akan lebih dulu dilakukan di Cap'n Web