atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
56d34031c32175c328592f822e27127d1e792ab2
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/http/mod.rs

782069860236atagen2026-10-10 12:44 UTC1
mod ctx;
8b8e83329a1datagen2026-10-12 12:01 UTC2
mod inventory;
8b8e83329a1datagen2026-10-12 12:01 UTC3
mod leaderboard;
8b8e83329a1datagen2026-10-12 12:01 UTC4
mod links;
782069860236atagen2026-10-10 12:44 UTC5
mod meta;
782069860236atagen2026-10-10 12:44 UTC6
mod middleware;
8b8e83329a1datagen2026-10-12 12:01 UTC7
mod parse;
159f0998263batagen2026-10-12 11:01 UTC8
mod registry;
782069860236atagen2026-10-10 12:44 UTC9
mod resource;
782069860236atagen2026-10-10 12:44 UTC10
mod response;
782069860236atagen2026-10-10 12:44 UTC11
mod run;
782069860236atagen2026-10-10 12:44 UTC12
mod runs;
8b8e83329a1datagen2026-10-12 12:01 UTC13
mod shaft;
8b8e83329a1datagen2026-10-12 12:01 UTC14
mod shame;
782069860236atagen2026-10-10 12:44 UTC15
mod ui;
782069860236atagen2026-10-10 12:44 UTC16
8b8e83329a1datagen2026-10-12 12:01 UTC17
use std::{
8b8e83329a1datagen2026-10-12 12:01 UTC18
    sync::Arc,
8b8e83329a1datagen2026-10-12 12:01 UTC19
    time::{Duration, Instant},
8b8e83329a1datagen2026-10-12 12:01 UTC20
};
782069860236atagen2026-10-10 12:44 UTC21
782069860236atagen2026-10-10 12:44 UTC22
use axum::{
782069860236atagen2026-10-10 12:44 UTC23
    Router,
782069860236atagen2026-10-10 12:44 UTC24
    extract::State,
159f0998263batagen2026-10-12 11:01 UTC25
    response::{Redirect, Response},
782069860236atagen2026-10-10 12:44 UTC26
    routing::{MethodRouter, any},
782069860236atagen2026-10-10 12:44 UTC27
};
782069860236atagen2026-10-10 12:44 UTC28
7288fc269bb6atagen2026-10-10 13:36 UTC29
use tower_http::catch_panic::CatchPanicLayer;
7288fc269bb6atagen2026-10-10 13:36 UTC30
8b8e83329a1datagen2026-10-12 12:01 UTC31
use crate::{
8b8e83329a1datagen2026-10-12 12:01 UTC32
    config::Config,
8b8e83329a1datagen2026-10-12 12:01 UTC33
    records::{Record, Records},
8b8e83329a1datagen2026-10-12 12:01 UTC34
};
782069860236atagen2026-10-10 12:44 UTC35
use ctx::Ctx;
8b8e83329a1datagen2026-10-12 12:01 UTC36
use registry::{Lock, Registry};
782069860236atagen2026-10-10 12:44 UTC37
use resource::Resource;
782069860236atagen2026-10-10 12:44 UTC38
use run::RunResource;
782069860236atagen2026-10-10 12:44 UTC39
782069860236atagen2026-10-10 12:44 UTC40
pub struct AppState {
159f0998263batagen2026-10-12 11:01 UTC41
    runs: Registry,
159f0998263batagen2026-10-12 11:01 UTC42
    records: Records,
782069860236atagen2026-10-10 12:44 UTC43
    config: Config,
782069860236atagen2026-10-10 12:44 UTC44
}
782069860236atagen2026-10-10 12:44 UTC45
782069860236atagen2026-10-10 12:44 UTC46
impl AppState {
8b8e83329a1datagen2026-10-12 12:01 UTC47
    pub fn new(config: Config) -> rusqlite::Result<Self> {
159f0998263batagen2026-10-12 11:01 UTC48
        let records = match &config.db {
8b8e83329a1datagen2026-10-12 12:01 UTC49
            Some(path) => Records::open(path)?,
159f0998263batagen2026-10-12 11:01 UTC50
            None => Records::in_memory(),
159f0998263batagen2026-10-12 11:01 UTC51
        };
8b8e83329a1datagen2026-10-12 12:01 UTC52
        Ok(Self {
159f0998263batagen2026-10-12 11:01 UTC53
            runs: Registry::default(),
159f0998263batagen2026-10-12 11:01 UTC54
            records,
782069860236atagen2026-10-10 12:44 UTC55
            config,
8b8e83329a1datagen2026-10-12 12:01 UTC56
        })
8b8e83329a1datagen2026-10-12 12:01 UTC57
    }
8b8e83329a1datagen2026-10-12 12:01 UTC58
8b8e83329a1datagen2026-10-12 12:01 UTC59
    /// Records and forgets the runs that have gone idle. The server calls it on a timer.
8b8e83329a1datagen2026-10-12 12:01 UTC60
    pub fn sweep(&self, now: Instant) -> rusqlite::Result<()> {
8b8e83329a1datagen2026-10-12 12:01 UTC61
        for slot in self.runs.evict_idle(now, self.config.idle_ttl) {
8b8e83329a1datagen2026-10-12 12:01 UTC62
            let record = match Lock::take(&slot) {
8b8e83329a1datagen2026-10-12 12:01 UTC63
                Lock::Held(run) | Lock::Poisoned(run) => Record::of(&run),
8b8e83329a1datagen2026-10-12 12:01 UTC64
            };
8b8e83329a1datagen2026-10-12 12:01 UTC65
            self.records.save(&record)?;
782069860236atagen2026-10-10 12:44 UTC66
        }
8b8e83329a1datagen2026-10-12 12:01 UTC67
        Ok(())
782069860236atagen2026-10-10 12:44 UTC68
    }
782069860236atagen2026-10-10 12:44 UTC69
782069860236atagen2026-10-10 12:44 UTC70
    #[cfg(test)]
159f0998263batagen2026-10-12 11:01 UTC71
    pub(crate) fn insert(&self, run: crate::game::Run) {
8b8e83329a1datagen2026-10-12 12:01 UTC72
        self.runs.insert_new(run);
782069860236atagen2026-10-10 12:44 UTC73
    }
782069860236atagen2026-10-10 12:44 UTC74
}
782069860236atagen2026-10-10 12:44 UTC75
782069860236atagen2026-10-10 12:44 UTC76
impl Default for AppState {
782069860236atagen2026-10-10 12:44 UTC77
    fn default() -> Self {
8b8e83329a1datagen2026-10-12 12:01 UTC78
        Self::new(Config::default()).expect("an in-memory database always opens")
782069860236atagen2026-10-10 12:44 UTC79
    }
782069860236atagen2026-10-10 12:44 UTC80
}
782069860236atagen2026-10-10 12:44 UTC81
782069860236atagen2026-10-10 12:44 UTC82
type Shared = Arc<AppState>;
782069860236atagen2026-10-10 12:44 UTC83
8b8e83329a1datagen2026-10-12 12:01 UTC84
pub fn app(config: Config) -> rusqlite::Result<Router> {
8b8e83329a1datagen2026-10-12 12:01 UTC85
    Ok(router(Arc::new(AppState::new(config)?)))
8b8e83329a1datagen2026-10-12 12:01 UTC86
}
8b8e83329a1datagen2026-10-12 12:01 UTC87
8b8e83329a1datagen2026-10-12 12:01 UTC88
/// Sweeps idle runs into the records every minute, so a quiet server still keeps its graveyard.
8b8e83329a1datagen2026-10-12 12:01 UTC89
pub async fn sweep_forever(state: Arc<AppState>) {
8b8e83329a1datagen2026-10-12 12:01 UTC90
    let mut ticks = tokio::time::interval(Duration::from_secs(60));
8b8e83329a1datagen2026-10-12 12:01 UTC91
    loop {
8b8e83329a1datagen2026-10-12 12:01 UTC92
        ticks.tick().await;
8b8e83329a1datagen2026-10-12 12:01 UTC93
        let swept = Arc::clone(&state);
8b8e83329a1datagen2026-10-12 12:01 UTC94
        let result = tokio::task::spawn_blocking(move || swept.sweep(Instant::now())).await;
8b8e83329a1datagen2026-10-12 12:01 UTC95
        if let Ok(Err(error)) = result {
8b8e83329a1datagen2026-10-12 12:01 UTC96
            eprintln!("jelqd could not sweep idle runs into the records: {error}");
8b8e83329a1datagen2026-10-12 12:01 UTC97
        }
8b8e83329a1datagen2026-10-12 12:01 UTC98
    }
782069860236atagen2026-10-10 12:44 UTC99
}
782069860236atagen2026-10-10 12:44 UTC100
782069860236atagen2026-10-10 12:44 UTC101
pub fn router(state: Shared) -> Router {
782069860236atagen2026-10-10 12:44 UTC102
    let api = api(state);
782069860236atagen2026-10-10 12:44 UTC103
    let ui = Router::new()
782069860236atagen2026-10-10 12:44 UTC104
        .route("/ui", any(|| async { Redirect::permanent("/ui/") }))
782069860236atagen2026-10-10 12:44 UTC105
        .route("/ui/", any(ui::page))
782069860236atagen2026-10-10 12:44 UTC106
        .route("/ui/send", any(ui::send))
782069860236atagen2026-10-10 12:44 UTC107
        .with_state(api.clone());
782069860236atagen2026-10-10 12:44 UTC108
    api.merge(ui)
7288fc269bb6atagen2026-10-10 13:36 UTC109
        .layer(CatchPanicLayer::custom(middleware::panicked))
782069860236atagen2026-10-10 12:44 UTC110
}
782069860236atagen2026-10-10 12:44 UTC111
782069860236atagen2026-10-10 12:44 UTC112
fn global(resource: &'static Resource<Shared>) -> MethodRouter<Shared> {
159f0998263batagen2026-10-12 11:01 UTC113
    any(move |State(state): State<Shared>, ctx: Ctx| async move {
159f0998263batagen2026-10-12 11:01 UTC114
        offload(state, move |mut state| resource.dispatch(&mut state, &ctx)).await
159f0998263batagen2026-10-12 11:01 UTC115
    })
782069860236atagen2026-10-10 12:44 UTC116
}
782069860236atagen2026-10-10 12:44 UTC117
782069860236atagen2026-10-10 12:44 UTC118
fn scoped(resource: &'static RunResource) -> MethodRouter<Shared> {
159f0998263batagen2026-10-12 11:01 UTC119
    any(move |State(state): State<Shared>, ctx: Ctx| async move {
159f0998263batagen2026-10-12 11:01 UTC120
        offload(state, move |state| run::serve(&state, ctx, resource)).await
159f0998263batagen2026-10-12 11:01 UTC121
    })
159f0998263batagen2026-10-12 11:01 UTC122
}
159f0998263batagen2026-10-12 11:01 UTC123
8b8e83329a1datagen2026-10-12 12:01 UTC124
/// Game turns block, on SQLite always and on HTTP for every roll under the model oracle, so they
8b8e83329a1datagen2026-10-12 12:01 UTC125
/// run on the blocking pool and leave the async workers free.
159f0998263batagen2026-10-12 11:01 UTC126
async fn offload<F>(state: Shared, work: F) -> Response
159f0998263batagen2026-10-12 11:01 UTC127
where
159f0998263batagen2026-10-12 11:01 UTC128
    F: FnOnce(Shared) -> Response + Send + 'static,
