8.5 KiB
Attachment Domain
Handles file sharing between peers over WebRTC data channels. Files are announced, chunked into 64 KB pieces, streamed peer-to-peer as base64, and optionally persisted to disk (Electron) or kept in memory (browser).
Module map
attachment/
├── application/
│ ├── facades/
│ │ └── attachment.facade.ts Thin entry point, delegates to manager
│ └── services/
│ ├── attachment-manager.service.ts Orchestrates lifecycle, auto-download, peer listeners
│ ├── attachment-transfer.service.ts P2P file transfer protocol (announce/request/chunk/cancel)
│ ├── attachment-transfer-transport.service.ts Base64 encode/decode, chunked streaming
│ ├── attachment-persistence.service.ts DB + filesystem persistence, migration from localStorage
│ └── attachment-runtime.store.ts In-memory signal-based state (Maps for attachments, chunks, pending)
│
├── domain/
│ ├── logic/
│ │ └── attachment.logic.ts isAttachmentMedia, shouldAutoRequestWhenWatched, shouldPersistDownloadedAttachment
│ ├── models/
│ │ ├── attachment.model.ts Attachment type extending AttachmentMeta with runtime state
│ │ └── attachment-transfer.model.ts Protocol event types (file-announce, file-chunk, file-request, ...)
│ └── constants/
│ ├── attachment.constants.ts MAX_AUTO_SAVE_SIZE_BYTES = 10 MB
│ └── attachment-transfer.constants.ts FILE_CHUNK_SIZE_BYTES = 64 KB, EWMA weights, error messages
│
├── infrastructure/
│ ├── services/
│ │ └── attachment-storage.service.ts Electron filesystem access (save / read / delete)
│ └── util/
│ └── attachment-storage.util.ts sanitizeAttachmentRoomName, resolveAttachmentStorageBucket
│
└── index.ts Barrel exports
Service composition
The facade is a thin pass-through. All real work happens inside the manager, which coordinates the transfer service (protocol), persistence service (DB/disk), and runtime store (signals).
graph TD
Facade[AttachmentFacade]
Manager[AttachmentManagerService]
Transfer[AttachmentTransferService]
Transport[AttachmentTransferTransportService]
Persistence[AttachmentPersistenceService]
Store[AttachmentRuntimeStore]
Storage[AttachmentStorageService]
Logic[attachment.logic]
Facade --> Manager
Manager --> Transfer
Manager --> Persistence
Manager --> Store
Manager --> Logic
Transfer --> Transport
Transfer --> Store
Persistence --> Storage
Persistence --> Store
Storage --> Helpers[attachment-storage.util]
click Facade "application/facades/attachment.facade.ts" "Thin entry point" _blank
click Manager "application/services/attachment-manager.service.ts" "Orchestrates lifecycle" _blank
click Transfer "application/services/attachment-transfer.service.ts" "P2P file transfer protocol" _blank
click Transport "application/services/attachment-transfer-transport.service.ts" "Base64 encode/decode, chunked streaming" _blank
click Persistence "application/services/attachment-persistence.service.ts" "DB + filesystem persistence" _blank
click Store "application/services/attachment-runtime.store.ts" "In-memory signal-based state" _blank
click Storage "infrastructure/services/attachment-storage.service.ts" "Electron filesystem access" _blank
click Helpers "infrastructure/util/attachment-storage.util.ts" "Path helpers" _blank
click Logic "domain/logic/attachment.logic.ts" "Pure decision functions" _blank
File transfer protocol
Files move between peers using a request/response pattern over the WebRTC data channel. The sender announces a file, the receiver requests it, and chunks flow back one by one.
When Electron serves a file from disk, the sender reads one chunk at a time and uses the buffered data-channel send path so large saved media does not get loaded into renderer memory or flood the receiver.
sequenceDiagram
participant S as Sender
participant R as Receiver
S->>R: file-announce (id, name, size, mimeType)
Note over R: Store metadata in runtime store
Note over R: shouldAutoRequestWhenWatched?
R->>S: file-request (attachmentId)
Note over S: Look up file in runtime store or on disk
loop Every 64 KB chunk
S->>R: file-chunk (attachmentId, index, data, progress, speed)
Note over R: Append to chunk buffer, or append media directly to disk on Electron
Note over R: Update progress + EWMA speed
end
Note over R: All chunks received
Note over R: Reassemble blob, or open completed Electron media from disk
Note over R: shouldPersistDownloadedAttachment? Save to disk
Failure handling
If the sender cannot find the file, it replies with file-not-found. The transfer service then tries the next connected peer that has announced the same attachment. Either side can send file-cancel to abort a transfer in progress.
sequenceDiagram
participant R as Receiver
participant P1 as Peer A
participant P2 as Peer B
R->>P1: file-request
P1->>R: file-not-found
Note over R: Try next peer
R->>P2: file-request
P2->>R: file-chunk (1/N)
P2->>R: file-chunk (2/N)
P2->>R: file-chunk (N/N)
Note over R: Transfer complete
Auto-download rules
When the user navigates to a room, the manager watches the route and decides which attachments to request automatically based on domain logic:
| Condition | Auto-download? |
|---|---|
| Image or video, size <= 10 MB | Yes |
| Image or video, size > 10 MB | No |
| Non-media file | No |
The decision lives in shouldAutoRequestWhenWatched() which calls isAttachmentMedia() and checks against MAX_AUTO_SAVE_SIZE_BYTES.
Browser chat views render audio/video larger than 50 MB with the same generic file interface as other downloads, even after the bytes are available. Attachments with audio/video MIME types that Chromium reports as unsupported also use the generic file interface instead of a broken native player.
An optional experimental VLC.js adapter can be enabled from General settings. When enabled, unsupported downloaded audio/video files show a manual Play action that lazy-loads /vlcjs/metoyou-vlc-player.js. The runtime is intentionally isolated in the experimental media domain and is not part of the default attachment path.
Persistence
On Electron, local audio/video uploads are played through the original filesystem path when Electron exposes one, and received audio/video downloads are appended to an app-data file as chunks arrive. Completed audio/video downloads are then played through a file-backed media URL instead of being reloaded into a renderer Blob, which avoids full-file renderer memory pressure during download, startup restore, and playback. The storage path for downloaded server-room files is resolved per room and bucket:
{appDataPath}/server/{roomName}/{bucket}/{attachmentId}.{ext?}
Direct-message attachments use the conversation id instead of the server-room path:
{appDataPath}/direct-messages/{conversationId}/{bucket}/{attachmentId}.{ext?}
Room and conversation names are sanitised to remove filesystem-unsafe characters. The bucket is video, audio, image, or files depending on the attachment type. The original filename is kept in attachment metadata for display and downloads, but the stored file uses the attachment ID plus the original extension so two uploads with the same visible name do not overwrite each other.
AttachmentPersistenceService handles startup migration from an older localStorage-based format into the database, and restores attachment metadata from the DB on init. On Electron, saved audio/video records are restored as file-backed URLs; other restored files still need their bytes loaded when a Blob URL is required. On browser builds, files stay in memory only.
Runtime store
AttachmentRuntimeStore is a signal-based in-memory store using Map instances for:
- attachments: all known attachments keyed by ID
- chunks: incoming chunk buffers during active transfers
- pendingRequests: outbound requests waiting for a response
- cancellations: IDs of transfers the user cancelled
Components read attachment state reactively through the store's signals. The store has no persistence of its own; that responsibility belongs to the persistence service.