~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
148 lines · 5.2 KB Raw History
//! What the pages follow as it happens, as server-sent events: a
//! repository's records as the index takes them in, so its pages draw
//! again, and a running check's output as its runner sends it
//! ([`crate::live`]). Each is followed only by whoever may read it, as its
//! page is, and for ten minutes at most: the browser then asks again, and is
//! proved again.

use super::{CheckParams, Result, indexed, named_check, not_found, repo};
use crate::App;
use crate::live::Run;
use axum::extract::{Query, State};
use axum::http::HeaderMap;
use axum::response::sse::{Event, KeepAlive, Sse};
use axum::response::{IntoResponse, Response};
use futures::{Stream, StreamExt, stream};
use serde::Deserialize;
use serde_json::Value;
use std::convert::Infallible;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::broadcast::Receiver;

/// How long one following lasts.
const FOLLOW: Duration = Duration::from_secs(600);

/// What `changed` and `ended` carry, which says nothing: a browser drops an
/// event with no data, unheard, and axum sends an empty one with none.
const NOTHING: &str = "null";

/// How long after a write others are waited for, so that a burst of them,
/// as a push or a run makes, draws a page again once.
const SETTLE: Duration = Duration::from_millis(250);

#[derive(Deserialize)]
pub struct RepoParams {
    repo: String,
}

/// `/_live/repo`: `changed`, each time the index takes in a record about
/// the repository `repo`: a push, a pull, a comment, a check.
pub async fn follow_repo(
    State(app): State<Arc<App>>,
    Query(p): Query<RepoParams>,
) -> Result<Response> {
    let repo = repo::found(&app, &p.repo).await?;
    let none = not_found(anyhow::anyhow!("no index of {} here", repo.name));
    let index = indexed(&app, &repo).ok_or(none)?;
    let topic: Arc<str> = format!("{}/{}", repo.did, repo.key).into();
    let changes = index.changes.subscribe();
    let changed = stream::unfold(changes, move |mut changes| {
        let topic = topic.clone();
        async move {
            // Behind, it may have missed one of its own.
            while !changes.recv().await.ok().is_none_or(|t| t == topic) {}
            tokio::time::sleep(SETTLE).await;
            while changes.try_recv().is_ok() {}
            Some((Event::default().event("changed").data(NOTHING), changes))
        }
    });
    Ok(events(&app, changed))
}

/// `/_live/check`: a check's output as its runner sends it, from where
/// `Last-Event-ID` says the browser had it, as `output`, each a JSON string,
/// its id where it ends; its steps as they end, as `steps`; the step going,
/// as `going`, its name or null; and `ended`.
pub async fn follow_check(
    State(app): State<Arc<App>>,
    headers: HeaderMap,
    Query(p): Query<CheckParams>,
) -> Result<Response> {
    named_check(&app, &p).await?;
    let full = not_found(anyhow::anyhow!("too many runs followed"));
    let run = app.live.run(&p.uri).ok_or(full)?;
    let had = headers.get("last-event-id").and_then(|v| v.to_str().ok());
    let following = Following {
        news: run.news.subscribe(),
        run,
        had: had.and_then(|v| v.parse().ok()),
        steps: Value::Null,
        going: None,
        ended: false,
    };
    let said = stream::unfold(following, |mut f| async move {
        while !f.ended {
            let said = f.said();
            if !said.is_empty() {
                return Some((stream::iter(said), f));
            }
            let _ = f.news.recv().await;
        }
        None
    });
    Ok(events(&app, said.flatten()))
}

/// One page's following of a run.
struct Following {
    run: Arc<Run>,
    news: Receiver<()>,
    had: Option<u64>,
    steps: Value,
    going: Option<String>,
    ended: bool,
}

impl Following {
    /// What the run has that this following was not yet sent.
    fn said(&mut self) -> Vec<Event> {
        let kept = self.run.kept();
        let mut said = vec![];
        let (at, text) = kept.since(self.had);
        if !text.is_empty() {
            let end = at + text.len() as u64;
            let text = String::from_utf8_lossy(text);
            let output = Event::default().event("output").id(end.to_string());
            said.push(output.json_data(text).expect("a string"));
            self.had = Some(end);
        }
        if kept.steps != self.steps {
            self.steps = kept.steps.clone();
            let steps = Event::default().event("steps");
            said.push(steps.json_data(&self.steps).expect("JSON"));
        }
        if kept.going != self.going {
            self.going = kept.going.clone();
            let going = Event::default().event("going");
            said.push(going.json_data(&self.going).expect("JSON"));
        }
        if kept.ended {
            self.ended = true;
            said.push(Event::default().event("ended").data(NOTHING));
        }
        said
    }
}

/// `events` as server-sent events, for [`FOLLOW`] or until the server
/// stops.
fn events(app: &App, events: impl Stream<Item = Event> + Send + 'static) -> Response {
    let events = events.map(Ok::<_, Infallible>);
    let stopping = app.live.stopping.clone().cancelled_owned();
    let over = tokio::time::timeout(FOLLOW, stopping);
    let events = events.take_until(over);
    Sse::new(events)
        .keep_alive(KeepAlive::default())
        .into_response()
}