159f0998263batagen2026-10-12 11:01 UTC129
{
159f0998263batagen2026-10-12 11:01 UTC130
    tokio::task::spawn_blocking(move || work(state))
159f0998263batagen2026-10-12 11:01 UTC131
        .await
159f0998263batagen2026-10-12 11:01 UTC132
        .unwrap_or_else(|e| std::panic::resume_unwind(e.into_panic()))
782069860236atagen2026-10-10 12:44 UTC133
}
782069860236atagen2026-10-10 12:44 UTC134
782069860236atagen2026-10-10 12:44 UTC135
fn api(state: Shared) -> Router {
782069860236atagen2026-10-10 12:44 UTC136
    Router::new()
782069860236atagen2026-10-10 12:44 UTC137
        .route("/", global(&meta::INDEX))
782069860236atagen2026-10-10 12:44 UTC138
        .route("/runs", global(&meta::RUNS))
782069860236atagen2026-10-10 12:44 UTC139
        .route("/runs/{id}", scoped(&runs::RUN))
782069860236atagen2026-10-10 12:44 UTC140
        .route("/runs/{id}/length", scoped(&runs::LENGTH))
782069860236atagen2026-10-10 12:44 UTC141
        .route("/runs/{id}/foreskin", scoped(&runs::FORESKIN))
782069860236atagen2026-10-10 12:44 UTC142
        .route("/runs/{id}/inventory", scoped(&runs::INVENTORY))
782069860236atagen2026-10-10 12:44 UTC143
        .route("/runs/{id}/inventory/{item}", scoped(&runs::ITEM))
782069860236atagen2026-10-10 12:44 UTC144
        .route("/runs/{id}/techniques", scoped(&runs::TECHNIQUES))
782069860236atagen2026-10-10 12:44 UTC145
        .route("/runs/{id}/drawer", scoped(&runs::DRAWER))
782069860236atagen2026-10-10 12:44 UTC146
        .route("/runs/{id}/branches/{choice}", scoped(&runs::BRANCH))
0a0bfee8329eatagen2026-10-11 05:11 UTC147
        .route("/runs/{id}/kitchen", scoped(&runs::KITCHEN))
0a0bfee8329eatagen2026-10-11 05:11 UTC148
        .route("/runs/{id}/kitchen/watermelon", scoped(&runs::MELON))
a11497af08aaatagen2026-10-13 12:04 UTC149
        .route("/runs/{id}/thread", scoped(&runs::THREAD))
782069860236atagen2026-10-10 12:44 UTC150
        .route("/runs/{id}/floor", scoped(&runs::FLOOR))
782069860236atagen2026-10-10 12:44 UTC151
        .route("/runs/{id}/shop", scoped(&runs::SHOP))
782069860236atagen2026-10-10 12:44 UTC152
        .route("/runs/{id}/shop/{item}", scoped(&runs::BUY))
782069860236atagen2026-10-10 12:44 UTC153
        .route("/runs/{id}/portal", scoped(&runs::PORTAL))
782069860236atagen2026-10-10 12:44 UTC154
        .route("/runs/{id}/elder", scoped(&runs::ELDER))
782069860236atagen2026-10-10 12:44 UTC155
        .route("/runs/{id}/grip", scoped(&runs::GRIP))
782069860236atagen2026-10-10 12:44 UTC156
        .route("/runs/{id}/diagnosis", scoped(&runs::DIAGNOSIS))
782069860236atagen2026-10-10 12:44 UTC157
        .route("/runs/{id}/footage", scoped(&runs::FOOTAGE))
782069860236atagen2026-10-10 12:44 UTC158
        .route("/forum/threads/{year}", global(&meta::THREAD))
782069860236atagen2026-10-10 12:44 UTC159
        .route("/graveyard", global(&meta::GRAVEYARD))
782069860236atagen2026-10-10 12:44 UTC160
        .route("/leaderboard", global(&meta::LEADERBOARD))
782069860236atagen2026-10-10 12:44 UTC161
        .fallback(meta::not_found)
782069860236atagen2026-10-10 12:44 UTC162
        .layer(axum::middleware::from_fn(middleware::chrome))
7288fc269bb6atagen2026-10-10 13:36 UTC163
        .layer(CatchPanicLayer::custom(middleware::panicked))
782069860236atagen2026-10-10 12:44 UTC164
        .with_state(state)
782069860236atagen2026-10-10 12:44 UTC165
}