# Building a Realtime Chat App with Actors

In this guide, we're building a realtime chat application using the Rivet library. The app consists of:

- **ChatRoom Actor**: A server-side component that:
  - Uses tags to create separate chat channels
  - Stores the message history in persistent state
  - Provides methods for sending messages and retrieving history
  - Broadcasts new messages to all connected clients

- **Web Client**: A browser-based UI that:
  - Prompts users for username and channel name
  - Connects to the appropriate channel via tags
  - Displays the chat interface
  - Loads message history on connection
  - Shows new messages in realtime
  - Allows users to send messages

## Set up your project
Create a new actor project:

```sh
npx create-actor@latest chat-room -p rivet -t counter
```

This command creates a new Rivet actor project with the necessary configuration files and dependencies. We're using the counter template as a starting point and will modify it for our chat application.

## Define the Chat Room actor
Create a file called `src/chat-room.ts` and add the base class structure:

```typescript
import { Actor, type Rpc } from "actor-core";

// State managed by the actor
export interface State {
  messages: { username: string; message: string }[];
}

export default class ChatRoom extends Actor<State> {
  // Methods will be added in the following steps
}
```

### Step 1: Initialize the actor state

First, add the `_onInitialize` method to set up the initial state:

```typescript
export default class ChatRoom extends Actor<State> {
  _onInitialize() {
    return { messages: [] };
  }
}
```

This method runs when the actor is first created, initializing an empty messages array.

### Step 2: Add message sending functionality

Next, add the method to send messages:

```typescript
export default class ChatRoom extends Actor<State> {
  // ...previous code...
  
  sendMessage(
    _rpc: Rpc<ChatRoom>,
    username: string, 
    message: string
  ): void {
    // Save message to persistent storage
    this._state.messages.push({ username, message });

    // Broadcast message to all connected clients
    this._broadcast("newMessage", username, message);
  }
}
```

This method:
- Takes a username and message as parameters
- Adds the message to the actor's state for persistence
- Broadcasts the message to all connected clients

### Step 3: Add history retrieval

Finally, add a method to retrieve chat history:

```typescript
export default class ChatRoom extends Actor<State> {
  // ...previous code...
  
  getHistory(_rpc: Rpc<ChatRoom>): { username: string; message: string }[] {
    return this._state.messages;
  }
}
```

This method allows clients to fetch all previous messages when they connect.

```typescript
import { Actor, type Rpc } from "actor-core";

// State managed by the actor
export interface State {
  messages: { username: string; message: string }[];
}

export default class ChatRoom extends Actor<State> {
  _onInitialize(): State {
    return { messages: [] };
  }
  
  sendMessage(
    _rpc: Rpc<ChatRoom>,
    username: string, 
    message: string
  ): void {
    // Save message to persistent storage
    this._state.messages.push({ username, message });

    // Broadcast message to all connected clients
    // Event name is 'newMessage', clients can listen for this event
    this._broadcast("newMessage", username, message);
  }
  
  getHistory(_rpc: Rpc<ChatRoom>): { username: string; message: string }[] {
    return this._state.messages;
  }
}
```

### Step 4: Deploy to Rivet
Deploy your actor with:

```sh
cd chat-room
npm run deploy
```

Follow the prompts to:
1. Sign in to your Rivet account
2. Create or select a project
3. Choose an environment

After deployment, you'll receive your Actor Manager URL, which clients will use to connect to your chat room.

## Build a web client
Create a simple web client to interact with your chat room:

### Step 1: Create the HTML structure

```html
<!DOCTYPE html>
<html>
<head>
  <title>Rivet Chat Room</title>
  <style>
    body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
    #message-list { height: 400px; overflow-y: auto; list-style: none; padding: 10px; border: 1px solid #ccc; margin-bottom: 10px; }
    #message-form { display: flex; }
    #message-input { flex: 1; padding: 8px; }
    button { padding: 8px 16px; background: #0070f3; color: white; border: none; }
  </style>
</head>
<body>
  <h1>Rivet Chat Room</h1>
  <ul id="message-list"></ul>
  <form id="message-form">
    <input id="message-input" placeholder="Type a message..." autocomplete="off">
    <button type="submit">Send</button>
  </form>
</body>
</html>
```

### Step 2: Add the client script

Add this script tag just before the closing `</head>` tag:

```html
<script type="module">
  import { Client } from 'https://unpkg.com/actor-core/dist/browser/index.js';

  // Replace with your Actor Manager URL from deployment
  const client = new Client('https://your-actor-manager-url.rivet.run');

  let username = prompt('Enter your username:');
  if (!username) username = 'Anonymous';
  
  let channel = prompt('Enter channel name:', 'general');
  if (!channel) channel = 'general';

  async function init() {
    // Connect to chat room with channel tag
    const chatRoom = await client.get({ 
      name: 'chat-room',
      channel,  // Use channel as a tag to separate different chat rooms
    });
    
    // Store reference for use in event handlers
    // In a production app, you'd use a more encapsulated approach
    window.chatRoom = chatRoom;
  }

  init().catch(console.error);
</script>
```

