Deno Deploy ile Gerçek Zamanlı WebSocket Uygulaması

Edge computing dünyasında bir şeyler değişiyor ve bu değişimi yakından takip etmek zorundayız. Deno Deploy, klasik sunucu paradigmasını tersine çeviren, kodunuzu dünya genelinde onlarca noktada çalıştıran bir platform. WebSocket desteğiyle birleşince gerçek zamanlı uygulamalar için oldukça ilginç bir senaryo ortaya çıkıyor. Bu yazıda sıfırdan başlayıp production-ready bir WebSocket uygulaması geliştireceğiz, yol boyunca da karşılaştığım tuzakları paylaşacağım.

Deno Deploy’u Anlamak: Sadece Başka Bir Hosting Değil

Deno Deploy’a ilk baktığımda “oh, bir tane daha serverless platform” dedim. Yanılmışım. Buradaki fark şu: V8 isolate’lar üzerinde çalışıyor, her request kendi izole ortamında işleniyor ve kodunuz gerçekten edge’de, kullanıcıya yakın bir noktada execute ediliyor. Istanbul’daki bir kullanıcı için Frankfurt ya da Amsterdam edge node’u devreye girebiliyor.

WebSocket için bu mimarinin önemi büyük. Geleneksel WebSocket uygulamalarında sticky session sorununuz olur, load balancer’ınızın WebSocket bağlantılarını aynı backend’e yönlendirmesi gerekir. Deno Deploy’da bu problem farklı bir boyut kazanıyor çünkü her isolate stateless. Bu hem kısıtlama hem de fırsat.

Başlamadan önce Deno’yu kuralım:

curl -fsSL https://deno.land/install.sh | sh

# Kurulumu dogrulayalim
deno --version

# Deno Deploy CLI'i kuralim
deno install --allow-read --allow-write --allow-env --allow-net --allow-run --no-check -r -f https://deno.land/x/deploy/deployctl.ts

Deno Deploy hesabınızı açtıktan sonra GitHub repository’nizi bağlayabilir ya da deployctl ile direkt push yapabilirsiniz. Ben genellikle GitHub entegrasyonunu tercih ediyorum, her push’ta otomatik deploy çok pratik.

Temel WebSocket Sunucusu

Deno’nun standart kütüphanesi WebSocket işlemlerini oldukça temiz hallediyor. Basit bir echo server ile başlayalım, sonra üstüne inşa edeceğiz:

// main.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";

function handleWebSocket(socket: WebSocket) {
  socket.onopen = () => {
    console.log("Yeni baglanti kuruldu");
    socket.send(JSON.stringify({ 
      type: "welcome", 
      message: "Baglantiniz basariyla kuruldu",
      timestamp: new Date().toISOString()
    }));
  };

  socket.onmessage = (event) => {
    try {
      const data = JSON.parse(event.data);
      console.log("Mesaj alindi:", data);
      
      // Echo server - gelen mesaji geri gonder
      socket.send(JSON.stringify({
        type: "echo",
        original: data,
        timestamp: new Date().toISOString()
      }));
    } catch {
      socket.send(JSON.stringify({ 
        type: "error", 
        message: "Gecersiz JSON formati" 
      }));
    }
  };

  socket.onerror = (error) => {
    console.error("WebSocket hatasi:", error);
  };

  socket.onclose = () => {
    console.log("Baglanti kapatildi");
  };
}

serve((req) => {
  if (req.headers.get("upgrade") === "websocket") {
    const { socket, response } = Deno.upgradeWebSocket(req);
    handleWebSocket(socket);
    return response;
  }
  
  return new Response("WebSocket sunucusu calisiyor", { status: 200 });
}, { port: 8080 });

Bu kodu local’de test edelim:

# Sunucuyu baslat
deno run --allow-net main.ts

# Baska bir terminalde wscat ile test et
npm install -g wscat
wscat -c ws://localhost:8080

# Baglantidan sonra mesaj gonder
> {"type": "ping", "data": "merhaba"}

Broadcast Mekanizması: Asıl Zorluk Burada

Gerçek dünya senaryosuna geçelim: Bir chat uygulaması ya da canlı dashboard düşünün. Bir kullanıcının gönderdiği mesajın diğer tüm bağlı kullanıcılara ulaşması gerekiyor. Deno Deploy’un stateless yapısı burada devreye giriyor.

Tek bir instance üzerinde çalışırken in-memory bir Map kullanabiliriz:

// broadcast-server.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";

interface Client {
  id: string;
  socket: WebSocket;
  username: string;
  connectedAt: Date;
}

// Bu Map sadece bu isolate'a ozgudur
const clients = new Map<string, Client>();

function generateId(): string {
  return crypto.randomUUID();
}

