Real-time чаты

Фреймворк Fresh построен на базе Deno и ориентирован на рендеринг на стороне сервера с минимальным использованием JavaScript на клиенте. Это делает его подходящим для реализации real-time чатов с высокой производительностью и низкой задержкой. Основой такого решения является WebSocket-соединение, обеспечивающее постоянный двусторонний канал между клиентом и сервером.

Ключевые элементы архитектуры:

  • Серверная часть отвечает за маршрутизацию WebSocket, хранение состояния подключений и обработку сообщений.
  • Клиентская часть обеспечивает открытие и поддержание соединения, отправку сообщений и их отображение.
  • Хранилище сообщений может быть как временным (в памяти сервера), так и долговременным (БД). Для масштабируемых решений предпочтительно использовать Redis или другой pub/sub механизм.

Настройка маршрута WebSocket

Во Fresh маршруты создаются в папке routes. Для WebSocket-чата создается отдельный маршрут, например routes/api/ws.ts. Основные шаги:

import { WebSocket, acceptWebSocket } from "https://deno.land/std/ws/mod.ts";

const clients = new Set<WebSocket>();

export const handler = async (req: Request) => {
  if (req.headers.get("upgrade") !== "websocket") {
    return new Response("Only websocket connections allowed", { status: 400 });
  }

  const { socket, response } = Deno.upgradeWebSocket(req);
  clients.add(socket);

  socket.onmess age = (event) => {
    for (const client of clients) {
      if (client !== socket) {
        client.send(event.data);
      }
    }
  };

  socket.oncl ose = () => clients.delete(socket);

  return response;
};

В этом примере сервер хранит все активные соединения в Set и рассылает полученные сообщения всем подключенным клиентам, кроме отправителя.

Работа с состоянием чата

Для минимизации нагрузки на сервер при большом числе участников необходимо использовать структуры данных с быстрым доступом, например:

  • Map<string, WebSocket> для привязки пользователей к сокетам.
  • Очередь сообщений с ограничением размера для хранения последних N сообщений.
  • Интеграция с Redis Pub/Sub для масштабирования на несколько серверов.

Пример управления пользователями:

const users = new Map<string, WebSocket>();

function addUser(id: string, socket: WebSocket) {
  users.set(id, socket);
}

function removeUser(id: string) {
  users.delete(id);
}

function broadcastMessage(senderId: string, message: string) {
  for (const [id, socket] of users) {
    if (id !== senderId) socket.send(message);
  }
}

Клиентская часть

На клиенте подключение к WebSocket выглядит следующим образом:

const socket = new WebSocket("ws://localhost:8000/api/ws");

socket.ono pen = () => {
  console.log("Connected to chat server");
};

socket.onmess age = (event) => {
  const messageList = document.getElementById("messages");
  const msgElem = document.createElement("li");
  msgElem.textContent = event.data;
  messageList.appendChild(msgElem);
};

function sendMessage(text: string) {
  socket.send(text);
}

Особенности Fresh: клиентский JavaScript можно минимизировать, поскольку рендеринг интерфейса производится на сервере, а клиент только обрабатывает события WebSocket.

Интеграция с серверными компонентами Fresh

Fresh использует islands architecture, где интерактивные элементы загружаются на клиент только при необходимости. Для чата это значит:

  • Компонент списка сообщений можно рендерить на сервере.
  • WebSocket-клиент оборачивается в island, которая загружается динамически.

Пример структуры компонента:

/** @jsx h */
import { h } from "preact";
import { useState, useEffect } from "preact/hooks";

export default function ChatIsland() {
  const [messages, setMessages] = useState<string[]>([]);
  useEffect(() => {
    const socket = new WebSocket("ws://localhost:8000/api/ws");
    socket.onmess age = (event) => setMessages((prev) => [...prev, event.data]);
  }, []);

  return (
    <ul>
      {messages.map((msg, i) => <li key={i}>{msg}</li>)}
    </ul>
  );
}

Масштабирование и безопасность

  • Аутентификация пользователей через токены JWT.
  • Шифрование WebSocket с использованием wss://.
  • Ограничение числа сообщений в секунду для предотвращения спама.
  • При использовании нескольких серверов — синхронизация через Redis Pub/Sub для рассылаемых сообщений.

Хранение истории сообщений

Для небольших чатов можно использовать in-memory массив, но для производственных решений:

  • Redis Lists для очереди сообщений.
  • PostgreSQL / MongoDB для долговременного хранения.
  • Индексация по времени и пользователю для быстрого запроса истории.

Поддержка расширенной функциональности

  • Поддержка личных сообщений: необходимо хранить mapping senderId -> receiverId -> socket.
  • Комнаты: создаются как отдельные Set/WebSocket коллекции.
  • Уведомления о наборе текста: добавляется отдельное событие через WebSocket.

Производительность

  • Fresh минимизирует фронтенд-код и использует SSR, что снижает нагрузку на клиент.
  • WebSocket-соединение поддерживает тысячи подключений с низкой задержкой при корректной архитектуре.
  • Рендеринг компонентов на сервере позволяет мгновенно показывать обновления сообщений при подключении нового пользователя.

Эта архитектура обеспечивает быстрый, масштабируемый и безопасный real-time чат с полным контролем над состоянием и логикой сообщений.