Home Doh Ref
Dohballs
  • 📁 doh_chat
    • 📁 chat_extensions
  • 📁 doh_modules
    • 📦 dataforge
    • 📦 express
    • 📁 sso
    • 📁 user

Chat Files

Peer-to-peer file transfer with WebRTC-first delivery and socket relay fallback.

Overview

chat_files provides file sharing within chat rooms. Files transfer directly between peers via WebRTC data channels when possible, falling back to a server-mediated socket relay when P2P connections can't be established. Files are cached locally in IndexedDB, and users who have downloaded a file become seeders for other peers. The module handles thumbnails for images, inline previews for media/PDF/text, and synchronized media playback across room participants.

Architecture

chat_files_server (nodejs)     chat_files_client (browser)
├── File metadata DB           ├── TransferManager (orchestration)
├── Seeder registry            ├── FileCache (IndexedDB)
├── Socket signal relay        ├── ChunkAccumulator
├── Relay chunk routing        ├── ChatFileCard (UI)
└── Preview size persistence   └── WebRTC DataChannel

Loaded by: chat.doh.yaml (no standalone package — part of core chat) Depends on: chat_server (rooms, messages, membership)

File Structure

File Purpose
chat_files_server.js Socket events, DB tables, file validation (~715 lines)
chat_files_client.js Transfer orchestration, caching, UI (~1830 lines)
chat_files.css File card and preview styles

Configuration

Key Type Default Description
chat_files.chunk_size number 65536 (64KB) Bytes per transfer chunk
chat_files.max_file_size number 524288000 (500MB) Maximum file size
chat_files.max_thumbnail_size number 51200 (50KB) Max thumbnail base64 size
chat_files.webrtc_timeout number 10000 (10s) WebRTC connection timeout before relay fallback
chat_files.relay_batch_size number 8 Chunks per relay batch before ack
chat_files.blob_merge_threshold number 10485760 (10MB) ChunkAccumulator merge-to-Blob threshold
chat_files.image_auto_download_max number 52428800 (50MB) Auto-download threshold for images
chat_files.cache_max_bytes number 500 IndexedDB cache limit (MB)
chat_files.probe_timeout number 3000 (3s) Peer probe timeout
chat_files.peer_wait_timeout number 60000 (60s) Passive wait for seeders timeout
chat_files.ice_servers array Google STUN servers WebRTC ICE server list
# pod.yaml override example
chat_files:
  max_file_size: 1073741824   # 1GB
  cache_max_bytes: 1000       # 1GB IndexedDB cache
  ice_servers:
    - urls: 'stun:stun.example.com:3478'
    - urls: 'turn:turn.example.com:3478'
      username: 'user'
      credential: 'pass'

Database Schema

Table Type Purpose
chat.files Idea File metadata registry
chat.file_seeds Idea Seeder availability tracking

File metadata shape (in chat.files):

{
  id: string,            // UUID
  room_id: string,
  name: string,          // Original filename
  size: number,          // Bytes
  mime_type: string,
  chunk_size: number,
  total_chunks: number,
  thumbnail: string,     // Base64 data URL (images only, capped at 50KB) or null
  sender: string,        // Username
  created_at: number     // Timestamp
}

Seeder record shape (in chat.file_seeds):

{
  id: string,            // "{file_id}:{username}"
  file_id: string,
  username: string,
  seeded_at: number
}

File message format (in chat.messages.content):

[FILE]{"file_id":"...","name":"...","size":123,"mime_type":"...","thumbnail":"..."}

REST API Reference

Method Path Auth Description
GET /api/chat/files/user Yes List user's files across all rooms

GET /api/chat/files/user

// Query params
{ room_id?: string, search?: string }
// Response
{
  files: [{
    id, name, size, mime_type, thumbnail, created_at,
    room_id, room_name, room_type, sender_chat_name
  }]
}

Socket Events Reference

File Sharing

Event Direction Payload Response
chat:file:share Client → Server { room_id, name, size, mime_type?, thumbnail? } cb({ success, file_id })

Side effects: creates chat.files entry, registers sender as seeder, creates [FILE] message, broadcasts chat:message_received.

Peer Discovery