function broadcast(message: string, excludeId?: string) {
  clients.forEach((client, id) => {
    if (id !== excludeId && client.socket.readyState === WebSocket.OPEN) {
      client.socket.send(message);
    }
  });
}

function handleChatSocket(socket: WebSocket) {
  const clientId = generateId();
  let username = `Kullanici_${clientId.slice(0, 6)}`;

  socket.onopen = () => {
    const client: Client = {
      id: clientId,
      socket,
      username,
      connectedAt: new Date()
    };
    
    clients.set(clientId, client);
    
    // Yeni kullaniciya mevcut kullanici listesini gonder
    socket.send(JSON.stringify({
      type: "init",
      clientId,
      username,
      onlineCount: clients.size
    }));
    
    // Diger kullanicilara bildir
    broadcast(JSON.stringify({
      type: "user_joined",
      username,
      onlineCount: clients.size
    }), clientId);
    
    console.log(`${username} baglandi. Toplam: ${clients.size}`);
  };

  socket.onmessage = (event) => {
    const client = clients.get(clientId);
    if (!client) return;

    try {
      const data = JSON.parse(event.data);
      
      switch (data.type) {
        case "message":
          broadcast(JSON.stringify({
            type: "message",
            from: client.username,
            content: data.content,
            timestamp: new Date().toISOString()
          }));
          break;
          
        case "set_username":
          const oldUsername = client.username;
          client.username = data.username;
          username = data.username;
          
          broadcast(JSON.stringify({
            type: "username_changed",
            oldUsername,
            newUsername: data.username
          }));
          break;
      }
    } catch (err) {
      console.error("Mesaj isleme hatasi:", err);
    }
  };

  socket.onclose = () => {
    clients.delete(clientId);
    broadcast(JSON.stringify({
      type: "user_left",
      username,
      onlineCount: clients.size
    }));
    console.log(`${username} ayrildi. Kalan: ${clients.size}`);
  };
}

serve((req) => {
  const url = new URL(req.url);
  
  if (req.headers.get("upgrade") === "websocket") {
    const { socket, response } = Deno.upgradeWebSocket(req);
    handleChatSocket(socket);
    return response;
  }
  
  if (url.pathname === "/status") {
    return new Response(JSON.stringify({
      online: clients.size,
      uptime: performance.now()
    }), { 
      headers: { "content-type": "application/json" } 
    });
  }
  
  return new Response("Chat sunucusu aktif", { status: 200 });
}, { port: 8080 });

Deno KV ile Kalıcı Durum Yönetimi

Gerçek production ortamında mesaj geçmişini saklamak, kullanıcı oturumlarını yönetmek gerekiyor. Deno KV tam burada işe yarıyor. Edge’de çalışan, global olarak replike edilen bir key-value store:

// kv-integration.ts
const kv = await Deno.openKv();

interface Message {
  id: string;
  roomId: string;
  username: string;
  content: string;
  timestamp: string;
}

async function saveMessage(message: Message): Promise<void> {
  const key = ["messages", message.roomId, message.timestamp, message.id];
  await kv.set(key, message);
  
  // Son 100 mesaj limitini korumak icin eski mesajlari sil
  // Basit bir TTL yaklasimi
  await kv.set(
    ["messages", message.roomId, message.timestamp, message.id],
    message,
    { expireIn: 7 * 24 * 60 * 60 * 1000 } // 7 gun
  );
}

async function getRoomHistory(roomId: string, limit = 50): Promise<Message[]> {
  const messages: Message[] = [];
  const prefix = ["messages", roomId];
  
  const iter = kv.list<Message>({ prefix }, { 
    limit,
    reverse: true 
  });
  
  for await (const entry of iter) {
    messages.unshift(entry.value);
  }
  
  return messages;
}

// Oda bazli baglanti sayisini takip et
async function incrementRoomCount(roomId: string): Promise<number> {
  const key = ["room_count", roomId];
  const result = await kv.atomic()
    .sum(key, 1n)
    .commit();
  
  if (!result.ok) {
    throw new Error("Atomic operation basarisiz");
  }
  
  const updated = await kv.get<Deno.KvU64>(key);
  return Number(updated.value?.value ?? 0n);
}

async function decrementRoomCount(roomId: string): Promise<void> {
  const key = ["room_count", roomId];
  const current = await kv.get<Deno.KvU64>(key);
  const currentVal = Number(current.value?.value ?? 0n);
  
  if (currentVal > 0) {
    await kv.set(key, new Deno.KvU64(BigInt(currentVal - 1)));
  }
}

export { saveMessage, getRoomHistory, incrementRoomCount, decrementRoomCount };

Oda Sistemi ile Tam Uygulama

Şimdiye kadar öğrendiklerimi birleştirip oda destekli bir uygulama yapalım. Slack benzeri bir yapı düşünebilirsiniz:

