Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

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. A desktop Telegram client, visually 1:1 with official native clients per-platform, written in Rust.
  2. 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.
  3. 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).
  4. Support a modding/plugin ecosystem safely (sandboxed, capability-gated).
  5. Time-to-market is explicitly not a priority; correctness, optimizability, and architectural cleanliness are.
  • Full voice/video call reimplementation (bindings to official tgcalls ship 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).
┌─────────────────────────────────────────────────────────┐
│ 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.

  • 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 ractor actors on the shared tokio runtime.
  • 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.
  • 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).
  • 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) with velato (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 via mmap so the OS manages residency; cache keyed by content hash, not file_id (which can change).
  • PredictorActor logs chat-activity events to local storage; Tier 1→2→3 heuristics share this log (ADR-019). Always lower priority than explicit user-driven fetches.
  • 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 update loop; 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/Element trait 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.
  • Windows/tray/notifications/deep-links/OS integration. Kept separate from domain logic; notification-worthy events arrive via the shared NotificationActor (§4.8).
  • 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.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.
  • 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).
  • 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).

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>).
  • frunk for 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).

  • Contract test suites per trait, run against every implementation.
  • ractor test harness for actor-level tests.
  • Pure-function unit tests for the Elm update loop.
  • 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.
  • criterion performance benchmarks, scoped per Engine module, against a fixed benchmark scene — exact scene parameters (message count, concurrent animated sticker count, etc.) are an open question, see OQ.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.