Event Direction Payload Response
chat:file:request_peers Client → Server { file_id, room_id } cb({ success, seeders[], file, diag })
chat:file:probe Client → Server { file_id, room_id } cb({ success }) — broadcasts to room
chat:file:probe_response Client → Server { file_id, has_file, requester_socket_id } Routed to requester

WebRTC Signaling

Event Direction Payload Response
chat:file:signal Client → Server { target_username, room_id, file_id, signal_type, signal_data } Relayed to target peer

signal_type: 'offer' | 'answer' | 'ice-candidate'

Socket Relay Transfer

Event Direction Payload Response
chat:file:relay_request Client → Server { file_id, room_id, source_username } cb({ success, transport: 'relay' })
chat:file:relay_chunk Source → Server → Requester { requester_socket_id, file_id, chunk_index, chunk_data, total_chunks } Relayed
chat:file:relay_ack Requester → Server → Source { source_socket_id, file_id, received_through } Relayed
chat:file:relay_complete Source → Server → Requester { requester_socket_id, file_id } Relayed

Relay chunks are base64-encoded. Acks sent every relay_batch_size (8) chunks.

Seeding & Sync

Event Direction Payload Response
chat:file:seed_registered Client → Server { file_id } cb({ success }) — broadcasts to room
chat:update_file_preview_size Client → Server { message_id, file_id, width, height, isLive } Broadcasts to room
chat:media_sync Client → Server { room_id, file_id, action, time } Broadcast (excludes sender)

action: 'play' | 'pause' | 'seek'

Transfer Flow

1. Check local memory (localFiles Map)
   └─ Found → complete (instant)

2. Check IndexedDB cache (FileCache)
   └─ Found → complete, register as seeder

3. Probe room peers (chat:file:probe, 3s timeout)
   └─ Peer responds → attempt transfer

4. Query seed registry (chat:file:request_peers)
   └─ Seeders found → attempt transfer

5. Wait passively (60s) for new seeders
   └─ chat:file:seeder_available / chat:room_peer_joined
   └─ Timeout → 'unavailable'

Transfer attempt:
  a. Try WebRTC DataChannel (10s timeout)
     └─ Success → complete, cache, register seeder
     └─ Fail → try next method

  b. Try socket relay (base64 chunks, batched with ack)
     └─ Success → complete, cache, register seeder
     └─ Fail → try next seeder, then give up

Client Architecture

TransferManager (Singleton)

Orchestrates all file transfers. Key methods:

Method Description
shareFile(roomId, file) Validate, thumbnail, emit share, cache locally
requestFile(fileId, roomId) Start transfer cascade (cache → probe → peers → wait)
cancelTransfer(fileId) Cancel active transfer, cleanup WebRTC
loadCachedFilesForRoom(roomId) Load from IndexedDB, re-register as seeder

FileCache (IndexedDB)

Method Description
put(fileId, blob, metadata) Store with LRU eviction
get(fileId) Retrieve blob
loadRoom(roomId, localFilesMap) Load all cached files for a room
clear() Wipe cache
getStats() { count, bytes }

DB: doh_chat_files, store: files, indices: room_id, cached_at

ChunkAccumulator

Efficiently accumulates binary chunks, merging to Blob at the blob_merge_threshold to prevent memory pressure on large files.

ChatFileCard Pattern

Property Type Description
file_id string File identifier
file_name string Display name
file_size number Bytes
mime_type string MIME type
thumbnail string Base64 data URL or null
_state string 'available' | 'connecting' | 'transferring' | 'complete' | 'error' | 'unavailable'
_progress number 0-100 transfer progress

Sub-objects: thumbEl, info (nameEl, metaEl), progressBar (fill), actionArea (downloadBtn, toggleBtn, cancelBtn, statusEl), previewArea, resizeHandle

Supports inline preview for images, video, audio, PDF, and text files. Previews are resizable with cross-peer size synchronization.

Testing

  1. Share a file in a chat room — verify [FILE] message appears
  2. Open a second browser/tab in the same room — download the file
  3. Verify WebRTC transfer (check console for "WebRTC" transport label)
  4. Test with WebRTC blocked (e.g., restrictive NAT) — verify socket relay fallback
  5. Close all tabs, reopen — verify cached file loads from IndexedDB
  6. Test media sync: play a video in one tab, verify playback syncs to other tabs
Last updated: 2/21/2026