Peer-to-peer file transfer with WebRTC-first delivery and socket relay fallback.
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.
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 | 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 |
| 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'
| 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":"..."}
| 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
}]
}
| 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.
| 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 |
| 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'
| 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.
| 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'
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
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 |
| 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
Efficiently accumulates binary chunks, merging to Blob at the blob_merge_threshold to prevent memory pressure on large files.
| 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.
[FILE] message appears