refactor(qconnect): route liveness/lifecycle/diagnostic emits through event sink (slice 3)
Add five QconnectAppEvent variants (RendererUnreachable, RendererDisconnected, ResyncComplete, LifecycleChanged, Diagnostic) and a Tauri mapper so the session/liveness/diagnostic signals flow through the QconnectEventSink trait instead of raw app_handle.emit calls. Move QconnectLifecycleState into qconnect-app (pure data); Tauri types.rs re-exports it for the command surface. No logic moves: the session logic still lives Tauri-side but now calls sink.on_event(variant) at each rewired emit site. The sink mapper turns each variant back into the exact same qconnect:* channel + payload, so the Svelte wire surface is byte-identical. Each dedicated channel still fires exactly once per occurrence (raw emits are replaced, not duplicated). The blanket qconnect:event emit now also carries the new variants, which is additive and ignored by the Svelte listener (pattern-matches known keys only). Rewired emit sites: - event_sink.rs freeze helper now takes/emits the freeze variant (RendererUnreachable/RendererDisconnected); the mapper emits the dedicated channel. State mutation precedes on_event and the variant arms only emit, so there is no re-entrancy/recursion into the watchdog/session dispatch. - service.rs resync-complete -> ResyncComplete; inbound_queue_event/cloud_error/ max_reconnect -> Diagnostic; update_lifecycle_state_if_running -> LifecycleChanged. Tests: add qconnect-app unit tests asserting the new variants flow through a sink and that QconnectLifecycleState serializes snake_case (matching the wire payload). crates green (qconnect-app 33 passed); src-tauri cargo test qconnect 42 passed. cc
This commit is contained in:
6 files changed
+233
-71
No files matched your search
@@ -3,6 +3,8 @@ use qconnect_core::{QConnectQueueState, QConnectRendererState, RendererCommand};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::session::QconnectLifecycleState;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum QconnectAppEvent {
|
||||
TransportConnected,
|
||||
@@ -45,6 +47,21 @@ pub enum QconnectAppEvent {
|
||||
message_type: String,
|
||||
payload: Value,
|
||||
},
|
||||
/// Active peer renderer went silent >=12s while PLAYING (liveness watchdog).
|
||||
RendererUnreachable { renderer_id: i32 },
|
||||
/// Active peer renderer left gracefully (status == ACTIVE_DISCONNECTED).
|
||||
RendererDisconnected { renderer_id: i32 },
|
||||
/// Post-reconnect resync has been issued (safe-to-replay advisory).
|
||||
ResyncComplete,
|
||||
/// Granular connection lifecycle changed (Connecting/Reconnecting/Connected/Exhausted/Off).
|
||||
LifecycleChanged { state: QconnectLifecycleState },
|
||||
/// Pass-through diagnostic (inbound-event preview, cloud error, max-reconnect, ...).
|
||||
/// `channel` is the legacy Tauri channel suffix so the mapper is mechanical.
|
||||
Diagnostic {
|
||||
channel: String,
|
||||
level: String,
|
||||
payload: Value,
|
||||
},
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -59,3 +76,88 @@ pub struct NoOpEventSink;
|
||||
impl QconnectEventSink for NoOpEventSink {
|
||||
async fn on_event(&self, _event: QconnectAppEvent) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct TestSink {
|
||||
events: Arc<Mutex<Vec<QconnectAppEvent>>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl QconnectEventSink for TestSink {
|
||||
async fn on_event(&self, event: QconnectAppEvent) {
|
||||
self.events.lock().await.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Slice 3: the five new liveness/lifecycle/diagnostic variants flow through
|
||||
/// the `QconnectEventSink` trait. A sink receives each one verbatim.
|
||||
#[tokio::test]
|
||||
async fn new_variants_flow_through_sink() {
|
||||
let sink = TestSink::default();
|
||||
sink.on_event(QconnectAppEvent::RendererUnreachable { renderer_id: 7 })
|
||||
.await;
|
||||
sink.on_event(QconnectAppEvent::RendererDisconnected { renderer_id: 9 })
|
||||
.await;
|
||||
sink.on_event(QconnectAppEvent::ResyncComplete).await;
|
||||
sink.on_event(QconnectAppEvent::LifecycleChanged {
|
||||
state: QconnectLifecycleState::Reconnecting,
|
||||
})
|
||||
.await;
|
||||
sink.on_event(QconnectAppEvent::Diagnostic {
|
||||
channel: "qconnect:cloud_error".to_string(),
|
||||
level: "warning".to_string(),
|
||||
payload: serde_json::json!({ "code": 13 }),
|
||||
})
|
||||
.await;
|
||||
|
||||
let events = sink.events.lock().await.clone();
|
||||
assert_eq!(events.len(), 5);
|
||||
assert!(matches!(
|
||||
events[0],
|
||||
QconnectAppEvent::RendererUnreachable { renderer_id: 7 }
|
||||
));
|
||||
assert!(matches!(
|
||||
events[1],
|
||||
QconnectAppEvent::RendererDisconnected { renderer_id: 9 }
|
||||
));
|
||||
assert!(matches!(events[2], QconnectAppEvent::ResyncComplete));
|
||||
assert!(matches!(
|
||||
events[3],
|
||||
QconnectAppEvent::LifecycleChanged {
|
||||
state: QconnectLifecycleState::Reconnecting
|
||||
}
|
||||
));
|
||||
assert!(matches!(
|
||||
&events[4],
|
||||
QconnectAppEvent::Diagnostic { channel, level, .. }
|
||||
if channel == "qconnect:cloud_error" && level == "warning"
|
||||
));
|
||||
}
|
||||
|
||||
/// The lifecycle state must serialize snake_case so the Tauri mapper's
|
||||
/// `{"state": <serialized>}` payload is byte-identical to the prior raw
|
||||
/// `qconnect:status_changed` emit.
|
||||
#[test]
|
||||
fn lifecycle_state_serializes_snake_case() {
|
||||
assert_eq!(
|
||||
serde_json::to_value(QconnectLifecycleState::Reconnecting).unwrap(),
|
||||
serde_json::json!("reconnecting")
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(QconnectLifecycleState::Connected).unwrap(),
|
||||
serde_json::json!("connected")
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(QconnectLifecycleState::Exhausted).unwrap(),
|
||||
serde_json::json!("exhausted")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,9 @@ pub use error::QconnectAppError;
|
||||
pub use events::{NoOpEventSink, QconnectAppEvent, QconnectEventSink};
|
||||
pub use session::{
|
||||
compute_connection_state, deferred_join_reason, quality_from_max_audio_quality,
|
||||
should_arm_renderer_watchdog, should_reask_queue_state, ConnectionDecision, RendererStatus,
|
||||
ServerActiveState, JOIN_SESSION_REASON_CONTROLLER_REQUEST, JOIN_SESSION_REASON_RECONNECTION,
|
||||
should_arm_renderer_watchdog, should_reask_queue_state, ConnectionDecision,
|
||||
QconnectLifecycleState, RendererStatus, ServerActiveState,
|
||||
JOIN_SESSION_REASON_CONTROLLER_REQUEST, JOIN_SESSION_REASON_RECONNECTION,
|
||||
QCONNECT_RENDERER_LOST_TIMEOUT_MS,
|
||||
};
|
||||
pub use feature_flags::{
|
||||
|
||||
@@ -6,6 +6,35 @@
|
||||
//! byte-for-byte identical to the prior Tauri-side definitions.
|
||||
|
||||
use qbz_models::Quality;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Lifecycle state surfaced to the UI so the toggle can reflect what the user
|
||||
/// asked for (`running`) separately from what the transport currently has
|
||||
/// (`Connecting`/`Connected`/`Reconnecting`). Without this distinction the UI
|
||||
/// reads the toggle as "off" while the backend reconnect loop is alive,
|
||||
/// leaving the user unable to disable QConnect (issue #358).
|
||||
///
|
||||
/// Pure data — no Tauri dependency. The Tauri `types.rs` re-exports this for the
|
||||
/// command surface; the session layer emits it via `QconnectAppEvent::LifecycleChanged`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QconnectLifecycleState {
|
||||
/// User has not enabled QConnect (or it has been fully torn down).
|
||||
#[default]
|
||||
Off,
|
||||
/// `connect()` has been called; transport is establishing the WS but no
|
||||
/// `SessionEstablished` yet.
|
||||
Connecting,
|
||||
/// Transport saw at least one `SESSION_STATE` frame on the active WS — the
|
||||
/// session-level handshake completed.
|
||||
Connected,
|
||||
/// Transport disconnected after at least one successful connect; the
|
||||
/// reconnect loop is running.
|
||||
Reconnecting,
|
||||
/// `MaxReconnectAttemptsExceeded` fired — runtime auto-stopped, last error
|
||||
/// surfaced. User can re-enable from UI.
|
||||
Exhausted,
|
||||
}
|
||||
|
||||
/// Playing-state wire value for PLAYING. Mirrors the Tauri adapter's
|
||||
/// `PLAYING_STATE_PLAYING` (the renderer reports playing_state == 2 while
|
||||
|
||||
@@ -22,6 +22,7 @@ use super::corebridge::{
|
||||
apply_renderer_command_to_corebridge, materialize_remote_queue_to_corebridge,
|
||||
};
|
||||
use super::queue_resolution::is_valid_ordered_queue_shuffle_order;
|
||||
use super::service::emit_qconnect_diagnostic;
|
||||
use super::session::{
|
||||
build_session_renderer_snapshot, cache_renderer_snapshot, ensure_session_renderer_state,
|
||||
is_peer_renderer_active, normalize_active_renderer_id, refresh_local_renderer_id,
|
||||
@@ -165,6 +166,37 @@ impl QconnectEventSink for TauriQconnectEventSink {
|
||||
self.report_active_renderer_ready().await;
|
||||
}
|
||||
}
|
||||
QconnectAppEvent::RendererUnreachable { renderer_id } => {
|
||||
self.emit_renderer_freeze_channel("qconnect:renderer_unreachable", *renderer_id);
|
||||
}
|
||||
QconnectAppEvent::RendererDisconnected { renderer_id } => {
|
||||
self.emit_renderer_freeze_channel("qconnect:renderer_disconnected", *renderer_id);
|
||||
}
|
||||
QconnectAppEvent::ResyncComplete => {
|
||||
emit_qconnect_diagnostic(
|
||||
&self.app_handle,
|
||||
"qconnect:resync_complete",
|
||||
"info",
|
||||
serde_json::json!({}),
|
||||
);
|
||||
}
|
||||
QconnectAppEvent::LifecycleChanged { state } => {
|
||||
let serialized =
|
||||
serde_json::to_value(state).unwrap_or_else(|_| serde_json::json!("unknown"));
|
||||
if let Err(err) = self.app_handle.emit(
|
||||
"qconnect:status_changed",
|
||||
serde_json::json!({ "state": serialized }),
|
||||
) {
|
||||
log::warn!("[QConnect] Failed to emit qconnect:status_changed: {err}");
|
||||
}
|
||||
}
|
||||
QconnectAppEvent::Diagnostic {
|
||||
channel,
|
||||
level,
|
||||
payload,
|
||||
} => {
|
||||
emit_qconnect_diagnostic(&self.app_handle, channel, level, payload.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -483,7 +515,7 @@ impl TauriQconnectEventSink {
|
||||
if let Some(renderer_id) = disconnected_renderer_id {
|
||||
self.freeze_active_renderer_projection_to_unknown(
|
||||
renderer_id,
|
||||
"qconnect:renderer_disconnected",
|
||||
QconnectAppEvent::RendererDisconnected { renderer_id },
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -517,7 +549,7 @@ impl TauriQconnectEventSink {
|
||||
);
|
||||
sink.freeze_active_renderer_projection_to_unknown(
|
||||
renderer_id,
|
||||
"qconnect:renderer_unreachable",
|
||||
QconnectAppEvent::RendererUnreachable { renderer_id },
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -534,15 +566,20 @@ impl TauriQconnectEventSink {
|
||||
}
|
||||
|
||||
/// Force the active renderer's cached projection to UNKNOWN/stopped and
|
||||
/// emit a freeze event. Shared by ACTIVE_DISCONNECTED (P0-2) and the
|
||||
/// silence watchdog (P0-1). Caller passes the event name to emit.
|
||||
/// signal a freeze event through the event sink. Shared by
|
||||
/// ACTIVE_DISCONNECTED (P0-2) and the silence watchdog (P0-1). Caller passes
|
||||
/// the `QconnectAppEvent` freeze variant (`RendererUnreachable` /
|
||||
/// `RendererDisconnected`) to emit.
|
||||
///
|
||||
/// This is a pure-state mutation + emit; it does NOT route back through
|
||||
/// `apply_session_management_event`, so it cannot re-arm the watchdog.
|
||||
/// This is a pure-state mutation followed by `on_event(variant)`. The sink's
|
||||
/// mapper arm for these variants ONLY emits the dedicated Tauri channel (+
|
||||
/// the blanket `qconnect:event`); it does NOT route back through
|
||||
/// `apply_session_management_event` or call this helper again, so it cannot
|
||||
/// re-arm the watchdog or recurse.
|
||||
async fn freeze_active_renderer_projection_to_unknown(
|
||||
&self,
|
||||
renderer_id: i32,
|
||||
event_name: &str,
|
||||
freeze_event: QconnectAppEvent,
|
||||
) {
|
||||
{
|
||||
let mut state = self.sync_state.lock().await;
|
||||
@@ -550,11 +587,19 @@ impl TauriQconnectEventSink {
|
||||
renderer_state.playing_state = Some(PLAYING_STATE_UNKNOWN);
|
||||
renderer_state.updated_at_ms = qconnect_now_ms();
|
||||
}
|
||||
self.on_event(freeze_event).await;
|
||||
}
|
||||
|
||||
/// Emit one renderer-freeze dedicated channel (`qconnect:renderer_unreachable`
|
||||
/// / `qconnect:renderer_disconnected`) with the legacy `{renderer_id}` payload.
|
||||
/// Called from the `on_event` mapper arms; kept as a small helper so both
|
||||
/// freeze channels share the exact same payload shape.
|
||||
fn emit_renderer_freeze_channel(&self, channel: &str, renderer_id: i32) {
|
||||
if let Err(err) = self
|
||||
.app_handle
|
||||
.emit(event_name, serde_json::json!({ "renderer_id": renderer_id }))
|
||||
.emit(channel, serde_json::json!({ "renderer_id": renderer_id }))
|
||||
{
|
||||
log::warn!("[QConnect] Failed to emit {event_name}: {err}");
|
||||
log::warn!("[QConnect] Failed to emit {channel}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use qconnect_app::{
|
||||
QConnectQueueState, QConnectRendererState, QconnectApp, QueueCommandType, RendererReport,
|
||||
RendererReportType,
|
||||
QConnectQueueState, QConnectRendererState, QconnectApp, QconnectAppEvent, QconnectEventSink,
|
||||
QueueCommandType, RendererReport, RendererReportType,
|
||||
};
|
||||
use qconnect_transport_ws::{NativeWsTransport, WsTransportConfig};
|
||||
use serde_json::{json, Value};
|
||||
@@ -214,7 +214,7 @@ fn queue_payload_track_preview(payload: &Value, key: &str) -> Vec<i64> {
|
||||
}
|
||||
async fn update_lifecycle_state_if_running(
|
||||
inner: &Arc<Mutex<QconnectServiceInner>>,
|
||||
app_handle: &AppHandle,
|
||||
sink: &TauriQconnectEventSink,
|
||||
next: QconnectLifecycleState,
|
||||
) {
|
||||
let mut guard = inner.lock().await;
|
||||
@@ -226,16 +226,19 @@ async fn update_lifecycle_state_if_running(
|
||||
}
|
||||
guard.lifecycle_state = next;
|
||||
drop(guard);
|
||||
let serialized = serde_json::to_value(next).unwrap_or_else(|_| json!("unknown"));
|
||||
let _ = app_handle.emit(
|
||||
"qconnect:status_changed",
|
||||
json!({
|
||||
"state": serialized,
|
||||
}),
|
||||
);
|
||||
// Slice 3: route through the event sink. The sink's `LifecycleChanged` arm
|
||||
// emits `qconnect:status_changed` with `{state}` — byte-identical to the
|
||||
// prior raw emit here.
|
||||
sink.on_event(QconnectAppEvent::LifecycleChanged { state: next })
|
||||
.await;
|
||||
}
|
||||
|
||||
fn emit_qconnect_diagnostic(app_handle: &AppHandle, channel: &str, level: &str, payload: Value) {
|
||||
pub(super) fn emit_qconnect_diagnostic(
|
||||
app_handle: &AppHandle,
|
||||
channel: &str,
|
||||
level: &str,
|
||||
payload: Value,
|
||||
) {
|
||||
let ts = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis() as u64)
|
||||
@@ -323,6 +326,11 @@ impl QconnectServiceState {
|
||||
let inner_for_loop = Arc::clone(&self.inner);
|
||||
let sync_state_for_loop = Arc::clone(&sync_state);
|
||||
let app_handle_for_status = app_handle.clone();
|
||||
// The session/liveness/diagnostic emits route THROUGH the event sink
|
||||
// (Slice 3): the sink maps each new QconnectAppEvent variant back to the
|
||||
// exact same `qconnect:*` channel + payload as the prior raw emits, so
|
||||
// the Svelte wire surface is byte-identical.
|
||||
let sink_for_loop = Arc::clone(&sink);
|
||||
// gap #7: when the transport is configured to idle-retry after
|
||||
// Exhausted, the transport loop does NOT terminate — it idles then
|
||||
// re-arms and emits fresh ReconnectScheduled / SessionEstablished
|
||||
@@ -392,7 +400,7 @@ impl QconnectServiceState {
|
||||
// SessionEstablished or MaxReconnectAttemptsExceeded.
|
||||
update_lifecycle_state_if_running(
|
||||
&inner_for_loop,
|
||||
&app_handle_for_status,
|
||||
&sink_for_loop,
|
||||
QconnectLifecycleState::Reconnecting,
|
||||
)
|
||||
.await;
|
||||
@@ -422,12 +430,10 @@ impl QconnectServiceState {
|
||||
// P1-11: signal the frontend that the post-reconnect
|
||||
// resync has been issued, so it can evaluate whether
|
||||
// to replay or advise on the last transport intent.
|
||||
emit_qconnect_diagnostic(
|
||||
&app_handle_for_status,
|
||||
"qconnect:resync_complete",
|
||||
"info",
|
||||
json!({}),
|
||||
);
|
||||
// Slice 3: routed through the sink; the `ResyncComplete`
|
||||
// arm emits the identical `qconnect:diagnostic`
|
||||
// (channel=qconnect:resync_complete, level=info, {}).
|
||||
sink_for_loop.on_event(QconnectAppEvent::ResyncComplete).await;
|
||||
}
|
||||
}
|
||||
qconnect_transport_ws::TransportEvent::KeepalivePingSent => {
|
||||
@@ -449,11 +455,10 @@ impl QconnectServiceState {
|
||||
evt.message_type(),
|
||||
evt.payload
|
||||
);
|
||||
emit_qconnect_diagnostic(
|
||||
&app_for_errors,
|
||||
"qconnect:inbound_queue_event",
|
||||
"info",
|
||||
json!({
|
||||
sink_for_loop.on_event(QconnectAppEvent::Diagnostic {
|
||||
channel: "qconnect:inbound_queue_event".to_string(),
|
||||
level: "info".to_string(),
|
||||
payload: json!({
|
||||
"message_type": evt.message_type(),
|
||||
"action_uuid": evt.action_uuid.clone(),
|
||||
"queue_version": evt.queue_version,
|
||||
@@ -462,7 +467,7 @@ impl QconnectServiceState {
|
||||
"preview_track_ids": queue_payload_track_preview(&evt.payload, "tracks"),
|
||||
"preview_autoplay_track_ids": queue_payload_track_preview(&evt.payload, "autoplay_tracks"),
|
||||
}),
|
||||
);
|
||||
}).await;
|
||||
}
|
||||
qconnect_transport_ws::TransportEvent::InboundRendererServerCommand(
|
||||
cmd,
|
||||
@@ -520,16 +525,15 @@ impl QconnectServiceState {
|
||||
code,
|
||||
descr
|
||||
);
|
||||
emit_qconnect_diagnostic(
|
||||
&app_for_errors,
|
||||
"qconnect:cloud_error",
|
||||
"warning",
|
||||
json!({
|
||||
sink_for_loop.on_event(QconnectAppEvent::Diagnostic {
|
||||
channel: "qconnect:cloud_error".to_string(),
|
||||
level: "warning".to_string(),
|
||||
payload: json!({
|
||||
"msg_id": msg_id,
|
||||
"code": code,
|
||||
"descr": descr,
|
||||
}),
|
||||
);
|
||||
}).await;
|
||||
}
|
||||
qconnect_transport_ws::TransportEvent::InboundReceived(_envelope) => {
|
||||
log::info!(
|
||||
@@ -542,7 +546,7 @@ impl QconnectServiceState {
|
||||
);
|
||||
update_lifecycle_state_if_running(
|
||||
&inner_for_loop,
|
||||
&app_handle_for_status,
|
||||
&sink_for_loop,
|
||||
QconnectLifecycleState::Connected,
|
||||
)
|
||||
.await;
|
||||
@@ -556,15 +560,14 @@ impl QconnectServiceState {
|
||||
attempts,
|
||||
last_reason
|
||||
);
|
||||
emit_qconnect_diagnostic(
|
||||
&app_for_errors,
|
||||
"qconnect:max_reconnect_attempts_exceeded",
|
||||
"error",
|
||||
json!({
|
||||
sink_for_loop.on_event(QconnectAppEvent::Diagnostic {
|
||||
channel: "qconnect:max_reconnect_attempts_exceeded".to_string(),
|
||||
level: "error".to_string(),
|
||||
payload: json!({
|
||||
"attempts": attempts,
|
||||
"last_reason": last_reason,
|
||||
}),
|
||||
);
|
||||
}).await;
|
||||
// Surface the Exhausted lifecycle to the UI in
|
||||
// both modes. The difference is what we do with
|
||||
// the runtime + event loop afterwards:
|
||||
|
||||
@@ -19,29 +19,11 @@ pub struct QconnectConnectOptions {
|
||||
}
|
||||
|
||||
/// Lifecycle state surfaced to the UI so the toggle can reflect what the user
|
||||
/// asked for (`running`) separately from what the transport currently has
|
||||
/// (`Connecting`/`Connected`/`Reconnecting`). Without this distinction the UI
|
||||
/// reads the toggle as "off" while the backend reconnect loop is alive,
|
||||
/// leaving the user unable to disable QConnect (issue #358).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QconnectLifecycleState {
|
||||
/// User has not enabled QConnect (or it has been fully torn down).
|
||||
#[default]
|
||||
Off,
|
||||
/// `connect()` has been called; transport is establishing the WS but no
|
||||
/// `SessionEstablished` yet.
|
||||
Connecting,
|
||||
/// Transport saw at least one `SESSION_STATE` frame on the active WS — the
|
||||
/// session-level handshake completed.
|
||||
Connected,
|
||||
/// Transport disconnected after at least one successful connect; the
|
||||
/// reconnect loop is running.
|
||||
Reconnecting,
|
||||
/// `MaxReconnectAttemptsExceeded` fired — runtime auto-stopped, last error
|
||||
/// surfaced. User can re-enable from UI.
|
||||
Exhausted,
|
||||
}
|
||||
/// asked for (`running`) separately from what the transport currently has.
|
||||
/// The enum now lives in `qconnect-app` (pure data, no Tauri dep) so the
|
||||
/// session layer can emit it via `QconnectAppEvent::LifecycleChanged`; it is
|
||||
/// re-exported here for the Tauri command surface (issue #358).
|
||||
pub use qconnect_app::QconnectLifecycleState;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct QconnectConnectionStatus {
|
||||
|
||||
Reference in new issue
Block a user