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

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

Pages are sent on as the app sends them, not once it is done

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:serve/stream-pages
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,