feat(frontend): add SSE auto-reconnect with toast notifications

- Automatic reconnection with exponential backoff (1s to 30s max)
- Toast when connection is lost: 'Sunucu bağlantısı kesildi'
- Toast when reconnected: 'Sunucu bağlantısı yeniden kuruldu'
- Backend rTorrent notifications still work via SSE
This commit is contained in:
spinline
2026-02-05 21:46:20 +03:00
parent 3bc4b0f364
commit ce6c3e01aa
3 changed files with 122 additions and 65 deletions

13
Cargo.lock generated
View File

@@ -695,6 +695,7 @@ dependencies = [
"console_log", "console_log",
"futures", "futures",
"gloo-net 0.5.0", "gloo-net 0.5.0",
"gloo-timers",
"js-sys", "js-sys",
"leptos", "leptos",
"leptos_router", "leptos_router",
@@ -874,6 +875,18 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "gloo-timers"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "gloo-utils" name = "gloo-utils"
version = "0.2.0" version = "0.2.0"

View File

@@ -16,6 +16,7 @@ log = "0.4"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
gloo-net = "0.5" gloo-net = "0.5"
gloo-timers = { version = "0.3", features = ["futures"] }
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
uuid = { version = "1", features = ["v4", "js"] } uuid = { version = "1", features = ["v4", "js"] }
futures = "0.3" futures = "0.3"

View File

@@ -138,12 +138,33 @@ pub fn provide_torrent_store() {
}; };
provide_context(store); provide_context(store);
// Initialize SSE connection // Initialize SSE connection with auto-reconnect
create_effect(move |_| { create_effect(move |_| {
spawn_local(async move { spawn_local(async move {
let mut es = EventSource::new("/api/events").unwrap(); let mut backoff_ms: u32 = 1000; // Start with 1 second
let mut stream = es.subscribe("message").unwrap(); let max_backoff_ms: u32 = 30000; // Max 30 seconds
let mut was_connected = false;
loop {
let es_result = EventSource::new("/api/events");
match es_result {
Ok(mut es) => {
match es.subscribe("message") {
Ok(mut stream) => {
// Connection established
if was_connected {
// We were previously connected and lost connection, now reconnected
show_toast_with_signal(
notifications,
NotificationLevel::Success,
"Sunucu bağlantısı yeniden kuruldu",
);
}
was_connected = true;
backoff_ms = 1000; // Reset backoff on successful connection
// Process messages
while let Some(Ok((_, msg))) = stream.next().await { while let Some(Ok((_, msg))) = stream.next().await {
if let Some(data_str) = msg.data().as_string() { if let Some(data_str) = msg.data().as_string() {
if let Ok(event) = serde_json::from_str::<AppEvent>(&data_str) { if let Ok(event) = serde_json::from_str::<AppEvent>(&data_str) {
@@ -192,27 +213,49 @@ pub fn provide_torrent_store() {
global_stats.set(stats); global_stats.set(stats);
} }
AppEvent::Notification(n) => { AppEvent::Notification(n) => {
let id = js_sys::Date::now() as u64; show_toast_with_signal(notifications, n.level, n.message);
let item = NotificationItem { }
id, }
notification: n, }
}; }
notifications.update(|list| list.push(item)); }
// Auto-remove after 5 seconds // Stream ended - connection lost
let notifications = notifications; if was_connected {
let _ = set_timeout( show_toast_with_signal(
move || { notifications,
notifications.update(|list| { NotificationLevel::Warning,
list.retain(|i| i.id != id); "Sunucu bağlantısı kesildi, yeniden bağlanılıyor...",
}); );
}, }
std::time::Duration::from_secs(5), }
Err(_) => {
// Failed to subscribe
if was_connected {
show_toast_with_signal(
notifications,
NotificationLevel::Warning,
"Sunucu bağlantısı kesildi, yeniden bağlanılıyor...",
); );
} }
} }
} }
} }
Err(_) => {
// Failed to create EventSource
if was_connected {
show_toast_with_signal(
notifications,
NotificationLevel::Warning,
"Sunucu bağlantısı kesildi, yeniden bağlanılıyor...",
);
}
}
}
// Wait before reconnecting (exponential backoff)
gloo_timers::future::TimeoutFuture::new(backoff_ms).await;
backoff_ms = std::cmp::min(backoff_ms * 2, max_backoff_ms);
} }
}); });
}); });