Skip to content

Latest commit

 

History

History
324 lines (240 loc) · 13 KB

File metadata and controls

324 lines (240 loc) · 13 KB

Repository Guidelines

This file covers both the backend and frontend. The backend guidelines live in the next section; the frontend section is reserved for upcoming frontend-specific notes.

System Overview

  • Frontend talks to backend via WebSocket (/ws).
  • Wire types are mirrored between backend (backend/src/domain/request.gleam, backend/src/domain/response.gleam) and frontend (frontend/src/api/types.ts).
  • Transport adapters are wire-only: encode/decode JSON, then delegate to handlers/pipeline. REST is not used; WebSocket is the only transport.

Critical Invariants

  • Subjects are the only inter-process addresses; never send to PIDs directly.
  • Room broadcast always uses member inbox Subjects; rooms are authoritative for membership.
  • WebSocket frames must be sent by the WebSocket controlling process.
  • Connection inboxes for room events are Subjects owned by the WS process and wired via a Selector (see WebSocket transport notes).

Backend (./backend)

Project Structure & Module Organization

  • src/ contains Gleam modules for the chat backend.
  • Domain types live under src/domain/ (e.g., src/domain/chat.gleam, src/domain/session.gleam).
  • Transport-agnostic request/response messages live in src/domain/request.gleam and src/domain/response.gleam.
  • Room processes and registry live in src/pipeline/room.gleam and src/room_registry.gleam.
  • src/handlers/ contains transport-agnostic logic grouped by feature (rooms, chat, session) with a shared dispatcher.
  • src/transport/ is the gate layer: incoming.gleam/outgoing.gleam are wire-only JSON decode/encode, websocket.gleam adapts to WebSocket.
  • src/pipeline/ contains pipeline stages (e.g., stage_validation.gleam) and the internal envelope/event types used between stages.
  • test/ holds Gleeunit tests; handler tests live under test/handlers/, transport adapter tests under test/transport/.
  • build/ is generated by Gleam; do not edit it manually.
  • gleam.toml and manifest.toml define package metadata and dependencies.

Instruction and Style

  • Update AGENTS.md whenever architecture, key commands, or module ownership changes in the backend or frontend.
  • Keep backend request/response wire types and the frontend protocol mirror in sync whenever either changes.

Messaging Model

This system is an ephemeral chat app with chatrooms. Each WebSocket connection is handled by a dedicated BEAM process. Chat messages go through a pipeline before being published to a room. Rooms are implemented as one process per room.

Important: In this codebase, inter-process messaging uses Subjects as typed inboxes/addresses. We do not send messages directly to PIDs.

WebSocket Transport Notes

  • Room events flow into the WebSocket process via a Subject owned by that process plus a Selector returned from on_init.
  • The WebSocket handler receives those events as mist.Custom(...) and sends frames from the controlling process.
  • Do not send frames from any other process or you will hit NotOnControllingProcess errors.

Pipeline Summary

  • Validation stage forwards accepted messages.
  • Processing stage routes messages to rooms by room_id.
  • Room process applies policy, then broadcasts RoomEvent to member inbox Subjects.

Core Concepts

Subjects are addresses (typed inboxes)

  • A Subject(T) is the address used to send a message of type T to a process.
  • Each long-lived process (connection, room, pipeline stage) exposes one or more Subjects that other processes use to talk to it.

Commands vs Events

  • Commands: sent to a room’s command Subject (e.g., Join, Leave, Publish).
  • Events: sent to a connection’s inbox Subject (e.g., RoomEvent), then forwarded to the WebSocket.

Core Processes

Connection Process (one per WebSocket)

Responsibilities

  • Perform authentication on connect (once).
  • Maintain connection state (user/session info, joined rooms, cached room_control per room).
  • Decode incoming frames into commands (join/leave/send).
  • Send chat messages into the pipeline.
  • Receive room events via its inbox Subject and forward them to the WebSocket.

Key subjects owned by the connection

  • conn_inbox: Subject(ConnMsg) (events/control messages directed to this connection)
    • includes RoomEvent(...) messages

Room Registry (directory)

Responsibilities

  • Maintain a mapping room_id -> room_control where room_control is a handle for contacting a room (at minimum, a room command Subject).
  • Provide room listing for discovery ([{room_id, name, ...}]).
  • Create rooms explicitly via CreateRoom; joins do not auto-create missing rooms.
  • Return the room_control to a connection process.
  • Sweep idle rooms hourly; rooms with last_sent older than 24 hours are stopped and removed.

Notes

  • Connection processes cache room_control after resolving a room to avoid repeated lookups.
  • A RoomHandle is a typed handle used to send commands to a room.
  • Initial rooms are seeded at boot in backend/src/backend.gleam.

Room Process (one per room)

A room is authoritative: it enforces room policies and controls who receives room events.

Responsibilities

  • Enforce room-level policies:
    • membership required, banned users, room closed, slow mode, capacity limits, etc.
  • Maintain authoritative membership:
    • members: Map(user_id, Member)
    • where Member contains at least:
      • inbox: Subject(ConnMsg) (the connection’s inbox subject used for delivering room events)
      • optional metadata (display name, join time, etc.)
    • optional: monitor the owner PID of each inbox for crash-safe cleanup.
  • Broadcast accepted messages to all connected members:
    • broadcast is done by iterating members and sending RoomEvent to each member’s inbox Subject.

Message Envelope

Chat messages are augmented early with routing info:

  • The client (connection process) builds a ChatMessage that includes:
    • room_id (required)
    • sender_id / username (required)
    • kind (text/image/…)
    • payload (content)
    • message_id (required)
    • optional metadata (timestamp)

room_id is carried through the whole pipeline so the final stage can route to the correct room.

