---
title: "Echtzeit-Chat in Astro mit Cloudflare Durable Objects und WebSocket-Hibernation"
description: "Erfahren Sie, wie Sie eine Echtzeit-Chat-Anwendung mit Astro und Cloudflare Durable Objects unter Nutzung der WebSocket-Hibernation erstellen. Implementieren Sie kosteneffiziente WebSocket-Verbindungen, Nachrichtenpersistenz und Benutzeranwesenheitsanzeigen am Edge."
source: "https://www.launchfa.st/de/blog/real-time-chat-astro-cloudflare-durable-objects"
author: "Rishi Raj Jain"
created_at: 2025-12-04T12:00:00.000Z
keywords: "astro, cloudflare, durable objects, websockets, websocket hibernation, real-time, chat, edge computing"
---

> **Site index for agents**
> Fetch https://www.launchfa.st/llms.txt to discover every page on this site.
> Every page is also available as Markdown by appending `.md` to its URL.

<img width="1600" height="900" alt="Echtzeit-Chat in Astro mit Cloudflare Durable Objects erstellen" decoding="async" loading="eager" class="mt-4 border rounded-sm bg-cover bg-center bg-no-repeat transform will-change-auto" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/astro-realtime-chat-durable-objects.png" />

Echtzeitfunktionen wie Chat, Benachrichtigungen oder kollaboratives Bearbeiten erfordern traditionell komplexe Infrastruktur mit WebSocket-Servern, Redis für Pub/Sub und verteilter Zustandsverwaltung. [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects/) ändert das, indem es zustandsbehaftete, stark konsistente Objekte am Edge bereitstellt, die WebSocket-Verbindungen nativ verarbeiten können.