// room-server.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { saveMessage, getRoomHistory } from "./kv-integration.ts";

interface RoomClient {
  id: string;
  socket: WebSocket;
  username: string;
  roomId: string;
}

// Oda bazli musteri haritasi: roomId -> Set of clientId
const rooms = new Map<string, Set<string>>();
const clients = new Map<string, RoomClient>();

function broadcastToRoom(roomId: string, message: string, excludeId?: string) {
  const roomClients = rooms.get(roomId);
  if (!roomClients) return;

  roomClients.forEach((clientId) => {
    if (clientId === excludeId) return;
    const client = clients.get(clientId);
    if (client?.socket.readyState === WebSocket.OPEN) {
      client.socket.send(message);
    }
  });
}

function joinRoom(clientId: string, roomId: string) {
  if (!rooms.has(roomId)) {
    rooms.set(roomId, new Set());
  }
  rooms.get(roomId)!.add(clientId);
}

function leaveRoom(clientId: string, roomId: string) {
  const room = rooms.get(roomId);
  if (room) {
    room.delete(clientId);
    if (room.size === 0) {
      rooms.delete(roomId);
    }
  }
}

async function handleRoomSocket(socket: WebSocket, initialRoom: string) {
  const clientId = crypto.randomUUID();
  let currentRoom = initialRoom || "genel";
  const username = `Kullanici_${clientId.slice(0, 8)}`;

  socket.onopen = async () => {
    const client: RoomClient = {
      id: clientId,
      socket,
      username,
      roomId: currentRoom
    };
    
    clients.set(clientId, client);
    joinRoom(clientId, currentRoom);

    // Oda gecmisini gonder
    const history = await getRoomHistory(currentRoom, 30);
    socket.send(JSON.stringify({
      type: "room_joined",
      roomId: currentRoom,
      username,
      history,
      memberCount: rooms.get(currentRoom)?.size ?? 0
    }));

    broadcastToRoom(currentRoom, JSON.stringify({
      type: "member_joined",
      username,
      roomId: currentRoom,
      memberCount: rooms.get(currentRoom)?.size ?? 0
    }), clientId);
  };

  socket.onmessage = async (event) => {
    const client = clients.get(clientId);
    if (!client) return;

    try {
      const data = JSON.parse(event.data);

      if (data.type === "send_message") {
        const message = {
          id: crypto.randomUUID(),
          roomId: currentRoom,
          username: client.username,
          content: data.content.slice(0, 2000), // Uzunluk siniri
          timestamp: new Date().toISOString()
        };

        await saveMessage(message);
        broadcastToRoom(currentRoom, JSON.stringify({
          type: "new_message",
          message
        }));
      }

      if (data.type === "switch_room") {
        const newRoom = data.roomId;
        
        leaveRoom(clientId, currentRoom);
        broadcastToRoom(currentRoom, JSON.stringify({
          type: "member_left",
          username: client.username,
          roomId: currentRoom
        }));
        
        currentRoom = newRoom;
        client.roomId = newRoom;
        joinRoom(clientId, newRoom);
        
        const history = await getRoomHistory(newRoom, 30);
        socket.send(JSON.stringify({
          type: "room_joined",
          roomId: newRoom,
          username: client.username,
          history,
          memberCount: rooms.get(newRoom)?.size ?? 0
        }));
      }
    } catch (err) {
      console.error("Mesaj hatasi:", err);
    }
  };

  socket.onclose = () => {
    leaveRoom(clientId, currentRoom);
    clients.delete(clientId);
    
    broadcastToRoom(currentRoom, JSON.stringify({
      type: "member_left",
      username,
      roomId: currentRoom,
      memberCount: rooms.get(currentRoom)?.size ?? 0
    }));
  };
}

serve(async (req) => {
  const url = new URL(req.url);
  
  if (req.headers.get("upgrade") === "websocket") {
    const roomId = url.searchParams.get("room") ?? "genel";
    const { socket, response } = Deno.upgradeWebSocket(req);
    await handleRoomSocket(socket, roomId);
    return response;
  }

  return new Response("Oda sunucusu hazir", { status: 200 });
}, { port: 8080 });

Deploy ve Test Süreci

Local geliştirme tamamlandı, şimdi Deno Deploy’a gönderelim:

# Proje dizininde deployctl ile deploy et
deployctl deploy --project=websocket-chat main.ts

# Ya da belirli bir token ile
deployctl deploy 
  --project=websocket-chat 
  --token=<DENO_DEPLOY_TOKEN> 
  room-server.ts

# Deploy durumunu kontrol et
deployctl deployments list --project=websocket-chat

GitHub Actions ile CI/CD pipeline kuralım:

# .github/workflows/deploy.yml
name: Deno Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    permissions:
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v3
      
      - name: Deno Deploy
        uses: denoland/deployctl@v1
        with:
          project: "websocket-chat"
          entrypoint: "room-server.ts"

