Merge pull request #1 from Markk116/claude/fix-sse-pipe-events-y7Nxd

Fix SSE pipe: subscriptions never registered, malformed events, fanout skipping
This commit is contained in:
Markk116
2026-03-08 21:08:37 +01:00
committed by GitHub
2 changed files with 17 additions and 25 deletions
+9 -14
View File
@@ -2,6 +2,7 @@ use crate::query::CacheKey;
use axum::response::sse::Event; use axum::response::sse::Event;
use dashmap::DashMap; use dashmap::DashMap;
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::Arc;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use uuid::Uuid; use uuid::Uuid;
@@ -106,26 +107,20 @@ impl ConnectionManager {
/// This is the hot path of the reactive pipeline: the fanout task calls /// This is the hot path of the reactive pipeline: the fanout task calls
/// `get_connections_for_key` on every broadcast invalidation event to find /// `get_connections_for_key` on every broadcast invalidation event to find
/// which connections need rerendering. /// which connections need rerendering.
///
/// Clone is cheap (Arc pointer copy) and all clones share the same underlying
/// maps, so updates from any clone are visible to the fanout task.
#[derive(Clone)]
pub struct SubscriptionRegistry { pub struct SubscriptionRegistry {
// DashMap is Arc-backed internally, so clone is cheap. key_to_conns: Arc<DashMap<CacheKey, HashSet<Uuid>>>,
key_to_conns: DashMap<CacheKey, HashSet<Uuid>>, conn_to_keys: Arc<DashMap<Uuid, HashSet<CacheKey>>>,
conn_to_keys: DashMap<Uuid, HashSet<CacheKey>>,
}
impl Clone for SubscriptionRegistry {
fn clone(&self) -> Self {
Self {
key_to_conns: self.key_to_conns.clone(),
conn_to_keys: self.conn_to_keys.clone(),
}
}
} }
impl SubscriptionRegistry { impl SubscriptionRegistry {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
key_to_conns: DashMap::new(), key_to_conns: Arc::new(DashMap::new()),
conn_to_keys: DashMap::new(), conn_to_keys: Arc::new(DashMap::new()),
} }
} }
+8 -11
View File
@@ -109,9 +109,8 @@ pub fn spawn_fanout(state: SharedState) {
let conn_ids = state.subscriptions.get_connections_for_key(&key); let conn_ids = state.subscriptions.get_connections_for_key(&key);
for conn_id in conn_ids { for conn_id in conn_ids {
let Some(signals_json) = state.connections.get_signals(&conn_id) else { let signals_json = state.connections.get_signals(&conn_id)
continue; .unwrap_or_else(|| "{}".to_string());
};
let Some(sse_tx) = state.connections.get_sender(&conn_id) else { let Some(sse_tx) = state.connections.get_sender(&conn_id) else {
continue; continue;
}; };
@@ -145,17 +144,15 @@ pub fn spawn_fanout(state: SharedState) {
Ok((html, keys)) => { Ok((html, keys)) => {
state_ref.subscriptions.update(conn_id, keys); state_ref.subscriptions.update(conn_id, keys);
// Build a single datastar-patch-elements SSE event. // Build a datastar-patch-elements SSE event.
// DataStar expects the event name set once, then one // Each HTML line gets an `elements ` prefix; axum's
// `data: elements <line>` entry per line of HTML. // Event::data() splits on \n and writes a `data:`
// axum's Event::data() sets a single data field, so // line for each, which is correct SSE multiline format.
// we pre-format the multi-line data payload manually
// and send it as one data value.
let data_payload: String = html let data_payload: String = html
.lines() .lines()
.map(|line| format!("elements {}", line)) .map(|line| format!("elements {}", line))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\ndata: "); .join("\n");
let event = Event::default() let event = Event::default()
.event("datastar-patch-elements") .event("datastar-patch-elements")
@@ -241,7 +238,7 @@ pub async fn sse_handler(
.lines() .lines()
.map(|line| format!("elements {}", line)) .map(|line| format!("elements {}", line))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\ndata: "); .join("\n");
let event = Event::default() let event = Event::default()
.event("datastar-patch-elements") .event("datastar-patch-elements")
.data(data_payload); .data(data_payload);