Use Convex WS client in desktop chat runtime

This commit is contained in:
rever-tecnologia 2025-12-09 15:31:08 -03:00
parent 988bf25010
commit 1d3580b187
4 changed files with 510 additions and 340 deletions

View file

@ -62,6 +62,7 @@ version = "0.1.0"
dependencies = [
"base64 0.22.1",
"chrono",
"convex",
"futures-util",
"get_if_addrs",
"hostname",
@ -94,6 +95,12 @@ dependencies = [
"derive_arbitrary",
]
[[package]]
name = "archery"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e0a5f99dfebb87bb342d0f53bb92c81842e100bbb915223e38349580e5441d"
[[package]]
name = "async-broadcast"
version = "0.7.2"
@ -275,6 +282,12 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.7"
@ -302,6 +315,12 @@ dependencies = [
"serde",
]
[[package]]
name = "bitmaps"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6"
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -571,6 +590,57 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "convert_case"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "convex"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16e7ab85cfc76e9a13d252da8a7933ab52f38b9c51de3f7bb8dbe4e2262bac04"
dependencies = [
"anyhow",
"async-trait",
"base64 0.13.1",
"bytes",
"convex_sync_types",
"futures",
"imbl",
"rand 0.9.2",
"serde_json",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tokio-tungstenite",
"tracing",
"url",
"uuid",
]
[[package]]
name = "convex_sync_types"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f819ce8fd4370f235f2f5e345499fa219d7d1827cd88923f1fa42942853604a0"
dependencies = [
"anyhow",
"base64 0.13.1",
"bytes",
"derive_more 2.1.0",
"headers",
"rand 0.9.2",
"serde",
"serde_json",
"strum",
"uuid",
]
[[package]]
name = "cookie"
version = "0.18.1"
@ -581,6 +651,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
@ -604,9 +684,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.9.4",
"core-foundation",
"core-foundation 0.10.1",
"core-graphics-types",
"foreign-types",
"foreign-types 0.5.0",
"libc",
]
@ -617,7 +697,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.9.4",
"core-foundation",
"core-foundation 0.10.1",
"libc",
]
@ -761,6 +841,12 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "deranged"
version = "0.5.4"
@ -788,13 +874,36 @@ version = "0.99.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
dependencies = [
"convert_case",
"convert_case 0.4.0",
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.106",
]
[[package]]
name = "derive_more"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b"
dependencies = [
"convert_case 0.10.0",
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.106",
"unicode-xid",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -1081,6 +1190,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared 0.1.1",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
@ -1088,7 +1206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared",
"foreign-types-shared 0.3.1",
]
[[package]]
@ -1102,6 +1220,12 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
@ -1127,6 +1251,21 @@ dependencies = [
"new_debug_unreachable",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@ -1202,6 +1341,7 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
@ -1569,6 +1709,30 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "headers"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
dependencies = [
"base64 0.22.1",
"bytes",
"headers-core",
"http",
"httpdate",
"mime",
"sha1",
]
[[package]]
name = "headers-core"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
dependencies = [
"http",
]
[[package]]
name = "heck"
version = "0.4.1"
@ -1656,6 +1820,12 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.7.0"
@ -1865,6 +2035,29 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "imbl"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4308a675e4cfc1920f36a8f4d8fb62d5533b7da106844bd1ec51c6f1fa94a0c"
dependencies = [
"archery",
"bitmaps",
"imbl-sized-chunks",
"rand_core 0.9.3",
"rand_xoshiro",
"version_check",
]
[[package]]
name = "imbl-sized-chunks"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d"
dependencies = [
"bitmaps",
]
[[package]]
name = "indexmap"
version = "1.9.3"
@ -2257,6 +2450,23 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "native-tls"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "ndk"
version = "0.9.0"
@ -2675,6 +2885,60 @@ dependencies = [
"pathdiff",
]
[[package]]
name = "openssl"
version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [
"bitflags 2.9.4",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-src"
version = "300.5.4+3.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@ -3280,6 +3544,15 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rand_xoshiro"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41"
dependencies = [
"rand_core 0.9.3",
]
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@ -3531,6 +3804,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "schemars"
version = "0.8.22"
@ -3588,6 +3870,29 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.9.4",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "selectors"
version = "0.24.0"
@ -3596,7 +3901,7 @@ checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416"
dependencies = [
"bitflags 1.3.2",
"cssparser",
"derive_more",
"derive_more 0.99.20",
"fxhash",
"log",
"phf 0.8.0",
@ -3675,6 +3980,7 @@ version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"indexmap 2.11.4",
"itoa",
"memchr",
"ryu",
@ -3786,6 +4092,17 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
@ -3861,7 +4178,7 @@ dependencies = [
"bytemuck",
"cfg_aliases",
"core-graphics",
"foreign-types",
"foreign-types 0.5.0",
"js-sys",
"log",
"objc2 0.5.2",
@ -3943,6 +4260,27 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "subtle"
version = "2.6.1"
@ -4037,7 +4375,7 @@ checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7"
dependencies = [
"bitflags 2.9.4",
"block2 0.6.2",
"core-foundation",
"core-foundation 0.10.1",
"core-graphics",
"crossbeam-channel",
"dispatch",
@ -4601,7 +4939,9 @@ dependencies = [
"io-uring",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"slab",
"socket2",
"tokio-macros",
@ -4619,6 +4959,16 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
@ -4629,6 +4979,32 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
"tokio-util",
]
[[package]]
name = "tokio-tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [
"futures-util",
"log",
"native-tls",
"tokio",
"tokio-native-tls",
"tungstenite",
]
[[package]]
name = "tokio-util"
version = "0.7.16"
@ -4842,6 +5218,25 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"native-tls",
"rand 0.9.2",
"sha1",
"thiserror 2.0.17",
"url",
"utf-8",
]
[[package]]
name = "typeid"
version = "1.0.3"
@ -4918,6 +5313,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "untrusted"
version = "0.9.0"
@ -4972,6 +5373,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version-compare"
version = "0.2.0"

View file

@ -39,6 +39,7 @@ parking_lot = "0.12"
hostname = "0.4"
base64 = "0.22"
sha2 = "0.10"
convex = "0.10.2"
# SSE usa reqwest com stream, nao precisa de websocket
[target.'cfg(windows)'.dependencies]

View file

@ -4,12 +4,13 @@
//! e clientes (Raven desktop). Usa Server-Sent Events (SSE) como metodo
//! primario para atualizacoes em tempo real, com fallback para HTTP polling.
use convex::{ConvexClient, FunctionResult, Value};
use futures_util::StreamExt;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
@ -58,6 +59,7 @@ pub struct ChatAttachment {
pub mime_type: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatPollResponse {
@ -66,6 +68,7 @@ pub struct ChatPollResponse {
pub total_unread: u32,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatSessionSummary {
@ -112,6 +115,7 @@ static CHAT_CLIENT: Lazy<Client> = Lazy::new(|| {
// API FUNCTIONS
// ============================================================================
#[allow(dead_code)]
pub async fn poll_chat_updates(
base_url: &str,
token: &str,
@ -399,59 +403,16 @@ pub async fn upload_file(
Ok(data.storage_id)
}
// ============================================================================
// SSE TYPES
// ============================================================================
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SseUpdateEvent {
has_active_sessions: bool,
sessions: Vec<ChatSessionSummary>,
total_unread: u32,
ts: i64,
}
/// Parser de eventos SSE
struct SseEvent {
event: String,
data: String,
}
fn parse_sse_line(buffer: &mut String, line: &str) -> Option<SseEvent> {
if line.starts_with("event:") {
buffer.clear();
let event_type = line.trim_start_matches("event:").trim();
buffer.push_str(event_type);
buffer.push('\0'); // Separador interno
None
} else if line.starts_with("data:") {
let data = line.trim_start_matches("data:").trim();
let parts: Vec<&str> = buffer.split('\0').collect();
let event_type = if parts.len() >= 1 && !parts[0].is_empty() {
parts[0].to_string()
} else {
"message".to_string()
};
Some(SseEvent {
event: event_type,
data: data.to_string(),
})
} else {
None
}
}
// ============================================================================
// CHAT RUNTIME
// ============================================================================
struct ChatPollerHandle {
struct ChatRealtimeHandle {
stop_flag: Arc<AtomicBool>,
join_handle: JoinHandle<()>,
}
impl ChatPollerHandle {
impl ChatRealtimeHandle {
fn stop(self) {
self.stop_flag.store(true, Ordering::Relaxed);
self.join_handle.abort();
@ -460,10 +421,10 @@ impl ChatPollerHandle {
#[derive(Default, Clone)]
pub struct ChatRuntime {
inner: Arc<Mutex<Option<ChatPollerHandle>>>,
inner: Arc<Mutex<Option<ChatRealtimeHandle>>>,
last_sessions: Arc<Mutex<Vec<ChatSession>>>,
last_unread_count: Arc<Mutex<u32>>,
is_using_sse: Arc<AtomicBool>,
is_connected: Arc<AtomicBool>,
}
impl ChatRuntime {
@ -472,20 +433,20 @@ impl ChatRuntime {
inner: Arc::new(Mutex::new(None)),
last_sessions: Arc::new(Mutex::new(Vec::new())),
last_unread_count: Arc::new(Mutex::new(0)),
is_using_sse: Arc::new(AtomicBool::new(false)),
is_connected: Arc::new(AtomicBool::new(false)),
}
}
/// Retorna true se esta usando SSE, false se usando polling HTTP
/// Retorna true se conexao WS Convex esta ativa
pub fn is_using_sse(&self) -> bool {
self.is_using_sse.load(Ordering::Relaxed)
self.is_connected.load(Ordering::Relaxed)
}
/// Inicia o sistema de atualizacoes de chat.
/// Tenta SSE primeiro, com fallback automatico para HTTP polling.
/// Inicia o sistema de atualizacoes de chat via WebSocket do Convex
pub fn start_polling(
&self,
base_url: String,
convex_url: String,
token: String,
app: tauri::AppHandle,
) -> Result<(), String> {
@ -493,6 +454,10 @@ impl ChatRuntime {
if sanitized_base.is_empty() {
return Err("URL base invalida".to_string());
}
let sanitized_convex = convex_url.trim().trim_end_matches('/').to_string();
if sanitized_convex.is_empty() {
return Err("URL do Convex inválida".to_string());
}
// Para polling/SSE existente
{
@ -505,74 +470,90 @@ impl ChatRuntime {
let stop_flag = Arc::new(AtomicBool::new(false));
let stop_clone = stop_flag.clone();
let base_clone = sanitized_base.clone();
let convex_clone = sanitized_convex.clone();
let token_clone = token.clone();
let last_sessions = self.last_sessions.clone();
let last_unread_count = self.last_unread_count.clone();
let is_using_sse = self.is_using_sse.clone();
let is_connected = self.is_connected.clone();
let join_handle = tauri::async_runtime::spawn(async move {
crate::log_info!("Chat iniciando (tentando SSE primeiro)");
crate::log_info!("Chat iniciando via Convex WebSocket");
// Loop principal com SSE + fallback para polling
loop {
// Verificar se deve parar
let client_result = ConvexClient::new(&convex_clone).await;
let mut client = match client_result {
Ok(c) => c,
Err(err) => {
crate::log_warn!("Falha ao criar cliente Convex: {err:?}");
return;
}
};
let mut args = BTreeMap::new();
args.insert("machineToken".to_string(), token_clone.clone().into());
let subscribe_result = client.subscribe("liveChat:checkMachineUpdates", args).await;
let mut subscription = match subscribe_result {
Ok(sub) => {
is_connected.store(true, Ordering::Relaxed);
sub
}
Err(err) => {
crate::log_warn!("Falha ao assinar liveChat:checkMachineUpdates: {err:?}");
return;
}
};
while let Some(next) = subscription.next().await {
if stop_clone.load(Ordering::Relaxed) {
crate::log_info!("Chat encerrado");
break;
}
match next {
FunctionResult::Value(Value::Object(obj)) => {
let has_active = obj
.get("hasActiveSessions")
.and_then(|v| match v {
Value::Boolean(b) => Some(*b),
_ => None,
})
.unwrap_or(false);
let total_unread = obj
.get("totalUnread")
.and_then(|v| match v {
Value::Int64(i) => Some(*i as u32),
Value::Float64(f) => Some(*f as u32),
_ => None,
})
.unwrap_or(0);
// Tentar SSE primeiro
let sse_result = run_sse_loop(
process_chat_update(
&base_clone,
&token_clone,
&app,
&last_sessions,
&last_unread_count,
&is_using_sse,
&stop_clone,
has_active,
total_unread,
)
.await;
// Verificar se deve parar
if stop_clone.load(Ordering::Relaxed) {
crate::log_info!("Chat encerrado");
break;
}
FunctionResult::ConvexError(err) => {
crate::log_warn!("Convex error em checkMachineUpdates: {err:?}");
}
FunctionResult::ErrorMessage(msg) => {
crate::log_warn!("Erro em checkMachineUpdates: {msg}");
}
FunctionResult::Value(other) => {
crate::log_warn!("Payload inesperado em checkMachineUpdates: {other:?}");
}
}
}
match sse_result {
Ok(()) => {
// SSE encerrado normalmente (stop signal)
break;
}
Err(e) => {
crate::log_warn!("SSE falhou: {e}. Usando polling HTTP...");
is_using_sse.store(false, Ordering::Relaxed);
// Executar polling HTTP por 5 minutos, depois tentar SSE novamente
let poll_duration = Duration::from_secs(300); // 5 minutos
let poll_result = run_polling_loop(
&base_clone,
&token_clone,
&app,
&last_sessions,
&last_unread_count,
&stop_clone,
poll_duration,
)
.await;
if poll_result.is_err() || stop_clone.load(Ordering::Relaxed) {
break;
}
crate::log_info!("Tentando reconectar SSE...");
}
}
}
is_connected.store(false, Ordering::Relaxed);
crate::log_info!("Chat encerrado (Convex WebSocket finalizado)");
});
let mut guard = self.inner.lock();
*guard = Some(ChatPollerHandle {
*guard = Some(ChatRealtimeHandle {
stop_flag,
join_handle,
});
@ -585,7 +566,7 @@ impl ChatRuntime {
if let Some(handle) = guard.take() {
handle.stop();
}
self.is_using_sse.store(false, Ordering::Relaxed);
self.is_connected.store(false, Ordering::Relaxed);
}
pub fn get_sessions(&self) -> Vec<ChatSession> {
@ -593,232 +574,6 @@ impl ChatRuntime {
}
}
// ============================================================================
// SSE LOOP
// ============================================================================
/// Cliente HTTP para SSE com timeout mais longo (conexao persistente)
static SSE_CLIENT: Lazy<Client> = Lazy::new(|| {
Client::builder()
.user_agent("raven-chat-sse/1.0")
.timeout(Duration::from_secs(120)) // Timeout longo para SSE
.connect_timeout(Duration::from_secs(15))
.use_rustls_tls()
.build()
.expect("failed to build SSE http client")
});
async fn run_sse_loop(
base_url: &str,
token: &str,
app: &tauri::AppHandle,
last_sessions: &Arc<Mutex<Vec<ChatSession>>>,
last_unread_count: &Arc<Mutex<u32>>,
is_using_sse: &Arc<AtomicBool>,
stop_flag: &Arc<AtomicBool>,
) -> Result<(), String> {
let sse_url = format!("{}/api/machines/chat/stream?token={}", base_url, token);
crate::log_info!("Conectando SSE: {}", sse_url);
// Iniciar request SSE
let response = SSE_CLIENT
.get(&sse_url)
.header("Accept", "text/event-stream")
.header("Cache-Control", "no-cache")
.send()
.await
.map_err(|e| format!("Falha ao conectar SSE: {e}"))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(format!("SSE falhou: status={}, body={}", status, body));
}
is_using_sse.store(true, Ordering::Relaxed);
crate::log_info!("SSE conectado com sucesso");
// Stream de bytes
let mut stream = response.bytes_stream();
let mut buffer = String::new();
let mut line_buffer = String::new();
// Timeout para detectar conexao morta (60s sem dados = reconectar)
let mut last_data_time = std::time::Instant::now();
let max_silence = Duration::from_secs(60);
loop {
if stop_flag.load(Ordering::Relaxed) {
crate::log_info!("SSE encerrado por stop flag");
return Ok(());
}
// Verificar timeout de silencio
if last_data_time.elapsed() > max_silence {
crate::log_warn!("SSE: timeout de silencio ({}s sem dados)", max_silence.as_secs());
return Err("SSE timeout - sem dados".to_string());
}
// Aguardar proximo chunk com timeout
let chunk_result = tokio::time::timeout(
Duration::from_secs(35), // Heartbeat do servidor e a cada 30s
stream.next()
).await;
match chunk_result {
Ok(Some(Ok(bytes))) => {
last_data_time = std::time::Instant::now();
let text = String::from_utf8_lossy(&bytes);
line_buffer.push_str(&text);
// Processar linhas completas
while let Some(newline_pos) = line_buffer.find('\n') {
let line = line_buffer[..newline_pos].trim_end_matches('\r').to_string();
line_buffer = line_buffer[newline_pos + 1..].to_string();
// Linha vazia = fim do evento
if line.is_empty() {
buffer.clear();
continue;
}
// Parsear evento SSE
if let Some(event) = parse_sse_line(&mut buffer, &line) {
handle_sse_event(
&event,
base_url,
token,
app,
last_sessions,
last_unread_count,
).await?;
}
}
}
Ok(Some(Err(e))) => {
crate::log_warn!("SSE erro de stream: {e}");
return Err(format!("Erro SSE: {e}"));
}
Ok(None) => {
crate::log_info!("SSE: stream encerrado pelo servidor");
return Err("SSE encerrado".to_string());
}
Err(_) => {
// Timeout aguardando chunk - verificar se conexao ainda viva
if last_data_time.elapsed() > max_silence {
return Err("SSE timeout".to_string());
}
// Caso contrario, continuar aguardando
}
}
}
}
async fn handle_sse_event(
event: &SseEvent,
base_url: &str,
token: &str,
app: &tauri::AppHandle,
last_sessions: &Arc<Mutex<Vec<ChatSession>>>,
last_unread_count: &Arc<Mutex<u32>>,
) -> Result<(), String> {
match event.event.as_str() {
"connected" => {
crate::log_info!("SSE: conectado");
}
"heartbeat" => {
// noop - apenas mantem conexao viva
}
"update" => {
let update: SseUpdateEvent = serde_json::from_str(&event.data)
.map_err(|e| format!("Payload update inválido: {e}"))?;
process_chat_update(
base_url,
token,
app,
last_sessions,
last_unread_count,
update.has_active_sessions,
update.total_unread,
)
.await;
}
"error" => {
let error_data: Value = serde_json::from_str(&event.data).unwrap_or_default();
let message = error_data
.get("message")
.and_then(Value::as_str)
.unwrap_or("Erro SSE");
return Err(message.to_string());
}
_ => {
crate::log_info!("SSE: evento desconhecido {}", event.event);
}
}
Ok(())
}
// ============================================================================
// HTTP POLLING LOOP (FALLBACK)
// ============================================================================
async fn run_polling_loop(
base_url: &str,
token: &str,
app: &tauri::AppHandle,
last_sessions: &Arc<Mutex<Vec<ChatSession>>>,
last_unread_count: &Arc<Mutex<u32>>,
stop_flag: &Arc<AtomicBool>,
max_duration: Duration,
) -> Result<(), String> {
crate::log_info!("Iniciando polling HTTP (fallback)");
let start = std::time::Instant::now();
let poll_interval = Duration::from_secs(1); // 1s para ser mais responsivo
let mut last_checked_at: Option<i64> = None;
loop {
// Verificar se deve parar ou se atingiu duracao maxima
if stop_flag.load(Ordering::Relaxed) {
crate::log_info!("Polling HTTP encerrado por stop flag");
return Ok(());
}
if start.elapsed() >= max_duration {
crate::log_info!("Polling HTTP: duracao maxima atingida");
return Ok(());
}
tokio::time::sleep(poll_interval).await;
// Verificar novamente apos sleep
if stop_flag.load(Ordering::Relaxed) {
return Ok(());
}
match poll_chat_updates(base_url, token, last_checked_at).await {
Ok(result) => {
last_checked_at = Some(chrono::Utc::now().timestamp_millis());
process_chat_update(
base_url,
token,
app,
last_sessions,
last_unread_count,
result.has_active_sessions,
result.total_unread,
)
.await;
}
Err(e) => {
crate::log_warn!("Falha no polling de chat: {e}");
}
}
}
}
// ============================================================================
// SHARED UPDATE PROCESSING
// ============================================================================

View file

@ -23,6 +23,8 @@ use winreg::enums::*;
#[cfg(target_os = "windows")]
use winreg::RegKey;
const DEFAULT_CONVEX_URL: &str = "https://convex.esdrasrenan.com.br";
// ============================================================================
// Sistema de Logging para Agente
// ============================================================================
@ -233,9 +235,11 @@ fn start_chat_polling(
state: tauri::State<ChatRuntime>,
app: tauri::AppHandle,
base_url: String,
convex_url: Option<String>,
token: String,
) -> Result<(), String> {
state.start_polling(base_url, token, app)
let url = convex_url.unwrap_or_else(|| DEFAULT_CONVEX_URL.to_string());
state.start_polling(base_url, url, token, app)
}
#[tauri::command]
@ -667,6 +671,11 @@ async fn try_start_background_agent(
.and_then(|v| v.as_str())
.unwrap_or("https://tickets.esdrasrenan.com.br");
let convex_url = config
.and_then(|c| c.get("convexUrl"))
.and_then(|v| v.as_str())
.unwrap_or(DEFAULT_CONVEX_URL);
let interval = config
.and_then(|c| c.get("heartbeatIntervalSec"))
.and_then(|v| v.as_u64())
@ -688,14 +697,12 @@ async fn try_start_background_agent(
.map_err(|e| format!("Falha ao iniciar heartbeat: {e}"))?;
// Iniciar sistema de chat (WebSocket + fallback HTTP polling)
if let Err(e) = chat_runtime.start_polling(
api_base_url.to_string(),
token.to_string(),
app.clone(),
) {
if let Err(e) =
chat_runtime.start_polling(api_base_url.to_string(), convex_url.to_string(), token.to_string(), app.clone())
{
log_warn!("Falha ao iniciar chat em background: {e}");
} else {
log_info!("Chat iniciado com sucesso (WebSocket + fallback polling)");
log_info!("Chat iniciado com sucesso (Convex WebSocket)");
}
log_info!("Agente iniciado com sucesso em background");