In diesem umfassenden Leitfaden erfahren Sie, wie Sie eine Echtzeit-Chat-Anwendung mit Astro und Cloudflare Durable Objects unter Nutzung der [WebSocket-Hibernation-API](https://developers.cloudflare.com/durable-objects/examples/websocket-hibernation-server/) erstellen. Wir behandeln WebSocket-Verbindungen, Nachrichtenpersistenz, Benutzeranwesenheitsanzeigen und raumbasierten Chat, alles am Edge mit null Serververwaltung und **deutlich reduzierten Kosten** dank Hibernation.

- [Beispielprojekt auf GitHub ansehen](https://github.com/rishi-raj-jain/my-realtime-chat-app)

## Demo

<img width="1600" height="900" alt="Demo: Echtzeit-Chat in Astro mit Cloudflare Durable Objects" decoding="async" loading="eager" class="mt-4 border rounded-sm bg-cover bg-center bg-no-repeat transform will-change-auto" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/astro-realtime-chat-demo.gif" />

## Voraussetzungen

Sie benötigen Folgendes:

- [Node.js 20](https://nodejs.org/en/blog/announcements/v20-release-announce) oder höher
- Ein [Cloudflare-Konto](https://dash.cloudflare.com/sign-up)

## Durable Objects und WebSockets verstehen

**Durable Objects** sind Cloudflares Lösung für zustandsbehaftete Anwendungen am Edge. Im Gegensatz zu traditionellen, zustandslosen Serverless-Funktionen halten Durable Objects Zustand im Speicher und können WebSocket-Verbindungen verarbeiten.

Wesentliche Merkmale:

- **Single-Threaded-Ausführung**: Jede Durable-Object-Instanz läuft an einem einzigen Standort und vermeidet Race Conditions
- **Starke Konsistenz**: Zustandsänderungen sind sofort für alle Verbindungen zu diesem Objekt sichtbar
- **Integrierter Speicher**: Persistenter Key-Value-Speicher, der Neustarts übersteht
- **WebSocket-Unterstützung**: Native Unterstützung für langlaufende WebSocket-Verbindungen
- **WebSocket-Hibernation**: Durable Objects können hibernieren und WebSocket-Verbindungen gleichzeitig offen halten, das senkt die Kosten erheblich

## Warum WebSocket-Hibernation?

Cloudflares [WebSocket-Hibernation-API](https://developers.cloudflare.com/durable-objects/best-practices/websockets/) ist der empfohlene Ansatz für WebSocket-Anwendungen auf Durable Objects. Gründe dafür:

**Ohne Hibernation:**
- Das Durable Object bleibt im Speicher, solange WebSocket-Verbindungen offen sind
- Sie zahlen für die gesamte Dauer, auch in Leerlaufphasen
- Höherer Speicherverbrauch in Ihrer Anwendung

**Mit Hibernation:**
- Das Durable Object kann bei Inaktivität aus dem Speicher entfernt werden
- WebSocket-Verbindungen bleiben offen, auch wenn das DO hiberniert
- Bei eingehender Nachricht wird das DO neu erstellt und die Nachricht zugestellt
- **Deutlich niedrigere Laufzeitkosten**, Sie zahlen nur, wenn das DO aktiv verarbeitet

Die Hibernation-API nutzt spezielle Methoden (`webSocketMessage`, `webSocketClose`, `webSocketError`) statt Event-Listener und verwendet `serializeAttachment`/`deserializeAttachment`, um Sitzungszustand über Hibernationszyklen hinweg zu persistieren.

## Wie sich Durable Objects von traditionellen WebSocket-Servern unterscheiden

Traditionelle WebSocket-Architektur:

```
Client → Load Balancer → WebSocket-Server (Node.js) → Redis (Pub/Sub) → Datenbank
                        ↓
                    Mehrere Serverinstanzen erfordern Koordination
```

Durable-Objects-Architektur:

```
Client → Cloudflare Edge → Durable Object (eine Instanz pro Chat-Raum)
                           ↓
                        Integrierter Zustand + Speicher
```

Vorteile:

- **Kein Redis nötig**: Zustand liegt im Speicher des Durable Objects
- **Keine Koordinationskomplexität**: Eine Instanz = keine Race Conditions
- **Globales Deployment**: Läuft am Cloudflare-Edge, nah bei den Nutzern
- **Automatische Skalierung**: Cloudflare erstellt Instanzen bei Bedarf
- **Null Serververwaltung**: Keine Server zu konfigurieren oder zu warten

## Architekturüberblick

Unsere Chat-Anwendung hat folgende Struktur:

```
1. Astro-Seite (src/pages/chat/[roomId].astro)
   → Rendert die Chat-Oberfläche
   → Enthält clientseitiges JavaScript für WebSocket

2. WebSocket-API-Endpunkt (src/pages/api/chat/[roomId].ts)
   → Verarbeitet WebSocket-Upgrade-Anfragen
   → Verbindet Clients mit dem passenden Durable Object

3. Worker mit ChatRoom-Durable-Object (src/worker.ts)
   → Exportiert die ChatRoom-Durable-Object-Klasse
   → Nutzt die WebSocket-Hibernation-API für Kosteneffizienz
   → Verwaltet WebSocket-Verbindungen für einen Raum
   → Sendet Nachrichten an alle verbundenen Clients
   → Persistiert Nachrichtenverlauf und Sitzungsdaten
   → Verfolgt Benutzeranwesenheit über Hibernationszyklen hinweg
```

Jeder Chat-Raum erhält eine eigene Durable-Object-Instanz, identifiziert durch die Raum-ID. Alle Clients in diesem Raum verbinden sich mit derselben Instanz und gewährleisten so starke Konsistenz.

## Neue Astro-Anwendung erstellen

Beginnen wir mit einem neuen Astro-Projekt. Führen Sie den folgenden Befehl aus:

```bash
npm create astro@latest realtime-chat
```

Wählen Sie bei den Prompts:

- `Use minimal (empty) template`, wenn gefragt wird, wie das neue Projekt starten soll.
- `Yes`, wenn Abhängigkeiten installiert werden sollen.
- `Yes`, wenn ein Git-Repository initialisiert werden soll.

Sobald das erledigt ist, wechseln Sie ins Projektverzeichnis und starten die App:

```bash
cd realtime-chat
npm install wrangler
npm run dev
```

Die App sollte unter [localhost:4321](http://localhost:4321/) laufen.

## Cloudflare-Adapter in Ihr Astro-Projekt integrieren

Um Ihr Astro-Projekt auf Cloudflare Workers bereitzustellen und Cloudflare KV zu nutzen, installieren Sie den Cloudflare-Adapter. Führen Sie den folgenden Befehl aus:

```bash
npx astro add cloudflare
```

Wählen Sie bei allen Prompts **Yes**.

Das installiert `@astrojs/cloudflare` und konfiguriert Ihr Projekt für serverseitiges Rendering auf Cloudflare Workers.

Aktualisieren Sie nun Ihre `astro.config.mjs`, um den Worker-Einstiegspunkt anzugeben:

```javascript
// File: astro.config.mjs

import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
  output: 'server',
  adapter: cloudflare({
    workerEntryPoint: {
      path: './src/worker.ts',
      namedExports: ['ChatRoom']
    }
  })
});
```

Die `workerEntryPoint`-Konfiguration teilt Astro mit, wo Ihre Durable-Object-Klasse zu finden ist. Das Array `namedExports` gibt an, welche Klassen aus dem Worker exportiert werden.

### wrangler.jsonc konfigurieren

Erstellen Sie eine `wrangler.jsonc`-Datei im Projektroot:

```jsonc
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "main": "dist/_worker.js/index.js",
  "name": "realtime-chat",
  "compatibility_date": "2025-12-04",
  "compatibility_flags": [
    "nodejs_compat",
    "global_fetch_strictly_public"
  ],
  "assets": {
    "binding": "ASSETS",
    "directory": "./dist"
  },
  "observability": {
    "enabled": true
  },
  "durable_objects": {
    "bindings": [
      {
        "name": "ChatRoom",
        "class_name": "ChatRoom"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": [
        "ChatRoom"
      ]
    }
  ]
}
```

Der Abschnitt `durable_objects.bindings` erstellt ein Binding namens `ChatRoom`, mit dem Ihr Code auf die Durable-Object-Klasse `ChatRoom` zugreifen kann. Die Migration `new_sqlite_classes` aktiviert SQLite-gestützten Speicher für bessere Performance. Die `compatibility_flags` stellen Node.js-Kompatibilität für Funktionen wie `crypto.randomUUID()` sicher.

### Umgebungstypen konfigurieren

Aktualisieren Sie `src/env.d.ts`, um TypeScript-Definitionen für Durable Objects hinzuzufügen:

```typescript
/// <reference types="astro/client" />
/// <reference types="@cloudflare/workers-types" />

type DurableObjectNamespace = import('@cloudflare/workers-types').DurableObjectNamespace

type ENV = {
  ChatRoom: DurableObjectNamespace
}

type Runtime = import('@astrojs/cloudflare').Runtime<ENV>

declare namespace App {
  interface Locals extends Runtime {
    // Add custom locals here
  }
}
```

Das sorgt für Typsicherheit beim Arbeiten mit Durable Objects in Ihrer Astro-Anwendung.

## ChatRoom-Durable-Object mit Hibernation erstellen

Durable Objects werden in der Worker-Datei zusammen mit den Handler-Exports definiert. Wir nutzen die [WebSocket-Hibernation-API](https://developers.cloudflare.com/durable-objects/examples/websocket-hibernation-server/) für bessere Kosteneffizienz. Erstellen Sie `src/worker.ts`:

```typescript
// File: src/worker.ts

import { handle } from '@astrojs/cloudflare/handler';
import type { SSRManifest } from 'astro';
import { App } from 'astro/app';
import { DurableObject } from 'cloudflare:workers';

interface SessionAttachment {
  id: string
  userId: string
  username: string
}

interface ChatMessage {
  id: string
  type: 'message' | 'join' | 'leave' | 'presence'
  userId: string
  username: string
  content?: string
  timestamp: number
}

class ChatRoom extends DurableObject<ENV> {
  // Map of WebSocket -> session data
  // When the DO hibernates, this gets reconstructed in the constructor
  private sessions: Map<WebSocket, SessionAttachment>
  private messageHistory: ChatMessage[]

  constructor(ctx: DurableObjectState, env: ENV) {
    super(ctx, env)
    
    this.sessions = new Map()
    this.messageHistory = []
    
    // Restore hibernating WebSocket connections
    // When the DO wakes up, we need to restore session data from attachments
    this.ctx.getWebSockets().forEach((ws) => {
      const attachment = ws.deserializeAttachment() as SessionAttachment | null
      if (attachment) {
        this.sessions.set(ws, attachment)
      }
    })
    
    // Set up automatic ping/pong responses
    // This keeps connections alive without waking the DO
    this.ctx.setWebSocketAutoResponse(
      new WebSocketRequestResponsePair('ping', 'pong')
    )
    
    // Load message history from storage on initialization
    this.ctx.blockConcurrencyWhile(async () => {
      const stored = await this.ctx.storage.get<ChatMessage[]>('messages')
      if (stored) this.messageHistory = stored
    })
  }

  /**
   * Handle HTTP requests to this Durable Object
   * This is called when a client wants to establish a WebSocket connection
   */
  async fetch(request: Request): Promise<Response> {
    // Parse the URL to get query parameters (userId and username)
    const url = new URL(request.url)
    const userId = url.searchParams.get('userId')
    const username = url.searchParams.get('username')
    if (!userId || !username)
      return new Response('Missing userId or username', { status: 400 })
    // Expect a WebSocket upgrade request
    const upgradeHeader = request.headers.get('Upgrade')
    if (upgradeHeader !== 'websocket')
      return new Response('Expected WebSocket upgrade', { status: 426 })
    // Create a WebSocket pair (client and server)
    const pair = new WebSocketPair()
    const [client, server] = Object.values(pair)

    // Accept the WebSocket connection using the Hibernation API
    // Unlike server.accept(), this allows the DO to hibernate while
    // keeping the WebSocket connection open
    this.ctx.acceptWebSocket(server)

    // Generate a unique session ID
    const id = crypto.randomUUID()

    // Create session attachment data
    const attachment: SessionAttachment = { id, userId, username }

    // Serialize the attachment to the WebSocket
    // This data persists across hibernation cycles
    server.serializeAttachment(attachment)

    // Add to active sessions
    this.sessions.set(server, attachment)

    // Send message history to the newly connected client
    this.sendMessageHistory(server)

    // Broadcast join notification to all clients
    this.broadcast({
      id: crypto.randomUUID(),
      type: 'join',
      userId,
      username,
      timestamp: Date.now(),
    })

    // Send current presence to the new user
    this.sendPresence(server)

    // Return the client WebSocket in the response
    return new Response(null, {
      status: 101,
      webSocket: client,
    })
  }

  /**
   * Handle incoming WebSocket messages (Hibernation API)
   * Called when a message is received, even after hibernation
   */
  async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string): Promise<void> {
    // Get session data from the map (or deserialize if just woken)
    let session = this.sessions.get(ws)
    if (!session) {
      session = ws.deserializeAttachment() as SessionAttachment
      if (session) this.sessions.set(ws, session)
    }
    if (!session) return

    try {
      const parsed = JSON.parse(message as string)
      if (parsed.type === 'message' && parsed.content) {
        // Create a chat message
        const chatMessage: ChatMessage = {
          id: crypto.randomUUID(),
          type: 'message',
          userId: session.userId,
          username: session.username,
          content: parsed.content,
          timestamp: Date.now(),
        }

        // Add to history
        this.messageHistory.push(chatMessage)
        // Persist to storage (limit to last 100 messages)
        if (this.messageHistory.length > 100)
          this.messageHistory = this.messageHistory.slice(-100)
        this.ctx.storage.put('messages', this.messageHistory)

        // Broadcast to all connected clients
        this.broadcast(chatMessage)
      }
    } catch (error) {
      console.error('Error handling message:', error)
    }
  }

  /**
   * Handle WebSocket close events (Hibernation API)
   * Called when a client disconnects
   */
  async webSocketClose(
    ws: WebSocket,
    code: number,
    reason: string,
    wasClean: boolean
  ): Promise<void> {
    // Get session data
    const session = this.sessions.get(ws) || 
      (ws.deserializeAttachment() as SessionAttachment | null)
    
    // Remove from sessions
    this.sessions.delete(ws)

    // Broadcast leave notification
    if (session) {
      this.broadcast({
        id: crypto.randomUUID(),
        type: 'leave',
        userId: session.userId,
        username: session.username,
        timestamp: Date.now(),
      })
    }

    // Close the WebSocket
    ws.close(code, 'Durable Object is closing WebSocket')
  }

  /**
   * Handle WebSocket errors (Hibernation API)
   */
  async webSocketError(ws: WebSocket, error: unknown): Promise<void> {
    console.error('WebSocket error:', error)
    // Treat errors as disconnections
    await this.webSocketClose(ws, 1011, 'WebSocket error', false)
  }

  /**
   * Broadcast a message to all connected clients
   */
  private broadcast(message: ChatMessage): void {
    const messageStr = JSON.stringify(message)

    // Send to all active sessions
    this.sessions.forEach((_, ws) => {
      try {
        ws.send(messageStr)
      } catch (error) {
        // Connection might be closed, will be cleaned up by close handler
        console.error('Error broadcasting to session:', error)
      }
    })
  }

  /**
   * Send message history to a specific WebSocket
   */
  private sendMessageHistory(ws: WebSocket): void {
    const historyMessage = JSON.stringify({
      type: 'history',
      messages: this.messageHistory,
    })
    try {
      ws.send(historyMessage)
    } catch (error) {
      console.error('Error sending history:', error)
    }
  }

  /**
   * Send current user presence to a specific WebSocket
   */
  private sendPresence(ws: WebSocket): void {
    const users = Array.from(this.sessions.values()).map((s) => ({
      userId: s.userId,
      username: s.username,
    }))
    const presenceMessage = JSON.stringify({
      type: 'presence',
      users,
    })

    try {
      ws.send(presenceMessage)
    } catch (error) {
      console.error('Error sending presence:', error)
    }
  }

  /**
   * Handle Durable Object alarm for cleanup tasks
   */
  async alarm(): Promise<void> {
    // Clean up old messages (older than 24 hours)
    const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000
    this.messageHistory = this.messageHistory.filter((msg) => msg.timestamp > oneDayAgo)
    await this.ctx.storage.put('messages', this.messageHistory)
    // Schedule next cleanup in 1 hour
    await this.ctx.storage.setAlarm(Date.now() + 60 * 60 * 1000)
  }
}

export function createExports(manifest: SSRManifest) {
  const app = new App(manifest);
  return {
    default: {
      async fetch(request, env, ctx) {
        // @ts-expect-error - request is not typed correctly
        return handle(manifest, app, request, env, ctx);
      }
    } satisfies ExportedHandler<ENV>,
    ChatRoom,
  }
}
```

Diese Worker-Datei implementiert die **WebSocket-Hibernation-API**, Cloudflares empfohlenen Ansatz für WebSocket-Anwendungen:

1. **Definiert das ChatRoom-Durable-Object** mit hibernationsbewusster Chat-Raum-Logik:
   - **WebSocket-Hibernation**: Nutzt `ctx.acceptWebSocket()` statt `server.accept()` und ermöglicht so die Hibernation des DO
   - **Sitzungspersistenz**: Nutzt `serializeAttachment()`/`deserializeAttachment()`, um Sitzungsdaten über Hibernation hinweg zu erhalten
   - **Automatisches Ping/Pong**: Nutzt `setWebSocketAutoResponse()`, um Verbindungen ohne Aufwecken des DO am Leben zu halten
   - **Hibernationsbewusste Handler**: Implementiert die Methoden `webSocketMessage`, `webSocketClose` und `webSocketError`
   - **Sitzungswiederherstellung**: Rekonstruiert die Sessions-Map aus hibernierenden WebSockets im Konstruktor
   - **Nachrichtenpersistenz**: Speichert die letzten 100 Nachrichten im Durable-Object-Speicher
   - **Benutzeranwesenheit**: Verfolgt, wer gerade im Raum ist
   - **Automatische Bereinigung**: Nutzt Alarms zur Bereinigung alter Nachrichten

2. **Exportiert Worker und Durable Object** über die Funktion `createExports`:
   - Der `default`-Export verarbeitet alle HTTP-Anfragen über Astros Handler
   - Der `ChatRoom`-Export macht das Durable Object für Cloudflare verfügbar
   - Die Funktion erhält das SSR-Manifest aus Astros Build-Prozess

### Wichtige Unterschiede zum Ansatz ohne Hibernation

| Merkmal | Ohne Hibernation | Mit Hibernation |
|---------|---------------------|------------------|
| WebSocket annehmen | `server.accept()` | `ctx.acceptWebSocket(server)` |
| Nachrichten verarbeiten | `addEventListener('message')` | `webSocketMessage()`-Methode |
| Schließen verarbeiten | `addEventListener('close')` | `webSocketClose()`-Methode |
| Sitzungsspeicher | Nur im Speicher | `serializeAttachment()` |
| DO-Speichernutzung | Immer im Speicher | Kann entfernt werden |
| Abrechnung | Gesamte Verbindungsdauer | Nur bei Verarbeitung |

## WebSocket-Verbindungsbehandlung mit Hibernation implementieren

Die `fetch`-Methode des Durable Objects verarbeitet WebSocket-Upgrade-Anfragen. Mit der Hibernation-API passiert beim Verbinden eines Clients Folgendes:

1. Validierung der Parameter `userId` und `username`
2. Erstellung eines WebSocket-Paars (eins für den Client, eins für den Server)
3. Annahme des serverseitigen WebSockets mit `ctx.acceptWebSocket()` (aktiviert Hibernation)
4. Erstellung von Sitzungsdaten und Serialisierung an den WebSocket mit `serializeAttachment()`
5. Hinzufügen des WebSockets zur Sessions-Map
6. Senden des Nachrichtenverlaufs an den neuen Client
7. Broadcast einer Beitrittsbenachrichtigung an alle anderen Clients
8. Rückgabe des clientseitigen WebSockets in der Antwort

Der zentrale Punkt: Jeder Chat-Raum ist eine einzelne Durable-Object-Instanz. Alle Clients, die demselben Raum beitreten, verbinden sich mit derselben Instanz. Mit Hibernation kann das DO in Leerlaufphasen aus dem Speicher entfernt werden, während WebSocket-Verbindungen offen bleiben. Bei eingehender Nachricht erstellt Cloudflare die DO-Instanz neu und liefert die Nachricht an die Methode `webSocketMessage`.

### Sitzungswiederherstellung nach Hibernation

Wenn das DO nach Hibernation aufwacht, läuft der Konstruktor erneut. Sitzungsdaten werden wiederhergestellt durch:

```typescript
// In the constructor
this.ctx.getWebSockets().forEach((ws) => {
  const attachment = ws.deserializeAttachment() as SessionAttachment | null
  if (attachment) {
    this.sessions.set(ws, attachment)
  }
})
```

Damit werden alle aktiven WebSocket-Verbindungen abgerufen und deren angehängte Sitzungsdaten deserialisiert, der Zustand vor der Hibernation ist vollständig wiederhergestellt.

## WebSocket-API-Endpunkt erstellen

Erstellen Sie nun einen API-Endpunkt, der WebSocket-Verbindungen an das passende Durable Object weiterleitet. Erstellen Sie `src/pages/api/chat/[roomId].ts`:

```typescript
// File: src/pages/api/chat/[roomId].ts

import type { APIRoute } from 'astro'

/**
 * GET /api/chat/:roomId
 * 
 * Handles WebSocket upgrade requests and connects clients to the chat room
 */
export const GET: APIRoute = async ({ params, request, locals }) => {
  const { roomId } = params
  if (!roomId)
    return new Response('Room ID is required', { status: 400 })
  // Get the ChatRoom Durable Object namespace
  const ChatRoomNamespace = locals.runtime.env.ChatRoom
  if (!ChatRoomNamespace)
    return new Response('Chat room binding not found', { status: 500 })
  // Get a Durable Object ID for this room
  // idFromName ensures the same room ID always maps to the same Durable Object
  const durableObjectId = ChatRoomNamespace.idFromName(roomId)
  // Get a stub (reference) to the Durable Object
  const durableObjectStub = ChatRoomNamespace.get(durableObjectId)
  // Forward the request to the Durable Object
  // The Durable Object's fetch method will handle the WebSocket upgrade
  return durableObjectStub.fetch(request)
}
```

Dieser Endpunkt ist einfach, aber entscheidend:

1. Extrahiert die `roomId` aus der URL
2. Holt den `ChatRoom`-Durable-Object-Namespace aus der Umgebung (entspricht dem Binding-Namen in `wrangler.jsonc`)
3. Erstellt eine Durable-Object-ID mit `idFromName(roomId)`, dieselbe Raum-ID führt immer zur selben Durable-Object-Instanz
4. Holt einen Stub (Referenz) auf dieses Durable Object
5. Leitet die gesamte Anfrage an die `fetch`-Methode des Durable Objects weiter

Die Methode `idFromName` ist wichtig: Sie mappt deterministisch einen String (die Raum-ID) auf eine bestimmte Durable-Object-Instanz. Alle Clients, die „room-123“ beitreten, verbinden sich mit exakt derselben Instanz.

## Chat-Client-Oberfläche erstellen

Erstellen Sie eine Chat-Raum-Seite unter `src/pages/chat/[roomId].astro`:

```astro
---
// File: src/pages/chat/[roomId].astro

const { roomId } = Astro.params;

if (!roomId) return Astro.redirect("/");

// Generate a random user ID and username (in production, use authentication)
const userId = crypto.randomUUID();
const defaultUsername = `User${Math.floor(Math.random() * 10000)}`;
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Chat Room: {roomId}</title>
  </head>
  <body>
    <div id="app">
      <h1>Chat Room: {roomId}</h1>
      
      <div id="connection-status">
        <span id="status-indicator">●</span>
        <span id="status-text">Connecting...</span>
      </div>

      <div id="presence-container">
        <strong>Online Users:</strong>
        <div id="user-list"></div>
      </div>

      <div id="messages-container"></div>

      <form id="message-form">
        <input
          type="text"
          id="message-input"
          placeholder="Type a message..."
          autocomplete="off"
          required
        />
        <button type="submit">Send</button>
      </form>
    </div>

    <script define:vars={{ roomId, userId, defaultUsername }}>
      // WebSocket connection
      let ws = null;
      let reconnectAttempts = 0;
      const maxReconnectAttempts = 5;
      let username = defaultUsername;

      // DOM elements
      const statusIndicator = document.getElementById("status-indicator");
      const statusText = document.getElementById("status-text");
      const messagesContainer =
        document.getElementById("messages-container");
      const messageForm = document.getElementById("message-form");
      const messageInput = document.getElementById("message-input");
      const userList = document.getElementById("user-list");

      /**
       * Connect to the WebSocket server
       */
      function connect() {
        // Determine WebSocket protocol (ws:// or wss://)
        const protocol =
          window.location.protocol === "https:" ? "wss:" : "ws:";
        const host = window.location.host;

        // Build WebSocket URL
        const wsUrl = `${protocol}//${host}/api/chat/${roomId}?userId=${userId}&username=${encodeURIComponent(username)}`;

        // Create WebSocket connection
        ws = new WebSocket(wsUrl);

        // Connection opened
        ws.addEventListener("open", () => {
          console.log("WebSocket connected");
          reconnectAttempts = 0;
          updateConnectionStatus("connected");
        });

        // Listen for messages
        ws.addEventListener("message", (event) => {
          handleMessage(event.data);
        });

        // Connection closed
        ws.addEventListener("close", () => {
          console.log("WebSocket closed");
          updateConnectionStatus("disconnected");
          attemptReconnect();
        });

        // Connection error
        ws.addEventListener("error", (error) => {
          console.error("WebSocket error:", error);
          updateConnectionStatus("error");
        });
      }

      /**
       * Handle incoming WebSocket messages
       */
      function handleMessage(data) {
        try {
          const message = JSON.parse(data);

          switch (message.type) {
            case "history":
              // Received message history
              message.messages.forEach((msg) =>
                displayMessage(msg),
              );
              break;

            case "message":
              // Received a new chat message
              displayMessage(message);
              break;

            case "join":
              // User joined
              displaySystemMessage(
                `${message.username} joined the room`,
              );
              break;

            case "leave":
              // User left
              displaySystemMessage(
                `${message.username} left the room`,
              );
              break;

            case "presence":
              // Update user list
              updateUserList(message.users);
              break;

            default:
              console.warn("Unknown message type:", message.type);
          }
        } catch (error) {
          console.error("Error parsing message:", error);
        }
      }

      /**
       * Display a chat message in the UI
       */
      function displayMessage(message) {
        const messageEl = document.createElement("div");
        messageEl.className = "message";

        const isOwnMessage = message.userId === userId;
        if (isOwnMessage) {
          messageEl.classList.add("own-message");
        }

        const timestamp = new Date(
          message.timestamp,
        ).toLocaleTimeString();

        messageEl.innerHTML = `
          <div class="message-header">
            <strong>${escapeHtml(message.username)}</strong>
            <span class="message-time">${timestamp}</span>
          </div>
          <div class="message-content">${escapeHtml(message.content)}</div>
        `;

        messagesContainer.appendChild(messageEl);
        messagesContainer.scrollTop = messagesContainer.scrollHeight;
      }

      /**
       * Display a system message (join/leave notifications)
       */
      function displaySystemMessage(text) {
        const messageEl = document.createElement("div");
        messageEl.className = "system-message";
        messageEl.textContent = text;
        messagesContainer.appendChild(messageEl);
        messagesContainer.scrollTop = messagesContainer.scrollHeight;
      }

      /**
       * Update the connection status indicator
       */
      function updateConnectionStatus(status) {
        if (status === "connected") {
          statusIndicator.style.color = "#10b981";
          statusText.textContent = "Connected";
        } else if (status === "disconnected") {
          statusIndicator.style.color = "#ef4444";
          statusText.textContent = "Disconnected";
        } else if (status === "error") {
          statusIndicator.style.color = "#f59e0b";
          statusText.textContent = "Connection error";
        } else {
          statusIndicator.style.color = "#6b7280";
          statusText.textContent = "Connecting...";
        }
      }

      /**
       * Update the user list
       */
      function updateUserList(users) {
        userList.innerHTML = "";
        users.forEach((user) => {
          const userEl = document.createElement("span");
          userEl.className = "user-badge";
          userEl.textContent = user.username;
          userList.appendChild(userEl);
        });
      }

      /**
       * Attempt to reconnect if connection is lost
       */
      function attemptReconnect() {
        if (reconnectAttempts >= maxReconnectAttempts) {
          displaySystemMessage(
            "Failed to reconnect. Please refresh the page.",
          );
          return;
        }

        reconnectAttempts++;
        const delay = Math.min(
          1000 * Math.pow(2, reconnectAttempts),
          10000,
        );

        displaySystemMessage(
          `Reconnecting in ${delay / 1000} seconds... (attempt ${reconnectAttempts}/${maxReconnectAttempts})`,
        );

        setTimeout(() => {
          connect();
        }, delay);
      }

      /**
       * Send a message
       */
      function sendMessage(content) {
        if (!ws || ws.readyState !== WebSocket.OPEN) {
          displaySystemMessage("Cannot send message: not connected");
          return;
        }

        ws.send(
          JSON.stringify({
            type: "message",
            content: content.trim(),
          }),
        );
      }

      /**
       * Escape HTML to prevent XSS
       */
      function escapeHtml(text) {
        const div = document.createElement("div");
        div.textContent = text;
        return div.innerHTML;
      }

      // Handle form submission
      messageForm.addEventListener("submit", (e) => {
        e.preventDefault();

        const content = messageInput.value.trim();
        if (content) {
          sendMessage(content);
          messageInput.value = "";
        }
      });

      // Prompt for username on page load
      const savedUsername = localStorage.getItem("chat-username");
      if (savedUsername) {
        username = savedUsername;
      } else {
        const promptedUsername = prompt(
          "Enter your username:",
          defaultUsername,
        );
        if (promptedUsername && promptedUsername.trim()) {
          username = promptedUsername.trim();
          localStorage.setItem("chat-username", username);
        }
      }

      // Connect on page load
      connect();

      // Clean up on page unload
      window.addEventListener("beforeunload", () => {
        if (ws) {
          ws.close();
        }
      });
    </script>
  </body>
</html>
```

Diese Seite bietet eine voll funktionsfähige Chat-Oberfläche. Sie verwaltet die WebSocket-Verbindung zum Backend, sendet und empfängt Nachrichten in Echtzeit und zeigt Benachrichtigungen an, wenn Nutzer dem Chat beitreten oder ihn verlassen. Die Oberfläche zeigt außerdem die Liste der aktuell online befindlichen Nutzer, verfolgt den Verbindungsstatus mit einer visuellen Anzeige und versucht bei Verbindungsabbruch automatisch eine Wiederverbindung mit progressiver Verzögerung. Für konsistente Benutzernamen über Sitzungen hinweg werden diese im localStorage des Browsers gespeichert. Alle nutzergenerierten Inhalte werden vor der Anzeige sorgfältig escaped, um die Anwendung vor Cross-Site-Scripting (XSS)-Angriffen zu schützen.

Die wichtigsten Teile:

- **WebSocket-URL-Konstruktion**: Nutzt den aktuellen Hostnamen und den Endpunkt `/api/chat/[roomId]`
- **Nachrichtenprotokoll**: Sendet/empfängt JSON-Nachrichten mit einem `type`-Feld
- **Wiederverbindungsstrategie**: Exponentielles Backoff (1 s, 2 s, 4 s, 8 s und 10 s (max.))
- **Benutzernamen-Persistenz**: Speichert Benutzernamen in localStorage

## Nachrichtenpersistenz mit Durable-Object-Speicher hinzufügen

Die Nachrichtenpersistenz ist bereits im `ChatRoom`-Durable-Object implementiert. So funktioniert es:

```typescript
// File: src/worker.ts

// In the constructor, load message history from storage
this.ctx.blockConcurrencyWhile(async () => {
  const stored = await this.ctx.storage.get<ChatMessage[]>('messages')
  if (stored) {
    this.messageHistory = stored
  }
})

// In webSocketMessage(), when a new message arrives, persist it
this.messageHistory.push(chatMessage)

// Limit to last 100 messages
if (this.messageHistory.length > 100) {
  this.messageHistory = this.messageHistory.slice(-100)
}

// Save to storage
this.ctx.storage.put('messages', this.messageHistory)
```

Die Methode `blockConcurrencyWhile` stellt sicher, dass der Speicher geladen ist, bevor Anfragen angenommen werden. Das verhindert Race Conditions während der Initialisierung. Mit Hibernation wird der Nachrichtenverlauf bei jedem Aufwachen des DO aus dem Speicher neu geladen, so bleibt die Datenkonsistenz gewahrt.

## Benutzeranwesenheit verfolgen

Die Benutzeranwesenheit wird über eine Map von WebSocket-Verbindungen zu Sitzungsdaten verfolgt:

```typescript
// File: src/worker.ts

// When a user connects (in fetch method)
this.sessions.set(server, attachment)

// Send current presence to the new user
const users = Array.from(this.sessions.values()).map((s) => ({
  userId: s.userId,
  username: s.username,
}))

// When a user disconnects (in webSocketClose method)
this.sessions.delete(ws)

// Broadcast leave notification
this.broadcast({
  type: 'leave',
  userId: session.userId,
  username: session.username,
  timestamp: Date.now(),
})
```

Das liefert Echtzeit-Updates zur Anwesenheit:

- Neue Nutzer sehen die aktuelle Nutzerliste sofort
- Alle Nutzer werden benachrichtigt, wenn jemand beitritt oder geht
- Sitzungsdaten bleiben über Hibernation hinweg via `serializeAttachment()` erhalten
- Bei Verbindungsabbruch wird der Nutzer automatisch über `webSocketClose()` entfernt
- Beim Aufwachen des DO stellt `ctx.getWebSockets()` alle aktiven Verbindungen wieder her

## Wiederverbindung und Fehlerbehandlung

Der Client implementiert robuste Wiederverbindungslogik:

```typescript
// File: src/pages/chat/[roomId].astro

function attemptReconnect() {
  if (reconnectAttempts >= maxReconnectAttempts) {
    displaySystemMessage('Failed to reconnect. Please refresh the page.')
    return
  }

  reconnectAttempts++
  // Exponential backoff: 1s, 2s, 4s, 8s, 10s (max)
  const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000)
  
  setTimeout(() => {
    connect()
  }, delay)
}
```

Exponentielles Backoff verhindert, dass der Server mit wiederholten Wiederverbindungsversuchen überflutet wird. Es gibt dem Server Zeit zur Erholung und trägt zur Netzwerkstabilität bei. Der Client versucht automatisch eine Wiederverbindung, wenn der WebSocket schließt, ein Fehler auftritt oder der Server neu startet.

## Automatisches Ping/Pong mit WebSocket-Hibernation

Eine leistungsstarke Funktion der Hibernation-API sind automatische Ping/Pong-Antworten, die das Durable Object nicht aufwecken:

```typescript
// In the constructor
this.ctx.setWebSocketAutoResponse(
  new WebSocketRequestResponsePair('ping', 'pong')
)
```

Das weist Cloudflare an, automatisch auf `"ping"`-Nachrichten mit `"pong"` auf Runtime-Ebene zu antworten, ohne Ihr Durable Object aufzurufen. Das:

- **Hält Verbindungen am Leben**, ohne Abrechnungskosten zu verursachen
- **Reduziert Latenz** für Heartbeat-Nachrichten
- **Senkt Kosten**, da das DO für Pings nicht aufwacht

Sie können prüfen, ob Auto-Responses verarbeitet wurden, indem Sie `ws.getLastAutoResponseTimestamp()` aufrufen, falls Sie die Verbindungsgesundheit verfolgen möchten.

## Bereitstellung auf Cloudflare Workers

Stellen Sie Ihre Echtzeit-Chat-Anwendung in Produktion bereit:

```bash
# Build the project
npm run build

# Deploy to Cloudflare Workers
npx wrangler deploy
```

## Fazit

Mit Astro und der WebSocket-Hibernation-API von Cloudflare Durable Objects haben Sie nun eine skalierbare, latenzarme Echtzeit-Chat-App. Nachrichten werden sofort an alle Clients gesendet, Benutzeranwesenheit und Räume werden effizient verwaltet, und Nachrichtenspeicherung ist integriert. Hibernation hält die Serverkosten niedrig, und automatisches Ping/Pong erhält Verbindungen ohne zusätzlichen Overhead. Für den Produktionseinsatz sollten Sie Authentifizierung, Missbrauchsschutz, Monitoring sowie robuste Wiederverbindungs- und Sicherheitsmaßnahmen implementieren.

Bei Fragen oder Anmerkungen erreichen Sie mich gerne auf [Twitter](https://twitter.com/direct_messages/create/rishi_raj_jain_).
