RFC: Native Rust Telegram Client — System Design
Источник:
dev/fera/project-design/context/RFC.md(read-only; сгенерировано scripts/sync-docs.mjs)
RFC: Native Rust Telegram Client — System Design
Section titled “RFC: Native Rust Telegram Client — System Design”Status: Draft for implementation planning
Audience: Future self, AI coding agents
Companion docs: ADR.md (decisions + rationale), TECHSPEC.md (concrete
build plan), QA.md / OQ.md (quality gates / open questions)
1. Goals
Section titled “1. Goals”- A desktop Telegram client, visually 1:1 with official native clients per-platform, written in Rust.
- Fix a known defect in official clients: locally cached history must remain available when offline — this is a first-class design requirement, not an afterthought.
- Maximize optimizability: the system must tolerate the author aggressively re-architecting individual subsystems over time without full rewrites. This is the primary driver behind the modular/hot-swap design (see §3).
- Support a modding/plugin ecosystem safely (sandboxed, capability-gated).
- Time-to-market is explicitly not a priority; correctness, optimizability, and architectural cleanliness are.
2. Non-goals (for initial release line)
Section titled “2. Non-goals (for initial release line)”- Full voice/video call reimplementation (bindings to official
tgcallsship first; see ADR-018). - Web-of-trust plugin signing (tracked as future direction, see
OQ.md). - Secret chats (not discussed in architecture sessions to date — flagged in
OQ.md).
3. High-level architecture
Section titled “3. High-level architecture”┌─────────────────────────────────────────────────────────┐│ launcher (thin, rarely updated) ││ - verifies + loads core module per signed trust manifest │└───────────────────────────┬─────────────────────────────┘ │ dlopen┌───────────────────────────▼─────────────────────────────┐│ core module ("module zero" — the actual client) ││ ││ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ ││ │ Protocol │──▶│ Domain/ │──▶│ UI │ ││ │ layer │ │ Actor layer │ │ (Elm+HoF, │ ││ │ (grammers) │ │ (ractor) │ │ see OQ-014) │ ││ │ (grammers) │ │ (ractor) │ │ │ ││ └─────────────┘ └──────┬───────┘ └────────────────┘ ││ │ ││ ┌──────▼───────┐ ││ │ Media │ ││ │ pipeline │ ││ └──────┬───────┘ ││ │ ││ ┌─────────────▼─────────────┐ ││ │ Storage / Search │ ││ │ (sanakirja / milli-fork) │ ││ └─────────────────────────────┘ │└─────────────────────────────────────────────────────────┘All boxes inside “core module” other than the Elm central-state loop and
local GPUI-model UI mechanics are, by design, swappable native modules
(nylon-ring) or WASM plugins. See ADR-001, ADR-009.
4. Layer responsibilities
Section titled “4. Layer responsibilities”4.1 Protocol layer
Section titled “4.1 Protocol layer”- Wraps
grammers-client. Owns the MTProto connection, session, DC switching, reconnect backoff+jitter, and a health-check independent of MTProto round-trip latency. - Exposes only domain types through a trait contract
(
ChatRepository-style) — no raw TL types leak above this layer. - Modeled as a typestate machine over the connection lifecycle (ADR-015):
Unauthenticated → Authenticated → Connected. - Runs as one or more
ractoractors on the shared tokio runtime.
4.2 Domain / Actor layer
Section titled “4.2 Domain / Actor layer”- One actor (or pool) per chat; isolation + crash/restart semantics per chat. Session/Auth actor. Media supervisor + fetch/decode actor pool. Predictor actor (see §4.5).
- Actor-to-actor messages are raw Rust types, no serialization (ADR-007).
- This layer is the only thing that talks to the protocol layer and to storage — UI never does either directly.
4.3 Storage / Search
Section titled “4.3 Storage / Search”- Local database (
sanakirja) is the source of truth for reads (ADR-010, offline-first). Protocol layer only ever writes into it and notifies of changes. - Outbox pattern for all outbound writes (messages, read-receipts):
pending → sent → failed, retried on reconnect. - Search: milli-fork over
sanakirja+ Charabia tokenizer (FTS) + PCRE2 two-phase regex search (ADR-012). - Tiered encryption (ADR-016): lightweight/hardware-accelerable for
metadata, XChaCha20-Poly1305 via Argon2id-derived key for content,
enforced at the type level via
Encrypted<T>/Plaintext<T>(ADR-015).
4.4 Media pipeline
Section titled “4.4 Media pipeline”- Three concerns, kept separate: fetch (priority queue, visible-first),
decode (pluggable
MediaDecoder, pure-Rust baseline + optional FFmpeg module, ADR-017), cache (in-memory LRU for previews/frames + on-disk mmap cache for pre-decoded sticker/animation frames). - Lottie/TGS stickers:
tlottie(CPU/SIMD baseline) withvelato(Vello, GPU) as an experimental alternate backend behind the same trait — decide by benchmarking on the fixed benchmark scene (§7), not by assumption. - Sticker frame cache format: header (
frame_count, width, height, frame_offsets[], frame_duration_ms) + raw/compressed frames, opened viammapso the OS manages residency; cache keyed by content hash, notfile_id(which can change).
4.5 Predictive prefetch
Section titled “4.5 Predictive prefetch”PredictorActorlogs chat-activity events to local storage; Tier 1→2→3 heuristics share this log (ADR-019). Always lower priority than explicit user-driven fetches.
4.6 UI
Section titled “4.6 UI”- Low-level paint/hit-test substrate: Vello + Parley + winit + AccessKit with the project’s own HoF layer on top (ADR-028); native text-input via the Native axis slots (ADR-023) — IME, spell-check, emoji picker, selection come from the platform, not from us.
- Central app state via an Elm-style pure
updateloop; local UI mechanics via native substrate models (ADR-008). - Rendering itself authored as an HoF/immediate-mode-style composition
layer atop the substrate, not its
View/Elementtrait objects directly. - Four UI axes, split by nature (ADR-009, ADR-023, ADR-029):
Engine / Feature / Native are native hot-swappable dylib
modules; Style is a declarative data layer (tokens, RON) loaded
by the Engine — theme distribution rides the Lattice
themes/subtree. Feature modules may hook into Engine behavior (e.g. scroll physics); their local state stays local, not routed through the central Elm loop. Native modules provide platform widgets behind a thin slot contract (text input, sidebar, menus, dialogs); the message list stays on the project’s own renderer. - Separate frame-rate budgets per content kind: ~24 fps UI chrome, ~45 fps message updates, 60–75 fps stickers/animations — no single shared frame loop.
4.7 Platform shell
Section titled “4.7 Platform shell”- Windows/tray/notifications/deep-links/OS integration. Kept separate from
domain logic; notification-worthy events arrive via the shared
NotificationActor(§4.8).
4.8 Multi-account notification handling
Section titled “4.8 Multi-account notification handling”- All MTProto client update-streams (one per account) are merged into a
single handler that feeds a shared
NotificationActor, which owns mute/batching/priority policy across all accounts, not per-account independently. Notification payloads carry an account identifier for UI display (icon-per-account on the OS notification), but policy decisions are global.
5. Module system
Section titled “5. Module system”5.1 First-party native modules (nylon-ring)
Section titled “5.1 First-party native modules (nylon-ring)”- Explicit version + struct-size validation at load; graceful unload with in-flight call draining; host-leased zero-copy output buffers.
- Batch-first trait design + host-side hot cache to amortize the boundary cost (ADR-005) — this is a hard requirement for any trait crossing this boundary at high frequency (storage, search).
- Hash-mismatch against the trust manifest is a warning, never a load refusal (ADR-004).
5.2 Third-party WASM plugins (wasmtime + wit-bindgen)
Section titled “5.2 Third-party WASM plugins (wasmtime + wit-bindgen)”- Sandboxed by construction. Read-only by default; write/action capabilities (send messages, mutate UI, network egress) are individually opt-in via settings, declared at the Component Model interface level so an unauthorized capability is not merely policy-blocked but not present in the plugin’s callable surface at all.
5.3 Trust & distribution
Section titled “5.3 Trust & distribution”- Self-hosted TUF-like signed manifest (ADR-003): module name → hash → signature, Ed25519/SSH root key embedded in the launcher.
- In-app trusted-key management UI: add/remove entries by name + URL or hex-encoded key.
- Update manifest/endpoint lives inside the signed manifest, never in a plaintext user-editable config file (lesson drawn from a real-world unofficial-tooling exploit against Discord’s plaintext update endpoint).
- Core module updates are restart-gated (a running module can’t replace its own on-disk file safely); all other first-party modules hot-swap at runtime without a restart (ADR-013).
5.4 Edition translators (ADR-021/022)
Section titled “5.4 Edition translators (ADR-021/022)”- Translators exist only for WASM plugins; native modules are versioned and loaded natively, never translated. The host supports editions [N, N-1]; beyond that, plugins go through translation.
- A translator is a separate module type: loaded without special rights, it returns a virtual plugin list; rights resolve per virtual plugin and can only narrow (original ∩ user-granted), never widen. The host reads rights from the signed package manifest, never from translator output; the UI shows the original plugin’s rights.
- Level 1 (default, trusted): deterministic declarative mapping — the translator is data (call renames, field re-laying, defaults, built-in converters), host-interpreted; chains statically validated (cycles, depth) at load.
- Level 2 (opt-in, flagged): behavioral translation (e.g. sync ↔ async/streaming) — declared per-function via flag, each capability gated on a separate explicit user opt-in, still sandboxed and fuel-limited.
- Plugin container format: open — the author’s own design pass (OQ-013).
6. Type-driven correctness (ADR-015)
Section titled “6. Type-driven correctness (ADR-015)”Applied selectively to invariants with real, recurring violation risk:
- Protocol client connection lifecycle → typestate.
- IDs / bounded values from the network → newtype + smart constructor.
- Multi-state data (read status, outbox status) → enums, not flag combinations.
- Encryption boundary → phantom types (
Encrypted<T>/Plaintext<T>). frunkfor heterogeneous statically-shaped data where it earns its complexity cost.
Goal: reduce the runtime-validation test surface by making invalid states unconstructible, not to maximize type-system cleverness for its own sake — see ADR-015 consequences for the anti-goal (type-complexity explosion).
7. Testing & benchmarking (ADR-014)
Section titled “7. Testing & benchmarking (ADR-014)”- Contract test suites per trait, run against every implementation.
ractortest harness for actor-level tests.- Pure-function unit tests for the Elm
updateloop. - Module-loading edge cases: version mismatch → fallback, hash mismatch → warning-not-block, hot-reload-under-load → no dropped in-flight calls.
- Pixel-diff visual regression, scoped per Style module.
criterionperformance benchmarks, scoped per Engine module, against a fixed benchmark scene — exact scene parameters (message count, concurrent animated sticker count, etc.) are an open question, seeOQ.md.
8. Explicit engineering principles carried through this design
Section titled “8. Explicit engineering principles carried through this design”- Offline-first is structural, not a fallback path: the UI has no code path that reads from the network directly; removing network access must not change which code path renders history.
- Boundary cost awareness: any design that introduces a module boundary must simultaneously address how that boundary avoids becoming a per-operation tax (batch APIs, host-side caching, zero-copy buffers).
- Security defaults favor the user, not convenience: read-only plugin default, warning-not-block on hash mismatch (freedom preserved) while the manifest itself remains cryptographically signed (real tampering still caught), signed update endpoints (no plaintext-editable redirect vector).
- Defer what’s genuinely separable: calls/streams (ADR-018), Tier 2/3 prediction (ADR-019), FFmpeg (ADR-017) are all scoped as additive-later, not launch-blocking, specifically because they’re architecturally decoupled enough that deferring them doesn’t require redesigning anything already built.