Событийная система
Файлы:
backend/src/events/mod.rs,events/tauri_bridge.rs,api/sse.rs
Push-уведомления от бекенда к фронтенду. EventBus — broadcast-канал, через который менеджеры и фоновые задачи отправляют события. Два транспорта доставляют их в UI: Tauri bridge (desktop) и SSE (headless/web).
Архитектура
AppEvent
pub struct AppEvent {
pub name: String,
pub payload: serde_json::Value,
}EventBus — обёртка над tokio::sync::broadcast::Sender<AppEvent>:
pub struct EventBus { tx: broadcast::Sender<AppEvent> }
impl EventBus {
pub fn new(capacity: usize) -> Self; // capacity = 256
pub fn emit<T: Serialize>(&self, name: &str, payload: &T) -> Result<()>;
pub fn subscribe(&self) -> broadcast::Receiver<AppEvent>;
}Транспорты
Tauri Bridge (desktop)
Файл:
backend/src/events/tauri_bridge.rs
Спавнится при запуске Tauri-приложения (#[cfg(not(feature = "headless"))]). Подписывается на EventBus и перенаправляет каждое событие через app.emit():
pub fn spawn_tauri_bridge(app: AppHandle, event_bus: EventBus) {
let mut rx = event_bus.subscribe();
tauri::async_runtime::spawn(async move {
loop {
match rx.recv().await {
Ok(event) => { app.emit(&event.name, event.payload)?; }
Err(RecvError::Lagged(n)) => { /* dropped events */ }
Err(RecvError::Closed) => break,
}
}
});
}SSE (headless / web)
Файл:
backend/src/api/sse.rs
Endpoint GET /api/events для headless-режима. Стримит AppEvent как Server-Sent Events с JSON payload. Фронтенд подключается через EventSource (shim tauri-event-shim.ts).
Реестр событий
Vault
| Событие | Источник | Payload | Frontend |
|---|---|---|---|
vault_locked | VaultManager.lock() | {} | useVaultStore |
vault_unlocked | VaultManager.unlock_with_pin(), unlock_with_recovery_phrase() | {} | useVaultStore |
Pairing
| Событие | Источник | Payload | Frontend |
|---|---|---|---|
qr_pairing_request | PairingManager | { token, device_name, device_type } | AddDeviceContent.vue |
pairing_request | PairingManager | PendingPairingRequest (peerid, initiator*, pin, device_count) | AddDeviceContent.vue |
pairing_confirmation | PairingManager | { approved, pin, confirmer_device?, confirmer_identity?, error? } | AddDeviceContent.vue |
Sharing — пиры
| Событие | Источник | Payload | Frontend |
|---|---|---|---|
sharing_peer_discovered | SharingManager | { identity_id, identity_display_name, device_name } | useDevicesStore |
peer_online | SharingManager | { device_id, device_name?, identity_id?, storage?, timestamp? } | useDevicesStore |
peer_offline | SharingManager | { device_id, device_name, timestamp } | useDevicesStore |
device_revoked | SharingManager | { old_identity_id, reason } | — |
Sharing — пространства и файлы
| Событие | Источник | Payload | Frontend |
|---|---|---|---|
space_received | SharingManager | { space_id, space_name, from_identity_id, from_identity_name, share_type, auto_accepted } | useSpacesStore, MainLayout |
devices_updated | SharingManager | { added_count?, removed_device_id?, source } | useDevicesStore |
spaces_updated | SharingManager | { added_count?, space_id?, deleted_space_id?, source } | — |
file_sync_progress | SharingManager | { space_id, file_hash, offset, total_size, progress_pct } | useSpacesStore, useSyncStore |
file_sync_complete | SharingManager | { space_id, file_hash, total_size } | useSpacesStore, useSyncStore |
P2P Network
| Событие | Источник | Payload | Frontend |
|---|---|---|---|
listening_addr | P2P network | String (multiaddr) | — |
peer_added | P2P network | { peer_id, count } | — |
peer_removed | P2P network | String (peer_id) | — |
S3 Sync
| Событие | Источник | Payload | Frontend |
|---|---|---|---|
sync_progress | sync_cmd.rs | { action, file, progress, completed, total } | — |
Frontend: подписка на события
Паттерн Store Listener
Pinia stores используют startListener() / stopListener() для подписки:
// entities/device/useDevicesStore.ts
let unlistenPeerOnline: UnlistenFn | null = null;
let unlistenPeerOffline: UnlistenFn | null = null;
async function startPeerStatusListener() {
unlistenPeerOnline = await listen('peer_online', (event) => {
// Обновить статус устройства
});
unlistenPeerOffline = await listen('peer_offline', (event) => {
// Пометить устройство как offline
});
}
function stopPeerStatusListener() {
unlistenPeerOnline?.();
unlistenPeerOffline?.();
}SSE shim (headless)
В headless-режиме @tauri-apps/api/event подменяется на tauri-event-shim.ts, который использует EventSource для подключения к GET /api/events. Подробнее: Frontend API.