Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
12 kB · 333 lines
Rust
at dev
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334//! The session loop: next square (fast loop, docs/04), completion write to the//! user's PDS, resonance channel (docs/02). All fragments; every write is//! optimistically mirrored into the AppView (Jetstream will idempotently//! re-deliver).
use axum::extract::{Form, State};use axum::http::StatusCode;use axum::response::{IntoResponse, Redirect, Response};use axum_extra::extract::cookie::CookieJar;use jacquard::client::AgentSessionExt;use jacquard::types::string::Datetime;use maud::{html, Markup};use serde::Deserialize;use smol_str::SmolStr;
use crate::atproto::records::{ChainRef, Completion, Mode, Resonance};use crate::bank::ServedSquare;use crate::infer::fast::{self, ChainState};use crate::state::SharedState;use crate::web::auth;use crate::web::pages::shell;
fn internal(e: anyhow::Error) -> Response { tracing::error!("session route error: {e:#}"); (StatusCode::INTERNAL_SERVER_ERROR, format!("something broke: {e}")).into_response()}
pub async fn session_page(State(state): State<SharedState>, jar: CookieJar) -> Response { if auth::viewer(&state, &jar).await.is_none() { return Redirect::to("/").into_response(); } shell( "session", true, html! { div #stage hx-get="/session/next" hx-trigger="load" hx-swap="innerHTML" { p { "drawing a square…" } } p class="frame-note" { "Refusing, joking, or answering “does not compute” are all recorded as answers — " "a square that fails to parse under your quilt is as diagnostic as one you complete fluently." } }, ) .into_response()}
async fn chain_state(state: &SharedState, cookie: &str) -> anyhow::Result<ChainState> { let row: Option<(Option<String>, Option<String>, i64, Option<String>)> = sqlx::query_as( "SELECT chain_parent_uri, chain_parent_cid, chain_depth, last_term FROM web_sessions WHERE cookie = ?", ) .bind(cookie) .fetch_optional(&state.pool) .await?; let (parent_uri, parent_cid, depth, last_term) = row.unwrap_or((None, None, 0, None)); Ok(ChainState { parent_uri, parent_cid, depth, last_term })}
fn render_square_form(sq: &ServedSquare, chain_depth: i64) -> Markup { let term = |v: &Option<String>, slot: &str| -> Markup { if sq.open_slot == slot { html! { span class="open" { "?" } } } else { html! { span { (v.as_deref().unwrap_or("?")) } } } }; html! { form hx-post="/session/complete" hx-target="#stage" hx-swap="innerHTML" { div class="square" { (term(&sq.a, "a")) " : " (term(&sq.b, "b")) " :: " (term(&sq.c, "c")) " : " (term(&sq.d, "d")) } input type="hidden" name="square_uri" value=(sq.uri); input type="hidden" name="square_cid" value=(sq.cid); input type="hidden" name="slot" value=(sq.open_slot); input type="hidden" name="shown_at" value=(crate::db::now()); input type="hidden" name="chain_depth" value=(chain_depth); div style="text-align:center" { input type="text" name="text" placeholder="complete the fourth term" autocomplete="off" autofocus; } div class="modes" { button type="submit" name="mode" value="completed" { "answer" } button type="submit" name="mode" value="refused" class="alt" { "refuse" } button type="submit" name="mode" value="noparse" class="alt" { "does not compute" } button type="submit" name="mode" value="joke" class="alt" { "(that was a joke)" } } } }}
pub async fn next_fragment(State(state): State<SharedState>, jar: CookieJar) -> Response { let Some(viewer) = auth::viewer(&state, &jar).await else { return (StatusCode::UNAUTHORIZED, "log in first").into_response(); }; let chain = match chain_state(&state, &viewer.cookie).await { Ok(c) => c, Err(e) => return internal(e), }; match fast::next_square(&state.pool, &state.publisher, &viewer.did, &chain).await { Ok(sq) => render_square_form(&sq, chain.depth).into_response(), Err(e) => internal(e), }}
#[derive(Deserialize)]pub struct CompleteForm { square_uri: String, square_cid: String, slot: String, #[serde(default)] text: String, mode: String, shown_at: i64, #[serde(default)] #[allow(dead_code)] // echoed by the form; chain state is authoritative server-side chain_depth: i64,}
fn latency_bucket(shown_at: i64) -> &'static str { match crate::db::now() - shown_at { i64::MIN..=1 => "lt2s", 2..=4 => "lt5s", 5..=14 => "lt15s", 15..=59 => "lt60s", _ => "gte60s", }}
pub async fn complete( State(state): State<SharedState>, jar: CookieJar, Form(form): Form<CompleteForm>,) -> Response { let Some(viewer) = auth::viewer(&state, &jar).await else { return (StatusCode::UNAUTHORIZED, "log in first").into_response(); }; match complete_inner(&state, &viewer, form).await { Ok(markup) => markup.into_response(), Err(e) => internal(e), }}
async fn complete_inner( state: &SharedState, viewer: &auth::Viewer, form: CompleteForm,) -> anyhow::Result<Markup> { let mode = Mode::parse(&form.mode).unwrap_or(Mode::Completed); let text = form.text.trim(); let mode = if mode == Mode::Completed && text.is_empty() { Mode::Refused // empty answer submitted as "answer" reads as refusal } else { mode };
let chain = chain_state(state, &viewer.cookie).await?; let square_ref = ServedSquare { uri: form.square_uri.clone(), cid: form.square_cid.clone(), a: None, b: None, c: None, d: None, open_slot: form.slot.clone(), } .strong_ref()?;
let chain_ref = match (&chain.parent_uri, &chain.parent_cid) { (Some(uri), Some(cid)) if chain.depth > 0 => Some(ChainRef { parent: jacquard::api::com_atproto::repo::strong_ref::StrongRef { uri: jacquard::types::string::AtUri::new(SmolStr::new(uri)) .map_err(|e| anyhow::anyhow!("{e}"))?, cid: jacquard::types::string::Cid::new_owned(cid.as_bytes()) .map_err(|e| anyhow::anyhow!("{e}"))?, extra_data: None, }, depth: chain.depth, }), _ => None, };
let record = Completion { square: square_ref, slot: SmolStr::new(&form.slot), text: (mode == Mode::Completed).then(|| SmolStr::new(text)), mode, chain: chain_ref, latency_bucket: Some(SmolStr::new(latency_bucket(form.shown_at))), created_at: Datetime::now(), };
// The thin slice through the riskiest dependency (docs/07 step 1): // write the record to the user's own PDS. let agent = viewer.agent(state).await?; let record_json = serde_json::to_value(&record)?; let out = agent .create_record(record, None) .await .map_err(|e| anyhow::anyhow!("PDS write failed: {e}"))?; let uri = out.uri.to_string(); let cid = out.cid.to_string();
// Optimistic mirror (read-after-write); Jetstream re-delivery is idempotent. crate::ingest::jetstream::index_record( state, &viewer.did, "app.ideonomics.completion", &uri, &cid, &record_json, ) .await?;
// Advance the chain: the user's own completion becomes a term (docs/02). if mode == Mode::Completed { sqlx::query( "UPDATE web_sessions SET chain_parent_uri = ?, chain_parent_cid = ?, chain_depth = chain_depth + 1, last_term = ? WHERE cookie = ?", ) .bind(&uri) .bind(&cid) .bind(text) .bind(&viewer.cookie) .execute(&state.pool) .await?; }
// Resonance channel: rival completions of the same square (docs/02). let rivals: Vec<(String, String)> = sqlx::query_as( "SELECT uri, text FROM completions WHERE square_uri = ? AND did != ? AND mode = 'completed' AND text IS NOT NULL ORDER BY indexed_at DESC LIMIT 3", ) .bind(&form.square_uri) .bind(&viewer.did) .fetch_all(&state.pool) .await?;
let new_chain = chain_state(state, &viewer.cookie).await?; let next = fast::next_square(&state.pool, &state.publisher, &viewer.did, &new_chain).await?;
Ok(html! { @if !rivals.is_empty() { div class="rivals" { p { "Others completed that square differently. Which of these lands? Which obscurely irritates?" } @for (rival_uri, rival_text) in &rivals { div class="rival" { span class="text" { "“" (rival_text) "”" } @for (reaction, label) in [("lands","lands"),("funny","funny"),("irritates","irritates"),("flat","flat")] { form hx-post="/session/resonate" hx-swap="outerHTML" style="display:inline" { input type="hidden" name="completion_uri" value=(rival_uri); input type="hidden" name="reaction" value=(reaction); button type="submit" { (label) } } } } } } } (render_square_form(&next, new_chain.depth)) p class="frame-note" { a href={"/sky/" (viewer.did)} { "view your sky" } } })}
#[derive(Deserialize)]pub struct ResonateForm { completion_uri: String, reaction: String,}
pub async fn resonate( State(state): State<SharedState>, jar: CookieJar, Form(form): Form<ResonateForm>,) -> Response { let Some(viewer) = auth::viewer(&state, &jar).await else { return (StatusCode::UNAUTHORIZED, "log in first").into_response(); }; match resonate_inner(&state, &viewer, form).await { Ok(m) => m.into_response(), Err(e) => internal(e), }}
async fn resonate_inner( state: &SharedState, viewer: &auth::Viewer, form: ResonateForm,) -> anyhow::Result<Markup> { let rival: Option<(Option<String>,)> = sqlx::query_as("SELECT cid FROM completions WHERE uri = ?") .bind(&form.completion_uri) .fetch_optional(&state.pool) .await?; let rival_cid = rival.and_then(|(c,)| c).unwrap_or_default();
let record = Resonance { completion: Some(jacquard::api::com_atproto::repo::strong_ref::StrongRef { uri: jacquard::types::string::AtUri::new(SmolStr::new(&form.completion_uri)) .map_err(|e| anyhow::anyhow!("{e}"))?, cid: jacquard::types::string::Cid::new_owned(rival_cid.as_bytes()) .map_err(|e| anyhow::anyhow!("{e}"))?, extra_data: None, }), square: None, archetype_text: None, reaction: SmolStr::new(&form.reaction), created_at: Datetime::now(), };
let agent = viewer.agent(state).await?; let record_json = serde_json::to_value(&record)?; let out = agent .create_record(record, None) .await .map_err(|e| anyhow::anyhow!("PDS write failed: {e}"))?; crate::ingest::jetstream::index_record( state, &viewer.did, "app.ideonomics.resonance", &out.uri.to_string(), &out.cid.to_string(), &record_json, ) .await?;
Ok(html! { span class="frame-note" { "☑ " (form.reaction) } })}