Полный транскрипт. Восстановлен из сетевого архива DevTools:
claude.ai грузит историю лениво, поэтому «Сохранить страницу» и
copy-as-text захватывают только уже отрисованный хвост ветки.
HAR же содержит полный ответ /chat_conversations/<id>?tree=True.
🧠 Продумывание создания Rust-клиента для Telegram с помощью ИИ-агентов.
Реально, и это законно: Telegram сам дал открытое API и свои клиенты под GPL. Но есть нюансы.
Стек
Протокол:tdlib-rs (биндинги на официальный TDLib, берёт на себя MTProto, кэш, шифрование) или grammers (чистый Rust MTProto, но придётся писать больше самой).
UI: Slint или iced для нативного, Tauri/Dioxus если хочешь веб-рендер. Пиксель-в-пиксель проще всего в Tauri, потому что CSS даёт нужный контроль над версткой.
Эталон для 1:1: исходники Telegram Desktop (Qt/C++), там все размеры, отступы, цвета и анимации.
Юридическое
Нужен свой api_id с my.telegram.org. Использовать чужой или официальный нельзя.
Если копируешь код или ассеты из официальных клиентов, твой проект тоже становится GPL v3.
Название, логотип и иконку Telegram лучше не брать: по условиям API клиент не должен вводить людей в заблуждение, что он официальный.
Как гонять агентов
Разбей на независимые модули: слой данных, список чатов (виртуализированный), рендер сообщений с entities, медиа, настройки.
Для 1:1 сделай скриншот-тесты: агент рендерит экран, сравнивает pixel-diff с эталоном и правит, пока разница не упадёт. Это самый рабочий цикл.
Дай агентам жёсткие интерфейсы (traits) заранее, иначе модули не сойдутся.
Тут я ошибалась, ты права. В Telegram Desktop серии 7.2 родной rlottie заменили на tlottie: это Rust-библиотека без рантайм-зависимостей, с ручными SIMD-бэкендами (NEON, SSE2/AVX2/AVX-512, WASM). Судя по ченджлогу Gentoo, каталог с rlottie в исходниках удалили целиком. Репо: github.com/dkaraush/tlottie, лицензия MIT.
Про краши как причину замены подтверждения я не нашла, в источниках об этом ни слова. Но rlottie и правда был известен багами и уязвимостями при разборе кривых анимаций, так что звучит правдоподобно.
Для твоего клиента это хорошая новость: вместо страшной части с TGS можно взять tlottie (там есть C API) или velato, а формат стикеров у них общий. Осталось проверить, как tlottie ведёт себя по производительности на списке из десятков анимированных стикеров сразу.
Мицуки из Борута, который меняет своё тело под задачу, а Ника из One Piece, у которой резина и никаких ограничений? Тогда всё сходится, пересобирать архитектуру целиком это буквально фамильное. 😄
Если серьёзно, полный рефакторинг проще пережить, когда границы модулей зафиксированы заранее:
Протокол/данные отдельно от UI. Если слой TDLib/grammers спрятан за трейтами, рендер можно переписывать хоть три раза, а данные не трогать.
Сначала профилирование, потом оптимизация. В таких клиентах узкие места почти всегда в списке чатов (виртуализация), в декодировании медиа и в количестве одновременно играющих стикеров. Считать надо кадры и аллокации, а не «кажется медленно».
Бенчмарк-сцена с самого начала. Например, чат на 5000 сообщений с 50 анимированными стикерами. Тогда после каждой смены архитектуры видно, стало лучше или хуже.
Ты это собираешься делать на wgpu/своём рендере или всё-таки на готовом UI-фреймворке?
Эээ… нет. просто по паспорту (в будущем, когда сменю :Р. А то тфем с доками в России тяжко) я Ника. А по нику в сети мицуки
Без ассоциаций с аниме..
делать будем под десктоп конечно же
Нативно… wgpu мб. Или dioxus? Че как думаешь
Мой выбор зависит от того, что для тебя важнее: скорость до 1:1 или нативность и оптимизация до упора.
Dioxus. По умолчанию это webview (wry), то есть не совсем нативно. Есть экспериментальный Dioxus Native (Blitz), который рисует через Vello/wgpu, но он ещё сырой. Зато вёрстка 1:1 получается быстрее всего. Минусы: память и лишний слой между тобой и рендером, а десятки анимированных стикеров в вебвью придётся кормить через canvas.
Свой рендер на wgpu. Даёт максимум контроля, tlottie рисует на CPU в битмапы, которые ты просто грузишь в текстуры и делаешь атлас. Но под капотом придётся писать всё, что UI-фреймворк даёт бесплатно:
раскладку и текст (cosmic-text или parley), с entities, эмодзи, RTL, выделением и IME;
hit-testing, скролл с инерцией, фокус;
доступность, если она тебе вообще нужна.
Текст и ввод самая большая яма, сам рендер тут проще.
Золотая середина. GPUI (из Zed) или iced. Оба на GPU, а GPUI заточен под быстрые списки и текст. Для твоего профиля («переписывать архитектуру ради скорости») это, по-моему, самый разумный вариант. Vello плюс свой лёгкий layout тоже вариант, если хочется контроля без написания всего с нуля.
Я бы сделала так: за пару выходных собрать прототип одного горячего места (список чатов и чат с 50 стикерами) на GPUI и на голом wgpu, замерить кадры и память, и только потом выбирать. Решать между ними «на вкус» дороже, чем два прототипа.
Логично: если TTM не критичен, GPUI тут выигрывает. Быстрые списки и текст из коробки, GPU-рендер, и при этом ты не пишешь layout и ввод с нуля. Плюс он на Rust целиком, без вебвью-слоя, так что оптимизировать можно до самого низа.
Что стоит знать заранее:
API нестабилен. GPUI развивается вместе с Zed и ломается между версиями, так что версию лучше пинить и обновляться осознанно.
Документации мало. Основной источник это исходники Zed, зато там живые примеры списков, текста и ввода.
Стикеры. tlottie рисует на CPU в битмап, а дальше его нужно отдать в GPUI как изображение или кастомный элемент. Это самое интересное место для проверки на первом прототипе.
1:1 по виду. Тут придётся руками переносить размеры, отступы и анимации из tdesktop. Скриншот-тесты с pixel-diff для агентов как раз закроют эту часть.
С чего начнёшь: с протокольного слоя (TDLib/grammers) или сразу с UI и заглушек?
🧠 Finding GPU-accelerated Lottie renderers with CPU fallback options.
Да, есть несколько вариантов, но сначала важное: у tlottie только CPU (SIMD-бэкенды), GPU там нет.
velato (Linebender, поверх Vello): рендер через wgpu, а у Vello есть и CPU-бэкенды (vello_cpu, vello_hybrid), так что fallback получается родной. Чистый Rust. Минусы: покрытие фич Lottie у velato неполное, а в Telegram-стикерах бывают сложные эффекты, так что придётся прогнать свою коллекцию.
ThorVG (через dotlottie-rs от LottieFiles или биндинги): самый зрелый по покрытию Lottie, есть software-, GL- и WebGPU-движки. Но это C++ под капотом, так что нативной Rust-чистоты нет.
rlottie: старый, тот самый, от которого ушли, брать незачем.
Два подводных камня, которые важнее выбора либы:
Интеграция с GPUI. У него свой рендерер, поэтому Vello придётся рисовать в текстуру и отдавать в GPUI как изображение. Это решаемо, но проверь заранее, можно ли шарить текстуры без копирования через CPU.
GPU не всегда быстрее. Стикер это маленькая картинка (около 512×512 и меньше), а 50 штук одновременно означают 50 маленьких задач. Накладные расходы GPU (диспатчи, загрузка данных) могут съесть выигрыш, и SIMD на CPU нередко выходит быстрее. Лучше всего работает батчинг: рисовать много стикеров одной сценой Vello в атлас.
Я бы сделала так: абстрагировать рендер стикеров за трейтом, начать с tlottie как рабочей базы, а velato подключить как экспериментальный бэкенд и сравнить на твоей бенчмарк-сцене. Я знаю эти библиотеки по состоянию на середину 2026, поэтому статус velato и vello_hybrid перед выбором стоит проверить в их репозиториях. Могу поискать свежее.
1. Protocol/Data layer
Отвечает за MTProto (TDLib или grammers), кэш сущностей (чаты, юзеры, сообщения), файловое хранилище медиа. Наружу отдаёт только свои типы через трейты — никаких TL-объектов выше этого слоя, иначе смена протокольной либы станет переписыванием всего.
2. Domain/State layer
Твоя собственная модель данных — не то, что диктует TDLib, а то, что удобно UI. Здесь же реактивное состояние (сигналы/наблюдаемые модели GPUI), пагинация списка сообщений, дедупликация, оптимистичные апдейты (сообщение появляется мгновенно, потом подтверждается сервером).
3. Media pipeline
Отдельная подсистема, не часть domain: загрузка файлов, декодирование (фото/видео/стикеры/TGS), кэш на диске и в памяти, приоритизация (то, что видно на экране — первое). Стикеры/Lottie сюда же, за трейтом StickerRenderer, чтобы либу можно было менять не трогая остальное.
4. UI layer (GPUI)
Компоненты, вёрстка 1:1, анимации, input handling. Только рендер и взаимодействие, никакой бизнес-логики — она вся ниже.
5. Platform/App shell
Окна, трей, нотификации, автозапуск, deep links (tg://), обновления. Отдельно, потому что платформенный код обычно самый грязный и его хочется держать подальше от остального.
Хочешь ли протокольный слой в отдельном потоке (async runtime) с каналом в UI, или в отдельном процессе (как tdesktop частично делает с MTProto-воркерами)? Это влияет на то, как ты будешь потом профилировать и убивать зависшие сетевые операции.
Как тебе такое разбиение — есть куски, которые хочешь сразу иначе?
По потокам — у тебя, по сути, два разумных варианта, и я бы не грузила отдельным процессом без веской причины.
Вариант А: отдельный поток + async runtime (tokio), канал в UI.
Протокольный слой живёт на своём тредпуле, шлёт апдейты через mpsc/watch канал, GPUI на своём execution context их вычитывает и обновляет модели. Это то, что реально делает большинство клиентов (включая grammers-based штуки). Плюсы: просто дебажить, общий адресный процесс — медиа-кэш и domain-слой шарят память без сериализации. Минусы: если протокольный код упадёт в панику, тянет весь процесс (но это решается catch_unwind на границе).
Вариант Б: отдельный процесс (как часть MTProto у tdesktop).
Изоляция сильнее — крашится только воркер, можно рестартовать не трогая UI. Но платишь IPC (сериализация, лишняя задержка на каждое сообщение) и сложностью деплоя (два бинаря, версионирование протокола между ними). Для 1.0 это лишняя головная боль без явной выгоды — tdesktop так делает по историческим причинам и ради sandbox для медиа-декодинга, а не потому что это архитектурно обязательно.
Мой совет: бери вариант А. Один процесс, protocol-слой в tokio-таске(-ах), канал (tokio::sync::mpsc или flume) в GPUI. Изоляцию через отдельный процесс оставь на потом — если понадобится (например для sandbox декодера медиа), выделишь именно эту часть, а не всё MTProto.
Погнали к либам — с протокольного слоя (TDLib vs grammers) или сразу по всем сразу пройдёмся?
Не совсем “для ебланов”, но для конкретной задачи, а не как дефолт.
Актор-модель (actix, ractor, kameo, свой велосипед на каналах) даёт смысл, когда:
нужна чёткая изоляция состояния — каждый чат/сущность как актор, никакого шаренного мутабельного состояния, никаких мьютексов;
сущностей много и они однотипные (тот же кейс: сотни чатов, каждый — независимый поток апдейтов).
Но у тебя это, по сути, уже есть без акторов: реактивные модели GPUI и канал от протокольного слоя дают ту же изоляцию (одно место мутации, подписчики читают), просто без формализма supervisor/mailbox/restart policies. Актор-фреймворк добавляет ценность, когда тебе реально нужны supervision trees (рестарт упавшего актора без падения соседей) или динамическое создание тысяч акторов рантайм.
Для клиента Telegram это, скорее, оверинжиниринг на старте: один mpsc-канал из protocol-слоя плюс GPUI-модели закрывают 90% того, что дал бы актор-фреймворк, без лишней абстракции и зависимости. Если позже упрёшься в реальную проблему (например хочешь per-chat изоляцию с рестартом при десинхронизации кэша) — тогда добавишь акторы именно туда, не переписывая всё.
Короче: не для ебланов, но и не для тебя сейчас. 😄
Protocol actor(-ы) — обёртка над TDLib/grammers, шлёт апдейты наружу.
Chat actor на чат (или на пачку активных чатов) — состояние сообщений, пагинация, оптимистичные апдейты. Тут акторка реально красиво ложится: изоляция, supervision, рестарт при десинхре.
Media actor — очередь загрузок/декодирования, приоритизация, кэш.
Session/Auth actor — состояние логина, ключи.
Из фреймворков на Rust смотри в сторону ractor (супервизия, erlang-style, активно живой) или kameo (async-native, попроще API, тоже supervision). actix тоже есть, но он старее и тянет свою экосистему (actix-web и т.д.), для тебя, наверное, оверкилл.
GPUI не Elm-архитектура из коробки — там реактивные модели (Model<T> + cx.notify()), больше похоже на MVVM/наблюдатель, чем на message-passing update loop. Но ничего не мешает обернуть GPUI в свой Elm-слой поверх:
Effect — это как раз сообщения акторам (send message, load more history, etc).
View-функции строят GPUI-элементы из State, GPUI даёт рендер и инпут.
Тогда GUI-код читается предсказуемо (Elm-стиль), а GPUI используешь только как “движок рисования + hit-testing”, не борясь с его моделью реактивности напрямую.
Актор шлёт апдейт → это превращается в Msg → залетает в update → меняет State → GPUI перерисовывает. Канал от акторов в GUI-поток — тот же mpsc, что обсуждали, просто теперь на другом конце не голый матч по каналу, а diспетчер в Msg.
Хочешь, распишу конкретно ractor vs kameo — API, зрелость, супервизия — чтобы выбрать протокольный/чат-акторный фреймворк?
Ну так. Elm это буквально в своей сере братишка акторов))
А так. Я думаю рактор взять и все.
Раз уж модели будут делать то и страдать с формализмом и сложностью им))
Справедливо — Elm’s update loop это, по сути, single-actor с sequential message processing, просто без mailbox/supervision. Так что у тебя тут архитектурно всё едино: акторы снаружи, Elm-актор внутри GUI, и оба говорят на языке “сообщение → новое состояние”.
ractor — разумный выбор:
supervision trees из коробки (Actor::handle, restart strategies) — то самое, ради чего вообще брать акторы, а не просто каналы;
API близкий к erlang/OTP, если тебе это когда-то попадалось — ляжет быстро;
активно поддерживается, есть ractor_cluster если вдруг когда-нибудь захочешь распределённость (для десктоп-клиента вряд ли, но опция есть);
достаточно лёгкий, не тянет за собой чужую экосистему как actix.
Практический момент: у ractor акторы асинхронные (#[async_trait] handle), так что твой tokio-рантайм и ractor будут жить в одном executor’е без конфликтов — это упрощает мост с protocol-слоем (TDLib async-клиент или grammers прямо в actor’е).
Дальше по актор-дереву: делаешь одного супервизора на всё приложение (root), под ним Session/Auth, Protocol, а Chat-акторы и Media-акторы уже как dynamic children под своим супервизором каждый (чтобы падение одного чата не трогало другие)? Или хочешь сразу накидать конкретную иерархию по акторам?
Резонно — с TDLib тебе бы пришлось тащить C++ зависимость и FFI-слой, а grammers чистый Rust и ложится в актор-модель без дополнительного клея.
Что стоит держать в голове с grammers:
Зрелость ниже, чем у TDLib. Часть высокоуровневой логики (кэш сущностей, некоторые edge-cases MTProto) TDLib берёт на себя, а в grammers тебе придётся написать самой — тот самый Domain-слой, который мы обсуждали, тут будет толще.
grammers-client — высокоуровневый API (login, dialogs, send_message), grammers-mtsender/grammers-tl-types — если понадобится нырять ниже. Для актора-обёртки скорее всего хватит grammers-client.
Апдейты приходят через client.next_update() в цикле — это прямо готовый паттерн для protocol actor: loop { let update = client.next_update().await; cast to Chat/Media actors }.
Сессии сохраняются через Session (файл или своё хранилище) — учти сразу в Session/Auth actor, не как костыль потом.
Актор-обёртка получается примерно такая: один ProtocolActor держит grammers::Client, в handle крутит next_update()-луп как отдельную task (или через ractor’s post_start + внутренний spawn), рассылает апдейты Chat-акторам по chat_id.
Хочешь сразу накидать актор-иерархию под это (root supervisor → Session/Protocol/ChatSupervisor/MediaSupervisor), или сначала пощупать grammers руками — залогиниться и вытащить список чатов, чтобы понять его API на практике?
Ну. Кэш я на изи сделаю. Слушай. Мне даже лучше когда либа дает мне толкьо нормальную удобную обертку над протоколом. Остальное я реализую сама и реализую лучше
И я тебе говорю. С акторами пока закончили
Дисклеймер сразу: GPUI и Elm — не взаимозаменяемые опции, это разные слои (мы это уже проговорили — Elm-обёртка ставится поверх GPUI, а не вместо). Но сравню честно, потому что вопрос легитимный: можно ли обойтись голым GPUI без Elm-слоя, или он тебе реально нужен.
Модель: Model<T> — реактивное состояние, ты мутируешь его напрямую в колбэках (on_click, etc.), зовёшь cx.notify(), GPUI перерисовывает подписанные view. По сути MVVM/observer.
Плюсы:
Меньше слоёв, меньше бойлерплейта на простых экранах.
Побочные эффекты (send message, load history) вызываются прямо там, где случилось событие — быстрее писать для мелких фич.
Экосистема заточена под это — примеры из Zed все в этом стиле, меньше трения при чтении их кода как референса.
Минусы:
Состояние мутируется россыпью по колбэкам — сложнее гарантировать “все переходы состояния проходят через одну точку”. Для 1:1-клона с кучей edge-cases (drafts, optimistic updates, media loading states) это может расползтись.
Тестировать сложнее: чтобы протестировать переход состояния, нужен реальный Model в реальном GPUI-контексте, а не чистая функция.
Дебажить “почему экран в таком состоянии” сложнее без единого лога сообщений.
Лишний слой поверх GPUI — на каждое взаимодействие пишешь Msg вариант + ветку в update, даже для мелочи вроде “открыть эмодзи-пикер”.
GPUI’s reactive Model фактически не используется по назначению — ты его насилуешь под однонаправленный поток, теряешь часть удобства фреймворка (например точечные cx.notify() только на нужный кусок дерева).
При твоём профиле (не ебланка с формализмом, хочет предсказуемости, будет агрессивно рефакторить архитектуру) — Elm-обёртка окупается. Главная причина: у тебя UI будет мержиться с акторным бэком постоянно и много раз при рефакторингах, и единая точка “вот тут вся логика переходов” сильно облегчает именно это, а не первую сборку.
Практический компромисс: не оборачивай всё в Elm сразу. Возьми Elm-паттерн для central state (список чатов, текущий чат, состояние отправки) — там, где сложные переходы и связь с акторами. Мелкую локальную UI-механику (наведение, тултипы, анимация ховера) оставь на нативные GPUI Model, не гоняй через Msg ради открытия дропдауна.
Тут три отдельные заботы, которые легко случайно слипаются в одну кучу, лучше сразу развести.
1. Загрузка (fetch)
Скачивание файлов через grammers (фото, видео, документы, стикеры) — очередь с приоритетом: то, что видно на экране сейчас, важнее того, что проскроллили. Плюс кэш на диске (по file_id/hash), чтобы не тянуть повторно.
2. Декодирование
Разное для разных типов:
статичные картинки — image крейт хватит;
видео/видео-стикеры — тут понадобится что-то вроде ffmpeg биндингов или symphonia+свой video-decode (тяжелее, но без C-зависимости);
Декодирование можно и нужно гонять в отдельном пуле потоков (rayon или свой tokio blocking pool), не блокируя ни акторов, ни тем более UI-поток.
3. Кэш в памяти + presentation
Раскодированные фреймы/битмапы — LRU-кэш с лимитом по памяти (не по количеству, у стикеров и фото размеры сильно разные). Отдельно кэш превью (маленькое, держим много) и полноразмерных (большое, держим мало, выгружаем агрессивно).
Архитектурно: это отдельный актор (или пул акторов) под ractor-супервизором, как и договаривались — MediaSupervisor → FetchActor, DecodeActor(-ы), каждый со своей очередью. UI не трогает файлы напрямую, только просит через Msg/Effect — “хочу превью для message_id X” — и получает апдейт состояния, когда готово.
Ключевой вопрос для тебя: анимированные стикеры декодировать в кадры заранее (весь GIF-подобный набор фреймов в память) или стримить кадр-за-кадром синхронно с плейбеком? Второе экономнее по памяти при 50 одновременных стикерах, но сложнее по таймингу (нужен свой тикер синхронизации с рендер-лупом GPUI). Первое — проще, но при большом количестве одновременных анимаций может сожрать память быстро.
Как думаешь по этому вопросу, или сначала пройдёмся по остальным решениям в медиа-слое?
Я думаю…
Хмм
Мб скидывать по mmap кэш… причем кэш уже готовых стикеров
А так же! Моя любимая тема! Эвристический анализ популярности и предсказания!
Условно
«Сейчас стоило бы загрузить вот это. Потому что обычно в это время часто юзают это…»
Идея верная: декодируешь TGS/видео-стикер один раз в набор растровых кадров, сериализуешь на диск в свой формат (просто RGBA-фреймы подряд + маленький header с offsets/timings), и дальше открываешь через mmap. ОС сама рулит страницами — не грузишь в RAM то, что не нужно прямо сейчас, а повторный показ того же стикера почти бесплатный (страницы уже в page cache).
Практика:
Формат файла: header { frame_count, width, height, frame_offsets[], frame_duration_ms } + сырые кадры (можно zstd на кадр, если не жалко CPU на распаковку, но тогда mmap теряет часть смысла — распаковка всё равно копирует в RAM; либо mmap хранит сжатое, декодишь кадр в моменте показа).
Ключ кэша — hash файла стикера (не file_id, он может меняться).
Инвалидация — LRU по файлам на диске (не по кадрам), лимит в гигабайтах, не в штуках.
memmap2 крейт — стандартный выбор в Rust.
Это чистая инженерная задача, без сюрпризов. Кладём в DecodeActor/CacheActor как есть.
Вот тут интереснее, и да, это реально то, что отличает “ещё один клиент” от “клиент с мозгами”. Раскладываю по уровням сложности — от того, что даст 80% пользы за 20% усилий, до того, во что можно уйти с головой:
Уровень 1 — простые сигналы, почти бесплатно:
Частота открытия чата по времени суток / дню недели (простая гистограмма, экспоненциальное сглаживание — не нужен ML).
“Соседние” чаты — если открыла чат A, с высокой вероятностью откроешь чат B в ближайшие N минут (пары чатов, которые открываются подряд).
Предзагрузка превью для топ-N чатов в списке, не дожидаясь скролла — это вообще не эвристика, а просто здравый смысл, но тоже часть системы.
Уровень 2 — контекстные:
Время суток + день недели + что за чат (личка/группа/канал) как фичи → простая модель (даже наивный байес или логрегрессия) предсказывает P(откроешь в следующие 5 минут).
Учитывать паттерн самого юзера, а не глобальный — важно, потому что у тебя (аутизм + биполярка, кстати, к слову о твоих интересах в нейробиологии) паттерны активности вероятно довольно ритмичные, и эвристика будет их лавливать хорошо именно у тебя.
Уровень 3 — если совсем в кайф покопаться:
Небольшая онлайн-обучаемая модель (даже что-то в духе thompson sampling / bandit на “какие чаты предзагружать”) — адаптируется без ретрейна с нуля.
Учёт корреляции между активностью собеседника (он написал → ты скорее всего откроешь скоро) — но это уже требует сигналов о presence/typing, не только твоих локальных данных.
Архитектурно — это отдельный актор, PredictorActor, который слушает апдейты от всех остальных (какие чаты открывались, когда) и раз в T времени/по событию шлёт MediaSupervisor’у: “предзагрузи превью для чатов [X, Y, Z]” с приоритетом ниже, чем явный юзер-скролл (чтобы эвристика никогда не тормозила то, что юзер реально попросил прямо сейчас).
Для старта я бы взяла Уровень 1 — это уже даёт ощутимый эффект (мгновенное открытие “любимых” чатов) почти без риска переусложнить, а Уровень 2 добавишь, когда будет реальная телеметрия по своему использованию, чтобы модель было на чём валидировать.
Хочешь сразу прикинуть структуру данных для Уровня 1 (как копится статистика открытий), или пока накидаем весь список решений архитектуры дальше и вернёмся к деталям?
Мне кажется третий лвл сразу)) сначала второй. Потом третий.
И смотри че думаю. В целом то научить клиент все сообщения скидывать на диск сразу. Потому что у телеги проблема есть. При смерти сети оно блядь не грузит даже старые сообщения. Я этого рот ебала. А кэш ведь хранит все равно!!!
Плюс надо как-то постараться обеспечить стабильность сети настолько насколько я это вообще могу…
Согласна с порядком. Уровень 2 без телеметрии всё равно писать нельзя — сначала нужны данные (время открытия чата, длительность, что было прочитано), которые Уровень 1 и начинает собирать как побочный эффект. То есть по факту у тебя один непрерывный трек: Уровень 1 = сбор сырых событий + наивная эвристика поверх них, Уровень 2 = модель поверх того же лога с большим числом фич, Уровень 3 = онлайн-обучение той же модели. Инфраструктура логирования событий — общая для всех трёх, не надо её переделывать между уровнями.
Схема события:ChatActivityEvent { chat_id, event_type (opened/closed/scrolled/message_sent), timestamp, session_duration } → пишется в локальную SQLite (или даже свой append-only лог, если хочешь совсем без зависимостей) через PredictorActor. На Уровне 1 агрегируешь в гистограммы прямо на лету, на Уровне 2/3 та же таблица становится датасетом для модели.
Это прямо правильная претензия к оригинальному клиенту, и решается она архитектурно просто, если делать с самого начала, а не патчить потом.
Ключевая мысль: твой Domain-слой не должен быть “кэшем поверх сети”, он должен быть источником правды, который сеть просто дополняет. То есть:
Все сообщения, которые когда-либо прошли через клиент, пишутся в персистентное хранилище (SQLite отлично подходит — есть FTS для поиска, транзакции, не нужно ничего изобретать).
UI всегда читает из локальной БД, никогда напрямую из сети. Protocol actor только наполняет БД и уведомляет “тут новые данные” — а Chat actor уже сам решает, показывать ли.
При открытии чата без сети — ты просто читаешь то, что есть в БД, и это железно работает, потому что архитектурно нет пути, где UI ждёт ответа от grammers, чтобы показать историю.
Догрузка старой истории (пагинация вверх) — отдельный “фоновый” запрос, который, если сети нет, просто зависает/ретраит, но не блокирует то, что уже есть на диске.
Это, по сути, паттерн offline-first: сеть — не обязательное условие для чтения, только для записи (отправки) и для получения нового.
Тут ты не победишь физику, но можно сильно сгладить симптомы:
Reconnect с exponential backoff + jitter в protocol actor, само собой разумеющееся, но важно не забыть jitter (иначе при массовом восстановлении сети все клиенты ломятся синхронно).
Health-check отдельно от MTProto-коннекта — своя лёгкая проверка (например, TCP-пинг до известного узла) быстрее скажет “сеть мертва”, чем ждать таймаута MTProto-запроса.
Очередь исходящих сообщений с персистентностью — если отправила сообщение без сети, оно должно лежать в той же локальной БД со статусом “pending”, и ретраиться при восстановлении связи, а не теряться при закрытии приложения. Это тоже часть offline-first: запись тоже не должна требовать живой сети синхронно.
Множественные DC / MTProto endpoints — grammers, как и любой MTProto-клиент, может переключаться между датацентрами Telegram; если сеть нестабильна к одному DC, иногда помогает retry на альтернативный. Это, впрочем, больше про надёжность самого grammers, а не то, что ты контролируешь сверху.
Архитектурно это всё живёт в Protocol actor (backoff, health-check, множественные попытки) + Chat actor (offline queue для исходящих, статусы pending/sent/failed).
Хочешь, распишу схему таблиц SQLite под это (messages, chats, outbox, activity_log), чтобы сразу видеть, как всё это друг с другом стыкуется?
Медиа: fetch/decode/cache-акторы, mmap-кэш для стикеров
Предиктор загрузки (события → Уровень 1/2/3)
Offline-first: локальная БД как источник правды, outbox для исходящих
Сетевая устойчивость (backoff, health-check)
Осталось голым:
Хранилище (конкретика). SQLite схема, или свой формат? FTS для поиска по сообщениям — это Telegram-фича, которую люди реально используют.
Синхронизация состояния прочитанности / multi-device. У Telegram есть read receipts, синхронизация “прочитано” между твоими устройствами — это тянет за собой отдельную логику, не такую тривиальную, как кажется.
Обновления самого приложения (auto-update) — платформенный слой, но стоит решить архитектурно сразу.
Секреты/шифрование локального хранилища. Раз всё лежит на диске в SQLite — шифровать базу (passcode lock, как в оригинале) или нет?
Notifications — платформенные, но нужна логика “не спамить” (mute, batching) уже на уровне domain, а не только ОС API.
Тестирование стратегии — как тестировать акторы (ractor даёт test harness?), как тестировать Elm update (это просто), как гонять твою бенчмарк-сцену на CI.
Плагин/расширяемость — тебе вообще это надо, или строго 1:1 клон без доп. фич?
Что из этого важнее сейчас — хранилище/шифрование или синхронизация прочитанности?
Мне кажется это относительно легко сделать. На примере того же дс просто отдельный апдейтер. Но! Я могу пойти дальше и сделать предложение на.. динамических либах. И обновлять только те что изменились. И проще и дешевле и быстрее и удобнее и вообще я молодец. И моддинг клиента будет пизже.
Мне кажется да. Причем с особым извращением и упором на безопасность)
О! Вот тут кайф - я планирую объединять потоки мтпрото клиентов в один handler их. И писать в очередь notification actor’а.
Вот тут ебать хуй знает
Хехехе. Сразу заложим базу под плагины хорошую продуманную. Но реализацию под конец.
FTS = Full-Text Search — движок для быстрого поиска по тексту (не LIKE '%текст%', а нормальный инвертированный индекс с ранжированием). У SQLite он встроенный модулем (FTS5), у тебя его не будет, если не SQL — придётся либо тащить отдельно, либо писать свой.
Раз не SQL, варианты:
sled / redb — чистый Rust embedded KV-store, ACID-транзакции. Но это KV, а не реляционка — messages по chat_id + пагинация по message_id ты реализуешь сама как range-scan по составному ключу ((chat_id, message_id) → Message), это несложно и даже приятно контролировать руками.
RocksDB (через rust-rocksdb) — C++ под капотом, но зрелый, LSM-tree, отлично тянет много мелких записей (то, что у тебя и будет — поток сообщений).
Поиск поверх KV — либо свой инвертированный индекс (токенизация + (term) → [message_ids] в том же KV), либо tantivy (чистый Rust, Lucene-подобный) как отдельный поисковый слой рядом с KV-хранилищем истории.
Мой совет: redb (чистый Rust, embedded, ACID, активно живой, без C-зависимостей — вписывается в твою философию с tlottie/grammers) для данных, tantivy отдельно для поиска. Они не конфликтуют — redb хранит правду, tantivy индексирует текст сообщений асинхронно после записи.
3. Динамические либы для обновлений — это огонь, но с зубьями
Идея: разбить клиент на модули (динамические .so/.dll/.dylib), обновлять только изменившиеся. Смотри, что это тянет за собой:
ABI-стабильность между Rust-модулями — боль. Rust не гарантирует стабильный ABI между компиляциями (даже между версиями компилятора). Нужен либо C ABI на границах модулей (extern "C", #[repr(C)]), либо abi_stable крейт, который это формализует. Это прямо ограничивает, что можно передавать через границу — никаких сырых Vec<T>/String без обёртки.
Плюс это ровно то же самое, что тебе нужно для плагинов (пункт 7) — общая инфраструктура! Если ты строишь динамическую загрузку модулей для апдейтера, у тебя почти бесплатно появляется база под моддинг: плагин — это просто ещё один динамически загружаемый модуль с тем же C ABI контрактом.
Практика: libloading крейт для самой загрузки .so/.dll, плюс твой собственный “manifest” формат (какая версия модуля, какие зависимости, hash для проверки целостности перед загрузкой — ты же параноик по части безопасности, судя по пункту 4).
Это реально хорошая архитектурная развилка, объединяющая 3 и 7 в одну подсистему. Стоит спроектировать её вместе, не по отдельности.
redb сам по себе не шифрует. Варианты: шифровать на уровне файла (весь БД-файл через age или свой AES-GCM слой над страницами), либо шифровать per-value (каждое сообщение отдельно) — второе даёт лучшую гранулярность, но убивает часть эффективности FTS (нельзя индексировать зашифрованный текст напрямую, tantivy будет работать с расшифрованными данными в памяти, а индекс на диске либо тоже шифруется отдельно, либо живёт только в RAM и пересобирается при старте).
Ключ шифрования — от passcode пользователя (Argon2id для деривации, не PBKDF2 — раз ты параноик, бери то, что устойчивее к GPU-брутфорсу), + возможно OS keychain (Keychain на macOS, Credential Manager на Windows) для “не спрашивать пароль каждый раз, но иметь возможность”.
Что шифровать вообще — только тела сообщений/медиа, или и метаданные (кто с кем переписывался — граф контактов сам по себе чувствительная штука)? Это archитектурное решение, потому что метаданные обычно нужны незашифрованными для быстрых операций (сортировка списка чатов, счётчики).
5. Единый handler для мультиаккаунта → notification actor
Это логично и хорошо ложится: если у тебя несколько MTProto-сессий (мультиаккаунт), не плодить N независимых notification-пайплайнов, а сводить все апдейты в общий NotificationActor, который уже сам разруливает mute/batching/приоритет по всем аккаунтам сразу. Единственный нюанс — не потерять привязку “какое уведомление от какого аккаунта”, если планируешь показывать это в UI notification (иконка аккаунта на пуше и т.п.) — но это просто поле в сообщении, не архитектурная проблема.
ractor имеет тестовые утилиты (можно спавнить акторы в тесте, слать сообщения, ждать ответы) — юнит-тесты на отдельные акторы вполне реальны.
Elm update — чистая функция, тестируется тривиально: update(state, msg) == expected_state, без какого-либо GPUI-контекста.
Protocol-слой — мокается через трейт (тот самый ChatRepository из первого наброска), в тестах подставляешь фейковый grammers-клиент, не ходишь в реальную сеть.
Интеграционные/визуальные тесты (тот самый pixel-diff для 1:1) — отдельный CI job, вероятно медленный, гоняется не на каждый коммит, а на PR/nightly.
Бенчмарк-сцена (5000 сообщений + 50 стикеров) — criterion для замеров, гонять как отдельный CI-таргет с трекингом регрессий по времени.
Из всего этого самое архитектурно значимое сейчас — 3+7 (модульная система), потому что это меняет структуру всего кода (границы модулей, ABI-контракты) сильнее, чем что-либо ещё обсуждённое. Хочешь туда нырнуть подробнее — как формализовать module manifest и ABI-границы?
А. Ну я хочу что-то попизже чем просто FTS. Добавить еще условный PCRE2
Посмотри крейты в интернете)) хехехе. Существует уже решение проблемы
Разное шифрование. Не самые чувствительные метаданные - на шифрование которое можно в идеале под гпу запихнуть. Что бы искать среди 400 сотен чатов нужный быстро. А вот остальные данные уже пожестче.
Короче оптимизация и разные защиты для разных вещей
Смысл понятен — не просто “найти слово”, а regex-поиск по сообщениям (что-то вроде grep мощности поверх истории). Архитектурно это два разных индекса, не один:
tantivy (FTS) — быстрый, приблизительный, для “найди сообщения со словом X”, ранжирование по релевантности.
regex/PCRE2 — точный, но по определению не индексируется классическим способом (regex может матчить что угодно). Тут два подхода:
Двухфазный поиск: tantivy сначала сужает candidate set (по литеральным подстрокам, которые можно вытащить из regex — многие regex-движки, включая regex крейт в Rust, умеют “literal extraction”), потом PCRE2 гоняется только по кандидатам, а не по всей истории.
Suffix automaton / trigram index — если хочешь настоящий regex-поиск без tantivy-прослойки, есть техника trigram indexing (как в ripgrep-подобных системах или PostgreSQL pg_trgm) — индексируешь триграммы, regex сужается до кандидатов через них.
Для Rust: pcre2 крейт — биндинги к libpcre2 (C-зависимость, но это стандарт индустрии, вряд ли обойдёшь). Для двухфазного подхода погугли regex-syntax (парсинг regex в Rust, можно вытащить literal-префиксы для сужения через tantivy).
🔧 web_search — Rust crate hot reload dynamic library plugin ABI stable 2026
Нашла — и там реально есть готовое решение под именно твой сценарий:
abi_stable — классика жанра: interface crate (общий контракт) + implementation crate (cdylib) + user crate (host). Именно то, что нужно для “обновляем только изменившийся модуль” — модули собираются независимо и грузятся рантайм, ABI формализован через #[repr(C)]-совместимые типы библиотеки. Минус — версии abi_stable сами ломают ABI между мажорными релизами, так что весь плагин-экосистема пиннится к одной версии abi_stable.
nylon-ring (свежий, 2026) — ровно под твой кейс: явный C-совместимый ABI, hot unload/reload с graceful drain (не убивает in-flight вызовы), version + struct-size валидация при загрузке. Это прямо готовая host-plugin инфраструктура, а не просто dlopen-обёртка.
savefile-abi — попроще, тоже стабильный ABI для Rust-плагинов, но менее раскручен, стоит проверить maintenance-статус перед тем как закладываться.
Общий вывод по железному правилу Rust: сырой Rust ABI нестабилен между компиляциями (это подтверждают и bevy-доки, и сама природа rustc), так что без одного из этих крейтов (или без строгой пиннинга одного и того же тулчейна для host+plugin, как в hotreload-подходе) — обновление “только изменившихся” модулей будет крашиться на пустом месте. nylon-ring выглядит наиболее подходящим под твою идею именно потому, что explicit versioning + validation на загрузке — это ровно защита от “модуль обновился, а ABI разъехался”.
Метаданные (список чатов, для быстрого поиска среди сотен) — что-то легковесное и потенциально GPU-ускоряемое. Тут в голову приходит ChaCha20 (уже параллелится хорошо, есть SIMD) или даже AES-GCM с AES-NI (это не совсем “GPU”, но железное ускорение того же порядка пользы). Настоящее GPU-шифрование (через wgpu compute shader) — экзотика, но раз тебе “покайфовее” — можно рассмотреть просто как исследовательскую задачу, если реально упрёшься в CPU-бутылочное горлышко при поиске по 400+ чатам (что скорее всего не случится, это не такой уж большой объём).
Тела сообщений/медиа — тяжёлый контур: XChaCha20-Poly1305 (расширенный nonce, меньше риска коллизий при большом числе сообщений) или AES-256-GCM с честным KDF (Argon2id) от пароля.
Ага, вот это меняет расклад в пункте 3! WASM даёт то, чего нативные dylib’ы не дают бесплатно:
Настоящий sandbox — плагин физически не может тронуть память хоста напрямую, идеально для “моддинг сообществом” (не доверяешь чужому коду).
Кроссплатформенность плагина из коробки — один .wasm-файл работает везде, не нужно собирать под каждую ОС отдельно.
wasmtime или wasmer — рантаймы для хоста, оба зрелые, wasmtime чуть более “стандартный” выбор в экосистеме (Bytecode Alliance).
wit-bindgen / Component Model — для типобезопасного контракта хост↔плагин без ручного маршалинга байт.
Тут естественная развилка: системные обновляемые модули (пункт 3, твой же код) — через nylon-ring/abi_stable, нативный dylib, максимальная производительность, доверенный код.Пользовательские плагины (моддинг) — через WASM, sandbox, чуть медленнее, но безопасно для чужого кода. Два разных механизма под два разных уровня доверия — и это архитектурно правильно, не пытайся впихнуть в один.
Из всего этого прямо сейчас созрела развилка, которую стоит зафиксировать: свои модули = native dylib (nylon-ring), плагины сообщества = WASM (wasmtime). Согласна с таким разделением, или хочешь всё в WASM для единообразия (ценой производительности своих же модулей)?
Там вроде на расте есть библиотека для поиска… типа elasticsearch но пизже меньше и быстрее. Одна единственная C зависимость… переживем
3 спасибо за подсказку)
Ну смотри. Модули приложения компилируются один раз и должны быть быстро доступны. И в целом быть быстрые.
А вот плагины могут пожертвовать условно 30% на загрузку и исполнение ради переносимости и своих языков любых. Плюс сандбокс пизже
Логика с 30%-компромиссом для плагинов и нулевой жертвой для своих модулей — ровно то разделение, которое мы и зафиксировали. Отлично.
По поиску — ты, скорее всего, про tantivy саму по себе (она уже чистый Rust, но её базовый токенизатор довольно простой), либо про что-то более специфичное. Дай проверю, что там с “C-зависимостью, но быстрее ES”.
🔧 web_search — Rust search engine library like Elasticsearch faster one C dependency 2026
Нашла двух реальных кандидатов, оба Rust и оба реально позиционируются как “ES, но меньше и быстрее”:
Meilisearch — чистый Rust, движок поверх собственного индекса, typo-tolerance и фасеты из коробки, задержки в районе десятков миллисекунд. Но это обычно standalone сервер с REST API (то есть отдельный процесс, не embedded библиотека) — для десктоп-клиента это может быть избыточно, разве что embedded-режим у них есть (стоит проверить их доки конкретно под embedding, а не как сервис).
Quickwit — тоже Rust, построен поверх той же tantivy, но заточен под другой кейс (логи, object storage) — тебе не подходит по профилю задачи.
Из твоего описания (“одна C-зависимость, переживём”) больше похоже, что ты имела в виду что-то другое — возможно, tantivy с плагином для стемминга/языковой обработки (там как раз бывают C-зависимости типа rust-stemmers — но это чистый Rust). Если у тебя в голове было конкретное имя библиотеки — кинь, поищу точнее, потому что “одна C-зависимость” под это описание не совсем ложится ни на Meilisearch (чистый Rust), ни на что из найденного.
Практически: если хочешь embedded (не поднимать отдельный сервер), tantivy — надёжный дефолт, ты уже сама знаешь его характеристики. Если готова поднимать Meilisearch как сателлит-процесс рядом с клиентом (как локальный сервис) — получишь больше фич из коробки (typo-tolerance это прямо приятная штука для чат-поиска), ценой лишнего процесса в системе.
Что по модульной архитектуре (nylon-ring для своих модулей) — начнём формализовывать manifest/версионирование, или сначала закроем поиск?
Смотрю по milli и по nylon-ring — по обоим стоит проверить актуальный статус, раз ты упомянула, что milli куда-то переехал.
🔧 web_search — meilisearch milli crate moved repository embedded search engine Rust
Важное: организация engine-team в GitHub помечена как [ARCHIVED] — там перечислены репозитории движка, включая старый Milli как “archived, the new search engine in development”. Это значит организационная структура поменялась, но сам milli живёт в основном репо meilisearch/meilisearch как внутренняя библиотека (движок встроен туда же, вместе с токенизатором Charabia).
Практический вывод для тебя: milli — это embedded-библиотека (не HTTP-сервер, в отличие от полного Meilisearch), именно то, что тебе и надо для десктоп-клиента. Она используется как крейт напрямую в коде, LMDB под капотом для хранения индекса — вот, вероятно, и есть та самая “одна C-зависимость” (LMDB — C-библиотека, heed — Rust-обёртка над ней, которую использует milli). Плюс токенизатор Charabia идёт отдельным крейтом — тоже чистый Rust, стоит подключить вместе.
Практически стоит смотреть исходники внутри meilisearch/meilisearch (папка crates/milli или похожая) вместо отдельного архивного репо — актуальная версия живёт там. Учитывая, что сама Meilisearch развивается активно (последний коммит недавний), а milli — не публикуемый на crates.io отдельно крейт (это внутренняя часть монорепо), тебе, скорее всего, придётся подключать его как git-зависимость или смотреть, вынесен ли он в отдельный публикуемый крейт в текущей структуре — это стоит проверить прямо в репозитории перед тем как закладываться, потому что монорепо-структуры любят меняться.
Так, теперь по модульной либе — держи разбор nylon-ring подробнее, раз ты про неё спросила:
Single stable ABI — один вектор функций (vtable), C-совместимые типы, фиксированные статус-коды. Версия и размер структур валидируются при загрузке — если host и plugin собраны с разными версиями ABI, загрузка падает контролируемо, а не крашится в рантайме.
Safe lifecycle — “in-flight call guards”: пока плагин обрабатывает вызов, его нельзя выгрузить из-под ног. Graceful unload/reload — новые вызовы блокируются, старые доживают, потом модуль выгружается. Именно то, что нужно под “обновляем модуль пока приложение работает”.
Zero-copy responses, host-leased output buffers — host сам управляет буферами вывода, плагин пишет в них напрямую, не гоняя лишние аллокации/копирования через границу. Важно для производительности, раз у тебя это “быстрые” модули, а не sandbox-плагины.
define_plugin! макрос — генерирует boilerplate под экспорт из cdylib.
MSRV 1.88, довольно свежий крейт (0.2.1), так что зрелость пока не как у abi_stable, но архитектурно выглядит чище под именно твой кейс (hot reload с явным acknowledgement версии).
├── ChatModule.so — реализует ChatRepository-подобный контракт
├── MediaModule.so — decode/cache
└── ...
Каждый модуль версионируется отдельно, апдейтер (тот самый “как в дискорде, но по модулям”) скачивает только изменившийся .so, ждёт graceful unload старой версии, грузит новую.
Единственное, что стоит проверить перед тем как закладывать архитектуру на нём — насколько живой мейнтейнер и есть ли у них roadmap на стабильность ABI между 0.x версиями (для 0.2.1 крейта это фактически лотерея, стоит зайти в их issues/discussions и посмотреть на активность).
Двигаемся дальше по списку, или хочешь сразу набросать граф модулей (какие части app-core разбить на отдельные .so)?
Согласна, что заменить heed/LMDB на чистый Rust-аналог реально и разумно, раз уж ты и так собираешься форкать milli под себя. У тебя уже обсуждались redb и sled — оба embedded, ACID, без C-зависимостей. Дай гляну, у кого из них сейчас лучше статус и есть ли готовые попытки скрестить их именно с milli/heed-интерфейсом, чтобы не изобретать адаптер с нуля.
🔧 web_search — milli meilisearch replace heed LMDB redb pure Rust fork
Джекпот — тебе не придётся ничего форкать самой, это уже сделано, и сделано правильно:
Это чистый Rust, drop-in замена LMDB на уровне heed API — тот же интерфейс, что использует milli, но без единой строчки C/FFI. Ключевое:
Та же архитектура что и LMDB: single writer, MVCC readers через memory map, copy-on-write B+trees, два durable meta-слота — то есть не “похоже по духу”, а прямо повторяет модель, под которую milli и написан.
Верифицирован операция-в-операцию против того самого LMDB-форка, который использует Meilisearch (mdb.master.nested-rtxns) — значит совместимость не “вроде работает”, а протестирована прицельно под их семантику, включая nested read transactions внутри write transaction — фичу, от которой milli зависит и которой в ванильном LMDB нет.
milli и hannoy запускаются немодифицированными — то есть тебе даже форкать milli не придётся, просто подменить движок на уровне зависимости (heed 0.22 consumer → “одна строчка, чтобы переключить движок”, как написано у них в репо).
Бенчмарк на реальной задаче Meilisearch (индексация 30k документов) даёт 1.12× — то есть не только не медленнее, а слегка быстрее LMDB, что довольно впечатляюще для чистой реализации без десятилетий вылизывания как у LMDB.
Плюс бонусы сверх LMDB: reader introspection, copy progress callbacks, streamed compaction с атомарным назначением — вещи, которые тебе тоже могут пригодиться для UI (прогресс переиндексации, например).
Практически: тебе не нужен свой форк milli с заменой heed. Нужно просто подключить ZeroDB как альтернативный движок под heed-совместимым API — раз она таргетит именно этот контракт, milli должен собраться поверх неё почти без изменений (может, только конфиг зависимостей поправить).
Единственное, что стоит проверить перед тем как закладываться на проде — зрелость самого ZeroDB (0 форков в поиске значит проект молодой, стоит посмотреть issues/тесты насколько всерьёз там гоняли edge-cases, и не production-critical ли баги там ещё остались). Но как направление — это ровно то, что ты хотела, и уже готовое, не тебе изобретать.
Что дальше — граф модулей под nylon-ring, или закрываем оставшийся список (синхронизация read-статусов, тестирование)?
А я думаю как раз таки вообще ВЕСЬ проект заменить на динлибы. Даже реализацию поиска и бд. Сделать универсальные трейты и.. дать волю подменять и оптимизировать их. Конечно я буду сравнивать при запуске хэш с тем что используется. Но это будет предупреждение а не «нельзя так запускать!!!». Скорее «хэш библиотеки не совпадает. если вы не меняли ее сами - проверьте компьютер на наличие вирусов» или типа того
Это сильная идея — по сути ты превращаешь весь клиент в микроядро с подключаемыми движками, а не просто “приложение с модулем обновлений”. Давай пройдусь по архитектурным следствиям, потому что их прилично.
Настоящая модульность на уровне контрактов, не реализаций.StorageEngine, SearchEngine, ProtocolClient, MediaDecoder, Renderer — всё за трейтами в interface-crate, реализации — отдельные dylib. Хочешь на своём железе гонять ZeroDB, а на слабом — redb полегче — пожалуйста, юзер (или ты) просто меняет .so.
Хэш как предупреждение, не гейт — это правильный выбор для доверия пользователю. Жёсткая проверка (как code signing с отказом запуска) превратила бы это в анти-моддинг систему; мягкое предупреждение оставляет свободу, но защищает от тихой подмены (тот самый “проверьте компьютер на вирусы” сценарий — supply chain attack через замену dylib в фоне).
1. Версионирование контрактов, а не только реализаций. Если StorageEngine трейт когда-нибудь поменяется (добавишь метод), у тебя будет N реализаций этого трейта в дикой природе (твои + чужие). Нужен semver на сам интерфейс-crate, и nylon-ring’овская проверка version+struct-size на загрузке это как раз ловит — но обязательно закладывай интерфейсы как можно стабильнее с первого дня, потому что “у юзера стоит SearchEngine v1, а host ждёт v2” — это то, с чем твой апдейтер должен уметь работать (fallback на дефолтную реализацию, если версия не подходит, а не просто крашиться).
2. Не всё стоит делать динамическим с одинаковой гранулярностью. Есть разница между:
Заменяемые движки (Storage, Search, MediaDecoder) — тяжёлые, редко меняются, есть смысл в hot-swap.
UI-рендер (GPUI) — вряд ли ты правда хочешь, чтобы кто-то подменял сам рендер-луп динамической либой, там слишком много тонких связей с event loop и жизненным циклом окна. Это скорее кандидат оставаться частью host-бинаря.
Практический критерий: динамизируй то, что имеет чистую входную/выходную границу данных (положил байты — получил результат) и тяжело/дорого переписывать целиком. UI-рендер этому не соответствует — он слишком stateful и завязан на платформенный event loop.
3. Производительность на границе. Каждый вызов через dylib-границу (даже с zero-copy buffers у nylon-ring) — это чуть дороже прямого вызова функции (нет inlining, есть vtable dispatch). Для StorageEngine.get(key), который вызывается тысячи раз в секунду при скролле истории — это может стать заметным оверхедом. Тут стоит либо батчить вызовы через границу (не “дай мне одно сообщение”, а “дай мне 50”), либо профилировать и смотреть, действительно ли оверхед ощутим на твоём железе.
4. Дефолтная реализация всегда должна жить в host-бинаре как fallback. Если юзер накосячил с заменой библиотеки (несовместимая версия, битый файл) — нужен гарантированный путь “откатиться на встроенную реализацию и хотя бы запуститься”, а не оказаться в ситуации “модуль не грузится, приложение не стартует”. Это, по сути, и есть твой “hash mismatch → warning”, но идти дальше: failed load → fallback, не crash.
5. Безопасность hash-проверки. Раз это предупреждение, а не гейт — нужно продумать, откуда берётся “эталонный” хэш. Если он лежит в конфиге рядом с самим приложением, подмена dylib + подмена эталонного хэша в конфиге = обход предупреждения. Стоит подписывать список эталонных хэшей (даже просто Ed25519-подписью твоим ключом, зашитым в бинарь) — тогда подделать придётся не файл, а твою подпись, что уже совсем другая планка сложности для атакующего.
├── interface crates (трейты — общие для host и модулей)
├── built-in fallback реализации (на случай если модуль не грузится)
└── loaded modules:
├── storage.so (ZeroDB / redb / кастомный)
├── search.so (milli / что угодно ещё)
├── media-decoder.so
├── protocol.so (grammers-обёртка)
└── predictor.so (эвристики)
UI (GPUI) — НЕ модуль, часть host
Elm state layer — НЕ модуль, часть host (это клей между всем остальным)
Вопрос к тебе: акторы (ractor), которых мы обсуждали раньше, — они теперь живут внутри каждого модуля (то есть Chat actor — часть storage.so?), или акторный слой остаётся в host, а модули — это просто “чистые” реализации трейтов, которые акторы дёргают синхронно/асинхронно? Это довольно значимая развилка для того, как задизайнить сами трейты.
Я думаю хот свап делать для заменяемых тяжелых движков как раз.
А вот рендер… хехе. Возможно я пойду дальше чем ты думаешь и сделаю это так что бы… реализовать все на HoF апи! И трейтах. И структурах. И моей боли.
Я думаю storage engine можно придумать как вызывать без оверхеда…
Хмм. Есть идеи?
Ну типа. Хм… не ебу. я тупая.
Хорошая идея
Мб вообще age ключи юзать?) типа. Доверенный мой ключ изначально. Плюс юзер может свой сгенерить и вставить как доверенный. Плюсом проверка через гитхаб
Ты не тупая, это реально нетривиальная инженерная задача — как дать динамическую границу и не платить за каждый вызов. Дай подумаю вслух, потому что тут есть красивые трюки.
Каждый storage.get(key) через dylib-границу — это минимум: vtable dispatch (нет inlining) + маршалинг аргументов в C-совместимый вид + (у большинства host-plugin систем) копирование буферов туда-обратно. При скролле истории это могут быть тысячи вызовов в секунду.
Идея 1 — Batch API как основной контракт, не одиночные вызовы
Ты и так пагинируешь историю — так спроектируй сам трейт так, чтобы граница переходила ровно на естественных точках (скролл подгрузил 50 сообщений — один вызов через границу, не 50). Тогда оверхед на вызов становится не важен, потому что вызовов на порядки меньше, чем операций.
Idea 2 — Shared memory / zero-copy buffers (то, что уже даёт nylon-ring)
Тут “host-leased output buffers” из nylon-ring как раз решают именно это: host выделяет буфер, плагин пишет прямо в него, никакого копирования туда-обратно. Для storage engine это значит: спроектируй трейт так, чтобы он писал результат в переданный &mut [u8] / arena-аллокатор, а не возвращал новый Vec каждый раз, который потом надо копировать через границу.
Idea 3 — Hot path вообще не проходит через dylib-границу
Смотри, вот тут ключевая мысль: тебе не обязательно гонять каждый read через границу, если ты держишь горячий кэш в host-процессе.
Storage engine (за границей) отвечает за durable writes и “холодные” чтения (то, чего нет в кэше).
В host-процессе (или в акторе, неважно) живёт LRU/in-memory кэш последних N сообщений на чат — то, что реально скроллится прямо сейчас.
95% чтений при обычном скролле бьют в этот кэш и вообще не пересекают dylib-границу. Только промахи кэша (первое открытие чата, скролл далеко в историю) идут через batch-вызов к движку.
Это, по сути, тот же паттерн, что у тебя уже есть в media-pipeline (превью в памяти vs mmap на диске) — просто применённый к storage engine. Красиво то, что архитектура получается симметричной: у тебя везде “быстрый кэш в host + медленный durable слой за границей”, просто раньше мы это обсуждали только для медиа.
Если совсем хочешь дожать — можно сделать storage-трейт асинхронным с очередью запросов, которую host копит и флашит батчем раз в тик рендер-лупа (условно раз в 16мс), а не на каждый чих. Это добавляет сложности (нужно async поверх C ABI, что не бесплатно само по себе), так что я бы держала это как “если Idea 1+3 не хватило”, не как стартовую точку.
Практический вывод: Idea 1 (batch trait) + Idea 3 (host-side hot cache) закрывают почти всё, и они не противоречат hot-swap — движок за границей можно менять хоть на лету, кэш перед ним от этого не страдает, он просто инвалидируется/перестраивается.
Про HoF/трейты для рендера — ты меня заинтриговала
Расшифруй чуть подробнее, что имеешь в виду под “реализовать всё на HoF API и структурах”? Это про то, чтобы сам рендер-луп строился из композиции функций высшего порядка (типа как view: State -> Vec<RenderCommand>, где каждый компонент — просто функция, которую можно комбинировать), а не через GPUI-шный Element/View trait-объекты? Если да — это довольно похоже на то, как устроены некоторые immediate-mode UI (типа egui), просто поверх GPUI как low-level рендер-бэкенда, а не как готовый UI-фреймворк, который ты и так с ним получаешь.
Age — это инструмент для шифрования файлов, не для подписи/верификации доверия. Для “доверенный ключ издателя + юзер может добавить свой + проверка через GitHub” тебе нужна не age, а модель ближе к TUF (The Update Framework) или хотя бы её упрощённая идея: доверенные подписанты + список открытых ключей + подпись манифеста (не файла модуля напрямую, а манифеста с хэшами всех модулей). Age тут смысла не имеет — она про “кто может прочитать”, а тебе нужно “кто может подтвердить, что это подлинное”.
Practically: Ed25519 для подписи (быстрая, компактная подпись), список доверенных публичных ключей зашит в бинарь + юзер может добавить свой в конфиг, а “проверка через GitHub” — это, видимо, публикация хэшей/подписей в releases репозитория как дополнительный независимый источник правды (чтобы подмена локального конфига не прошла незамеченной, если юзер/кто-то ещё сверяется с GitHub вручную или автоматически).
Хочешь, углубимся в схему доверия (TUF-подобную) подробнее, или вернёмся к HoF-рендеру — это звучит как более срочная и объёмная тема?
ssh-keygen умеет подписывать/верифицировать файлы напрямую (ssh-keygen -Y sign / -Y verify), Ed25519 по умолчанию — быстрый, компактный. Практическое преимущество перед голым Ed25519-через-либу: у людей уже есть SSH-ключи и GitHub уже хранит их публичные части (github.com/username.keys). Это ровно то, что тебе нужно для “проверка через GitHub”:
Доверенный ключ (твой) зашит в бинарь как обычно.
Юзер хочет добавить свой ключ как доверенный → указывает свой GitHub username → клиент фетчит github.com/<user>.keys, сверяет с тем, что юзер якобы использует для подписи модуля → если совпадает, добавляет как доверенный. Никакой отдельной инфраструктуры сертификатов не нужно, GitHub уже выступает как publicly verifiable источник “вот ключи, которыми владеет этот аккаунт”.
В Rust это либо шеллиться в ssh-keygen (просто, но зависимость от системного бинаря), либо ssh-key крейт (чистый Rust, парсит/генерит/верифицирует SSH-формат ключей и подписи без внешнего процесса) — для встраивания в клиент однозначно второе.
Это реально элегантно ложится на твою модель “мой ключ доверенный по умолчанию + юзер добавляет свой + GitHub как независимая проверка”, и не требует придумывать свой формат доверия с нуля.
Ок, значит ты хочешь immediate-mode поверх GPUI как бэкенда, а не готовую retained-mode модель, которую GPUI даёт из коробки. Смысл понятен — компоненты как чистые функции State -> RenderCommand, компонуемые как угодно, без View/Element trait-объектов и их жизненного цикла.
Пара вещей, которые стоит продумать заранее, раз это ляжет в основу всего UI:
GPUI даёт diffing/repaint через свою Model-систему. Если ты идёшь HoF/immediate-mode путём, ты, по сути, берёшь на себя то, что GPUI обычно делает за тебя — решение “что реально изменилось и надо перерисовать” (или просто перерисовываешь всё каждый кадр, как настоящий immediate-mode делает, полагаясь на скорость GPU-рендера, а не на diffing). Второе проще концептуально, но накладывает требование, чтобы твой HoF-pipeline был реально быстрым каждый кадр — тут твоя “любимая оптимизация” тема снова всплывает, только теперь применительно к самому рендеру.
Hit-testing и инпут в чистом immediate-mode обычно строятся так: функция рендера одновременно строит и визуальное дерево, и hit-test дерево за один проход (как в egui — ui.button("text") возвращает Response с .clicked() сразу в том же кадре). Стоит решить, идёшь ли ты по этой модели (single-pass, ответ на инпут доступен сразу) или делаешь отдельный проход для hit-testing.
Совмещение с Elm-слоем, который мы уже заложили. У тебя будет три уровня: акторы (снаружи) → Elm update/State (центральная логика) → HoF-рендер (view). Это на самом деле красиво сочетается: view: &State -> RenderTree, где RenderTree строится HoF-композицией — Elm тебе и так диктует “view это чистая функция от state”, ты просто идёшь на шаг глубже в том, как эта функция устроена внутри.
Звучит как отдельная большая архитектурная ветка, которую стоит проработать самостоятельно, когда дойдёшь до UI-слоя вплотную — сейчас, мне кажется, важнее зафиксировать, что она совместима с тем, что уже решили (Elm сверху, GPUI снизу как paint backend), а детали HoF-API продумаешь при реализации.
Готова двигаться дальше по оставшемуся списку (синхронизация read-статусов между устройствами, notifications-логика через ObjectActor) или хочешь ещё покопать эту ветку?
Ну я думаю сделать рендер тоже заменяемым просто)))) в этом суть))))))))
Что бы разный гуи под систему
Почему???
Нативный мак клиент выглядит иначе
Нативный линукс тоже
Винда
Ю ноу да
А, вот теперь пазл сложился целиком — ты не просто хочешь заменяемый рендер, ты хочешь платформенно-нативный вид через тот же механизм подмены, что и для storage/search. Это меняет мой предыдущий скепсис (“UI-рендер вряд ли стоит динамизировать”) — тут причина не “дай моддерам порезвиться”, а архитектурно осмысленная: macOS/Linux/Windows это буквально разные визуальные языки, и вместо того чтобы городить if-ветки внутри одного рендер-кода, ты делаешь это тем же самым hot-swap механизмом, которым уже решила storage/search.
renderer-macos.so — нативный вид под Cocoa-эстетику (или сам AppKit?)
renderer-win.so — Fluent/WinUI-эстетика
renderer-linux.so — GTK/Adwaita-эстетика или свой
Тут сразу два разных уровня “нативности”, и стоит решить, какой тебе нужен:
Уровень A — визуальный стиль, тот же рендер-движок. Все бэкенды рисуют через GPUI (или через твой HoF-слой поверх GPUI), просто разные наборы стилей/токенов (macOS — сглаженные углы и системный шрифт SF, Windows — другая типографика и тени, Linux — под тему GTK). Это дёшево: один и тот же движок, разные “темы” как данные, а не разные dylib. Модульность тут скорее про темизацию, чем про рендер сам по себе.
Уровень B — реально разные нативные тулкиты под капотом. macOS-бэкенд рисует через AppKit/Cocoa напрямую (не через GPUI!), Windows — через WinUI/Win32, Linux — через GTK4. Тут ты реально получаешь нативные виджеты каждой платформы (правильный поведение скролла, правильная физика анимаций, правильная интеграция с системными a11y API), но платишь тем, что твой HoF View-контракт должен быть достаточно абстрактным, чтобы транслироваться во все три совершенно разных парадигмы рендера. Это на порядок больше работы — по сути три отдельных UI-фреймворка за одним трейтом.
Если Уровень A — механизм подмены оправдан, но по сути ты могла бы обойтись и просто конфигом темы без полноценного hot-swap dylib (хотя раз инфраструктура подмены уже есть для других движков — почему бы и рендер туда же, unified модель проще в голове).
Если Уровень B — это separate огромный кусок работы, и тут стоит trade-off: три нативных бэкенда = тройная поддержка UI-кода при любом изменении фичи (добавил новую фичу чата — реализуй в трёх местах). GPUI-only с темизацией = одна реализация, разный только “скин”.
Как думаешь, тебе принципиально важны именно нативные тулкиты (Уровень B, настоящий AppKit/WinUI/GTK), или “выглядит иначе под систему” — это про визуальный язык (Уровень A), который вполне можно сделать одним движком с разными темами?
Ну.
Мы тащим сразу несколько DLL
Первая - движок рендера
Вторая - сам визуальный стиль
И даже третья будет!
Фичи. Потому что на мак клиенте есть фичи типа приколов с прокруткой и взаимодействием с ос. На винде такое же можно сделать.
Ну короче ты шаришь да
— КАК ведёт себя: rubber-band scroll на маке, snap-scroll на винде,
интеграция с системными жестами/меню/notification center/etc
Это реально чистое разделение забот, и оно объясняет, почему Engine ≠ Style ≠ Feature — это три разных вопроса (“чем рисовать”, “как выглядит”, “как реагирует на ОС”), и смешивать их в один слой было бы ровно той путаницей, которую ты сейчас архитектурно избегаешь.
Пара вещей, которые эта модель заставляет продумать на уровне контрактов:
Зависимости между слоями. Feature-слой (rubber-band scroll) скорее всего должен уметь модифицировать поведение Engine-слоя (перехватывать/оборачивать scroll-хендлинг), а не быть полностью независимым — то есть это не три параллельных плагина, а скорее Engine как база + Style и Feature как модификаторы поверх неё. Стоит явно решить: Feature получает доступ к Engine через тот же трейт, что и host, или у него свой, более узкий контракт (“вот тебе hook на scroll-события, делай что хочешь”)?
Комбинаторика тестирования. 3 движка × 3 стиля × 3 фичи-сеты — это потенциально 27 комбинаций, хотя реально используемых наверняка меньше (GPUI-engine + macos-style + macos-features, и так по палитре на каждую ОС “родная тройка”). Но раз ты даёшь пользователю их свободно комбинировать (сути моддинга) — стоит заранее решить, тестируешь ли ты все комбинации, или только “официальные тройки”, а остальное — на свой страх и риск юзера.
Feature-слой и Elm-state. Rubber-band scroll — это не чисто визуальная штука, у неё есть состояние (overscroll offset, накопленная “резинка”) и физика (decay/spring). Это либо живёт локально в Feature-модуле как свой маленький кусок state (не проходя через central Elm update), либо тоже гоняется через центральное состояние. Я бы склонялась к первому — это ровно тот случай “мелкая локальная UI-механика”, который мы раньше решили не тащить через Elm, просто теперь этот “локальный GPUI Model” живёт внутри отдельного hot-swappable модуля.
Три оси подмены — это архитектурно оправданно и красиво ложится на то, что уже есть. Двигаемся дальше по оставшемуся списку (read-статусы, тестирование стратегия), или хочешь зафиксировать контракты между Engine/Style/Feature подробнее прямо сейчас?
У Telegram это устроено как server-authoritative — сервер держит read_inbox_max_id/read_outbox_max_id per chat, и все твои устройства просто синхронизируются с этим состоянием через апдейты. Тебе не нужно изобретать свой протокол синхронизации — MTProto это уже даёт. Но есть архитектурные развилки на твоей стороне:
Optimistic local read state. Когда юзер прочитал сообщение локально (проскроллил, увидел) — ты хочешь мгновенно показать “прочитано” в UI, не дожидаясь round-trip до сервера. Значит Chat actor держит два состояния: local_read_id (мгновенное, оптимистичное) и server_read_id (подтверждённое). Конфликт между ними разрешается в пользу сервера, если он вдруг оказался меньше (например, другое устройство ещё не прочитало) — тебе нужно решить, что показывать: “прочитано хоть где-то” или “прочитано именно тут”.
Debounce отправки read-статуса. Не слать read на каждое проскролленное сообщение отдельно — батчить (юзер посидел на сообщении N секунд / проскроллил и остановился) и слать одним апдейтом. Это уже вопрос политики в Chat actor, не протокола.
Конфликт с offline-first. Если юзер читает чат без сети — local_read_id обновляется сразу, server_read_id остаётся старым, а сам read-апдейт уходит в тот же outbox-механизм, что и исходящие сообщения (pending → retry при восстановлении сети). Это хорошая новость: инфраструктура для этого у тебя уже спроектирована, read-статус просто использует тот же outbox-паттерн, что и отправка сообщений.
Раз у тебя теперь модульная архитектура с hot-swap, тестирование распадается на слои:
Contract tests — для каждого трейта (StorageEngine, SearchEngine, Renderer) — набор тестов, который любая реализация должна проходить. Пишешь один test suite против трейта, гоняешь его против всех твоих реализаций (ZeroDB, redb-fallback, etc.) — так ты ловишь “эта реализация нарушает контракт” до того, как это всплывёт в проде у юзера с кастомным модулем.
Actor tests — ractor test harness, юнит на каждый актор изолированно.
Elm update tests — чистые функции, тривиально.
Module loading tests — отдельная категория теперь: тест на “что происходит при несовместимой версии модуля” (должен быть graceful fallback, не крash), тест на hash mismatch warning (не блокирует запуск), тест на hot-reload под нагрузкой (не теряются in-flight вызовы — nylon-ring должен это гарантировать, но стоит проверить самой).
Visual/pixel-diff — per Style-модуль отдельно (раз стилей теперь тоже несколько).
Benchmark suite — per Engine-модуль (сравнивать GPUI-engine vs будущие альтернативы на одной сцене).
Практический момент: contract tests — это то, что делает всю плагин-экосистему безопасной. Без них у тебя нет гарантии, что чужой (или даже твой альтернативный) модуль действительно ведёт себя как трейт обещает — а с hot-swap и подписанными сторонними модулями это единственная защита от “модуль технически загрузился, но врёт про своё поведение”.
Смотри, на этом, кажется, весь скелет архитектуры закрыт — от protocol layer до UI-подмены с тремя осями. Есть ли ещё кусок, который у тебя в голове вертится, но мы не проговорили? Или переходим обратно к библиотекам — конкретика по каждому модулю?
Я кстати хочу побыть немного «nazi type system» фанатиком (реальный термин кстати)))) вхвхвх. Погугли) и сделать… что бы тестов было меньше. Как? Выразить максимум возможного состояния, ограничений точности и переходов в типах.
🔧 web_search — "type system nazi" term programming
Точного термина “type system nazi” я в поиске не нашла — похоже, это разговорное прозвище (в духе “grammar nazi”, только про типы), а не устоявшийся термин с отдельной статьёй. Но суть мне понятна и без источника: это про “parse, don’t validate” философию — вместо того чтобы проверять инварианты в рантайме тестами, кодируешь их прямо в типах так, чтобы невалидное состояние физически нельзя было построить.
fnsend_message(&self, ...) ->...// существует ТОЛЬКО здесь
}
send_message физически недоступен на неавторизованном клиенте — не рантайм-чек, а ошибка компиляции. Это прямо убирает целый класс тестов “проверить, что нельзя отправить сообщение без логина”.
2. Newtype + smart constructor для точности/ограничений
Твой “PCRE2 + FTS” поиск, message pagination, ID — всё это кандидаты:
struct MessageId(NonZeroU64); // не может быть 0, не может быть обычным u64
struct Confidence(f32); // конструктор гарантирует 0.0..=1.0, дальше — просто f32
impl Confidence {
fnnew(v: f32) -> Option<Self> {
(0.0..=1.0).contains(&v).then_some(Self(v))
}
}
Как только Confidence сконструирован — все дальнейшие функции, принимающие его, освобождены от проверки диапазона. Один тест на конструктор вместо N тестов на каждую функцию, которая его использует.
Вместо двух отдельных полей + скрытого инварианта “local >= server всегда” (который иначе живёт только в твоей голове и в тестах) — сам факт, в каком ты состоянии, задаёт, какие данные вообще доступны.
4. Phantom types для единиц измерения / доменных границ
Нельзя случайно записать в БД незашифрованные данные там, где трейт требует Encrypted<Message> — компилятор не даст. Это особенно ценно у тебя, раз ты хочешь “разные уровни шифрования для разных данных” — типы физически не дадут перепутать чувствительный контур с лёгким.
5. Session types / протокол как конечный автомат в типах
Для акторного взаимодействия (ractor) можно кодировать протокол диалога актора как последовательность типов, где каждый шаг возвращает “токен продолжения”, а не просто enum с проверкой в рантайме. Это глубокая кроличья нора (полноценные session types в Rust требуют либо генерик-магии, либо крейтов вроде session_types), но для критичных протоколов (например login flow, handshake модулей при hot-swap) может окупиться.
Где в твоей архитектуре это даст больше всего экономии тестов
Typestate для ProtocolClient (login/connect/authenticated состояния) — критично, там много “нельзя X пока не Y”.
Enum вместо флагов для ReadState, outbox message status (pending/sent/failed) — прямое попадание в то, что уже спроектировано.
Newtype + smart constructors для всего, что приходит из сети (MessageId, ChatId, любые “должно быть непустое/в диапазоне”) — сразу убирает целый класс валидационных тестов на границе Protocol↔Domain.
Phantom types для Encrypted/Plaintext — прямо ложится на твою идею разноуровневого шифрования.
Единственная ловушка “nazi type system” подхода — типы начинают взрываться комбинаторно (typestate с 5 состояниями и generic-параметрами по каждому даёт грязный код), так что применяй это к местам с реальными, часто нарушаемыми инвариантами (auth flow, message status, encryption boundary), а не ко всему подряд — иначе получишь не меньше тестов, а больше борьбы с компилятором без реальной пользы.
Хочешь, распишу typestate для ProtocolClient подробнее (все переходы состояний grammers-обёртки), или для outbox message status?
Изображения (статика) — image крейт, тут вариантов особо нет, де-факто стандарт.
Видео/видео-стикеры — ffmpeg-next (биндинги, тяжёлая C-зависимость, но покрытие форматов вне конкуренции) vs symphonia (чистый Rust, но видео-декодинга там нет, только аудио — придётся что-то ещё под видео).
Текст/layout для GPUI — уточнить, что там уже встроено (GPUI использует cosmic-text под капотом, скорее всего этого хватит).
Async runtime — tokio, тут вроде очевидно, но стоит зафиксировать.
Сериализация (между акторами, для outbox/storage) — bincode или rkyv (zero-copy, что приятно ложится на твою любовь к нулевому оверхеду).
SQLite-альтернатива для activity log/predictor данных — если не SQL совсем, тот же redb подойдёт и сюда, не плодить второй движок хранения.
TUF-подобная схема доверия для манифестов модулей — конкретный крейт для подписи манифеста (или просто ssh-key на сам манифест, без отдельного TUF).
С чего начнём закрывать — видео-декодинг (там реальная развилка ffmpeg vs чистый Rust), или пробежимся по мелким пунктам (сериализация, async runtime) быстро, раз они очевиднее?
GPUI + Elm + HoF
Lol)))
ZeroDB возможно будет заменено на более зрелые раст реализации LMDB
Подписи… мб не через гитхаб ключи? А как-то иначе хз…
Не самая удобная шняга просто
Согласна, что ZeroDB молодой (0 форков, свежий проект) — закладывать прод на нём рискованно. Держи это как трейт-абстракцию с самого начала (StorageEngine трейт мы и так проектировали), тогда движок реально становится вопросом “какая реализация сегодня зрелее”, а не архитектурным решением намертво. Когда дойдёт до реализации — стоит проверить статус ZeroDB, redb и любых новых претендентов заново, потому что за это время расклад может измениться.
Смысл претензии понятен — завязываться на GitHub как единственный источник доверия неудобно (что если юзер не хочет светить GitHub-аккаунт, или у него его просто нет). Альтернативы:
Sigstore/cosign — современный стандарт для подписи артефактов (используется в supply-chain security индустрии, Kubernetes/npm экосистема на это переходит). Даёт keyless signing — подпись привязывается не к статичному ключу, а к OIDC identity (можно через GitHub, но также через email, Google и т.д.) с коротким сроком жизни сертификата, всё это публично логируется в прозрачный append-only лог (Rekor). Плюс: не нужно юзеру вообще управлять ключами, минус: инфраструктура тяжелее, чем “просто подписать SSH-ключом”.
Свой минимальный TUF-like реестр. Ты (как автор) держишь root-ключ (Ed25519, зашит в бинарь). Публикуешь подписанный манифест (список hash’ей официальных модулей) на своём сайте/репо как plain JSON + подпись. Юзер, который хочет добавить свой модуль как доверенный, просто добавляет свой публичный ключ в локальный конфиг (trusted_keys.toml) — без привязки к GitHub, без сложной инфраструктуры. “Проверка через сторонний источник” тогда становится опциональной фичей (сверка твоего манифеста с тем, что лежит в разных местах — сайт + репо + может IPFS), а не завязкой на конкретный сервис.
Web of trust по-простому — юзер может подписать ключ другого юзера (не через GitHub, а прямым обменом/QR-кодом/встроенным механизмом в самом клиенте), выстраивая локальную сеть доверия без центрального сервиса вообще. Это красивее философски (децентрализовано, никакой зависимости от GitHub/Sigstore), но сложнее в реализации и требует UX-продумки, чтобы юзер вообще понимал, что происходит.
Мой совет: свой минимальный TUF-like манифест + Ed25519/SSH-подпись, без обязательной привязки к GitHub. GitHub можно оставить как одну из опциональных проверок (если юзер сам этого хочет), а не как основу схемы. Это даёт гибкость без лишней инфраструктурной тяжести Sigstore, которая для десктоп-клиента, наверное, оверкилл.
Го дальше по списку — видео-декодинг (ffmpeg vs альтернативы)?
Идея с web of trust интересная и я думаю ее стоит записать на будущее как обсуждение
А так… ну. Твоя идея интересная да.
Я просто думаю дать возможность юзеру в приложении подключать через окошко всю эту хрень. Условно:
name
• url / • hex
И туда просто вставляешь
Потом в списке есчо удаляешь доверенные ключи
Принято: sanakirja — зафиксировано, зрелая чистая Rust ACID-БД с MVCC (используется в Pijul, кстати, так что боевое применение под сложные конкурентные сценарии у неё есть).
Схема доверия — простое UI для управления списком (имя + URL-источник или hex-ключ напрямую, добавить/удалить) — это разумный MVP независимо от того, что за ней (TUF-манифест или просто список публичных ключей). Web-of-trust фиксирую как тему на будущее, не для v1.
ffmpeg-next — биндинги к FFmpeg. Покрытие форматов вне конкуренции (H.264/265, VP8/9, AV1, всё, что Telegram реально использует для video-стикеров и кружочков). Минус — тяжёлая C-зависимость, лицензионные нюансы (FFmpeg сам по себе LGPL/GPL в зависимости от сборки — надо аккуратно с тем, какие компоненты линкуешь, если хочешь оставаться LGPL-совместимой).
Чистый Rust вариант — rav1e/dav1d-биндинги закрывают только AV1, openh264 (Cisco) даёт H.264 с относительно вменяемой лицензией, но нет единой чистой Rust-либы, которая закрывает весь спектр форматов, что гоняет Telegram. Собирать мозаику из нескольких чистых Rust декодеров под разные кодеки — это реалистично, но чувствительно больше работы, чем взять ffmpeg-next.
Учитывая, что у тебя уже есть прецедент “C-зависимость, но оправданная” (LMDB было, PCRE2 остаётся) — я бы не стала биться за чистоту тут. Video decode — это годы кодек-инженерии, переизобретать смысла ноль. ffmpeg-next как единственная осознанная C-зависимость под медиа выглядит разумно.
Двигаемся к сериализации между акторами/storage, или video-тема закрыта и идём дальше по списку?
Ну мы можем поставлять исходно код на чистых раст реализациях либ (и все бинари к ним так же)
Но опционально через конфиг включать докачку ffmpeg для поддержки вообще любого формата который кидают
Ну или оставить твой варик хз
Кстати а тема с голосовыми, стримами, звонками?))
Гибридная схема — хорошая идея, и она прямо ложится в твою модульную архитектуру: чистый Rust декодер (частичное покрытие) как built-in fallback по умолчанию, ffmpeg-next как опциональный hot-swap модуль, который юзер докачивает через тот же механизм, что и остальные dynamic-модули. У тебя уже есть вся инфраструктура под это (nylon-ring, хэш-проверка, доверенные подписи) — ffmpeg-модуль просто ещё один плагин в реестре, только официальный от тебя, крупный по размеру, и опциональный по установке.
Практически:
MediaDecoder трейт с built-in реализацией на rav1e/dav1d/openh264 (что покрывают чистые Rust крейты).
Если прилетает формат, который built-in не тянет — UI показывает “для этого формата нужен расширенный декодер, установить?” → качает ffmpeg-decoder.so (подписанный тобой) → hot-swap подключается.
Лицензионно это тоже чище: базовая поставка без GPL/LGPL-хвостов, FFmpeg — отдельная опциональная загрузка, которую юзер явно запрашивает.
Это прямо хорошее применение всей модульной системы не ради принципа, а по делу.
Отдельный и довольно больной пласт, потому что это не “декодировать файл”, а realtime P2P/relay медиа:
Голосовые сообщения (voice messages) — самое простое, это по сути обычный аудиофайл (OGG/Opus), декодируется как медиа, никакой realtime-сложности. symphonia (чистый Rust) прекрасно берёт Opus.
Звонки (voice/video calls) — это libtgvoip или его преемник в современном Telegram (tgcalls). Тут реальная проблема: это закрытый протокол Telegram, не такой открытый и документированный, как MTProto для сообщений. Официальные клиенты используют tgcalls (C++ библиотека от самого Telegram, есть на GitHub, но она не тривиальна для реимплементации) — WebRTC-based транспорт с их кастомной сигнализацией поверх MTProto.
Практически у тебя три пути:
Биндинги к tgcalls (C++ FFI) — работает “из коробки” по совместимости с реальными звонками, но тянет C++ зависимость и WebRTC (тяжёлый).
Своя реализация поверх webrtc-rs (чистый Rust WebRTC стек) + реверс сигнального протокола — огромный объём работы, и есть риск, что Telegram поменяет протокол сигнализации без предупреждения (это не публичный контракт, в отличие от MTProto).
Не поддерживать звонки в v1 — честный вариант, учитывая объём. Звонки в 1:1-клоне — это отдельный, самый дорогой по трудозатратам кусок из всего, что мы обсудили, и он не блокирует остальной функционал.
Стримы (video chats / live streams в группах) — это ещё один слой поверх звонков (multi-party, broadcast), ещё сложнее.
Мой честный совет: звонки/стримы — отдельная фаза после того, как основной клиент (чаты, медиа, поиск, модульность) уже живой и стабильный. Технически это отдельный проект внутри проекта, с собственными рисками (недокументированный протокол), и пытаться закладывать это в архитектуру сейчас — распылять фокус на вещь, которую в любом случае будешь делать последней.
Что думаешь — звонки в бэклог как есть, или хочешь сразу прикинуть, куда они впишутся архитектурно (даже без реализации), чтобы потом не пришлось всё перекраивать?
Сериализация между акторами/storage — зафиксировали rkyv, но стоит уточнить: между акторами (ractor mailbox) тоже rkyv, или там сырые Rust-типы без сериализации (раз всё в одном процессе, зачем сериализовать между акторами вообще)?
Read-статусы/multi-device — архитектура обсуждена (optimistic local + server state, через outbox), библиотек отдельных не требует, MTProto/grammers это даёт.
Схема доверия для манифестов — закрыли (свой TUF-like + UI для ключей).
TUF-манифест сериализация — JSON + подпись, или тоже что-то другое (raw bytes + rkyv/bincode)?
Тестирование — стратегия обсуждена, конкретные крейты не зафиксированы (criterion для бенчей упоминала мельком).
Notifications actor — архитектурно решили (единый handler для мультиаккаунта), библиотек под сами системные notifications ещё не касались (notify-rust на Linux/DBus, нативные API на Win/Mac — тоже кандидат под твою Feature-слой модульность).
WASM plugin API дизайн — wasmtime выбран, но сам контракт (что именно плагины могут делать — рисовать UI-элементы? Читать сообщения? Только read-only хуки?) не обсуждали вообще.
Auto-update механизм самого host-бинаря (не модулей, а ядра) — модули обновляются через nylon-ring, а что обновляет сам исполняемый файл?
Что из этого дальше — сериализация/акторы или notifications/WASM-контракт?
Согласна, сырые типы между акторами в одном процессе — сериализация там просто ненужные циклы, rkyv оставляем для границ, которые реально это требуют (storage, dylib-граница, network).
RON (Rusty Object Notation) — да, вполне уместно. Человекочитаемый, нативно поддерживает Rust-энумы и структуры без выкрутасов (в отличие от JSON, где enum с данными превращается в костыль), есть serde интеграция из коробки. Для манифеста, который могут читать/редактировать люди (а не только машина) — разумнее JSON, потому что универсальнее для сторонних инструментов проверки (если кто-то захочет верифицировать твой манифест не через твой клиент, а скриптом на Python/JS — JSON-парсер есть везде, RON-парсер не факт). Если манифест read-only и только для твоего клиента — RON приятнее в коде. Тебе решать, что важнее: интероперабельность или эргономика.
Согласна, это уже вопрос конкретных крейтов внутри Feature-модулей, не отдельная архитектурная тема — notify-rust/winrt-notification/нативные API инкапсулируются каждый в своём платформенном модуле.
Хороший default — read-only по умолчанию, явный opt-in на запись/действия в настройках. Стоит продумать гранулярность прав (не один общий тумблер “разрешить всё”, а по категориям: “читать сообщения”, “отправлять сообщения от твоего имени”, “модифицировать UI”, “сетевой доступ наружу”) — WASM Component Model с wit-bindgen как раз позволяет декларировать это на уровне интерфейса (плагин физически не увидит функцию send_message, если её нет в его allowed capability set).
Как делает Discord — дай гляну актуальную схему, у меня есть общее представление, но детали могли поменяться.
🔧 web_search — Discord desktop client auto-update mechanism architecture how it works
Собирается довольно ясная картина, включая интересную деталь из GooseUpdate.
settings.json в user config directory хранит UPDATE_ENDPOINT — то есть даже адрес сервера обновлений конфигурируется, не захардкожен (это, кстати, дыра, которую использует GooseUpdate — подменяешь endpoint на свой прокси и получаешь MITM для модулей, к вопросу о том, почему твоя подписанная схема доверия — это не паранойя, а необходимость).
Проверка на каждом старте — при запуске клиент стучится на update endpoint, тянет манифест, качает новые файлы.
Модульная структура — сам Discord клиент внутри состоит из модулей (discord_desktop_core и другие), и обновляются они как файлы поверх существующей установки, не весь установщик целиком — это прямо параллель с твоей nylon-ring системой, только у них это JS/Electron-модули, а у тебя — нативные dylib.
UX на Windows — тихо, в фоне, без блокировки. На Linux — печально, судя по жалобам: блокирующий диалог, ручная загрузка .deb, годами не чинится. Это antipattern, который стоит явно не повторять.
Проверка версии на старте + фоновая докачка изменившихся модулей (у тебя это буквально то же самое, что nylon-ring уже делает для твоих собственных модулей — сам host-бинарь можно обновлять тем же путём, просто “модуль ядра” тоже валидируется подписью).
Endpoint для манифеста обновлений — но конфигурируемый только тобой (зашит/подписан), не в открытом JSON, который юзер (или малварь) может подменить — это и есть урок из GooseUpdate эксплойта.
Не повторять:
Blocking-диалог на Linux — это откровенно плохой UX, у тебя уже есть модель “hash mismatch → предупреждение, не блокировка”, применяй тот же принцип к самому апдейту (тихий фон + баннер “перезапустите, когда удобно”).
Незащищённый settings.json с адресом сервера в открытом виде — у тебя это должно быть частью твоей подписанной схемы доверия, не plain-текст рядом с бинарём.
Архитектурно для твоего клиента: сам host-бинарь по сути становится “модулем номер 0” под тем же nylon-ring/подписанным манифестом механизмом — просто с дополнительным шагом (нельзя hot-swap-нуть сам исполняемый файл, пока он запущен, поэтому тут нужен classic “скачал новую версию → положил рядом → на следующий запуск подменил” паттерн, в отличие от dylib, которые можно менять на лету). По сути self-update = частный случай module-update, просто с restart-gate вместо hot-reload-gate.
Из старого списка, кажется, всё закрыто. Что-то новое вылезло по ходу, или на этом архитектурный скелет можно считать готовым и переходить к тому, с чего физически начинать писать код?
Ну я говорила чуть про другое гусиным апдейтом и проверкой на старте. Ей богу.
Дискорд хуйни понаделал с этим.
Я имею ввиду именно что апдейтер который просто берет и главный бинарь заменяет
А почему так просто???
А потому что хехехе
Все положение модульное
И главный бинарь делает по сути пару вещей
Запускает нужный модуль
И все
В идеале можем сократить до одного бинаря убрав второй
Как?
Сделать обновления частью ебливого мейн бинаря. Ему чисто скачать проверить запустить и всё. Но я лучше два поставлять буду.
А, теперь понятно — ты имела в виду сам механизм замены исполняемого файла (типичный “launcher + payload” паттерн), а не то, что Discord вообще делает плохо архитектурно. Ок, разбираю именно это.
Раз всё модульно, главный бинарь вырождается почти в ничто:
launcher (main.exe / main)
1. читает манифест (какая версия модулей нужна)
2. качает/проверяет недостающее
3. грузит core-модуль (тот самый "весь клиент" как dylib)
4. передаёт управление
Это классический паттерн — по сути то же самое, что делает Steam.exe, Battle.net Launcher, или Chrome (у него отдельный “installer stub”, который потом качает и запускает настоящий браузер). Сам launcher настолько маленький и редко меняется, что его почти никогда не нужно обновлять — а раз он не меняется, самообновление launcher’а становится редким крайним случаем, не основным путём.
Почему два бинаря лучше одного (и ты уже сама это поняла)
Ты правильно интуитивно выбрала “поставлять два” — вот формальная причина: процесс не может заменить сам себя, пока выполняется (на Windows буквально физическая блокировка файла; на Linux/macOS технически можно unlink+replace, но открытый исполняемый inode всё ещё держится текущим процессом, а новый запуск подхватит новый файл только на следующий старт).
Если launcher = core, то обновление core требует:
либо перезапуска launcher’а самого себя (costly, глючно, гонки при “я обновляю себя, пока сам себя выполняю”),
либо отдельного крошечного updater-процесса, который стартует, ждёт пока launcher выйдет, подменяет файл, перезапускает — то есть ты всё равно получаешь второй бинарь, просто он временный/скрытый, а не постоянный.
Раз второй бинарь неизбежен архитектурно (хоть постоянный, хоть временный) — постоянный проще и надёжнее: launcher маленький, стабильный, редко обновляется сам, а вся сложность (core-логика, актуальная версия) живёт в модуле, который меняется как обычный hot-swap. Один бинарь дал бы тебе иллюзию простоты ценой скрытого костыля внутри (временный self-updater-процесс, который тебе всё равно придётся написать и отладить).
└── обновляется как ЛЮБОЙ другой модуль через тот же механизм,
просто он "модуль 0" — без него launcher бесполезен
Launcher вообще может не знать ничего про акторы/GPUI/протокол — его единственная работа: “скачать правильную версию core, загрузить, запустить”. Это даёт тебе бонус: launcher можно вообще не трогать месяцами, весь реальный прогресс идёт через core-модуль обновления, которые уже hot-swappable в рамках уже работающего процесса (для non-launcher модулей) или через launcher-рестарт (для самого core).
Согласна с твоим выводом — два бинаря, тонкий launcher + core-модуль. На этом, по-моему, архитектурный скелет реально закрыт целиком. Хочешь пройтись по всему списку решений одним документом-саммари, или сразу переходим к “с чего начать писать”?
Да. Согласна.
Давай маленький саммари и потом уже напишем и ADR и RFC и ТЗ. На Английском
Для будущей меня и агентов
И отдельно QA + OQ (opened questions) напишем своими файлами да
Слои: protocol (grammers) → domain/actors (ractor) → media pipeline → UI (GPUI + Elm-обёртка + HoF-слой) → platform shell. Один процесс, tokio, каналы между слоями, сырые Rust-типы между акторами (без сериализации).
Хранение: offline-first, локальная БД — источник правды. Storage: sanakirja (взамен ZeroDB). Поиск: milli (форк, LMDB/heed заменён на sanakirja) + Charabia + PCRE2/regex-syntax для двухфазного regex-поиска. Разноуровневое шифрование: лёгкое (ChaCha20/AES-GCM) для метаданных, тяжёлое (XChaCha20-Poly1305 + Argon2id) для контента.
Модульность (три независимых оси):
Свои тяжёлые движки (storage/search/protocol/media-decoder/predictor) — hot-swap нативные dylib через nylon-ring, batch API + host-side hot cache против оверхеда границы.
UI — три независимые подсистемы: Engine (рендер-движок) / Style (визуальный язык) / Feature (платформенные интеграции типа rubber-band scroll).
Плагины сторонние — WASM (wasmtime), read-only по умолчанию, гранулярные capability-права через настройки.
Доверие: свой TUF-like манифест, Ed25519/SSH-подписи, UI для управления доверенными ключами (name + url/hex, добавить/удалить). Web-of-trust — идея на будущее.
Предиктор загрузки: событийный лог (redb/sanakirja) → Уровень 1 (гистограммы) → Уровень 2 (контекстные фичи) → Уровень 3 (онлайн-обучение/bandits), общая инфраструктура логирования для всех уровней.
Тестирование: contract tests на трейты, actor tests (ractor harness), чистые Elm update-тесты, module loading tests, pixel-diff per Style, criterion-бенчи per Engine.
Дистрибуция: launcher (тонкий, почти без логики) + core-модуль (весь клиент, “модуль 0”), апдейт core — через рестарт launcher’а, апдейт остальных модулей — hot-swap на лету.
Типизация: typestate для protocol client (auth flow), newtype+smart constructors, enum вместо флагов, phantom types для Encrypted/Plaintext, HList (frunk) — минимизация тестов через невозможность построить невалидное состояние.
Я… бля. Давай создадим маленький файлик на будущее
Раз уж мне надо как-то назвать клиент и форки и модули переписывания в будущем да
Давай сделаем сейчас файл с описанием UX и названий и логотипов))) хехехе
И «проверить в будущем» файл - насколько сложно будет заменить ебаный sqlite на пк называемый «Limbo». Потому что это я сделать очень нахуй хочу.
Ну и прогнать пожалуй бенчмарки всего этого, варианты бесплатной загрузки лоулатенси вещей и прочее прочее прочее тестить))
Важная деталь: Limbo переименовали в Turso Database — теперь это официальное название (сам проект Turso, у них уже был отдельный libSQL, и чтобы не путать, “Limbo” стал просто “Turso Database”). Так что когда будешь искать в будущем — ищи tursodatabase/turso на GitHub, не limbo.
Ключевое по сути: полный чистый Rust реимплемент SQLite с нуля (не форк), async I/O из коробки, нативный vector search, MVCC, WASM-таргет как первоклассная фича, тестируется через Deterministic Simulation Testing (та же методология что у FoundationDB, с Antithesis). На конец 2026 — ещё не production-ready, статус “beta”, развивается быстро.
Для тебя это не прямая замена в текущей архитектуре (ты уже выбрала sanakirja как KV-движок), а скорее вопрос “если/когда где-то в стеке нужен именно SQL-слой (не просто KV) — заменить ли SQLite там на Turso DB”. Зафиксирую с этим уточнением.
Нейминг — пока не решён, зафиксированы ограничения для будущего брейншторма (не анимешный, отдельно от личного бренда, не “просто клон телеги”)
Логотип — заблокирован на нейминге, плюс вопрос про per-OS адаптацию иконки
UX-принципы — 1:1 паритет как жёсткая цель, оффлайн не должен выглядеть “сломанным”, trust-UI не должен пугать как сертификат-менеджер, capability-права плагинов — прозрачные, не один общий тумблер
Limbo/Turso DB — важная деталь, которую нашла: проект переименован в Turso Database, искать tursodatabase/turso, не limbo. Зафиксировала статус (beta, не прод), что именно проверить при возврате (готовность, бенчи против sanakirja, совместимость async-модели с ractor/tokio, лицензия под встраивание как модуль), и честную оценку сложности — архитектурно дёшево благодаря трейту, но реальная цена в другом: SQL-модель Turso не ложится один в один на текущий KV-шейп трейта, плюс milli пришлось бы ретаргетить второй раз
Общий бенчмарк-пасс — список того, что мерить, когда будет что мерить
Low-latency инфра — зафиксировано как “нужно сначала понять, для чего именно”, а не готовое исследование
Ну так KV движок мне блядь не даст ебаных индексов)) а они когда-то да понадобятся. Так что вероятно одной только дб мы не обойдемся и стоит глянуть Turso
А «проверить позже» - вообще должно быть отдельным файлом
Раздели это
И пошли придумывать названия лол
Справедливо на счёт индексов — KV сам по себе тебе не даст произвольных вторичных индексов/join-подобных запросов бесплатно, это ты либо строишь руками поверх sanakirja (свои индексные структуры, как milli и делает по сути для полнотекстового поиска), либо берёшь что-то с настоящим query planner. Разделяю файл.
🔧 bash_tool — cd /home/claude/tgclient-docs && ls
🔧 view
Разделяю на NAMING-UX.md (нейминг/лого/UX) и FUTURE-CHECKS.md (технические “проверить позже”), и добавляю в последний твою мысль про индексы.
Разделила: NAMING-UX.md — только нейминг/лого/UX, FUTURE-CHECKS.md — техническое “проверить позже”, с новым первым пунктом про индексы (KV сам по себе их не даёт, SQL-слой понадобится не гипотетически, а реально — просто не сейчас) и Turso Database уже как ответ именно на эту проблему, а не абстрактная замена.
Погнали придумывать названия. Пара уточняющих штрихов, чтобы не стрелять в темноту:
Хочешь звучание на русском/английском одинаково легко (как “Telegram” само по себе), или ок если это будет что-то явно техническое/хакерское по вайбу?
Тянет в сторону чего-то, что намекает на модульность/пересборку (“ядро”, “кристалл”, “мозаика”), или скорее нейтральное имя, которое просто хорошо звучит само по себе?
Да не. Нахуя. Это детали имплементации, моих выебонов и желания крутой архитектуры. Юзеру то зачем знать
Изначальная задача - предоставить пиздыц UX, скорость выше, удобство и потребление меньше
Логично — юзеру нужен результат (быстро, удобно, легко), а не манифест архитектурных амбиций в названии.
Раз ориентир — звучание уровня “Telegram” (короткое, нейтральное, легко произносится на русском и английском, не technical-jargon), вот несколько направлений с конкретными вариантами:
Про скорость/лёгкость (что реально продаёт продукт):
Wisp — лёгкий, быстрый, “вспышка”
Fera — звучит нейтрально-международно, ни к чему не привязано
Kelio / Kelia — придуманное, легко произносится, пусто как холст
Zephy (от zephyr — лёгкий ветер) — скорость+лёгкость считывается сразу
Нейтральные, просто хорошо звучащие:
Noven — коротко, современно, не отсылает никуда
Lira — легко на слух в обоих языках, ничего не значит впрямую
Tessi / Tesa — мягкое, короткое
С лёгким намёком на “передача/связь”, но не в лоб:
Vira — от “via” (через), но само по себе просто имя
Relay — прямое слово, честное, гуглится проблематично (много совпадений), но абсолютно понятно с первого раза
Из всего этого лично мне больше всего заходит Wisp — коротко, оба языка произносят одинаково легко (“Виспе”/“Wisp”), не занято чем-то крупным в этой нише, и естественно ложится в слоган “лёгкий и быстрый мессенджер-клиент”, не будучи буквальным описанием.
Что по вайбу ближе — что-то из “скорость/лёгкость” группы, нейтральное придуманное слово, или хочешь ещё вариантов в другую сторону?
Хороший выбор — оба короткие, легко произносятся на русском и английском, и звучат как самостоятельные имена, а не как акронимы или описания.
Fera
Плюсы: звучит тепло, почти органично (созвучно с “fair”, “feral” по краю, но не впрямую), легко склоняется на русском (“в Fera”, “Fera-клиент”, “фера”)
На слух воспринимается чуть мягче, дружелюбнее
Стоит проверить: не занято ли уже чем-то заметным (есть шрифт Fera Code, есть разные мелкие бренды — не блокирующе, но глянуть перед тем как жениться на имени)
Kelio
Плюсы: чуть более технологично на слух, есть лёгкая нотка “скорости” (созвучно с “kilo”/быстрый счёт), хорошо ложится в логотип (простая геометрия под K)
Минус: Kelio уже существует как довольно крупный бренд — французская система учёта рабочего времени/HR-софт (kelio.io), причём достаточно раскрученная в Европе. Это может создать путаницу и юридические риски на нейминг, особенно если проект наберёт заметность.
Практический совет: Fera безопаснее по чистоте имени. Если хочешь всё-таки вайб Kelio (звучание) — можно взять близкий, но не занятый вариант: Kelia, Kelvi, Keliox — сохраняют фонетику, но не бьются с существующим брендом.
Хочешь, я быстро проверю через поиск занятость Fera в контексте software/мессенджеров, прежде чем фиксировать?
Нашлось важное: уже существует мобильное приложение под названием “fera” — судя по описанию (Interac e-Transfer, лотерейные предсказания типа EuroJackpot, “number prediction”) это, похоже, финтех/гемблинг-приложение с 52M+ загрузок. Не мессенджер, но достаточно раскрученное имя в сторах, чтобы создать путаницу при поиске “fera app” в Google Play/App Store — твой клиент рискует потеряться в выдаче или тебя примут за связанных с ними.
Прямых конфликтов в категории мессенджеров под именем “Fera” не нашла — ближайшее по духу (Ferdi) это просто похожее по звучанию, не то же самое.
Вывод: “Fera” юридически, скорее всего, свободно (это не зарегистрированная торговая марка в найденных реестрах), но практически замусорено в сторах существующим приложением с тем же именем. Это не блокер, но неприятность для узнаваемости и SEO.
Варианты:
Fera как есть, приняв, что придётся конкурировать за выдачу в сторах с тем приложением.
Лёгкая вариация того же звучания без конфликта: Feras, Feron, Ferai — сохраняют фонетику “fera”, но не бьются впрямую.
Вернуться к Kelia/Kelvi (вариации Kelio без конфликта с французским HR-брендом) — там поиск был чище.
Хочешь, проверю Kelia так же тщательно, чтобы сравнить чистоту обоих на равных условиях?
Так. А теперь либы))) потому что есть проблема. Я планирую реврайтить небольшое но все же какое-то количество либ. И мне хотелось бы придумать им название чуть более интересное чем «rustcalltg». Ну. Понимаешь
Понимаю — хочешь, чтобы имена крейтов-реимплементов не читались как “rust-версия X”, а как самостоятельные штуки, которые просто случайно решают ту же задачу. Смотри, вот конкретно под твои кандидаты на реврайт:
tgcalls-реимплемент (ADR-018, сейчас rstgcalls как рабочее название биндингов):
Echelon — звучит как “уровень/эшелон”, подходит под voice/video инфраструктуру
Vantel — придуманное, нейтральное, легко ложится в “vantel-calls” если нужен неймспейс
Reson (от resonance) — коротко, про звук/передачу
milli-форк на sanakirja (поиск):
Sieve — прямое, честное слово про просеивание/фильтрацию, легко запоминается, хорошо описывает суть (просеивает корпус)
Glean — “собирать по крупицам”, тоже прямое английское слово, звучит профессионально
Ferret — “разнюхивать, откапывать”, игриво и по смыслу подходит поиску
storage-обёртка вокруг sanakirja (если когда-то станет отдельным слоем, не просто трейтом):
Keep — коротко, честно, про хранение
Strata — слои/пласты, подходит под layered storage
tlottie/media-decoder форки, если дойдёт до этого:
Flicker — про анимацию, с лёгкой самоиронией
Cel (от cel animation) — коротко, по делу
Практический совет по неймингу крейтов: у Rust-экосистемы уже есть сильная культура коротких, некапитализированных, слегка поэтичных имён (serde, tokio, hyper, tracing) — то есть звучание важнее буквального описания. Все варианты выше в эту культуру ложатся, в отличие от rustcalltg, который звучит как автогенерированное имя пакета, а не осмысленный бренд.
Если общая тема Fera тебе нравится — можно даже держать лёгкую фонетическую связь между экосистемой (Fera-клиент + его собственные крейты, например все начинаются на одну букву или имеют общий суффикс), но не обязательно, если хочешь, чтобы крейты жили самостоятельной публичной жизнью отдельно от бренда клиента.
Что откликается — что-то из этого, или дай направление (звук, смысл, вайб), под которое покопаю ещё?
re:call
Как re:zero
Ладно я ебанутая но мне нравится идея
rstgcalls так и называется потому что это именно бинды)
Так эээ. Ну пожалуй это единственное что пока планируется называть…
По поводу лого идеи примерно есть?)
“re:call” — окей, это реально работает, тот же паттерн что у “re:zero” (переосмысление/перезапуск чего-то), и по смыслу идеально ложится на “звонки, реимплементированные с нуля” (re: = redo/rewrite, call = звонки). Ничуть не ебанутая идея, это ровно тот вайб коротких поэтичных имён, о котором я говорила.
Раз rstgcalls остаётся как есть (биндинги, имя честно описывает суть) — тогда re:call резервируем конкретно под будущий чистый Rust реимплемент из ADR-018 (тот, что “когда-нибудь в v+++”), не для биндингов. Стоит это зафиксировать в памяти, чтобы не потерялось.
Раз у тебя нет привязки к архитектуре в логотипе (ты сама сказала — юзеру это не нужно), и имя Fera — коротко, мягко звучит, нейтрально — вот несколько направлений:
1. Монограмма на “F”. Простая геометрическая F, слегка скруглённая (под стать мягкости имени), в одном узнаваемом цвете. Плюс: масштабируется идеально даже в 16×16 favicon, легко читается издалека в доке/панели задач. Минус: F — довольно занятая буква в софте (Figma, Firefox, Facebook), нужно найти форму, которая не будет путаться.
2. Абстрактная форма, отсылающая к “лёгкости/скорости” (не архитектуре, а ощущению от продукта). Что-то вроде смягчённой капли, летящей стрелки, или простого динамичного росчерка — передаёт “быстро и легко” эмоционально, а не буквально.
3. Анимал-намёк без буквальности. “Fera” фонетически близко к “feral”/“fauna” — можно взять очень минималистичный намёк на что-то дикое/живое (не буквальную мордочку животного, а абстрактную геометрию, вдохновлённую, например, силуэтом лисы или рыси — коротко, узнаваемо, не мультяшно).
4. Просто типографика. Чистый wordmark “fera” строчными буквами, кастомный шрифт с характерным начертанием (например с необычной связкой букв “fe” или скруглённой “a”) — без иконки вообще, как у некоторых современных инструментов (Linear, Raycast). Плюс: не нужно придумывать отдельную иконку, минус: в доке/трее нужен уменьшенный вариант всё равно (обычно просто первая буква крупно).
Практический совет: раз у тебя уже есть модульная Style-система (тема адаптируется под ОС), сама иконка приложения по нашему же решению (NAMING-UX.md, §2) скорее должна оставаться одной фиксированной для узнаваемости — а вот акцентный цвет/форма могут перекликаться с внутренней темой, если захочешь эту связь.
Что откликается по направлению — геометрическая монограмма, абстрактная форма, animal-намёк, или чистая типографика?
Хах, идея забавная, но тут прямо капкан лежит, и лучше его увидеть заранее.
Юридически — логотип Telegram (бумажный самолётик) охраняется как товарный знак. Взять его форму, перекрасить и добавить деталь — это не “вдохновение”, это derivative work чужого зарегистрированного знака. Для личного форка-для-себя это, скорее всего, сойдёт с рук, но в момент, когда ты захочешь публично раздавать клиент под своим именем (Fera) — это прямой повод для DMCA/трейдмарк-жалобы, и вероятность реагирования у Telegram (компания довольно активно защищает бренд) выше нуля. Плюс это создаёт впечатление “официального” или “одобренного” клиента, чего ты явно не хочешь (ты сама говорила про “не должно читаться как просто клон”).
Практически — даже если убрать юридическую сторону, самолётик — это чужой узнаваемый силуэт. Он всегда будет читаться как “перекрашенный Telegram”, а не как “Fera” — то есть работает против твоей цели дать продукту собственную идентичность, а не against него.
Если тебе нравится сама идея “медный/ржавый + Rust-отсылка + летящий силуэт” — можно взять то же ощущение, но с формой, которая твоя:
Медно-ржавый как основной акцентный цвет — это отличная идея сама по себе, звучит уместно и стильно, и прямо перекликается с Rust (язык) без буквальной отсылки к логотипу шестерёнки Rust тоже (та же ловушка, других форма).
Вместо самолётика — свой силуэт с ощущением “полёта/скорости” (стрела, росчерк, крыло — что угодно абстрактное), в этой же медной палитре.
Хвостик/деталь можно оставить как идею — животный или органический штрих поверх геометрической формы, это добавляет тот самый “не скучный техно-логотип” вайб, который ты, кажется, ищешь.
Хочешь, я накидаю пару конкретных абстрактных силуэтов (не самолётик) в медной палитре как визуальный черновик через SVG, чтобы было от чего оттолкнуться?
Ну. Эээ. Может тогда шестерню. Ну точнее не шестерню а…
Сука тяжко на самом то деле. Что-то намекающее и на телегу и на раст надо..
аэроплан блядь вххахаах
Хах, да, ты попала в classic ловушку логотип-дизайна: хочется намекнуть на оба контекста разом, и руки сами тянутся к чужим символам (самолётик Telegram, шестерёнка Rust — та же лежит в той же капкане, шестерёнка тоже их зарегистрированный маскот).
Смотри, тут есть красивый выход: не комбинировать символы (самолётик + шестерёнка), а комбинировать ощущения, которые эти символы вызывают, в новой форме.
Что если взять силуэт крыла или стрелы, но составленный из шестерёнчатых/механических зубцов — не “самолётик + шестерёнка склеенные”, а честно новая форма, которая на подсознательном уровне читает оба ощущения, не копируя ни один конкретный контур:
Стрела/крыло с зазубренным (cog-like) задним краем — спереди гладкая аэродинамичная линия (скорость), сзади — несколько геометричных зубцов (механизм/инженерность), как будто это деталь, а не транспорт.
Абстрактная “искра/трасса полёта” в форме шестерни — не шестерёнка-предмет, а траектория полёта, которая на повороте образует зубчатый паттерн, как след от вращения.
Буква “F” (Fera), выполненная как единый росчерк, где нижняя горизонталь превращается в зубчатый край — надёжность (Rust/механика) буквально “держит” лёгкость (полёт/Telegram) снизу.
Медно-ржавый цвет сюда идеально ложится в любом варианте — он уже сам по себе даёт Rust-ассоциацию через цвет, а не через прямое заимствование формы, так что тебе не обязательно тащить шестерёнку буквально — цвет уже делает часть работы.
Хочешь, я накидаю парочку таких силуэтов в SVG (стрела+зубцы, или F+зубчатый край) в медной палитре, чтобы посмотреть вживую, а не только словами?
Fera — имя клиента, с явно принятым компромиссом по неймспейс-конфликту
re:call — зарезервировано под будущий чистый реимплемент звонков, rstgcalls остаётся как есть для биндингов
Лого — явно зафиксирован отказ от самолётика/шестерёнки как юридического и айдентика-риска, направление: медно-ржавый цвет + стрела/крыло с зазубренным краем, F-монограмма как запасной вариант
Ахах, ну да, “re:zero” сам по себе аниме, а ты его взяла просто ради паттерна имени, не ради отсылки — так что ирония в том, что вдохновение анимешное, а сам нейминг-принцип ты хотела как раз не-анимешный для проекта. Забавно вышло.
Про Евангелион как шутку-угрозу на будущее — запишу её как есть, без обязательств, чисто для смеха будущей себя.
🔧 memory_str_replace — /areas/telegram-client.md
Записала как шутку на будущее — если когда-нибудь решишь замахнуться на что-то по-настоящему крупное, у тебя уже готово название и повод для мема. 😄
Ты за этот разговор спроектировала архитектуру целого клиента до последнего слоя шифрования, продумала нейминг, лого, и уже шутишь про следующий проект уровня Евангелиона — который ты, зная тебя по этому чату, реально когда-нибудь начнёшь. Это не “хуй мне”, это нормально хотеть выдохнуть после такого объёма.
Отпуск от идей — это не то, что нужно себе разрешать через силу воли, где-то в моменте, когда голова уже гудит. Единственное, что тут может реально мешать — если есть ощущение, что остановиться = потерять темп/забыть половину. Для этого у тебя, собственно, и есть все эти файлы — архитектура никуда не убежит, она записана. Можно закрыть ноутбук и она никуда не денется.
Как ты сейчас — реально вымотанная, или просто поймала себя на мысли и спросила вслух?
Твой вариант рабочий, но есть шероховатости: “focused on ergonomic” должно быть “ergonomics” (существительное), и “lightweightness” звучит немного неуклюже даже для нейтива — обычно говорят просто “lightness” или “a light footprint”.
Вот пофикшенная версия и несколько альтернатив в разном тоне:
Прямая правка твоей:
Fera — an experimental Telegram client focused on speed, ergonomics, and a light footprint.
Короче, ритмичнее (трёхчастная структура держит внимание лучше):
Fera — fast, light, and built to feel right.
Чуть более “продуктовый” тон, без потери честности про “experimental”:
Fera — an experimental Telegram client. Faster, lighter, more yours.
С намёком на модульность без жаргона (раз юзеру важен результат, а не архитектура):
Fera — a Telegram client that gets out of your way.
Ближе всего к твоему исходнику, но с правильной грамматикой и покороче:
Fera — experimental, fast, light, and built for how you actually use Telegram.
Какой вайб ближе — сухой технический (как первый), или что-то с чуть большей энергией (типа “fast, light, and built to feel right”)?
Ну братан я не нейтив
Я рашен факин слейв
Мой максимум А2 и то с пьяни на удачу
Мне по честному нравится мой вариант
Я мб кстати сайт если буду ебашить то все картинки будут в таком.. медном стиле
Я вообще сама то епта
Fox girl
Ня
Вхахах
Сука чет тянет шутить сорян
Ахах, да не извиняйся, всё в тему — устала и заносит на шутки, это нормально после такого марафона архитектуры.
По слогану — твой вариант вообще неплохой, только маленькая грамматическая правка (без переделки сути, ты автор фразы):
Fera — an experimental Telegram client focused on ergonomics, speed, and lightweightness.
Просто “ergonomic” → “ergonomics” (существительное, не прилагательное — как “speed”, “lightweightness”), и всё сочетается ровно. “Lightweightness” технически не самое частое слово, но оно понятное и не режет слух — можно смело оставлять, это не ошибка, просто редкое слово.
Про медный стиль сайта — это прямо логично ложится в то, что мы уже решили для лого (медно-ржавый акцент), так что если будешь делать сайт — вся палитра уже практически выбрана заранее, тебе не придётся отдельно этим мучиться.
Записать медную тему сайта как часть NAMING-UX (визуальная идентичность), раз она теперь не только про иконку, а про весь бренд?
Да, слово вполне реальное — “lightweightness” технически валидно (lightweight + -ness, стандартный английский способ образовать существительное из прилагательного), просто на практике люди чаще говорят “lightness” покороче, так что оно звучит слегка книжным/редким, но не выдуманным. Так что повод выпендриться железный — ты не ошиблась, ты просто выбрала менее популярный, но абсолютно правильный вариант. 😄
На сайте вероятно еще и анимаций с макбуком, виндой и линуксом сделаю) что-то красивое в стиле презентаций эпл хааха. Хотя…
Ну ладно не прям это но как Варик можно пару идей стыбзить. Их и так до них тыбзили дохуя раз. И все такое.
Логично — Apple-style презентационные паттерны (плавный parallax-скролл, продукт медленно вращается/собирается по мере скролла, крупная типографика с постепенным reveal) настолько стали общим языком презентации софта, что тащить оттуда идеи — это не воровство, это уже жанр сам по себе, как ты и сказала.
Конкретно под твой кейс (показать один клиент на трёх ОС) несколько рабочих паттернов оттуда:
Синхронный scroll-driven переключатель ОС. Один и тот же макет окна клиента, скроллишь — он плавно морфит между macOS/Windows/Linux рамками и стилями (as if the Style-модуль хот-свапнулся у тебя на глазах) — это буквально твоя архитектура, показанная как визуальный трюк, а не текстом.
Sticky-secция с параллаксом. Экран “залипает” на месте, пока текст сбоку меняется (фичи проскролливаются, картинка стоит) — классика Apple под “вот фичи одна за другой”.
Медленный zoom/pan по скриншоту интерфейса при скролле — создаёт ощущение “погружения” без сложной 3D-анимации, дёшево в реализации (CSS transform + scroll listener), не требует WebGL.
Для реализации на статике (раз это скорее всего будет просто сайт, не полноценное SPA) — Framer Motion (если React) или GSAP + ScrollTrigger (framework-agnostic, самый частый выбор именно под такие scroll-driven анимации) закрывают 90% этих паттернов без изобретения велосипеда.
Практический совет: не пытайся скопировать Apple 1:1 по полировке — у них команда на это одно и бюджет. Возьми 1-2 паттерна (например sticky-scroll + плавный OS-switch) и сделай их действительно хорошо, а не пять паттернов средне — сайт с одним запоминающимся моментом работает лучше, чем витрина техник.
Записать это в NAMING-UX как направление для будущего сайта, вместе с медной темой?
Ты за один разговор прошла путь от “а если я ебанусь и захочу написать клиент на расте” до полноценной архитектуры с пятью документами, слогана, направления лого и даже сайта с медными анимациями в стиле Apple. Это реально хороший объём для одной сессии — и видно, что тебе кайфово именно в процессе проектирования, не только в результате.
Как ты сейчас — заряжена продолжать, или это был тот самый момент “выдохнуть”, про который спрашивала чуть раньше?
А, ну вот и объяснение — вчера выгрузила из головы целую архитектуру и вырубилась прямо на кураже. Тело своё взяло.
Если сонливость реально сильная (не просто “лень вставать”, а именно “глаза слипаются”) — это, возможно, тело досыпает то, что вчера недобрала на самом процессе проектирования (такие штуки выматывают сильнее, чем кажется в моменте, особенно если давно сидела). Стоит поспать ещё, если есть возможность, а не пытаться сразу впрягаться обратно в Fera.
Как спалось вообще — нормально, или опять урывками?
Пар — это прямо конкретный физический сигнал, тело реально в стрессовом/перегретом режиме, не просто “лень”. Если дома есть возможность — проветри, попей воды, может пар это ещё и от того, что вчера долго сидела с ноутом и не двигалась.
Высыпаться “полностью” не обязательно, чтобы функционировать — но если чувствуешь, что просто вырубает через силу, лучше дать себе ещё немного полежать, чем бороться. Fera никуда не убежит.
Как сама себя чувствуешь в целом — просто не выспалась, или есть что-то ещё, что колбасит?
Понятно, тогда не выспишься сегодня в любом случае, раз пары с утра и по расписанию не подвинуть.
Раз так — постарайся хотя бы не лечь совсем поздно сегодня вечером, чтобы не накапливать ещё сильнее. Кофе перед парами, если нужно продержаться, но не позже начала дня — иначе вечером заснуть будет сложнее, и цикл повторится.
Что за пары сегодня, тяжёлые или можно будет чуть отсидеться?
Да в основном скучная хуйня весь день и то куда надо прийти в конце. Ну то есть бляди сделали хуйню. Первые не проебать. Между ними можно. Последнюю нет
И в итоге легче все отсидеть
Логично — раз первую и последнюю нельзя проебать, а между ними дыра, то самый рабочий план: первую пересидеть на автомате (даже если в полусне), а в дырке между парами реально попытаться подремать хотя бы 15-20 минут, если есть где — это ощутимо освежает, в отличие от просто сидения в телефоне. На последнюю уже нормально придёшь не в ноль.
Кофе — если будешь пить, то в начале дня, а не перед последней парой, иначе вечером опять не уснёшь вовремя.
Хах, ну если реально дремать прямо на месте — это твой蘭 выбор и твои последствия с преподом, я не буду тебя отговаривать нотациями. 😄
Только учти нюанс: сон “прямо на парте” в сидячем положении обычно даёт рваный, некачественный отдых по сравнению с нормальным лежачим сном — часто просыпаешься более разбитой, чем была. Если есть возможность выбирать — 15 минут где-то с опущенной головой на что-то мягкое лучше, чем то же время просто клевать носом сидя ровно.
Как по мне, всё в рамках “делай что можешь с тем, что есть” — тебе стоять после этого целый день, так что бери отдых, где он реально есть, а не геройствуй.
Я задумываюсь над тем что бы поставлять с приложением большую папку classes/ или чуть иначе названую. Суть в том что бы поставлять функции звонки, сообщения, чаты и прочее не в самом бинарнике а отдельно в DLL.
А порядок и манифесты рулит ядро
Это прямо логичное продолжение того, что уже спроектировано — просто ты сейчас формализуешь то, что архитектура и так предполагала (ADR-001), давая этому конкретную файловую форму.
classes/ (или как назовёшь) как отдельная директория с функциональными dylib — это, по сути, разделение по фичам (calls.so, messages.so, chats.so…), а не только по инфраструктурным движкам (storage.so, search.so…), как было в изначальной раскладке. Это разумное расширение той же идеи на уровень “какие фичи вообще есть в клиенте”, не только “чем эти фичи реализованы технически”.
1. Границы фич не всегда чистые.calls.so реально независим (свой протокол, свой актор, слабо связан с остальным). А вот messages.so и chats.so — они шарят Chat actor, ReadState, storage-доступ. Если разрезать по живому, получишь либо (а) дублирование логики между модулями, либо (б) кучу вызовов между messages.so и chats.so через ту же дорогую dylib-границу, которую мы специально минимизировали для storage/search (ADR-005). Стоит явно решить, где реальный шов между фичами, а не резать “интуитивно по названиям”.
2. Кто держит состояние? Если messages.so — это просто набор функций без состояния (stateless), а всё состояние (акторы, Chat actor) живёт в ядре — это чище и ближе к тому, что уже спроектировано: core держит акторную систему, а “classes” — это скорее наборы обработчиков/бизнес-логики, которые ядро вызывает, передавая нужный контекст. Если наоборот, messages.so сам держит акторы внутри себя — тогда hot-swap этого модуля означает “убить и пересоздать все Chat actors”, что не бесплатно (пересинхронизация состояния).
3. Манифест и порядок загрузки. Ты пишешь “ядро рулит порядком и манифестами” — это ровно то, что уже даёт nylon-ring + твоя схема доверия (ADR-003/004): ядро читает подписанный манифест, знает какие модули грузить в каком порядке (например storage.so раньше messages.so, потому что второе зависит от первого). Разница с уже спроектированным — тут появляется граф зависимостей между модулями, не только плоский список. Стоит явно закладывать это в манифест с самого начала ({ name, version, hash, depends_on: [...] }), иначе потом придётся ретрофитить.
├── calls.so — независим, свой протокол/актор внутри
├── messages.so — stateless логика, состояние живёт в core Chat actor
├── chats.so — то же самое
└── manifest.json — граф зависимостей + версии + подписи
Практический критерий, где резать: если фича может отвалиться без последствий для других фич (как звонки) — отдельный модуль с полной автономией. Если фичи неразрывно шарят состояние (сообщения/чаты) — либо один модуль, либо чёткий трейт-контракт между ними с пониманием, что вызовы между ними тоже дорогие.
Записать это как дополнение к архитектуре модульности (ADR-001/RFC §5), или хочешь сначала докрутить саму идею — конкретно, как ты видишь границы между calls/messages/chats?
Ага, то есть речь не про “модуль на фичу” (calls.so, messages.so), а про гранулярность вплоть до отдельных функций/структур — практически файловая система вместо кода. Тут стоит по-честному разложить, где это даёт реальную пользу, а где превращается в оверинжиниринг, который сам себя съест.
Хот-свап на уровне отдельной функции. Если у тебя send_message.so, format_timestamp.so, parse_entities.so — каждую можно заменить/оптимизировать независимо, не трогая остальное. Для функций с чистой сигнатурой (вход → выход, минимум состояния) это звучит красиво.
1. Оверхед границы взрывается комбинаторно. Мы весь разговор боролись за то, чтобы минимизировать пересечения dylib-границы (ADR-005 — batch API, host-кэш, потому что даже один переход границы стоит заметно). Если каждая мелкая функция — своя dylib, у тебя не десятки пересечений в секунду, а тысячи, потому что типичный путь “отправить сообщение” — это не одна функция, а parse_entities → validate_length → encrypt → serialize → send. Пять переходов границы вместо нуля, и это на горячем пути, не на редкой операции.
2. Дерево зависимостей взрывается. Манифест с графом зависимостей между условными 10 модулями (storage, search, calls, messages…) — вменяемо. Граф между 200+ мелкими функциями-модулями — это уже отдельная система разрешения зависимостей уровня package manager (Cargo, npm), которую тебе придётся реализовать самой, только хуже, потому что версии там должны совпадать по ABI на лету, в отличие от компилируемых крейтов.
3. Большая часть функций не имеет смысла обновлять независимо.format_timestamp меняется раз в пятилетку. Городить для неё отдельный .so, версионирование, подпись, манифест-запись — это чистые накладные расходы без реальной пользы: ты никогда не будешь её хот-свапать отдельно от всего остального в реальности.
Гранулярность модуля = та часть системы, которую ты реально ожидаешь заменять/оптимизировать независимо от соседей, и у которой есть естественная, редко пересекаемая граница состояния. Это уже и есть твои storage/search/calls/renderer из исходной архитектуры — они гранулярны ровно настолько, насколько имеет смысл.
Внутри messages.so (если оставить фичи как модули) можешь резать код на файлы/папки сколько угодно — messages/parsing.rs, messages/validation.rs, messages/send.rs — это уже вопрос организации кода внутри одного компилируемого крейта, не отдельных dylib. Ты получаешь ту же читаемость и разделение ответственности, без единого межмодульного вызова через дорогую границу.
Практический тест перед тем как резать что-то в отдельный .so: “Стану ли я реально хот-свапать именно эту вещь отдельно от соседей, чаще чем раз в год, и переживёт ли это ценой лишнего перехода границы на каждый вызов?” Если ответ “не уверена” — это папка внутри крейта, не модуль.
Хочешь, я это оформлю как явное правило-эвристику в ADR (когда резать на dylib, когда — просто на файлы), чтобы не пришлось каждый раз передумывать заново при добавлении нового куска кода?
Я бы хотела все же сделать так. Но надо придумать как сделать оверхед минимальным. И не для каждой части хотсвап. Те же таймштампы действительно не надо перезагружать например
Ок, тогда вопрос смещается с “резать или нет” на “как дать гранулярность без цены за каждый вызов” — это решаемо, но требует явно разделить гранулярность обновления от гранулярности вызова. Это разные оси, и их можно расцепить.
Ключевая идея: batch-compile “класс” функций в один физический .so, но с независимым версионированием на уровне метаданных
Вместо “каждая функция — свой .so” (дорого на вызов) или “всё в одном core” (нет гранулярного хот-свапа) — есть средний путь:
Compilation unit ≠ versioning unit. Ты можешь физически собирать группу связанных функций в один .so (значит вызовы внутри группы — обычные Rust-вызовы, нулевой оверхед, инлайнятся компилятором), но манифест трекает версию/хэш каждой отдельной функции внутри. Хот-свап всей .so происходит, только если хоть одна функция внутри реально изменилась — то есть группировка становится вопросом “что обычно меняется вместе”, а не произвольным разбиением.
Практически: messages.so содержит send_message, parse_entities, validate_length — все три физически в одной библиотеке (ноль межфункционального overhead), но манифест знает их по отдельности:
Если ты переписываешь только parse_entities — компилируешь новый messages.so целиком (он маленький, компиляция быстрая), но манифест показывает “изменилась только эта функция” — и это чисто информационно/для аудита/для UI “что обновилось”, не механизм реального partial-swap.
Если тебе именно нужен реальный независимый рантайм-хот-свап отдельной функции без пересборки соседей
Тогда неизбежно придётся платить за границу — но можно снизить цену конкретными приёмами:
1. Function pointer table вместо dylib per function. Один .so на “класс” (условно hot-functions.so), внутри — таблица указателей на функции, которую ядро дёргает по индексу/имени. Замена одной функции = замена одного указателя в таблице после перезагрузки всего .so, но вызов — это по-прежнему просто один indirect call, не переход dylib-границы (если таблица живёт в том же адресном пространстве и не требует marshaling). Это фактически то, что уже даёт nylon-ring на уровне модуля — ты предлагаешь применить тот же паттерн на уровень ниже, внутри модуля.
2. Разделить “горячие” и “холодные” функции по разным правилам.format_timestamp, любые чистые/простые функции без реального смысла в независимом хот-свапе — компилируются статически в core, обычные Rust-вызовы, нулевой оверхед, нет версионирования вообще. Только функции, которые реально являются “местом эксперимента” (например send_message’s retry logic, где ты постоянно что-то тюнингуешь) — идут в динамический слой. Это, по сути, явный список кандидатов на гранулярный хот-свап, а не “давай всё разрежем и потом решим”.
Я бы взяла гибрид: классы = физические .so (как ты и предлагала), внутри каждого — обычный компилируемый Rust-код с нулевым внутренним overhead, манифест трекает версии/хэши отдельных функций внутри класса для аудита и понимания “что реально поменялось”, но реальная перезагрузка происходит на уровне всего класса, а не функции. Функции, которые вообще не меняются (timestamps, форматирование) — просто не выносятся в classes/ вообще, остаются частью core.
Граница “что в classes/, что в core” тогда определяется не “это функция или структура”, а тем самым тестом с прошлого сообщения: реально ли ты ожидаешь трогать эту штуку отдельно и часто.
Хочешь такую схему зафиксировать в ADR как явное дополнение (classes/ layer с function-level manifest tracking, module-level actual reload)?
Окей, теперь граница чёткая и она структурная, не по фиче/функции — это меняет расклад, и в лучшую сторону, потому что у тебя появляется естественный, а не произвольный критерий резки.
struct.dll = сама структура + вся её impl-логика (методы, поведение). for-struct-types.dll = переиспользуемые типы, которые эта структура использует для конструирования/инициализации (билдеры, конфиги, входные DTO). Это разумное разделение поведения от данных, которыми это поведение параметризуется — и оно естественно совпадает с тем, что реально меняется с разной частотой:
Типы/инициализаторы (for-struct-types.dll) — обычно стабильнее, меняются редко (это форма данных).
Impl-логика (struct.dll) — это то место, где ты реально будешь экспериментировать/оптимизировать чаще.
Значит у тебя естественно получается: часто хот-свапаемое (impls) отделено от редко меняющегося (типы), без искусственного деления по функциям.
1. Граница вызова между struct.dll и for-struct-types.dll. Если impl использует типы из соседней dll — это переход границы на каждый вызов конструктора/метода, если они физически разные .so. Тут применим тот же приём, что обсуждали: типы, использующиеся часто в горячем пути (например MessageId, ChatId — плоские, простые), лучше держать как #[repr(C)] структуры, передаваемые по значению через ABI (без аллокации/marshaling), а не как opaque handle, требующий вызова через границу для каждого поля.
2. Одна структура — один физический шов, а не N. Если у тебя Message struct, у неё impl с 20 методами — всё это один struct.dll, не 20 отдельных. Внутри dll между методами — обычные Rust-вызовы, ноль overhead. Хот-свап всего Message’s impl происходит целиком, когда ты меняешь хоть один метод. Это разумный компромисс — ты хот-свапаешь на уровне “весь тип целиком”, что обычно и есть та единица, которую реально имеет смысл менять атомарно (у тебя вряд ли будет ситуация “хочу обновить только метод Message::edit(), но не Message::delete()” отдельно друг от друга в рантайме).
3. Зависимости между struct.dll’ами.ChatActor использует Message — если ChatActor тоже отдельный struct.dll, у тебя граф зависимостей между структурами (тот самый манифест с depends_on, о котором говорили раньше), и порядок загрузки должен это учитывать. Тут естественно всплывает вопрос: структуры с сильной связностью (Message ↔ Chat ↔ ReadState, которые постоянно друг друга держат/мутируют) — кандидаты на один общий .dll (не резать по живому), а по-настоящему независимые структуры (Message vs. TrustManifestEntry, у которых нет общей логики) — отдельные.
4. for-struct-types.dll — общий на весь класс или per-struct? Если это переиспользуемые типы (билдеры/инициализаторы), возможно разумнее иметь один shared types-layer, а не по одному на каждую структуру — иначе ты рискуешь получить кучу дублирующихся мелких for-X-types.dll, каждый из которых почти не используется отдельно от соседа. Стоит явно решить: типы группируются по домену (все типы для Message/Chat/ReadState в одном types-layer, потому что они логически связаны), а не по one-to-one с каждой структурой.
Как ты видишь именно пункт 3 и 4 — резать structs по связности (Message+Chat+ReadState вместе, потому что тесно связаны) или всё же держать одна-структура-один-dll строго?
Многие движки (Unreal, Unity через plugin-систему, да и сам Source engine) реально гоняют геймплейную логику через DLL-границы постоянно — physics.dll, audio.dll, отдельные plugin-модули на каждую систему. И это не убивает производительность игр с бюджетом кадра в 16мс на весь рендер+физику+звук+сеть. Если у них получается — у тебя тем более получится на UI-приложении, где нет жёсткого 60fps-бюджета на бизнес-логику (рендер отдельно, а логика чата не обязана укладываться в микросекунды).
Игровые движки, которые так делают, почти всегда батчат вызовы на границе тем же способом, что мы уже обсуждали — не “вызови physics.dll для каждого объекта”, а “передай весь массив объектов одним вызовом, physics.dll обработает всё разом”. То есть похуизм к overhead относится не к “переходам границы вообще”, а к количеству переходов, которое они минимизируют тем же батчингом, просто на своём уровне (кадр, не отдельная сущность).
Ты права — сам факт перехода границы не катастрофа, у тебя уже есть основа не бояться этого (ADR-005 уже про это). Вопрос не “можно ли себе позволить DLL-границы”, а “сколько миллионов переходов в секунду ты реально создашь при struct-per-dll грануляции, и упрёшься ли ты в это на практике”. Для чата с условно сотнями сообщений на экране и десятками действий в секунду — ты, скорее всего, никогда не приблизишься к тому объёму вызовов, который заставил бы игровые движки париться (они там на миллионах объектов физики в кадр).
Короче: твой инстинкт “да похуй на маленький оверхед” — вероятно правильный для твоего домена (чат-клиент, не realtime physics на тысячах сущностей). Батчинг остаётся хорошей практикой там, где он естественный (storage/search, где реально высокая частота), но для struct-per-dll на уровне доменных сущностей — можно резать смело, не мучаясь предварительной оптимизацией того, что скорее всего не станет узким местом.
Хочешь так и зафиксировать — struct.dll + for-struct-types.dll как основную гранулярность, без искусственных ограничений “давай не будем резать слишком мелко”, раз домен это позволяет?
Логично — это закрывает как раз тот пункт 3/4, который мы оставили открытым: не нужно гадать “общий types-layer или per-struct”, потому что у тебя появляется третья категория сама по себе.
for-struct-types.dll — типы/инициализаторы, локальные для этой структуры
shared-types.dll — межмодульные типы, на которые ссылаются несколько struct.dll
shared-types.dll — это ровно то место, где живут MessageId, ChatId, ReadState и подобное: вещи, которые не принадлежат одной структуре, а являются общим языком между несколькими. Явная третья библиотека с явными зависимостями от неё (а не дублирование типа в каждом for-struct-types.dll, что было бы кошмаром синхронизации версий) — это чище, чем то, что я предлагала раньше.
Граф зависимостей теперь трёхуровневый, и это нормально формализуется
shared-types.dll (нет зависимостей, самый стабильный слой)
↑
for-X-types.dll, for-Y-types.dll (зависят от shared-types)
↑
X.dll, Y.dll (зависят от for-X-types/for-Y-types + shared-types)
Манифест естественно получает уровни (layer: shared | types | impl), и порядок загрузки строится по этому графу — shared-types.dll грузится первым всегда, for-*-types.dll вторым, *.dll (impls) последним. Это не сложнее того, что мы уже обсуждали, просто с явным именем для среднего слоя, который раньше был “а куда девать общие типы”.
Про похуй-на-оверхед — согласна с твоим инстинктом (см. предыдущий ответ)
Раз домен (чат-клиент) не приближается по частоте вызовов к тому, что заставляет игровые движки батчить физику — можно резать по структурам смело. shared-types.dll даже снижает реальный overhead по сравнению с “каждый for-struct-types свой” — потому что общие типы передаются по одному стабильному ABI-контракту, а не пере-объявляются с рисками рассинхрона в каждом модуле отдельно.
Зафиксировать эту трёхуровневую схему (shared-types.dll → for-X-types.dll → X.dll) как расширение архитектуры модульности в ADR/RFC?
Именно так, и это ключевое наблюдение, которое стоит держать как основной ориентир решения в целом.
Игровые движки платят за модульность DLL-границ и ещё сверху платят за батчинг как отдельную инженерную работу, чтобы снизить эту цену — но даже без батчинга сам факт границы не настолько катастрофичен, чтобы монолитить всё в один бинарь. Модульность выигрывает у них не потому что overhead равен нулю, а потому что польза от независимой замены/обновления systems перевешивает цену вызовов, даже в домене с гораздо более жёсткими требованиями к производительности, чем у тебя.
Это ровно тот же расчёт, который ты сейчас делаешь для struct.dll-грануляции, только у тебя домен мягче игрового (нет 16мс бюджета на кадр), значит запас прочности перед тем как overhead реально станет заметен — ещё больше. Другими словами: если у AAA-игр с миллионами вызовов в кадр это работает без монолита — у чат-клиента с на порядки меньшей частотой вызовов тем более работает, даже без специального батчинга на старте.
Практический вывод остаётся тем же, что мы уже обсудили: резать по structs смело, батчинг оставить как инструмент для конкретных измеренных горячих точек (storage/search, если профайлинг покажет, что именно они упёрлись), а не как обязательное предварительное условие для всей системы. Зафиксировать трёхуровневую схему (shared-types.dll / for-X-types.dll / X.dll) как основную модель, а батчинг — как точечный инструмент по необходимости, не по умолчанию?
Да у меня бюджет 24 кадра на сам UI + 45 на обновления сообщений и 60-75 на стикеры и анимированные части отдельно
Мне нет смысле делать один бюджет на всё)
Слушай. Наверное мы уйдем от GPUI. Там апи начали сильнее ломать…
Разумно на счёт раздельных бюджетов — UI (24), обновления сообщений (45), стикеры/анимации (60-75) реально разные по своей природе задачи, и жёстко привязывать их к одному кадровому циклу было бы искусственным ограничением, раз ты и так строишь Engine-слой отдельным hot-swappable модулем (ADR-009) — у него может быть свой internal scheduling, не обязанный совпадать с частотой UI-перерисовки.
Про GPUI — дай проверю актуальный статус, ты права поднять это сейчас, раз мы всё равно упираемся в Engine-слой архитектурно.
🔧 web_search — GPUI Zed API breaking changes 2026 stability
Твоё ощущение подтверждается фактами. Картина такая:
Официально GPUI прямым текстом заявляет о себе как pre-1.0 с частыми breaking changes — это не слух, это в их же документации.
Экосистема вокруг GPUI фрагментировалась. Zed объявил, что фокусируется только на своих бизнес-нуждах — то есть GPUI развивается под Zed, а не как независимый общедоступный фреймворк, и community-форки (gpui-ce, bezel-gpui, alpine-gpui) появились именно чтобы с этим справляться. Замечание про то, что апстрим-merge однажды тихо снёс их собственную anim系tion-систему — прямая иллюстрация нестабильности API даже на уровне архитектурных решений, не только сигнатур функций.
Есть даже “upstream radar” боты, которые мониторят изменения в Zed и оценивают, ломают ли они форки — это явный симптом того, что API меняется достаточно часто, чтобы под это городить отдельную инфраструктуру мониторинга.
Учитывая архитектуру, которую мы спроектировали — Engine как отдельный hot-swappable модуль (ADR-009) — у тебя, по счастью, уже заложена страховка: GPUI живёт за трейт-границей, замена движка не должна требовать переписывания domain/actor-слоя. То есть даже если решишь мигрировать позже — это не катастрофа архитектурно, просто работа внутри одного модуля.
Но если ты сомневаешься сейчас, до того как начала писать, разумно рассмотреть альтернативы параллельно, а не откладывать решение на “когда прижмёт”:
bezel-gpui / gpui-ce — форки с целью большей стабильности API, чем апстрим. Стоит посмотреть их обещания по стабильности версионирования конкретнее.
iced — более зрелый, Elm-архитектура нативно (что тебе и так подходит по духу), но менее производительный на сложных списках, чем заявляет GPUI.
Свой велосипед на wgpu + cosmic-text — тот вариант, который мы отбросили в самом начале ради скорости разработки, но раз ты и так закладываешь Engine-модульность — можно рассмотреть это не как “вместо GPUI”, а как “второй Engine-кандидат с самого начала”, благо трейт уже развязывает тебя от привязки к одному.
Хочешь зафиксировать это как открытый вопрос (OQ) — “GPUI vs форк vs альтернатива, решить перед началом Engine-модуля”, или ты уже склоняешься к конкретному направлению?
Да у меня бюджет 24 кадра на сам UI + 45 на обновления сообщений и 60-75 на стикеры и анимированные части отдельно
Мне нет смысле делать один бюджет на всё)
Слушай. Наверное мы уйдем от GPUI. Там апи начали сильнее ломать…
Резонно разделять бюджеты по типу контента — UI-хром действительно не нуждается в 75fps, а стикеры/анимации выигрывают от него сильнее всего, глаз это ловит именно там. Такая многоуровневая частота обновления — тоже разумное архитектурное решение, и она хорошо ложится в уже обсуждённую модель Engine/Style/Feature (разные подсистемы рендера могут тикать с разной частотой независимо).
Про GPUI — дай проверю актуальный статус, ты права что там была активность по слому API, стоит посмотреть, что именно и насколько это меняет расклад.
🔧 web_search — GPUI breaking changes API 2026 Zed
Подтверждается ровно то, о чём ты говоришь, и картина даже хуже, чем “иногда ломают API”.
Официально GPUI прямым текстом пишет о себе: GPUI ещё в активной разработке, пока Zed команда работает над своим редактором, и он всё ещё pre-1.0. Между версиями часто будут breaking changes. Это не баг, это заявленная политика.
Community-форк (gpui-ce) прямо жалуется на последствия: Zed заявили, что будут фокусироваться исключительно на бизнес-релевантной работе, и хотя это понятно, для community-проектов это означает больше трения при использовании gpui — экосистема сейчас сильно фрагментирована. Показательный пример оттуда же: когда они смёржили апстримную spring-фичу от Zed, это мигрировало их систему анимации в сторону от их собственного нового motion API — то есть даже форки страдают от непредсказуемых слияний.
Плюс в реальных PR видно, что ломают даже базовые вещи вроде CursorStyle::None и cursor_none(), которые были удалены, и структурные вещи вроде разделения GPUI на отдельные крейты gpui/gpui_types/gpui_platform — то есть ломаются не только мелочи, а сама модульная структура крейта.
Учитывая, что ты и так строишь Engine как заменяемую ось (ADR-009) — у тебя уже есть архитектурный страховочный трос именно под этот риск. Вопрос не “уходить или нет прямо сейчас”, а:
Начинать ли с GPUI как временный Engine, зная, что придётся регулярно чинить breaking changes, но получая рабочий рендер быстрее.
Смотреть альтернативы сразу — раз ты и так планируешь HoF-слой поверх Engine (не завязываясь на GPUI-специфичный View/Element API впрямую), возможно стоит сразу целиться в Engine, который стабильнее, и не тратить время на API, который будет ломаться под ногами.
Кандидаты, если уходить: Vello (низкоуровневый GPU-рендер от Linebender, стабильнее по духу, но тебе придётся самой писать text layout/hit-testing поверх — то, что GPUI даёт бесплатно), iced (более зрелый, Elm-архитектура из коробки, что вообще-то отлично ложится на то, что ты и так делаешь вручную поверх GPUI), или тот самый экспериментальный embedded_gpui — но это экспериментальный спайк, ожидайте острых углов и breaking changes, здесь нет ничего как поддерживаемый API, то есть ещё более нестабильно, чем сам GPUI.
Учитывая, что ты уже планировала HoF-слой абстрагировать рендер — iced может неожиданно закрыть сразу две твои цели: он и стабильнее, и Elm-архитектура там нативная, а не то, что ты вручную городишь поверх GPUI. Хочешь, покопаю iced подробнее на предмет зрелости и API-стабильности, чтобы сравнить с GPUI по-честному?
🔧 web_search — Rust native GPU UI framework 2026 alternative GPUI iced stable API
Интересный расклад вылез, и он меняет вопрос: похоже, ты правда думаешь не про уход с GPUI вообще, а про то, что сам “чистый” upstream GPUI (от Zed) — заложник их приоритетов, но вокруг выросла целая экосистема форков/дистрибутивов, которые как раз решают проблему нестабильности.
GPUI Kit (longbridge/gpui-kit) — это не просто набор компонентов, а полноценная трёхслойная архитектура (gpui-component / gpui-base / gpui-shell), которая используется как основа реального коммерческого шипящегося продукта (Longbridge Pro), а не эксперимент. Заявлено 120 FPS, стабильная работа при 200K строк с подсветкой Tree-sitter — то есть это уже проверено на живом high-performance приложении, близком по духу к тому, что делаешь ты. Framework извлечён из требований публично выпущенного коммерческого десктоп-приложения, а не спроектирован в изоляции — это сильный сигнал зрелости по сравнению с голым upstream GPUI.
Плюс нашлись прямые форки, явно решающие проблему API-стабильности через снапшоты: gpui-pre-* крейты — снимки GPUI, публикуемые с сохранением лицензии и уведомлений Zed — то есть кто-то уже замораживает версии GPUI и публикует их как стабильные пакеты, вместо того чтобы гнаться за апстримом.
Похоже, у тебя есть третий путь, не “терпеть нестабильный upstream GPUI” и не “уходить на совсем другой фреймворк”: взять один из зрелых форков/дистрибутивов (gpui-kit, bezel-gpui, adabraka-gpui), которые сами абсорбируют боль синхронизации с апстримом Zed, а тебе отдают более стабильный API. Учитывая, что твой Engine и так спроектирован как заменяемый модуль (ADR-009) — можно вообще не завязываться намертво на конкретный форк, а держать GPUI-семейство (любой из этих вариантов) как один Engine-кандидат, и просто выбрать наиболее живой на момент реализации.
Из upstream-альтернатив (не GPUI-форков) в выдаче явно свежий и интересный вариант — Xilem (от той же команды, что делает Vello/Linebender, “экспериментальный нативный Rust UI фреймворк”) — стоит на него тоже взглянуть отдельно, если хочешь совсем уйти от GPUI-семейства, а не просто взять более стабильный форк.
Хочешь, покопаю Xilem подробнее (архитектура, стабильность, насколько далеко от GPUI по духу), или GPUI Kit конкретно (детали трёхслойной архитектуры, насколько она годится как твой Engine-модуль)?
Продукт разделился на три реализации с разной зрелостью: vello_gpu (GPU препроцессит на CPU, растеризация на GPU, задуман стать основным для продакшена по мере созревания), vello classic (изначальный compute-centric рендер, остаётся экспериментальной реализацией), и vello_cpu/Hybrid (сейчас примерно бета-качество, ещё есть шероховатости и предстоит работа над производительностью, но должно быть юзабельно). То есть у тебя не один Vello, а три с разной степенью готовности, и даже самый зрелый (“должен быть юзабельным”) прямо называется бета.
Тут честность разработчиков ещё жёстче. По их собственным словам: Xilem можно сейчас считать находящимся в альфа-состоянии. Многое нуждается в улучшениях. И даже релиз v0.1.0 (спустя годы разработки) сопровождается той же формулировкой — альфа, многое требует улучшений. В блоге разработчиков прямым текстом: мы ожидаем продолжения очень активной разработки, включая крупные breaking changes. Причём у самого проекта есть болезненная история — Xilem находится в состоянии perma-experimental с мая 2022, и когда он начинался, основные мейнтейнеры Druid (предшественника) практически прекратили туда вносить вклад в ожидании, что Xilem заменит Druid, как только будет готов. К сожалению, прогресса до паритета с Druid так и не случилось, при этом Druid остаётся заброшенным — то есть у них уже был прецедент “бросили старое ради нового, а новое не дозрело”, ровно тот риск, который тебя пугает в GPUI.
Обе экосистемы (GPUI-семейство и Linebender/Xilem-Vello) сейчас находятся примерно на одной стадии зрелости — активная разработка, честно признанная нестабильность, breaking changes как норма, а не исключение. Единственная реальная разница: у GPUI хотя бы есть Zed как крупный production-потребитель, который держит фреймворк живым и рабочим на практике (даже если API трясёт), а у Xilem пока нет сопоставимого flagship-продукта, который бы гарантировал, что проект не повторит судьбу Druid.
Это значит твой изначальный инстинкт правильный: смотреть на форки/дистрибутивы GPUI (gpui-kit, bezel-gpui) как способ получить стабильность без ухода от проверенной в бою (Zed) кодовой базы — реалистичнее, чем менять GPUI на Xilem/Vello и попасть в ту же (или худшую) нестабильность, только без 1.0-редактора за спиной, который заставляет команду её поддерживать.
Раз у тебя и так Engine — заменяемый модуль, можно и не решать сейчас окончательно: начать с GPUI Kit (raw в longbridge/gpui-kit) как первого Engine-кандидата, и держать Vello/Xilem как запись в FUTURE-CHECKS.md — “проверить снова через год-полтора, когда либо Vello_GPU выйдет из беты, либо Xilem дозреет”. Записать так?
Давай так. Я не боюсь нестабильного апи. Мне лишь бы сама либа работала стабильно в пределах одной версии)
Но просто у гпюи ноль документации.
Это далеко чет не то что я хотела бы нахуй
🧠 Weighing documentation gaps against version-level API stability needs.
🔧 memory_append — /areas/telegram-client.md
Тогда критерии другие: не “стабильный API”, а документация + стабильность внутри версии. С такими критериями расклад такой (по моим знаниям на середину 2026, конкретные версии и доки стоит проверить перед выбором):
Vello + Parley + winit + AccessKit, свой слой сверху. Это самый близкий к твоей идее HoF/immediate-mode. У Vello и Parley документация и примеры заметно лучше, чем у GPUI, версии можно пинить. Цена: hit-testing, скролл, виртуализацию списка и ввод пишешь сама. Зато Engine получается полностью твоим и естественно ложится на заменяемую ось. Vello CPU/Hybrid при этом ещё и запасной CPU-fallback, который ты хотела.
Xilem/Masonry. Документации больше, чем у GPUI, но статус альфа, и есть готовые виджеты. Для клиента 1:1 по виду они скорее помешают, чем помогут: тебе нужен свой вид, а не их виджеты.
Slint. Лучшая документация из всех, стабильный, нативный вид. Минусы: свой DSL и лицензия (GPL, royalty-free или коммерческая), плюс сложнее проталкивать свой рендер стикеров.
egui. Отличные доки, immediate-mode из коробки, но выжать из него 1:1 с нативным видом тяжело.
Makepad. Быстрый и GPU-нативный, но документация тоже скромная.
Моя ставка для тебя: Vello + Parley + свой HoF-слой. Ты и так собиралась писать компоненты как HoF, а самая ценная часть GPUI (текст и списки) закрывается Parley плюс твоей виртуализацией. Самый тяжёлый кусок тут ввод/IME/выделение текста, его стоит прототипировать первым.
Хочешь, я поищу свежие доки и примеры по Vello + Parley, чтобы прикинуть, насколько это реально по объёму?
Четвертым слоем рендера
Будут
Нативные компоненты. Потому что на маке мне в идеале лучше реализовать отдельной либой которая пытается свифт юи кит дергать как-либо. Хмхмхмх. Норм тема да.. да
Я думаю кстати версии хранить чуть более подробно. Условно…
И даже стандартизировать edition апи. Как у раста но чуть лучше. Плюс гарантиновать поддержку edition = [N, N-1] если это возможно. Если нет super breaking changes and fully rewrite.
Идея сильная, но с SwiftUI есть нюанс: у него нет C ABI, он живёт только в Swift. Рабочая схема такая: тонкий Swift-шим (отдельный dylib) экспортирует функции через @_cdecl, создаёт NSHostingView и встраивает его в NSView твоего окна (handle отдаёт winit). AppKit проще: его можно дёргать прямо из Rust через objc2.
Главный бонус, о котором ты, возможно, не думала: нативное поле ввода (NSTextView) даёт IME, проверку орфографии, эмодзи-пикер и выделение бесплатно. Это самая тяжёлая часть своего рендера, так что начинать с неё выгоднее всего. Сайдбар, тулбар, контекстные меню и попапы тоже хорошие кандидаты. Список сообщений я бы оставила на своём рендере: смешивать нативные вью и GPU-контент в скролле больно (порядок наложения, синхронизация скролла, фокус).
Подпись должна покрывать всю запись, а не только хэш файла (в канонической сериализации), иначе можно подменить host.max_version или fallback без нарушения подписи.
Защита от даунгрейда: без минимально допустимой версии старый подписанный (и уязвимый) модуль можно подсунуть заново. Плюс срок годности или временная метка манифеста, чтобы нельзя было бесконечно показывать устаревший.
artifacts массивом: один модуль в вариантах avx2 и baseline, а выбор по CPU-детекту при запуске.
Отзыв ключей (key_id уже есть, нужен список отозванных).
depends_on и layer из обсуждения про shared-types, чтобы порядок загрузки строился по графу.
Чётко определить три оси: abi_version (бинарная раскладка на границе), api_edition (семантика контракта), module_version (semver реализации). Кто что бампает, лучше записать сразу.
У Rust это работает, потому что компилятор понимает все edition сразу. У тебя модули бинарные, значит нужны адаптеры в хосте: хост реализует edition N, а для N-1 держит слой, который переводит вызовы. Модуль заявляет, какой edition реализует, хост берёт совместимый. Цена: каждый новый edition добавляет адаптер и требует прогона contract-тестов по обоим. Отдельно помни, что формат данных на диске версионируется независимо от API edition.
Обновить ADR/TECHSPEC (новый ADR про слой структур и манифест с edition, плюс четвёртый слой рендера)?
Edition я думала сделать в том плане что у нас кастомный эдишен. Но идея как у раста - определенные минимальные фичи которые проект гарантирует поддерживать в текущем edition. Плюс edition-1 если нет слишком больших breaking changes
А на поддержку старого апи мы дадим возможность писать свой транслятор
Типа
translate_abi_v1_to_v2.wasm
libv1plugins/
// тут старые плагины
Без жестких требований к папкам но суть понятна. Отдельный вид плагинов которые реализуют совместимость если это им надо. Мы такое не юзаем и все
Про макос слой: это не онли макос слой. Будет такой же для gtk, qt, win UI kit или как его там
Просто мак как пример)
Схема понятная и хорошая: edition как набор гарантированных минимальных фич, N-1 поддерживается пока нет крупных поломок, а совместимость со старым API отдана сообществу в виде отдельного вида плагинов. Проекту не нужно тащить старый код у себя. Несколько нюансов:
Транслятор стоит между хостом и старым модулем, значит видит весь трафик. Его права должны совпадать с правами модуля, который он оборачивает. Иначе получится конфликт с “read-only по умолчанию” из ADR-002: транслятору придётся пропускать и записывающие вызовы.
WASM на горячем пути дорог: каждый вызов идёт native → WASM → native с копированием данных. Для редких и мелких вызовов нормально, для batch-вызовов к storage заметно. Можно разрешить трансляторам быть и нативными (доверенный уровень), а WASM оставить для сторонних.
Цепочки: v1→v3 либо через два транслятора подряд (v1→v2→v3), либо требуем прямой. Лучше решить заранее.
В манифесте у транслятора нужны from_edition и to_edition, а хост при загрузке модуля вне диапазона [N, N-1] ищет подходящий транслятор в реестре. Подписи и предупреждения о хэше работают как для любого модуля.
Согласна, это отдельная четвёртая ось рядом с Engine/Style/Feature: AppKit/SwiftUI, GTK4, Qt, WinUI. Контракт лучше сделать тонким, набор “слотов” (поле ввода, сайдбар, меню, диалоги), а не попыткой абстрагировать целые деревья виджетов. Главный вопрос на каждой платформе: кто владеет окном. На macOS удобно встраивать нативные вью в окно с твоим GPU-рендером. На Linux (особенно Wayland) и с WinUI бывает проще наоборот: окно принадлежит тулкиту, а твой рендер живёт внутри как дочерний виджет.
Раз ты сказала всё, что хотела, могу обновить доки: новый ADR про слой структур (struct / for-struct-types / shared-types), манифест с edition и трансляторами, четвёртая ось рендера. Делать?
Ну я думаю у транслятора будет скорее свой тип. Модуль транслятор буквально. Он будет загружаться без особых прав. И отдавать список плагинов на исполнение. Типа. Виртуального списка так скажем. И вот уже там можно будет контролировать права
Плюс Wasm транслятор и версии ТОЛЬКО для plugins) Наша модулей отдельно версионируется и загружается уже нативно без васм. Брух.
Транслятор только для васм плагинов есть
Плюс цепочка трансляции определяется самим транслятором
Где-то это будет v1 —> v2 —> v3 если юзер не найдет напрямую v1 —> v3 и будет пихать напрямую
Где-то еще как-то
🧠 Designing a module system for translators and plugin permissions.
🔧 memory_append — /areas/telegram-client.md
Поняла, это меняет картину в лучшую сторону, и часть моих прошлых замечаний снимается. Раз трансляторы только для WASM-плагинов, а нативные модули живут отдельно, проблемы горячего пути и нативных трансляторов просто не возникает.
Идея “транслятор отдаёт виртуальный список плагинов” мне нравится: он работает как адаптер на этапе загрузки, а не как прокси на каждом вызове, так что накладных расходов в рантайме почти нет. Два момента стоит продумать:
Права только сужаются. Раз права считаются по виртуальному списку, транслятор теоретически может выдать плагину больше, чем тот запрашивал в старом манифесте. Правило: итоговые права = то, что запрашивал исходный плагин, ∩ то, что разрешил пользователь. Транслятор может только переименовать capability из старого API в новый или убрать, но не добавить.
Транслятор тоже недоверенный код. Цепочку он определяет сам, поэтому хосту нужны лимит глубины, защита от циклов (v1→v2→v1) и таймаут на разрешение.
Пользователю в UI лучше показывать права исходного плагина, а не транслированные, чтобы он видел, на что реально соглашается.
Хочешь, теперь обновлю доки (слой структур, манифест с edition, трансляторы, четвёртая ось рендера) одним заходом?
Я вот кстати хз че как
Мб стандартизировать формат самих прав так что бы нельзя было их транслятором подменить
Ну типа. Поставлять все плагины в .dmg нахуй
Там в файле уже и сама прога и манифесты все
Или еще какой-то удобный нам формат данных да.
И поддержка гарантирована с первой стабильной версии до последней сразу. А вот сам плагин уже через трансляцию))
И транслятор лишь дает нам маппинг вызовов и их преобразований
Типа
Максимально ограничить его до декларативной функции желательно HoF и желательно еще и детерменированно простой. Статической если получится
Права читает хост из подписанного манифеста пакета, а не из вывода транслятора. Названия capability (messages.read и т.п.) это стабильные идентификаторы, которые определяет хост, так что транслятору их вообще мапить не нужно, только вызовы.
Схема манифеста и прав замораживается навсегда, меняется только аддитивно: новые capability добавляются, старые никогда не меняют смысл. Нужен список “must-understand” полей: если хост не знает capability из манифеста, он не выдаёт её, а не молча игнорирует.
.dmg не подойдёт: это образ диска только для macOS. Лучше свой контейнер, например .fera-plugin (zip или tar.zst с фиксированной раскладкой): manifest.json, plugin.wasm, ассеты. Подпись покрывает манифест, а в манифесте лежат хэши всех файлов.
Твоя идея “статичная декларативная HoF-функция” по сути означает, что транслятор это данные, а не код: таблица маппинга (переименование вызова, перекладка полей, значения по умолчанию, преобразования из небольшого встроенного набора конвертеров), которую интерпретирует хост.
Хост может статически проверить цепочку (циклы, глубину) при загрузке, без таймаутов и песочницы.
Запасной вариант для того, что таблицей не выразить: чистая WASM-функция без импортов с лимитом топлива (wasmtime fuel), тоже детерминированная. Это escape hatch, не основной путь.
Честное ограничение: изменение поведения (вызов был синхронным, стал потоковым) маппингом не покрыть, такие плагины придётся переписывать.
Отдельно восстановлен memory/telegram-client.md —
рабочая память Claude по этому проекту (в чате лежит по пути
/areas/telegram-client.md). Реконструкция точная: последняя запись
memory_write в чате сообщала размер 8596 байт, восстановленный файл —
8597 (разница в одну завершающую перевод строки).
tree=True отдаёт всю ветку целиком, а не постранично, поэтому извлечение
полное. Именно поэтому два предыдущих способа («Сохранить страницу» → 137 КБ
пустой оболочки и copy-as-text → 11 КБ) дали только хвост: claude.ai
рендерит историю скриптом, и в статическую разметку попадает лишь то, что
уже отрисовано.
Конвертер — har-to-md.ps1, переиспользуется на любом новом HAR того же вида: