chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
Pages are sent on as the app sends them, not once it is done
crates/knotless-serve/src/web/pages.rs+15−2
| @@ -132,7 +132,8 @@ async fn page(web: &str, req: Request) -> Response { | ||
} | ||
} | ||
| ||
/// `req`, as the app at `web` answers it. The app sees the host it was | ||
/// `req`, as the app at `web` answers it, its body sent on as it comes. | ||
/// The app sees the host it was | ||
/// asked for, as `x-forwarded-host`. A read that finds the connection gone, | ||
/// as when the app is being replaced, is asked once more; a write may have | ||
/// been done, so is not. | ||
| @@ -169,7 +170,19 @@ async fn handed(web: &str, req: Request) -> anyhow::Result<Response> { | ||
.or_insert(HeaderValue::from_static(value)); | ||
} | ||
*res.headers_mut().unwrap() = headers; | ||
Ok(res.body(Body::from(answer.bytes().await?))?) | ||
Ok(res.body(Body::from_stream(streamed(answer)))?) | ||
} | ||
| ||
/// `answer`'s body as it comes, each part sent on as it is read: what a | ||
/// page streams, as a directory's last commits, reaches the browser as the | ||
/// app sends it, not once it has sent the last of it. | ||
fn streamed( | ||
answer: reqwest::Response, | ||
) -> impl futures::Stream<Item = reqwest::Result<axum::body::Bytes>> { | ||
futures::stream::try_unfold(answer, |mut answer| async move { | ||
let part = answer.chunk().await?; | ||
Ok(part.map(|part| (part, answer))) | ||
}) | ||
} | ||
| ||
/// `headers`, but for those about one hop. | ||
crates/knotless-serve/tests/web.rs+53−0
| @@ -2748,6 +2748,59 @@ fn pages_drawn_elsewhere() { | ||
}; | ||
assert_eq!(squeezed("/alice.test/tally"), "br"); | ||
assert_eq!(squeezed("/packed"), "zstd"); | ||
// A page is sent on as the app sends it, compressed or not: its first | ||
// part reaches the browser while the app still holds the rest. | ||
let holding = || { | ||
let (app, at) = listen(); | ||
let gate = Arc::new(tokio::sync::Notify::new()); | ||
let held = gate.clone(); | ||
let page = axum::Router::new().fallback(move || { | ||
let held = held.clone(); | ||
async move { | ||
let parts = futures::stream::unfold(0, move |n| { | ||
let held = held.clone(); | ||
async move { | ||
if n == 1 { | ||
held.notified().await; | ||
} | ||
let part = ["first ", "rest"].get(n)?.to_string(); | ||
Some((Ok::<_, std::io::Error>(part), n + 1)) | ||
} | ||
}); | ||
let mut res = axum::body::Body::from_stream(parts).into_response(); | ||
let html = "text/html".parse().unwrap(); | ||
res.headers_mut().insert("content-type", html); | ||
res | ||
} | ||
}); | ||
rt.spawn(async move { axum::serve(app, page).await.unwrap() }); | ||
(gate, at) | ||
}; | ||
let patient = Client::builder() | ||
.timeout(Duration::from_secs(30)) | ||
.build() | ||
.unwrap(); | ||
for encoding in ["identity", "br"] { | ||
let (gate, at) = holding(); | ||
let mut res = patient | ||
.get(format!("{}/page", front(at))) | ||
.header("accept-encoding", encoding) | ||
.send() | ||
.unwrap(); | ||
let mut first = [0; 64]; | ||
let n = std::io::Read::read(&mut res, &mut first).unwrap(); | ||
assert!(n > 0, "{encoding}: nothing before the rest"); | ||
gate.notify_one(); | ||
let mut all = first[..n].to_vec(); | ||
std::io::Read::read_to_end(&mut res, &mut all).unwrap(); | ||
let sent = res.headers().get("content-encoding").cloned(); | ||
if encoding == "identity" { | ||
assert!(b"first ".starts_with(&first[..n]), "{encoding}"); | ||
assert_eq!((all.as_slice(), sent), (&b"first rest"[..], None)); | ||
} else { | ||
assert_eq!(sent.unwrap(), "br"); | ||
} | ||
} | ||
// What knotless-serve answers itself is not handed on. | ||
let ours = client | ||
.get(format!("{base}/xrpc/dev.knotless.getFeed")) | ||
docs/design/serve.md+4−1
| @@ -719,7 +719,10 @@ The pages are a SvelteKit app in `web/`, run separately. The server hands it | ||
every request it does not answer itself, and the app asks the server for each | ||
page's data, then draws the HTML. The server compresses what the app answers | ||
as it does its own, brotli to a browser that takes it, but for what the app | ||
sends compressed already, as its scripts. It asks `dev.knotless.*` XRPC queries, as | ||
sends compressed already, as its scripts. It sends each page on as the app | ||
sends it, not once the app is done: what a page streams, as a directory's | ||
last commits, reaches the browser as soon as the app has it. The app asks | ||
`dev.knotless.*` XRPC queries, as | ||
atproto's appviews answer theirs: each with a lexicon under `lexicons/`, | ||
sharing the views of `dev.knotless.defs` (a repository, a commit, a check, a | ||
diff, a pull or an issue). Pictures a page shows, a check's or an account's own, | ||