Join Flow

  1. Client requests room list:
    • Connection → RoomRegistry: ListRooms
  2. Client selects a room and joins:
    • Connection → RoomRegistry: GetOrCreate(room_id) → returns room_control
    • Connection caches room_control in state.
  3. Connection registers itself as a room member:
    • Connection → Room (via room command Subject): Join(sender_id, conn_inbox, …)
  4. Room process:
    • applies join policy (banned/capacity/etc.)
    • if accepted: stores membership entry in members
    • if rejected: replies with error (connection sends error frame to client)
Client
  │
  │  WebSocket connect
  ▼
Connection Process
  │
  │  Auth (once, on connect)
  │
  │─────────────── ListRooms ───────────────▶
  │                                        RoomRegistry
  │◀────────────── room list ────────────────
  │
  │──────────── GetOrCreate(room_id) ─────────▶
  │                                        RoomRegistry
  │◀────────────── room_control (cmd subject) ────
  │
  │  (cache room_id → room_control)
  │
  │──────────── Join(sender_id, conn_inbox) ─▶
  │                                        Room Process
  │                                        - apply join policy
  │                                        - store member inbox subject
  │◀────────────── Ok / Error ────────────────
  │
  │  if Ok:
  │  connection is now a member of the room

Room cleanup (recommended)

  • On clean disconnect, the connection sends Leave(room_id, sender_id) to the room.

  • For crash-safe cleanup, the room may also monitor the owner PID of each member inbox subject.

    • If the connection process dies, the room removes the member from members.

Send Message Flow

When a user sends a message over WebSocket:

  1. Connection decodes the frame and builds ChatMessage (includes room_id).

  2. Connection sends the message into the pipeline (see below).

  3. After the pipeline finishes successfully, the message is routed to the room:

    • Final pipeline stage sends Publish(msg) to the room command subject using cached room_control.
  4. Room process applies room-level publish policy:

    • banned/muted, slow mode, membership required, etc.
  5. If accepted, the room broadcasts to all member inbox Subjects:

    • for each member.inbox: send ConnMsg.RoomEvent(msg) to that inbox
  6. Each connection process receiving RoomEvent(msg) forwards it to its WebSocket.

Client
  │
  │  send chat frame
  ▼
Connection Process
  │
  │  decode frame
  │  build ChatMessage
  │  (includes room_id)
  │
  │──────────────▶ Pipeline: Validation
  │                (via Subject)
  │
  │──────────────▶ Pipeline: Safety Checks
  │                (text/image logic)
  │
  │──────────────▶ Pipeline: Logging
  │                (side-effect)
  │
  │──────────────▶ Pipeline: Routing
  │
  │  use cached room_control (cmd subject)
  │
  │──────────── Publish(msg) ────────────────▶
  │                                        Room Process
  │                                        - apply room policy
  │                                        - membership required
  │                                        - slow mode / bans
  │
  │                                        if accepted:
  │                                        for each member inbox:
  │                                          send RoomEvent(msg)
  │
  │◀──────────── RoomEvent(msg) ──────────────
  │
  │  forward to WebSocket
  ▼
Client

Pipeline Model

Messages go through a pipeline before reaching a room. Each pipeline node communicates via a Subject.

Why Subjects here

  • Pipeline nodes are decoupled: a node publishes output to the next node’s subject.
  • Multiple “side listeners” can subscribe to pipeline events (e.g., logging/metrics) without changing the core flow.

Pipeline Structure

  • Each stage is a small process that:

    • subscribes to an input subject
    • processes messages
    • either publishes to the next stage’s subject or drops/rejects
  • The pipeline runs inside the BEAM VM and is ephemeral (no durability/replay requirements at this level).

Example stages (exact set can evolve):

  1. Validation

    • schema/type validation, size limits, required fields (room_id, etc.)
  2. Safety Checks

    • different logic depending on kind (text vs image)
  3. Logging

    • optional side-effect logging of accepted/rejected messages
  4. Routing

    • final stage that sends Publish to the room command subject

Success vs Failure

  • On failure at any stage, the message is rejected and the connection process is notified (e.g., “invalid message”, “unsafe content”, “rate limited”).
  • Room-level policy checks still exist as a final gate even if most checks happen in the pipeline.

Broadcast Model

Rooms do not use Subjects as “topics” for membership/broadcast fan-out.

  • Membership is tracked explicitly in the room process via member inbox Subjects.
  • This allows strong control for policies, capacity, and cleanup.
  • Broadcasting is implemented by iterating members and sending RoomEvent to each member inbox Subject.

Key invariants

  • Connection process

    • owns the socket
    • owns the pipeline entry
    • caches room_control
    • exposes conn_inbox subject for room events
  • Pipeline

    • uses Subjects for decoupled stages
    • augments/filters messages before they reach rooms
  • Room process

    • is authoritative
    • owns membership and policies
    • broadcasts by sending to member inbox Subjects
  • Subjects

    • used as typed inboxes/addresses (everywhere)
    • not relied upon for room membership introspection or automatic fan-out

Frontend (./frontend)

Project Structure & Module Organization

  • React app with React Compiler enabled.
  • Tailwind CSS for styling.
  • DaisyUI as the component/theme layer on top of Tailwind.
  • src/api/types.ts mirrors backend domain/request.gleam and domain/response.gleam wire types; src/api/ws.ts provides transport helpers.

Integration Testing

  • Integration tests run via node:test + tsx from frontend.
  • Backend must be running on http://localhost:8080 for integration tests.
  • Command: pnpm test:integration.
  • Tests live under frontend/tests/integration/ with helpers in frontend/tests/helpers/.
  • UI/E2E tests are not used; integration coverage is exercised through the frontend test runner.