### Step 3: Load messages and listen for updates

Update your init function and add the addMessage helper function:

```html
<script type="module">
  // ...previous code...

  async function init() {
    // ...previous code...

    try {
      // Load chat history
      const messages = await chatRoom.getHistory();
      messages.forEach(msg => {
        addMessage(msg.username, msg.message);
      });

      // Listen for new messages
      chatRoom.on('newMessage', (username, message) => {
        addMessage(username, message);
      });
    } catch (error) {
      console.error("Failed to load chat history:", error);
      alert("Error loading chat history. Please try refreshing the page.");
    }
  }

  function addMessage(username, message) {
    const messageList = document.getElementById('message-list');
    const item = document.createElement('li');
    
    // Create elements instead of using innerHTML to prevent XSS
    const usernameSpan = document.createElement('strong');
    usernameSpan.textContent = username;
    
    item.appendChild(usernameSpan);
    item.appendChild(document.createTextNode(': ' + message));
    
    messageList.appendChild(item);
    messageList.scrollTop = messageList.scrollHeight;
  }
</script>
```

### Step 4: Handle sending messages

Add the form submit handler to your init function:

```html
<script type="module">
  // ...previous code...

  async function init() {
    // ...previous code...

    // Update page title with channel name
    document.title = `Chat: ${channel}`;
    
    // Add channel name to the UI
    const heading = document.querySelector('h1');
    heading.textContent = `Rivet Chat Room - ${channel}`;

    // Send message on form submit
    document.getElementById('message-form').addEventListener('submit', async (e) => {
      e.preventDefault();
      const input = document.getElementById('message-input');
      const message = input.value.trim();
      
      if (message) {
        try {
          await chatRoom.sendMessage(username, message);
          input.value = '';
        } catch (error) {
          console.error("Failed to send message:", error);
          alert("Error sending message. Please try again.");
        }
      }
    });
  }
</script>
```

```html
<!DOCTYPE html>
<html>
<head>
  <title>Rivet Chat Room</title>
  <style>
    body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
    #message-list { height: 400px; overflow-y: auto; list-style: none; padding: 10px; border: 1px solid #ccc; margin-bottom: 10px; }
    #message-form { display: flex; }
    #message-input { flex: 1; padding: 8px; }
    button { padding: 8px 16px; background: #0070f3; color: white; border: none; }
  </style>
  <script type="module">
    import { Client } from 'https://unpkg.com/actor-core/dist/browser/index.js';

    // Replace with your Actor Manager URL from deployment
    const client = new Client('https://your-actor-manager-url.rivet.run');

    let username = prompt('Enter your username:');
    if (!username) username = 'Anonymous';
    
    let channel = prompt('Enter channel name:', 'general');
    if (!channel) channel = 'general';

    async function init() {
      // Connect to chat room with channel tag
      const chatRoom = await client.get({ 
        name: 'chat-room',
        channel,  // Use channel as a tag to separate different chat rooms
      });
      
      // Store reference for use in event handlers
      // In a production app, you'd use a more encapsulated approach
      window.chatRoom = chatRoom;

      try {
        // Load chat history
        const messages = await chatRoom.getHistory();
        messages.forEach(msg => {
          addMessage(msg.username, msg.message);
        });

        // Listen for new messages
        chatRoom.on('newMessage', (username, message) => {
          addMessage(username, message);
        });
      } catch (error) {
        console.error("Failed to load chat history:", error);
        alert("Error loading chat history. Please try refreshing the page.");
      }
      
      // Update page title with channel name
      document.title = `Chat: ${channel}`;
      
      // Add channel name to the UI
      const heading = document.querySelector('h1');
      heading.textContent = `Rivet Chat Room - ${channel}`;

      // Send message on form submit
      document.getElementById('message-form').addEventListener('submit', async (e) => {
        e.preventDefault();
        const input = document.getElementById('message-input');
        const message = input.value.trim();
        
        if (message) {
          try {
            await chatRoom.sendMessage(username, message);
            input.value = '';
          } catch (error) {
            console.error("Failed to send message:", error);
            alert("Error sending message. Please try again.");
          }
        }
      });
    }

    function addMessage(username, message) {
      const messageList = document.getElementById('message-list');
      const item = document.createElement('li');
      
      // Create elements instead of using innerHTML to prevent XSS
      const usernameSpan = document.createElement('strong');
      usernameSpan.textContent = username;
      
      item.appendChild(usernameSpan);
      item.appendChild(document.createTextNode(': ' + message));
      
      messageList.appendChild(item);
      messageList.scrollTop = messageList.scrollHeight;
    }

    init().catch(console.error);
  </script>
</head>
<body>
  <h1>Rivet Chat Room</h1>
  <ul id="message-list"></ul>
  <form id="message-form">
    <input id="message-input" placeholder="Type a message..." autocomplete="off">
    <button type="submit">Send</button>
  </form>
</body>
</html>
```