atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
beabb5985f4a6e2cdc6fa8ab67e7e6c7c31333e8
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/mod.rsRust170 lines5.3 KB
1
mod ctx;
2
mod inventory;
3
mod leaderboard;
4
mod links;
5
mod meta;
6
mod middleware;
7
mod parse;
8
mod registry;
9
mod resource;
10
mod response;
11
mod run;
12
mod runs;
13
mod shaft;
14
mod shame;
15
mod ui;
16
17
use std::{
18
    sync::Arc,
19
    time::{Duration, Instant},
20
};
21
22
use axum::{
23
    Router,
24
    extract::State,
25
    response::{Redirect, Response},
26
    routing::{MethodRouter, any},
27
};
28
29
use tower_http::catch_panic::CatchPanicLayer;
30
31
use crate::{
32
    config::Config,
33
    records::{Record, Records},
34
};
35
use ctx::Ctx;
36
use registry::{Lock, Registry};
37
use resource::Resource;
38
use run::RunResource;
39
40
pub struct AppState {
41
    runs: Registry,
42
    records: Records,
43
    config: Config,
44
}
45
46
impl AppState {
47
    pub fn new(config: Config) -> rusqlite::Result<Self> {
48
        let records = match &config.db {
49
            Some(path) => Records::open(path)?,
50
            None => Records::in_memory(),
51
        };
52
        Ok(Self {
53
            runs: Registry::default(),
54
            records,
55
            config,
56
        })
57
    }
58
59
    /// Records and forgets the runs that have gone idle. The server calls it on a timer.
60
    pub fn sweep(&self, now: Instant) -> rusqlite::Result<()> {
61
        for slot in self.runs.evict_idle(now, self.config.idle_ttl) {
62
            let record = match Lock::take(&slot) {
63
                Lock::Held(run) | Lock::Poisoned(run) => Record::of(&run),
64
            };
65
            self.records.save(&record)?;
66
        }
67
        Ok(())
68
    }
69
70
    #[cfg(test)]
71
    pub(crate) fn insert(&self, run: crate::game::Run) {
72
        self.runs.insert_new(run);
73
    }
74
75
    #[cfg(test)]
76
    pub(crate) fn inspect<T>(&self, visit: impl FnMut(&crate::game::Run) -> T) -> Vec<T> {
77
        self.runs.each(visit)
78
    }
79
}
80
81
impl Default for AppState {
82
    fn default() -> Self {
83
        Self::new(Config::default()).expect("an in-memory database always opens")
84
    }
85
}
86
87
type Shared = Arc<AppState>;
88
89
pub fn app(config: Config) -> rusqlite::Result<Router> {
90
    Ok(router(Arc::new(AppState::new(config)?)))
91
}
92
93
/// Sweeps idle runs into the records every minute, so a quiet server still keeps its graveyard.
94
pub async fn sweep_forever(state: Arc<AppState>) {
95
    let mut ticks = tokio::time::interval(Duration::from_secs(60));
96
    loop {
97
        ticks.tick().await;
98
        let swept = Arc::clone(&state);
99
        let result = tokio::task::spawn_blocking(move || swept.sweep(Instant::now())).await;
100
        if let Ok(Err(error)) = result {
101
            eprintln!("jelqd could not sweep idle runs into the records: {error}");
102
        }
103
    }
104
}
105
106
pub fn router(state: Shared) -> Router {
107
    let api = api(state);
108
    let ui = Router::new()
109
        .route("/ui", any(|| async { Redirect::permanent("/ui/") }))
110
        .route("/ui/", any(ui::page))
111
        .route("/ui/send", any(ui::send))
112
        .with_state(api.clone());
113
    api.merge(ui)
114
        .layer(CatchPanicLayer::custom(middleware::panicked))
115
}
116
117
fn global(resource: &'static Resource<Shared>) -> MethodRouter<Shared> {
118
    any(move |State(state): State<Shared>, ctx: Ctx| async move {
119
        offload(state, move |mut state| resource.dispatch(&mut state, &ctx)).await
120
    })
121
}
122
123
fn scoped(resource: &'static RunResource) -> MethodRouter<Shared> {
124
    any(move |State(state): State<Shared>, ctx: Ctx| async move {
125
        offload(state, move |state| run::serve(&state, ctx, resource)).await
126
    })
127
}
128
129
/// Game turns block, on SQLite always and on HTTP for every roll under the model oracle, so they
130
/// run on the blocking pool and leave the async workers free.
131
async fn offload<F>(state: Shared, work: F) -> Response
132
where
133
    F: FnOnce(Shared) -> Response + Send + 'static,
134
{
135
    tokio::task::spawn_blocking(move || work(state))
136
        .await
137
        .unwrap_or_else(|e| std::panic::resume_unwind(e.into_panic()))
138
}
139
140
fn api(state: Shared) -> Router {
141
    Router::new()
142
        .route("/", global(&meta::INDEX))
143
        .route("/runs", global(&meta::RUNS))
144
        .route("/runs/{id}", scoped(&runs::RUN))
145
        .route("/runs/{id}/length", scoped(&runs::LENGTH))
146
        .route("/runs/{id}/foreskin", scoped(&runs::FORESKIN))
147
        .route("/runs/{id}/inventory", scoped(&runs::INVENTORY))
148
        .route("/runs/{id}/inventory/{item}", scoped(&runs::ITEM))
149
        .route("/runs/{id}/techniques", scoped(&runs::TECHNIQUES))
150
        .route("/runs/{id}/drawer", scoped(&runs::DRAWER))
151
        .route("/runs/{id}/branches/{choice}", scoped(&runs::BRANCH))
152
        .route("/runs/{id}/kitchen", scoped(&runs::KITCHEN))
153
        .route("/runs/{id}/kitchen/watermelon", scoped(&runs::MELON))
154
        .route("/runs/{id}/thread", scoped(&runs::THREAD))
155
        .route("/runs/{id}/floor", scoped(&runs::FLOOR))
156
        .route("/runs/{id}/shop", scoped(&runs::SHOP))
157
        .route("/runs/{id}/shop/{item}", scoped(&runs::BUY))
158
        .route("/runs/{id}/portal", scoped(&runs::PORTAL))
159
        .route("/runs/{id}/elder", scoped(&runs::ELDER))
160
        .route("/runs/{id}/grip", scoped(&runs::GRIP))
161
        .route("/runs/{id}/diagnosis", scoped(&runs::DIAGNOSIS))
162
        .route("/runs/{id}/footage", scoped(&runs::FOOTAGE))
163
        .route("/forum/threads/{year}", global(&meta::THREAD))
164
        .route("/graveyard", global(&meta::GRAVEYARD))
165
        .route("/leaderboard", global(&meta::LEADERBOARD))
166
        .fallback(meta::not_found)
167
        .layer(axum::middleware::from_fn(middleware::chrome))
168
        .layer(CatchPanicLayer::custom(middleware::panicked))
169
        .with_state(state)
170
}