diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 73cf5ab..7f3e68e 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -6036,28 +6036,29 @@ function connectDecode() { if (window.resetCwHistoryView) window.resetCwHistoryView(); if (window.resetFt8HistoryView) window.resetFt8HistoryView(); if (window.resetWsprHistoryView) window.resetWsprHistoryView(); - const historyBuffer = []; - let historyDone = false; + // Live messages arrive after the history event; gate on this flag so + // onmessage does not dispatch before drainDecodeHistory has started. + let historyReceived = false; decodeSource = new EventSource("/decode"); decodeSource.onopen = () => { decodeConnected = true; updateDecodeStatus("Connected, listening for packets"); }; - decodeSource.addEventListener("history_done", () => { - historyDone = true; - drainDecodeHistory(historyBuffer, 0); + // The server sends the entire history as one named "history" event (JSON + // array). A single JSON.parse + chunked drain is far cheaper than N + // individual EventSource callbacks each blocking the main thread. + decodeSource.addEventListener("history", (evt) => { + try { + const msgs = JSON.parse(evt.data); + if (Array.isArray(msgs)) drainDecodeHistory(msgs, 0); + } catch (e) { /* ignore parse errors */ } + historyReceived = true; }); decodeSource.onmessage = (evt) => { + if (!historyReceived) return; // skip anything before history event try { - const msg = JSON.parse(evt.data); - if (!historyDone) { - historyBuffer.push(msg); - } else { - dispatchDecodeMessage(msg); - } - } catch (e) { - // ignore parse errors - } + dispatchDecodeMessage(JSON.parse(evt.data)); + } catch (e) { /* ignore parse errors */ } }; decodeSource.onerror = () => { // readyState CLOSED (2) = server rejected (404/error), CONNECTING (0) = temporary drop diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs index 212bf4e..ffdd63c 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs @@ -326,14 +326,15 @@ pub async fn decode_events( out }; - let history_stream = futures_util::stream::iter(history.into_iter().filter_map(|msg| { - serde_json::to_string(&msg) - .ok() - .map(|json| Ok::(Bytes::from(format!("data: {json}\n\n")))) - })); - - let history_done = - once(async { Ok::(Bytes::from("event: history_done\ndata: {}\n\n")) }); + // Send the entire history as a single named "history" event (JSON array). + // Sending N individual events causes N EventSource callbacks in the browser, + // each blocking the main thread — for large histories this interrupts audio + // and spectrum rendering for tens of seconds. + let history_event = { + let json = serde_json::to_string(&history).unwrap_or_else(|_| "[]".to_string()); + Bytes::from(format!("event: history\ndata: {json}\n\n")) + }; + let history_stream = once(async move { Ok::(history_event) }); let decode_stream = futures_util::stream::unfold(decode_rx, |mut rx| async move { loop { @@ -355,7 +356,7 @@ pub async fn decode_events( let pings = IntervalStream::new(time::interval(Duration::from_secs(15))) .map(|_| Ok::(Bytes::from(": ping\n\n"))); - let stream = history_stream.chain(history_done).chain(select(pings, decode_stream)); + let stream = history_stream.chain(select(pings, decode_stream)); Ok(HttpResponse::Ok() .insert_header((header::CONTENT_TYPE, "text/event-stream"))