Tarayıcılar ve web sunucuları için yeni bir RPC sistemi: Cap'n Web
(blog.cloudflare.com)- Cap'n Web, TypeScript ile geliştirilmiş yeni bir RPC protokolü olup web ortamına optimize edilmiştir ve çeşitli JavaScript çalışma zamanlarında çalışır
- Şema veya zahmetli boilerplate olmadan, JSON tabanlı serileştirme ve insan tarafından okunabilir veri biçimi sunar
- Nesne-yetki tabanlı model sayesinde çift yönlü çağrılar, fonksiyon·nesne referansı aktarımı, promise pipelining ve güvenlik desenleri uygulanabilir
- WebSocket, HTTP, postMessage gibi çeşitli ağ ortamlarını destekler ve 10kB'nin altında hafif bir açık kaynak projedir
- Yalnızca GraphQL'e benzer waterfall sorununu çözmekle kalmaz, aynı zamanda sıradan JavaScript API'leri gibi doğal RPC modellemesine olanak tanır
Cap'n Web nedir
- Cap'n Web, Cloudflare tarafından geliştirilen TypeScript tabanlı açık kaynak bir RPC (protocol) sistemidir
- Cap'n Proto'dan ilham almıştır ancak ayrı bir şema tanımı olmadan çalışır ve JSON kullanan insan dostu serileştirme yaklaşımını benimser
- TypeScript ile entegre çalışarak otomatik tamamlama, tip kontrolü gibi geliştirici deneyimini iyileştirir; çalışma zamanı tip doğrulaması ise ayrıca (
type guardvb.) ele alınabilir - HTTP, WebSocket, postMessage gibi ağ protokollerini destekler ve başlıca tarayıcılarda, Cloudflare Workers ve Node.js üzerinde çalışır
- Bağımlılığı olmayan hafif yapısıyla minify + gzip sonrasında 10kB'den küçük boyutta sunulur
Cap'n Web'in nesne-yetki tabanlı modeli (OCap)
- Nesne-yetki (object-capability) tabanlı model benimsenerek, geleneksel RPC sistemlerinden daha zengin ifade imkanı sunulur
- Çift yönlü çağrılar: İstemci ve sunucu birbirlerinin fonksiyonlarını çağırabilir
- Fonksiyon·nesne referansı aktarımı: Bir fonksiyon veya nesne RPC ile gönderildiğinde, karşı taraf bir stub alır ve çağrı sırasında asıl bulunduğu yerde çalıştırır
- Promise Pipelining: Birden fazla RPC zincir halinde bağlandığında tek bir ağ gidiş-dönüşüyle işlenir
- Güvenlik desenleri: Yetkilendirme ve oturum yönetimi gibi güvenlik kontrolleri doğal şekilde uygulanabilir
Temel kullanım
-
İstemci örneği
import { newWebSocketRpcSession } from "capnweb" let api = newWebSocketRpcSession("wss://example.com/api") let result = await api.hello("World") console.log(result) -
Sunucu örneği (Cloudflare Worker tabanlı)
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}) } } -
API'ye metot eklemek, istemcinin callback fonksiyonlarını aktarmak, TypeScript arayüzlerini tanımlamak ve uygulamak kolaydır
RPC nedir ve Cap'n Web'deki özellikleri nelerdir
- RPC (Remote Procedure Call), ağ üzerindeki iki programın sanki fonksiyon çağrısı yapıyormuş gibi iletişim kurmasını sağlayan bir kavramdır
- Geleneksel HTTP/REST protokollerinden farklı olarak RPC, fonksiyon çağrısı soyutlamasıyla geliştiricinin düşünme biçimiyle uyumlu kod yazılmasına olanak tanır
- Cap'n Web, async/await, Promise, Exception desteği gibi modern JavaScript akışlarıyla iyi uyum sağlar
- RPC'nin tarihsel tartışmalarının (senkron çağrılar, ağ hataları) aksine, modern JS ortamlarında daha güvenli ve verimli kullanım mümkündür
Cap'n Web kullanım senaryoları
- İki JavaScript uygulaması arasında ağ iletişimi gereken her ortamda kullanılabilir
- İstemci-sunucu, mikroservisler arası çağrılar vb.
- Özellikle gerçek zamanlı iş birliği web uygulamaları ve karmaşık güvenlik sınırlarını aşan etkileşimler için uygundur
- Deneysel aşamada olduğundan, yeni teknolojileri benimsemeye açık geliştiriciler için daha faydalı olabilir
Çeşitli özellikler
HTTP batch modu
-
Kalıcı bağlantının gerekli olmadığı durumlarda HTTP batch modu ile birden fazla RPC çağrısı tek seferde gruplanıp işlenebilir
import { newHttpBatchRpcSession } from "capnweb" let batch = newHttpBatchRpcSession("https://example.com/api") let result = await batch.hello("World") console.log(result) -
Tek bir batch içinde birden fazla çağrı eşzamanlı yürütülebilir ve sonuçlar paralel olarak alınabilir
let promise1 = batch.hello("Alice") let promise2 = batch.hello("Bob") let [result1, result2] = await Promise.all([promise1, promise2])
Promise Pipelining (zincir çağrılar)
-
Önceki çağrının sonucunu beklemeden, sonucu doğrudan bir sonraki çağrının argümanı olarak kullanma yöntemi desteklenir
-
Örnek)
getMyName()sonucundaki Promise'i doğrudanhello()içine aktararak tek bir ağ gidiş-dönüşüyle işlem yapılırlet namePromise = batch.getMyName() let result = await batch.hello(namePromise) -
Cap'n Web'in Promise yapısı bir proxy nesnesi gibi davranır; ek metot çağrıları da bekleme olmadan zincirlenebilir
let sessionPromise = batch.authenticate(apiKey) let name = await sessionPromise.whoami()
Güvenlik: kimlik doğrulama ve nesne-yetki
authenticatemetodu üzerinden başarılı olursa yetki (oturum) nesnesi atanır; sonrasında ek kimlik doğrulama adımı olmadan fonksiyonlar çağrılabilir- Geleneksel RPC'den farklı olarak oturum nesnesi taklit edilemez ve kimlik doğrulama olmadan yetki gerektiren metotlara erişilemez
- WebSocket'in yapısal sınırlamalarını doğal biçimde aşar ve kimlik doğrulama mantığının tutarlılığını korur
- API arayüzü TypeScript ile tanımlandığında istemci ile sunucu arasında otomatik uygulanabilir; otomatik tamamlama ve tip güvenliği sağlar
GraphQL ile karşılaştırma ve Cap'n Web'in farkı
-
GraphQL, REST'in waterfall (çok aşamalı çağrı) sorununu hafifletir ancak yeni bir dil·şema·toolchain kullanımını gerektirir
-
Cap'n Web ise yalnızca JavaScript koduyla waterfall sorununu çözer ve
- promise pipelining/nesne referansları desteği sayesinde iç içe çağrılar veya karmaşık işlem mantıkları doğal şekilde modellenebilir
let user = api.createUser({ name: "Alice" }) let friendRequest = await user.sendFriendRequest("Bob") -
GraphQL'in karmaşıklığı ve öğrenme·yönetim maliyeti olmadan, JavaScript API'lerine benzer şekilde kullanılabilir
Dizi işlemleri (array.map vb.) ve optimizasyon
-
Cap'n Web'de dizinin her bir öğesi için ek ağ gidiş-dönüşü olmadan
mapişlemi yapılabilir -
mapcallback fonksiyonu istemcide bir kez çalıştırılarak işlem içeriği kaydedilir (record-replay), ardından sunucuya gönderilip sunucu tarafında toplu işlenirlet friendsWithPhotos = friendsPromise.map(friend => { return {friend, photo: api.getUserPhoto(friend.id)} }) let results = await friendsWithPhotos -
Sınırlı bir alan-özgü dil (DSL) aracılığıyla JavaScript fonksiyonu gibi ifade edilir, ancak gerçekte çoklu çağrılar Cap'n Web protokolüyle optimize edilerek işlenir
Dahili protokol yapısı ve iletişim akışı
- JSON + özel ön işleme ile yapılandırılmış veri aktarımı yapılır; diziler, tarihler gibi özel tipler desteklenir
- Simetrik bir protokol olduğundan istemci-sunucu ayrımı olmaksızın çift yönlü iletişim mümkündür
- Her taraf (ör. Alice ve Bob) export/import tablolarını yönetir ve nesne·fonksiyon referanslarını ID'lerle ayırt eder
- push/pull mesajları ve Promise ID atamaları sayesinde tek bir round trip içinde çok sayıda çağrı işlenebilir
Durum ve kullanım örnekleri
- Cap'n Web halen deneysel bir açık kaynak proje olup, Cloudflare Wrangler'ın remote bindings gibi gerçek hizmetlerde kullanılmaktadır
- Ek blog yazıları ve çeşitli frontend deneyleri planlanmaktadır
- MIT lisansı ile yayımlanmıştır ve herkes tarafından özgürce uygulanabilir
- GitHub deposuna git
1 yorum
Hacker News görüşleri
İki şeyi merak ediyorum
Gerçekten yenilikçi bir çalışma olduğunu düşünüyorum
Eğer callback içeren bir abonelik (
subscription) nesnesi varsa, API’yi başlangıç anında “en son görülen mesajı” belirtmeye izin verecek şekilde tasarlamalısınız. Böylece tam kaldığı yerden veri almaya devam edebilir ve arada bir şey kaçırmazsınızSanırım bunun gibi tasarım kalıplarını bir blog yazısı serisiyle toparlamamız lazım
Dizi sorununu nasıl çözdükleriyle ilgili bölüm gerçekten çok ilginç ve aynı zamanda biraz ürkütücü blog bağlantısı
.map()durumunda doğrudan sunucuya JavaScript kodu gönderilmiyor ama “kod benzeri” bir şey gönderiliyor; bu da sınırlı bir alan-özel dil (DSL) kullanıyor. İstemci tarafında callback, placeholder değerlerle bir kez çalıştırılıyor ve davranışı record-replay yöntemiyle izlenerek sunucuya bir instruction set gönderiliyor. Sunucu da bu instruction’ları alıp dizinin her bir üyesi için çalıştırıyor.Yani geliştirici sadece normal JS metotlarını kullanmış gibi görünüyor ama aslında bunun dar bir DSL’ye dönüştürülmesini sağlayan bir numara uygulanıyor. Callback yalnızca senkron çalışmalı;
awaitkullanılamıyor. Bunun yerine sadece promise pipelining’e izin veriliyor; böylece tüm süreç yakalanıp sunucuya aktarılıyor ve sunucuda gerektiğinde yeniden yürütülüyorC#’ta bu tür sorunları çözmek için expression tree yapısı var. Entity Framework, lambda ifadelerini alıp SQL sorgularına dönüştürürken bunu kullanıyor. Yani kodu gerçekten çalıştırmadan taramak veya dönüştürmek mümkün oluyor
Örneğin
db.People.Where(p => p.Name == "Joe")ifadesinde Where gerçek bir predicate fonksiyonu almak yerine bir expression aldığı için, gelen kodu tarayıp Name alanının "Joe" ile eşleşip eşleşmediğini kontrol edebiliyor ve bunu SQLWHEREkoşuluna dönüştürüyorJavaScript’te böyle bir mekanizma olmadığı için placeholder değerler verip nasıl davrandığını tek tek kaydederek bunu taklit ediyorlar
Kısa süre önce Tanstack DB’nin sorgu DSL’sini oluştururken de bu record-replay numarasını kullandık rehber bağlantısı. where/select/join callback’lerine RefProxy nesneleri verip, bu nesneler üzerinde hangi prop/işlemlerin gerçekleştiğini izledik.
JS’te normal operatörleri (
==,>vb.) doğrudan yakalayamadığınız için, izlenebilir küçükeq/gt/notgibi fonksiyonlar oluşturup callback’i bir kez çalıştırarak bağlı ifadeyi yakalayıp IR’ye dönüştürdükİlginç şekilde JS spread operatörünü bile izlemeyi başardık
Kenton, bunu capnweb’e de sahte operatörler (
eq,gt,invb.) olarak ekleyip uzaktan tracing özelliği kazandırmanız mümkün mü, merak ediyorumKoşullu ifadeler yasak gibi görünüyor (tıpkı React hook kuralları gibi); bu tür kısıtları nasıl uyguladıklarını merak ediyorum
Bu proje ilginç görünüyor
ML derleyici kütüphanelerine (TensorFlow 1, JAX jit, PyTorch compile vb.) benzer yanları var. İşlemlerin grafiğini tracing ile kuruyor, sonra bunu compile ediyor ya da bir VM’e uygun hâle getirip çalıştırıyor
Bugün dinamik diller yeni bir DSL tanımlamak yerine bir frontend olarak kullanılıyor; AST dönüşümü mevcut betik dilinin içine gizleniyor
ML’de GPU/linalg kernel çalıştırmaları ertelenerek kernel’ler birleştirilirken, Cap'n Web gibi RPC sistemlerinde ağ istekleri ertelenip birden çok network çağrısı birleştirilebiliyor
Sonuçta asıl mesele instruction/data plane ayrımı ve çok küçük ölçekte tek bir CPU bile aslında dağıtık sistem benzeri bir yapı taşıyor (komut/veri önbellek ayrımı)
Cap'n Web’de RPC grafiğinin kendisi instruction rolünü üstleniyor
Bu kalıp gerçekten çok ilginç; sanki katmanlı yapı (derleyicinin üstünde yorumlayıcı, onun üstünde yine derleyici...) sonsuza dek tekrar ediyor gibi. Lispy “code is data, data is code” fikrinin başka bir versiyonu gibi geliyor. Sanki bunun arkasında daha derin bir hikâye var
Dinamik diller artık yeni DSL’lerin frontend’i oluyor ve yeni söz dizimi tanımlamak yerine betiğin içine AST üretimi gömülüyor
Burada TypeScript’in oyun değiştirici olduğunu düşünüyorum. Çünkü JavaScript’in çalışma zamanı esnekliğini (Cap'n Web’in akıllıca kullandığı
Proxygibi) ve tip güvenliğini aynı anda sağlayabiliyorSon zamanlarda ORM tarafında bu fikre takmış durumdayım. Çoğu ORM sıralı ve eager çalışıyor; bu yüzden sorgular yalnızca yürütme anından hemen önce değiştirilebiliyor
Gerçekten composable bir ORM’nin derleyici gibi çalışması gerektiğini düşünüyorum: TypeScript ile SQL üzerinde tamamen tip güvenli bir DSL tanımlayıp bir sorgu AST’si oluşturmalı, sonra en sonda bunu SQL’e derlemeli
Geliştirdiğim Typegres de tam olarak bu fikir üzerine kurulu. Bu tür kalıplar ilginizi çekiyorsa bakabilirsiniz
RPC kütüphanelerinin temel sorunu, round-trip’in nerede ve nasıl gerçekleştiğini gizlemeye çalışmaları
Sadece Cap'n Web’in array
.map()örneğine bakınca bile network round-trip’in gerçekte nerede oluştuğunu anlamak zor.Bunun bir “özellik” değil, daha çok bir “hata” olduğunu düşünüyorum — koda bakınca davranışı doğrudan anlayabilmelisiniz; bunu gizlemek iyi değil
ilgili bağlantı
awaitkullandığınız anda oluşurpromise pipelining sayesinde birden fazla statement’ı arada
awaitolmadan art arda kurabilirsiniz; böylece arada ek network gidiş-gelişi olmaz. En sonda tek birawaityaparsınız, hepsi budurgRPC ve web ile çalıştıysanız, Protobuf’u web’e taşımaya çalışmanın ne kadar acı verici olduğunu bilirsiniz
Cap'n Web’in sadeliği gerçekten çok hoş capnproto belgeleri
Cap'n Web, Cap'n Proto’dan farklı olarak tamamen şemasız. Neredeyse hiç gereksiz boilerplate yok; Cloudflare Workers’ın JavaScript’e özgü yerel RPC hissini güçlü biçimde veriyor
github bağlantısı
kentonv’nin yeni kütüphanesini görünce hemen geldim
GitHub’daki koda bakınca beklenmedik derecede küçük olduğunu görünce şaşırdım. Gerçekten hepsi bu mu diye merak ettim
Teorik olarak sunucu tarafını başka dillere taşımak da çok zor görünmüyor; ben bunu bir Elixir sunucu ve JS/TS frontend ile kullanmak isterdim
Böyle bir dil portunu LLM’ye yaptırmak da eğlenceli olabilir. Bu repoda LLM tabanlı kod olup olmadığını merak ediyorum. Birkaç ay önce kentonv’nin AI tarafından yazılmış (insan tarafından gözden geçirilmiş) bir POC yaptığına dair bir şey görmüştüm
Bugünkü hâliyle bir LLM’nin bu kütüphaneyi yapabileceğini sanmıyorum. İç yapısı çok hassas şekilde birbirine geçen bir yapboz gibi tasarlandı
Kodun kendisinden çok tasarım üzerine düşünmeye zaman harcadım
Bu, iyi bilinen bir spesifikasyonu yeni bir şekilde uygulayan workers-oauth-provider kütüphanesinden tamamen farklı
Kod yapısı Python gibi dinamik dillere görece kolay taşınabilir ama statik tipli dillere zor olur diye düşünüyorum. Çünkü keyfî nesne tiplerine dayanan birçok yer var
OCapN ile benzerlikleri ve önemli farkları da var bakınız
İkisi de capability transfer, promise pipelining ve şemasız modeli destekliyor
Cap'n Web’de OCapN’deki sturdyref (geri yüklenebilir URI) gibi bant dışı capability yok. Bu yüzden API anahtarıyla kimlik doğrulama gerektiğini tahmin ediyorum. sturdyref, elde bulunduran kişiye ilgili endpoint’e erişim hakkı veren, tahmin edilmesi zor bir token gibi düşünülebilir
Ayrıca Cap'n Web’de Alice’in Bob’u Carol ile tanıştırdığı üç taraflı handoff özelliği de yok. Bu dağıtık uygulamalar için kritik; bu nedenle Cap'n Web daha çok geleneksel SaaS tarzı istemci-sunucu kullanımına ve bazı ocap özelliklerini koruyan servislere yakın görünüyor
SturdyRef’in ise her platformda geri yüklenme biçimi farklı olduğu için RPC protokol seviyesinden çok, platforma özgü olarak uygulanması gerektiğini düşünüyorum
Örneğin Cloudflare Workers’ta yakında Durable Object storage üzerinden capability kalıcılığı mümkün olacak, ama uygulama biçimi Workers platformuna özgü
Sandstorm’da da kalıcı capability var ama yalnızca iç servisler için
Bu yüzden Cap’n Proto’dan kalıcı capability kavramını tamamen çıkardık ve web standartlarında buna en yakın şey OAuth oldu
OAuth refresh token tabanlı bir sturdyref tanımı hayal edilebilir ama bu da platformdan bağımsız bir yapı olmaz
Hızlıca göz attığım kadarıyla bu sistem, import/export tablolarını ya da nesne durumunu sunucu tarafında stateful olarak saklamayı gerektiriyor ya da en azından teşvik ediyor gibi görünüyor
Geleneksel RPC’de tüm çağrılar en üst seviyeden gelir ve her çağrıda anahtar vb. iletildiği için istekler birden fazla sunucuya dağıtılsa da sorun olmaz, ama Cap’n Web’de durum öyle görünmüyor
Tabloları serileştirip DB’ye yazarak aynı şekilde sunucu dağıtımı yapmak mümkün mü, yoksa sunucu affinity’si ya da Durable Objects benzeri yapılar mı zorunlu, merak ediyorum
Durum yalnızca tek bir RPC oturumu boyunca korunur
WebSocket kullanıldığında, WebSocket bağlantısı yaşadığı sürece durum da yaşar
HTTP batch taşıması kullanıldığında ise oturum tek bir HTTP isteğinin tamamıyla sınırlıdır ve o istekteki tüm çağrılar tek seferde işlenir
Dolayısıyla Cap’n Web’in birden fazla HTTP isteği/bağlantısı boyunca durumu korumasına gerek yoktur
Ancak bir oturum ortada koparsa ve tüm capability’ler kaybolursa bunun sorun yaratacağı tasarımlardan kaçınmak gerekir. Bağlantı her zaman yeniden kurulabilmeli ve capability’ler geri getirilebilmelidir
Belgeleri okuyunca bunun WebSocket ile affinity koruyan bir yapı gibi göründüğünü düşünüyorum
HTTP batching ise tüm istekleri tek seferde gönderip yanıt bekleme modeli
Bu yaklaşım load balancing’i zorlaştırır. Çok sayıda sohbet istemcisi varsa bağlantılar belli sunucularda yığılabilir. O zaman o sunucuların aşırı yüklenmesi riski oluşur
Sunucuları scale in/out yapmak da zahmetli hâle gelir. Uzun ömürlü bağlantılar sürerken birden fazla isteğin eşzamanlı işlendiği durumda yönetim gerçekten zorlaşır
Bir de şu var: İstemci sürekli push event gönderip hiç yanıt tüketmezse, sunucu o yanıtları bellekte tutmak zorunda kalır; bu da bence DDoS saldırılarını kolaylaştırır
Cap'n Proto belgelerini yıllar önce okuduğuma göre, sunucu ile istemci karşılıklı olarak peer stub değiş tokuşu yapabiliyor
Eğer sunucu C, istemci B üzerinden A’da oluşturulmuş bir stub alırsa, C doğrudan A’yı da çağırabilir
“RPC” aslında uzaktaki bir çağrının yerel bir fonksiyon çağrısından ayırt edilemeyecekmiş gibi görünmesini sağlamayı amaçlayan bir programlama paradigmasıdır
Bunun için pratikte bir wire protocol, istemci/sunucu kütüphaneleri vb. gerekir
Son yıllarda algı çok değişti; artık daha çok REST endpoint’leri gibi, fonksiyon imzası taşıyan yapılardan söz ediliyor
Future, Optional vb. dil özellikleri geldiğinden beri “bu işlem gecikebilir” ya da “başarısız olabilir” gibi özellikleri açıkça ayırt etmek mümkün
Eski RPC’de tüm bunlar gizlenirdi
Tam olarak ne demek istediğinizi merak ettim. Asenkron programlama birçok dilde var. JavaScript, C++, Python, Rust, C# vb. neredeyse hepsini kullandım
Anlatılmak istenen şu: İlk dönem RPC sistemleri ağ isteği sürerken çağıran thread’i blokluyordu ve bu gerçekten kötü bir tasarımdı; bugün asenkronluğun doğal kabul edilmesinin nedeni de bu
Cap'n Web’in yalnızca Cloudflare ürünlerine bağlı kalmayıp bağımsız da var olması beni çok heyecanlandırdı
Belgelerdeki şu kısmı okuyunca merak ettim
Hatta Cap'n Web’in Workers RPC’den daha da ileri gidebileceğini düşünüyorum (aslında pipeline özelliklerinde şimdiden önde)
Cap'n Web’in yapısı çok daha basit olduğu için yeni özellik denemelerini önce Cap'n Web üzerinde yapmamız muhtemel