Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

crates/dwarves-web-client/src/lib.rs

//! **dwarves-web-client** — the grpc-web (browser) client for a running
//! `dwarves-server`'s `dwarf.v1` forge service.
//!
//! ## Reuse (no twin): same client, only the transport differs
//!
//! The generated [`pb`] module is produced from the SAME proto file the native embed
//! client (`dwarves-embed`) uses (`crates/dwarves-embed/proto/dwarf.proto`, wired in
//! `build.rs`), so the [`DwarfClient`](pb::dwarf_client::DwarfClient) and every message
//! type are byte-identical to the native client's. The ONLY difference is the transport:
//!
//! - **native** (`dwarves-embed`): a tonic [`Channel`] over HTTP/2.
//! - **wasm** (this crate): [`tonic_web_wasm_client::Client`] — grpc-web over the browser
//!   `fetch` API — because a browser cannot open a raw HTTP/2 gRPC socket.
//!
//! The `dwarves-server` answers BOTH on the SAME port when built with its `grpc-web`
//! feature (content-negotiated), which is exactly what `dwarves-server`'s `embed-all` turns on.
//!
//! ## Auth
//!
//! Every RPC carries `authorization: Bearer <token>` in its grpc metadata — the same
//! bearer the shared `nornir-service` interceptor enforces server-side. The one-line stamp
//! is inlined here (rather than reusing `nornir_service::attach_bearer`) because
//! `nornir-service` links tonic's native `transport` feature and does not compile to wasm;
//! this is documented, not a silent twin.
//!
//! ## Config (browser)
//!
//! The caller passes a base URL (e.g. `https://cicd.g11.se:7900`) and the bearer token —
//! `dwarves-gui` reads both from the page's `?server=…&token=…` query params, so nothing
//! is hardcoded and no secret is baked into the bundle.

/// The generated `dwarf.v1` client + wire types — identical to `dwarves_embed::pb` (same
/// proto file). `History`/`GetRunReport`/`AgentPool`/`ResolveHolgerBuild` + their messages.
pub mod pb {
    tonic::include_proto!("dwarf.v1");
}

#[cfg(target_arch = "wasm32")]
mod web {
    use super::pb::dwarf_client::DwarfClient;
    use super::pb::{HistoryReply, HistoryRequest, RunReport, RunStatusRequest};
    use tonic_web_wasm_client::Client;

    /// The grpc-web `DwarfClient` bound to a browser `fetch` transport.
    pub type WebDwarfClient = DwarfClient<Client>;

    /// Connect a grpc-web client to `base_url` (e.g. `https://cicd.g11.se:7900`). No socket
    /// is opened until the first RPC — the transport rides the browser `fetch` API.
    pub fn connect(base_url: &str) -> WebDwarfClient {
        let base = base_url.trim_end_matches('/').to_string();
        DwarfClient::new(Client::new(base))
    }

    /// Stamp `authorization: Bearer <token>` onto a request (skipped when `token` is empty,
    /// which matches the server's open/dev mode). The wasm twin of the native
    /// `dwarves::thin_client::attach_bearer` — see the module docs for why it is inlined.
    fn bearer<T>(mut req: tonic::Request<T>, token: &str) -> tonic::Request<T> {
        if !token.is_empty() {
            if let Ok(value) = format!("Bearer {token}").parse() {
                req.metadata_mut().insert("authorization", value);
            }
        }
        req
    }

    /// Fetch the recent-run roster over grpc-web (`Dwarf.History`, newest-first, capped at
    /// `limit`). This is what the wasm forge view calls to replace its fixture roster with
    /// LIVE runs from the server.
    pub async fn fetch_history(
        base_url: &str,
        token: &str,
        limit: u32,
    ) -> Result<HistoryReply, tonic::Status> {
        let mut client = connect(base_url);
        let req = bearer(
            tonic::Request::new(HistoryRequest {
                limit,
                pipeline: String::new(),
            }),
            token,
        );
        Ok(client.history(req).await?.into_inner())
    }

    /// Fetch one run's full report over grpc-web (`Dwarf.GetRunReport`) — the parent
    /// `RunRow` + a `BuildRow` per step with its `needs` DAG edges. Lets the live view
    /// reconstruct the tree + DAG of a selected run (parity with the native embed pane).
    pub async fn fetch_run_report(
        base_url: &str,
        token: &str,
        run_id: &str,
    ) -> Result<RunReport, tonic::Status> {
        let mut client = connect(base_url);
        let req = bearer(
            tonic::Request::new(RunStatusRequest {
                run_id: run_id.to_string(),
            }),
            token,
        );
        Ok(client.get_run_report(req).await?.into_inner())
    }
}

#[cfg(target_arch = "wasm32")]
pub use web::{connect, fetch_history, fetch_run_report, WebDwarfClient};

#[cfg(test)]
mod tests {
    use super::pb;

    /// The generated message set is reachable and matches the shared proto (RED-when-broken
    /// if the proto path in `build.rs` breaks or the codegen is dropped): construct the wire
    /// types the client sends/receives.
    #[test]
    fn generated_dwarf_v1_types_are_reachable() {
        let req = pb::HistoryRequest {
            limit: 50,
            pipeline: String::new(),
        };
        assert_eq!(req.limit, 50);
        let row = pb::RunRow {
            run_id: "#1".into(),
            pipeline: "build".into(),
            project: "dwarves".into(),
            trigger: "push".into(),
            status: "passed".into(),
            steps: 4,
            passed: 4,
            duration_ms: 1000,
            finished_at: String::new(),
        };
        assert_eq!(row.passed, 4);
        // The full report shape the live view reconstructs a tree/DAG from.
        let report = pb::RunReport {
            run: Some(row),
            steps: Vec::new(),
            release_json: String::new(),
        };
        assert!(report.run.is_some());
    }
}