Deploy sonrası bağlantıyı test edelim:

# wscat ile production endpoint'i test et
wscat -c "wss://websocket-chat.deno.dev?room=genel"

# Baglantidan sonra mesaj gonder
> {"type": "send_message", "content": "Merhaba dunya!"}

# Oda degistir
> {"type": "switch_room", "roomId": "teknoloji"}

# Baska bir terminalde ayni odaya baglan ve mesajlasmayi gozlemle
wscat -c "wss://websocket-chat.deno.dev?room=genel"

Dikkat Edilmesi Gereken Noktalar

Deno Deploy ile WebSocket uygulaması geliştirirken birkaç önemli konuya değinmem gerekiyor.

Isolate başına bağlantı limiti: Deno Deploy, tek bir isolate üzerinde sınırlı sayıda eşzamanlı bağlantıyı destekler. Yüksek trafikli uygulamalarda bu limiti aşabilirsiniz. Deno’nun resmi dokümantasyonu bu konuda net değer vermese de pratikte ölçekleme için Deno KV’nin broadcast mekanizmasını veya harici bir pub/sub sistemini (Redis, Upstash) kullanmanız gerekebilir.

In-memory durum paylaşımı yapılamaz: Farklı edge lokasyonlarındaki isolate’lar arasında memory paylaşımı yok. Frankfurt’taki bir kullanıcı ile Amsterdam’daki kullanıcı farklı isolate’larda olabilir. Broadcast’in tüm kullanıcılara ulaşması için Deno KV’nin BroadcastChannel API’sini kullanın:

// KV tabanli broadcast kanali
const channel = new BroadcastChannel("chat_room");

channel.onmessage = (event) => {
  const { roomId, message } = event.data;
  // Bu isolate'daki o odadaki kullanicilara ilet
  broadcastToRoom(roomId, JSON.stringify(message));
};

function globalBroadcast(roomId: string, message: object) {
  channel.postMessage({ roomId, message });
  // Lokaldeki kullanicilara da ilet
  broadcastToRoom(roomId, JSON.stringify(message));
}

Heartbeat mekanizması: WebSocket bağlantıları sessiz kalırsa edge proxy’leri bağlantıyı kesebilir. 30 saniyede bir ping göndermek iyi bir pratik:

function setupHeartbeat(socket: WebSocket): number {
  return setInterval(() => {
    if (socket.readyState === WebSocket.OPEN) {
      socket.send(JSON.stringify({ type: "ping" }));
    }
  }, 25000);
}

Mesaj boyutu: Edge ortamında büyük mesajlar hem gecikmeye hem maliyet artışına neden olur. Mesaj içeriklerini 10KB ile sınırlandırmak mantıklı.

  • CPU time limiti: Her request için CPU süresi sınırı var, uzun süren işlemler için dikkatli olun
  • Bellek kullanımı: Isolate başına bellek sınırı var, büyük in-memory yapılardan kaçının
  • Cold start: İlk bağlantıda hafif bir gecikme olabilir, kritik uygulamalar için warm-up stratejisi düşünebilirsiniz
  • WebSocket timeout: Deno Deploy’da WebSocket bağlantıları varsayılan olarak belirli bir süre inaktif kalırsa kapatılabilir

Sonuç

Deno Deploy ile WebSocket uygulaması geliştirmek, geleneksel Node.js yaklaşımından farklı düşünmeyi gerektiriyor. Stateless mimari başlangıçta kısıtlama gibi görünse de aslında ölçeklenebilirliği zorla sağlıyor; iyi pratikleri benimsemek zorunda kalıyorsunuz.

Uygulamaları production’a almadan önce mutlaka şunları düşünün: Global broadcast için BroadcastChannel veya harici pub/sub entegrasyonu, mesaj kalıcılığı için Deno KV TTL stratejisi ve bağlantı yönetimi için heartbeat mekanizması. Bu üç temel olmadan gerçek dünya yükünü kaldırmak güç.

Edge computing paradigması WebSocket için doğal bir eşleşme sunuyor, gecikme süreleri gerçekten düşüyor. Ancak bu avantajı kullanabilmek için mimarinin kısıtlamalarını da içselleştirmek gerekiyor. Deno Deploy’un henüz olgunlaşmakta olan bir platform olduğunu, API’lerinin değişebildiğini ve bazı özelliklerin beta aşamasında olduğunu da aklın bir köşesinde tutun.

Uçtan uca bir WebSocket uygulaması için gereken altyapı bu yazıda mevcut. KV entegrasyonunu projenizin ihtiyacına göre şekillendirin, oda mantığını kendi kullanım senaryonuza uyarlayın ve load test yapmadan production’a çıkmayın.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir