feat: add reactive streaming and remove dead panel stream code

- Subscribe to StreamCascadeReactiveUpdates for real-time cascade state diffs
- Fall back to timer-based polling if streaming RPC unavailable
- Remove StreamCascadePanelReactiveUpdates code (dead end, only has plan_status/user_settings)
- Remove debug diff file-saving code
- Add stream_reactive_rpc() helper to backend
This commit is contained in:
Nikketryhard
2026-02-14 21:39:04 -06:00
parent 3d7a7f492b
commit b965be3f60
3 changed files with 169 additions and 5 deletions

View File

@@ -366,6 +366,111 @@ impl Backend {
let body = serde_json::json!({"cascadeId": cascade_id});
self.call_json("GetCascadeTrajectory", &body).await
}
/// Open a server-streaming reactive updates RPC.
/// `rpc_method` is the ConnectRPC method name, e.g. "StreamCascadeReactiveUpdates".
async fn stream_reactive_rpc(
&self,
rpc_method: &str,
cascade_id: &str,
) -> Result<tokio::sync::mpsc::Receiver<serde_json::Value>, String> {
let (base, csrf) = {
let guard = self.inner.read().await;
(
format!("https://127.0.0.1:{}", guard.https_port),
guard.csrf.clone(),
)
};
let url = format!("{base}/{LS_SERVICE}/{rpc_method}");
let body = serde_json::json!({
"protocolVersion": 1,
"id": cascade_id,
});
let mut headers = Self::common_headers(&csrf);
headers.insert("Content-Type", HeaderValue::from_static("application/connect+json"));
headers.insert("Connect-Protocol-Version", HeaderValue::from_static("1"));
// Connect protocol envelope: [flags:1][length:4][payload]
let json_bytes = serde_json::to_vec(&body).unwrap();
let mut envelope = Vec::with_capacity(5 + json_bytes.len());
envelope.push(0x00);
envelope.extend_from_slice(&(json_bytes.len() as u32).to_be_bytes());
envelope.extend_from_slice(&json_bytes);
let mut resp = self
.client
.post(&url)
.headers(headers)
.body(envelope)
.send()
.await
.map_err(|e| format!("{rpc_method} HTTP error: {e}"))?;
let status = resp.status().as_u16();
if status != 200 {
let err_body = resp.bytes().await.unwrap_or_default();
let err_text = String::from_utf8_lossy(&err_body);
return Err(format!("{rpc_method} failed: {status}{err_text}"));
}
let resp_ct = resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("unknown")
.to_string();
debug!("{rpc_method}: connected for cascade {cascade_id}, content-type: {resp_ct}");
let (tx, rx) = tokio::sync::mpsc::channel::<serde_json::Value>(64);
let method = rpc_method.to_string();
let cid = cascade_id.to_string();
tokio::spawn(async move {
let mut buf = Vec::new();
while let Ok(Some(chunk)) = resp.chunk().await {
if chunk.is_empty() {
continue;
}
buf.extend_from_slice(&chunk);
while buf.len() >= 5 {
let flags = buf[0];
let len = u32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]) as usize;
if buf.len() < 5 + len {
break;
}
let payload = &buf[5..5 + len];
if flags == 0x02 {
let text = String::from_utf8_lossy(payload);
debug!("{method}: end frame: {text}");
} else if let Ok(json) = serde_json::from_slice::<serde_json::Value>(payload) {
if tx.send(json).await.is_err() {
buf.drain(..5 + len);
break;
}
}
buf.drain(..5 + len);
}
}
debug!("{method}: stream ended for {cid}");
});
Ok(rx)
}
/// StreamCascadeReactiveUpdates — real-time cascade state diffs.
pub async fn stream_cascade_updates(
&self,
cascade_id: &str,
) -> Result<tokio::sync::mpsc::Receiver<serde_json::Value>, String> {
self.stream_reactive_rpc("StreamCascadeReactiveUpdates", cascade_id).await
}
}
// ─── Discovery helpers ───────────────────────────────────────────────────────