Bun ile Gerçek Zamanlı Uygulamalar Geliştirmek

Selamlar, bu yazımda Bun ile gerçek zamanlı (realtime) uygulamalar geliştirme konusunu biraz dağıtmak istiyorum. Konu sadece 'WebSocket nasıl yazılır' değil; aynı zamanda Bun'u Node.js karşısında gerçekten ne zaman tercih etmeli, ne zaman 'aman bir saniye' demeli mesele bu. Lafı uzatmadan başlayalım.

Son birkaç yıldır chat uygulaması, canlı bildirim, işbirlikli editör, oyun lobisi gibi anlık güncelleme isteyen ürünler artık 'lüks özellik' değil, beklenti. Sayfayı yenilemek isteyen kullanıcı kalmadı. Bu da backend tarafında bizi sürekli açık bağlantı tutmaya, mesaj broadcast etmeye, bağlantıların yaşam döngüsünü yönetmeye zorluyor. İşte tam burada Bun ilginç bir aday olarak sahneye çıkıyor.

Bun gerçek zamanlı işler için neden cazip?

Bun, JavaScript dünyasında 'her şeyi yapan tek runtime' iddiasıyla ortaya çıktı. Test runner, paket yöneticisi, bundler, hepsi içinde. Ama bence gerçek zamanlı uygulamalar için onu öne çıkaran şey şu özellikler:

  • Yerleşik WebSocket desteği: ws veya socket.io gibi bir kütüphane çekmenize gerek yok. Bun.serve doğrudan WebSocket upgrade'i konuşuyor.
  • Yüksek bağlantı kapasitesi: Aynı donanımda Node'a göre belirgin biçimde daha fazla eşzamanlı bağlantı kaldırabiliyor. Tabii rakam kovalamak istemiyorum, kendi yükünüzde test etmek şart.
  • Düşük başlangıç gecikmesi: Cold start hızlı. Edge ya da serverless senaryolarında bu fark hissediliyor.
  • TypeScript hazır geliyor: Transpile etmeden direkt .ts dosyası çalıştırıyorsunuz, geliştirme döngüsü kısalıyor.

Şahsi kanaatim, bu listenin en değerli maddesi ilki. Çünkü WebSocket için harici bağımlılık çekmemek hem güvenlik yüzeyini küçültüyor hem de pub/sub gibi konularda runtime'la entegre, daha hızlı bir API'ye erişim sağlıyor.

Minimum bir WebSocket sunucusu

Hadi minimum bir örnek görelim. Aşağıdaki kod hem HTTP isteklerini karşılıyor hem de /ws yoluna gelen istekleri WebSocket'e yükseltiyor:

const server = Bun.serve({
  port: 3000,

  fetch(req, server) {
    const url = new URL(req.url);

    if (url.pathname === '/ws') {
      const upgraded = server.upgrade(req, {
        data: { userId: crypto.randomUUID(), connectedAt: Date.now() },
      });
      return upgraded ? undefined : new Response('Upgrade failed', { status: 400 });
    }

    return new Response('Realtime server up');
  },

  websocket: {
    open(ws) {
      console.log(`baglandi: ${ws.data.userId}`);
    },
    message(ws, msg) {
      ws.send(`echo: ${msg}`);
    },
    close(ws) {
      console.log(`ayrildi: ${ws.data.userId}`);
    },
  },
});

console.log(`http://localhost:${server.port}`);

Burada dikkat çekmek istediğim nokta şu: server.upgrade çağrısına geçirdiğimiz data alanı, o bağlantı boyunca ws.data üzerinden erişilebilir. Yani kullanıcı kimliği, oda listesi, son aktivite zamanı gibi bilgileri taşımak için ayrıca bir map tutmanıza gerek kalmıyor. Bu, bence Bun'un en sevdiğim küçük ergonomik kazanımlarından biri.

Pub/sub ile odalar

Gerçek zamanlı uygulamaların büyük çoğunluğu 'oda' ya da 'kanal' kavramına ihtiyaç duyar. Bun'un WebSocket API'sinde subscribe, unsubscribe, publish çağrıları doğrudan bağlantı üstünde yer alıyor:

websocket: {
  open(ws) {
    ws.subscribe(`user:${ws.data.userId}`);
  },
  message(ws, raw) {
    const msg = JSON.parse(raw.toString());
    if (msg.type === 'join') {
      ws.subscribe(`room:${msg.room}`);
      ws.publish(`room:${msg.room}`, JSON.stringify({
        type: 'joined',
        user: ws.data.userId,
      }));
    }
  },
}

Çok temiz değil mi? Tek bir process içinde kalacaksanız ek bir broker'a ihtiyaç bile duymuyorsunuz. Ama dikkat: tek process'te kalmak demek, tek bir makinenin sınırlarına da bağlı kalmak demek. İşte burada hikaye değişiyor.

Ölçeklenmek istediğinde Redis

Birden fazla instance açtığınız anda Bun'un kendi pub/sub'ı yetmiyor; çünkü her instance kendi abonelerini görüyor, diğerlerini görmüyor. Çözüm klasik: araya Redis (veya benzeri bir broker) koyup mesajları instance'lar arasında dağıtmak. Bun'un buna özel bir sihri yok; redis paketini çekip publish/subscribe köprüsünü kendiniz kuruyorsunuz. Yani 'Bun aldım, ölçek geldi' diye bir şey yok - mimariyi siz kuracaksınız.

Sık karşılaşılan tuzaklar

  • Heartbeat'siz bağlantı yönetimi: TCP seviyesinde kapanmış ama sunucunun haberi olmayan zombi bağlantılar birikir, bellek şişer. 30-60 saniyelik ping/pong döngüsü neredeyse zorunlu.
  • ws.data'yı her şeyin deposu yapmak: Kullanışlı diye data içine devasa nesneler koymak, binlerce bağlantıda ciddi bellek baskısı yaratır. Sadece o bağlantıya özgü küçük metaverileri tutun.
  • Tek instance'a güvenip ölçeklendirme planı yapmamak: Erken aşamada Redis adaptörü çekmek fazla geliyor ama prod'da ikinci instance'ı açtığınız anda 'mesajlar neden bazı kullanıcılara gitmiyor' sorusu kapıda. Mimariyi başından çok-instance düşünün.
  • JSON.parse'i try/catch'siz çağırmak: Bozuk bir mesaj tüm bağlantı handler'ını çökertebilir. Her gelen mesaj potansiyel olarak güvenilmezdir.

Peki Bun, Node'un yerini alır mı?

Açıkçası kestirme cevabım: hayır, en azından şu an için. Bun gerçekten hızlı, API'leri zevkli ve WebSocket tarafında işi kolaylaştırıyor. Ama ekosistem hâlâ Node kadar olgun değil; bazı popüler kütüphanelerde uyumsuzluklar çıkıyor, prod tecrübesi daha yeni birikiyor. Bence yeni bir realtime servis kuruyorsanız ve bağımlılıklarınız sade ise Bun çok mantıklı bir tercih. Ama mevcut, büyük bir Node tabanını taşımak gibi bir gündeminiz varsa acele etmeyin, sıradan bir endpoint'i taşıyıp ölçün, sonra karar verin.

Kapanış

Bu yazıda Bun ile gerçek zamanlı uygulama yazmanın temel taşlarına, pub/sub mantığına ve ölçek konusundaki gerçeklere baktık. Bana sorarsanız Bun'un en güçlü olduğu yer tam da burası: küçük bir kod parçasıyla hatırı sayılır bir realtime servis ayağa kaldırabiliyorsunuz. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.