nornir 0.4.1

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
Documentation
//! Thin-client timeline loader: fetch a [`Timeline`] from a running
//! `nornir-server` over the `Viz.Timeline` gRPC instead of opening a local
//! Iceberg warehouse. The server builds the timeline from the warehouse it
//! owns (it holds the redb lock) and returns it as JSON; we deserialize into
//! the same [`Timeline`] the embedded path produces, so the egui app is
//! source-agnostic.

use anyhow::{Context, Result};

use super::model::Timeline;

mod pb {
    tonic::include_proto!("nornir.v1");
}

/// Fetch the timeline for `workspace` from `endpoint` (e.g.
/// `http://127.0.0.1:7878`), authenticating with the bearer `token`. Runs the
/// async tonic call on a private current-thread runtime so it's safe to call
/// from the synchronous egui update loop.
pub fn fetch_timeline(endpoint: &str, token: &str, workspace: &str) -> Result<Timeline> {
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .context("build tokio runtime for viz client")?;
    rt.block_on(async {
        let endpoint = if endpoint.starts_with("http") {
            endpoint.to_string()
        } else {
            format!("http://{endpoint}")
        };
        let bearer: tonic::metadata::MetadataValue<tonic::metadata::Ascii> =
            format!("Bearer {token}").parse().context("parse bearer token")?;
        let channel = tonic::transport::Channel::from_shared(endpoint.clone())
            .with_context(|| format!("invalid server url `{endpoint}`"))?
            .connect()
            .await
            .with_context(|| format!("connect to nornir-server at {endpoint}"))?;
        let mut client = pb::viz_client::VizClient::with_interceptor(
            channel,
            move |mut req: tonic::Request<()>| {
                req.metadata_mut().insert("authorization", bearer.clone());
                Ok(req)
            },
        );
        let resp = client
            .timeline(pb::VizTimelineRequest { workspace: workspace.to_string() })
            .await
            .context("Viz.Timeline RPC")?
            .into_inner();
        let timeline: Timeline =
            serde_json::from_str(&resp.json).context("decode timeline json from server")?;
        Ok(timeline)
    })
}