swactor/crates/dashboard/src/lib.rs
Zachery Aaron Shores-Chmielewski b06583f598 refactor(datastream): cleanup
Trim the datastream crate to its dumb-pipe core: drop the frame-timing sidecar, make mux positions gap-free for accepted frames, and simplify the endpoint fanout.

- mux: defer position assignment from submit to drain so an overflowed submission no longer consumes a position (no synthetic gaps); submit now returns bool and the Mutex<Receiver> is removed since positions are assigned only to accepted frames
- endpoint/mux: switch from std::sync::mpsc to crossbeam-channel and drop per-event event_matches_request filtering — request filters now apply only to the initial catalog snapshot, and future events broadcast to all subscribers
- endpoint (DeliveryFanout): snapshot sender handles under the lock and deliver outside it via FanoutTarget/FanoutReport, so large batches or slow subscribers no longer block subscribe/snapshot control-plane ops
- emit/endpoint/producer: drop set_frame_timing_enabled/frame_timing_enabled and the Position return from submit_record/submit_text/submit_bytes, and add submit_text_owned taking owned String
- timing/lib/spec: delete the timing module and FRAME_TIME_CHANNEL/FRAME_TIME_CHANNEL_ID/FrameTimeSample re-exports (including the auto-registered timing channel in ChannelCatalogState) and renumber the DATASTREAM_SPEC.md section references across frame/ingest/store/mux
- tests: remove the 567-line shared datastream_support/mod.rs harness

Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 13:38:16 +04:00

155 lines
4.8 KiB
Rust

mod hardware_view;
mod live_explorer;
mod server;
mod store;
pub mod swactor;
pub mod view;
use std::sync::Arc;
use datastream::frame::{ChannelId, Frame, Lifetime, NodeId, Position, StreamId};
use serde::Serialize;
use tokio::sync::broadcast;
use crate::store::DashboardStore;
use crate::view::{DashboardView, ViewRegistry};
/// Configuration for the datastream dashboard server.
#[derive(Debug, Clone)]
pub struct DashboardConfig {
pub port: u16,
/// Number of raw frame events retained by the SSE channel for slow clients.
pub frame_buffer: usize,
/// Number of recent raw frames retained for `/api/frames`.
pub raw_frame_history: usize,
}
impl Default for DashboardConfig {
fn default() -> Self {
Self {
port: 9090,
frame_buffer: 1024,
raw_frame_history: 1024,
}
}
}
/// JSON shape emitted for each incoming datastream frame.
#[derive(Debug, Clone, Serialize)]
pub struct FrameEvent {
pub stream: StreamEvent,
pub channel: String,
pub position: u64,
pub payload: Vec<u8>,
}
#[derive(Debug, Clone, Serialize)]
pub struct StreamEvent {
pub node: String,
pub life: u64,
}
impl FrameEvent {
pub fn new(stream: &StreamId, frame: &Frame) -> Self {
Self {
stream: StreamEvent {
node: stream.node.as_str().to_string(),
life: stream.life.0,
},
channel: frame.channel.to_string(),
position: frame.position.0,
payload: frame.payload.clone(),
}
}
pub(crate) fn to_datastream_parts(&self) -> Option<(StreamId, Frame)> {
let stream = StreamId::new(NodeId::new(&self.stream.node), Lifetime(self.stream.life));
let channel = self
.channel
.parse::<u32>()
.map(ChannelId)
.unwrap_or(ChannelId(0));
let frame = Frame::new(channel, Position(self.position), self.payload.clone());
Some((stream, frame))
}
}
/// Handle to the read-only dashboard server.
///
/// The handle's data path publishes observed datastream frames to HTTP clients
/// and registered views. It does not send signals back to producers or mutate
/// runtime state.
pub struct DashboardHandle {
port: u16,
frames: broadcast::Sender<FrameEvent>,
store: Arc<DashboardStore>,
views: Arc<ViewRegistry>,
shutdown_notify: Arc<tokio::sync::Notify>,
}
impl DashboardHandle {
/// Create the datastream dashboard state.
///
/// The HTTP server is not started until `start_http` or `start_http_standalone`
/// is called.
pub fn new(config: DashboardConfig) -> Self {
let views = Arc::new(ViewRegistry::new());
views.register(Arc::new(live_explorer::LiveDatastreamExplorer::default()));
views.register(Arc::new(hardware_view::HardwareDashboardView::default()));
views.register(swactor::worker_view());
let store = Arc::new(DashboardStore::new(
config.raw_frame_history,
Arc::clone(&views),
));
let (frames, _) = broadcast::channel(config.frame_buffer.max(1));
Self {
port: config.port,
frames,
store,
views,
shutdown_notify: Arc::new(tokio::sync::Notify::new()),
}
}
/// Register a read-only view. External crates can keep their interpretation
/// code beside their component and plug it into this registry.
pub fn register_view(&self, view: Arc<dyn DashboardView>) {
self.views.register(view);
}
/// Publish one incoming datastream frame to raw clients and all matching views.
pub fn ingest(&self, stream: &StreamId, frame: &Frame) {
let event = self.store.ingest(stream, frame);
let _ = self.frames.send(event);
}
/// Publish an already-serialized frame event to raw clients and views.
pub fn publish(&self, event: FrameEvent) {
self.store.publish(event.clone());
let _ = self.frames.send(event);
}
/// Stop the HTTP server.
pub fn shutdown(&self) {
self.shutdown_notify.notify_waiters();
}
/// Build the HTTP server future for an embedding runtime to poll directly.
pub fn http_server(&self) -> impl Future<Output = ()> + Send + 'static {
let state = server::AppState {
frames: self.frames.clone(),
store: Arc::clone(&self.store),
views: Arc::clone(&self.views),
shutdown_notify: Arc::clone(&self.shutdown_notify),
};
let port = self.port;
async move {
server::run_server(state, port).await;
}
}
/// Spawn the HTTP server on an existing Tokio runtime and return its task handle.
pub fn spawn_http(&self, handle: &tokio::runtime::Handle) -> tokio::task::JoinHandle<()> {
handle.spawn(self.http_server())
}
}