alot of progress

This commit is contained in:
2025-10-29 18:49:52 -07:00
parent 989561deef
commit 11ad1d9652
4 changed files with 493 additions and 321 deletions

310
Cargo.lock generated
View File

@@ -46,6 +46,7 @@ dependencies = [
"tower", "tower",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]
@@ -64,6 +65,30 @@ dependencies = [
"sync_wrapper", "sync_wrapper",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
]
[[package]]
name = "axum-extra"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96"
dependencies = [
"axum",
"axum-core",
"bytes",
"cookie",
"futures-util",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"serde_core",
"tower-layer",
"tower-service",
"tracing",
] ]
[[package]] [[package]]
@@ -77,6 +102,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.19.0" version = "3.19.0"
@@ -106,6 +137,26 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "deranged"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.5"
@@ -193,6 +244,20 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"wasip2",
"wasm-bindgen",
]
[[package]] [[package]]
name = "http" name = "http"
version = "1.3.1" version = "1.3.1"
@@ -350,12 +415,27 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.0" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]] [[package]]
name = "matchit" name = "matchit"
version = "0.7.3" version = "0.7.3"
@@ -407,16 +487,24 @@ name = "mis-interpreter"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"futures-util", "axum-extra",
"getrandom",
"maud", "maud",
"rand",
"serde", "serde",
"serde_json", "tower-cookies",
"tower-service", "tower-service",
"urlencoding", "uuid",
"worker", "worker",
"worker-macros", "worker-macros",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -432,6 +520,29 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@@ -479,6 +590,21 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.103" version = "1.0.103"
@@ -509,6 +635,50 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.22" version = "1.0.22"
@@ -521,6 +691,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@@ -598,6 +774,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "sha1_smol"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.11" version = "0.4.11"
@@ -644,6 +826,37 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "time"
version = "0.3.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
[[package]]
name = "time-macros"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.1" version = "0.8.1"
@@ -677,6 +890,22 @@ dependencies = [
"tower-service", "tower-service",
] ]
[[package]]
name = "tower-cookies"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36"
dependencies = [
"axum-core",
"cookie",
"futures-util",
"http",
"parking_lot",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "tower-layer" name = "tower-layer"
version = "0.3.3" version = "0.3.3"
@@ -689,6 +918,22 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.20" version = "1.0.20"
@@ -707,24 +952,39 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "utf8_iter" name = "utf8_iter"
version = "1.0.4" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [
"getrandom",
"js-sys",
"sha1_smol",
"wasm-bindgen",
]
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.105" version = "0.2.105"
@@ -806,6 +1066,18 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]] [[package]]
name = "worker" name = "worker"
version = "0.6.7" version = "0.6.7"
@@ -895,6 +1167,26 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zerocopy"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.6" version = "0.1.6"

View File

@@ -7,25 +7,22 @@ authors = ["Rivulet <cadenream@cadencoaster.com>"]
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
# (Wrangler will ignore these; fine to keep or remove)
[package.metadata.wasm-pack] [package.metadata.wasm-pack]
target = "web" target = "web"
out-dir = "build" out-dir = "build"
out-name = "index" out-name = "index"
[dependencies] [dependencies]
# Cloudflare Workers (HTTP + Axum adapter) worker = { version = "0.6", features = ['http', 'axum'] }
worker = { version = "0.6", features = ["http", "axum"] } worker-macros = { version = "0.6", features = ['http'] }
worker-macros = { version = "0.6", features = ["http"] }
# Axum *without* tokio/hyper; we don't use its WS, so don't enable it.
axum = { version = "0.8", default-features = false, features = ["form", "macros"] } axum = { version = "0.8", default-features = false, features = ["form", "macros"] }
tower-service = "0.3.3" tower-service = "0.3.3"
maud = { version = "0.27.0", default-features = false } maud = { version = "0.27.0", default-features = false }
rand = { version = "0.9.2",default-features = false, features = ["thread_rng"] }
serde = { version = "1.0.228", default-features = false, features = ["derive"] }
tower-cookies = "0.11.0"
axum-extra = { version = "0.10.3", features = ["cookie"] }
uuid = { version = "1.18", features = ["v4", "v7", "v5", "js"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.3", features = ["wasm_js"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
urlencoding = "2.1.3"
futures-util = "0.3.31"

View File

@@ -1,339 +1,214 @@
use axum::{ use axum::http::{status, HeaderMap, HeaderValue, StatusCode};
body::{to_bytes, Body}, use axum::response::{Html, Redirect };
http::{Response as AxumResponse, StatusCode}, use axum::routing::post;
response::Html, use axum::{routing::get, Extension, Form, Router};
routing::{get, post}, use axum::http::header::SET_COOKIE;
Form, Router, use axum_extra::extract::cookie::Cookie;
}; use axum_extra::extract::CookieJar;
use futures_util::StreamExt; use maud::html;
use maud::{html, Markup}; use tower_cookies::{CookieManagerLayer, Cookies};
use serde::Deserialize;
use tower_service::Service; use tower_service::Service;
use uuid::Uuid;
use worker::*; use worker::*;
use worker::wasm_bindgen_futures::spawn_local;
/* ===================== Router ===================== */ #[durable_object]
pub struct Room {
fn router() -> Router { state: State,
Router::new()
.route("/", get(root))
.route("/create", post(create_game))
// /ws is handled in fetch() using CF WebSocketPair.
} }
/* ===================== Cloudflare entry ===================== */ fn gen_id() -> String {
Uuid::now_v7().to_string()
#[event(fetch)]
async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result<Response> {
// Intercept WebSocket before handing off to Axum
let is_ws = req
.headers()
.get("Upgrade")
.and_then(|v| v.to_str().ok())
.map(|v| v.eq_ignore_ascii_case("websocket"))
.unwrap_or(false);
if req.uri().path() == "/ws" && is_ws {
return handle_ws_upgrade(req).await;
}
// Otherwise hand off to Axum and adapt Axum -> Worker Response
let axum_resp: AxumResponse<Body> = router().call(req).await?;
axum_to_worker(axum_resp).await
} }
/* Convert an Axum response into a worker::Response */ fn load_jar(req: &Request) -> CookieJar {
async fn axum_to_worker(ax: AxumResponse<Body>) -> Result<Response> { let mut jar = CookieJar::new();
let (parts, body) = ax.into_parts(); if let Ok(Some(header)) = req.headers().get("Cookie") {
let bytes = to_bytes(body, usize::MAX).await.unwrap_or_default(); for c in Cookie::split_parse(header) {
if let Ok(c) = c {
let mut resp = Response::from_bytes(bytes.to_vec())?; jar = jar.add(c);
resp = resp.with_status(parts.status.as_u16());
for (name, value) in parts.headers.iter() {
if let Ok(val) = value.to_str() {
resp.headers_mut().set(name.as_str(), val)?; // <- note: no ? after headers_mut()
} }
} }
Ok(resp) }
jar
} }
/* ===================== Pages ===================== */ impl DurableObject for Room {
fn new(state: State, env: Env) -> Self {
Self { state }
}
/// GET / async fn fetch(&self, req: Request) -> Result<Response> {
pub async fn root() -> Html<String> { let cookie = load_jar(&req);
// If you want to read cookies here, switch to a custom Response and pass headers in via extractors. let url = req.url()?;
let page = render_page(None); match (req.method(), url.path()) {
Html::from(page) (Method::Get, "/ws") => {
}
/// POST /create (classic PRG with cookie flash)
#[derive(Deserialize)]
struct CreateForm {
game_name: Option<String>,
password: Option<String>,
player_name: Option<String>,
}
pub async fn create_game(Form(form): Form<CreateForm>) -> AxumResponse<Body> {
let msg = flash_msg(&form);
AxumResponse::builder()
.status(StatusCode::SEE_OTHER)
.header("Location", "/")
.header(
"Set-Cookie",
format!(
"flash={}; Path=/; Max-Age=30; HttpOnly; SameSite=Lax",
urlencoding::encode(&msg)
),
)
.body(Body::empty())
.unwrap()
}
/* ===================== WebSocket (HTMX ws-send) ===================== */
async fn handle_ws_upgrade(_req: HttpRequest) -> Result<Response> {
// Create the CF WebSocketPair at the worker layer
let pair = WebSocketPair::new()?; let pair = WebSocketPair::new()?;
let server = pair.server; let server = pair.server;
let client = pair.client; let client = pair.client;
server.accept()?; server.accept()?;
// Spawn a local task to read messages and push OOB HTML back. Response::from_websocket(client)
wasm_bindgen_futures::spawn_local(async move { }
// Create the borrow-bound event stream *inside* the task and keep the clone alive. (Method::Post, "/setname") => {
let server_clone = server.clone(); let name = req.headers().get("Name").unwrap_or(None);
let mut events = match server_clone.events() { if name.is_some() {
Ok(e) => e, self.state.storage().put("name", name.unwrap()).await.expect("failed to write to storage");
Err(_) => return, Response::ok("Ok")
};
while let Some(Ok(evt)) = events.next().await {
if let WebsocketEvent::Message(m) = evt {
if let Some(txt) = m.text() {
if let Ok(parsed) = serde_json::from_str::<CreateForm>(&*txt) {
let msg = flash_msg(&parsed);
let oob = render_flash_oob(&msg);
let _ = server_clone.send_with_str(oob);
} else { } else {
let _ = server_clone.send_with_str(render_log_oob("Unrecognized message")); Response::error("No name provided", StatusCode::BAD_REQUEST.as_u16())
} }
} }
_ => todo!(),
} }
} }
}
fn router() -> Router {
Router::new()
.route("/", get(root))
.route("/create", post(create))
.route("/lobby", get(lobby))
}
#[derive(Clone)]
pub struct Params {
env: Env,
}
#[event(fetch)]
async fn fetch(
mut req: HttpRequest,
env: Env,
_ctx: Context,
) -> Result<axum::http::Response<axum::body::Body>> {
let url = req.uri();
let path = url.path();
// Expect: /ws/{name}
if path == "/ws" {
let jar = load_jar(&req.try_into()?);
let name = jar.get("game_id").expect("failed to get game_id").to_string();
// Resolve the DO instance deterministically by name
let ns = env.durable_object("ROOM")?; // binding in wrangler.toml
let id = ns.id_from_name(&*name)?;
let stub = id.get_stub()?;
return Ok(stub.fetch_with_request(Request::new("http://do/ws", Method::Get)?).await.expect("failed to connect").try_into()?);
}
req.extensions_mut().insert( Params{ env});
Ok(router().call(req).await?)
}
#[derive(serde::Deserialize, Debug, Clone)]
pub struct CreateGameForm {
game_name: Option<String>, // <- String, not &str
password: Option<String>,
player_name: String,
}
#[axum::debug_handler]
pub async fn create(Extension(params): Extension<Params>, mut jar: CookieJar, Form(form): Form<CreateGameForm>) -> (CookieJar, Redirect) {
let id = gen_id();
jar = jar.add(Cookie::new("game_id", id.clone()));
jar = jar.add(Cookie::new("player_name", form.player_name.clone()));
if !form.game_name.clone().unwrap_or("".to_string()).is_empty() {
jar = jar.add(Cookie::new("game_name", form.game_name.as_ref().unwrap_or(&"".to_string()).clone()));
}
if !form.password.clone().unwrap_or("".to_string()).is_empty() {
jar = jar.add(Cookie::new("password", form.password.as_ref().unwrap_or(&"".to_string()).clone()));
}
let ns = params.env.durable_object("ROOM").unwrap();
let id = ns.id_from_name(&*format!("room-{}",id)).expect("Failed to find game_name");
let stub = id.get_stub().expect("Failed to find stub");
let req = Request::new("http://do/setname", Method::Post).unwrap();
req.headers().set("Name", &*form.game_name.clone().unwrap_or("".to_string())).expect("Failed to set header");
let name = form.game_name.clone().unwrap_or("".to_string());
spawn_local(async move {
if let Err(e) = stub.fetch_with_request(req).await {
console_error!("failed to set name: {}",e)
}
}); });
// Return the upgrade response (must be worker::Response) console_log!("{:?} id:{}", form, id);
Response::from_websocket(client) (jar, Redirect::to("/lobby"))
} }
pub async fn root() -> Html<String> {
/* ===================== View / HTML ===================== */ Html::from(html! {
fn render_page(flash: Option<&str>) -> String {
(html! {
(maud::DOCTYPE) (maud::DOCTYPE)
html lang="en" { html lang="en" {
head { head {
meta charset="utf-8"; meta charset="utf-8";
meta name="viewport" content="width=device-width, initial-scale=1"; meta name="viewport" content="width=device-width, initial-scale=1";
title { "Mis-Interpreter" } title { "Mis-Interpreter" }
// HTMX + WS Extension
script src="https://unpkg.com/htmx.org@2.0.4" {}
script src="https://unpkg.com/htmx-ext-ws@2.0.3/ws.js" {}
style { (PASTEL_CSS) } style { (PASTEL_CSS) }
} }
body { body {
main class="shell" { main class="shell" {
section class="card cartoon" { section class="card cartoon" {
h1 class="title" { "Mis-Interpreter" } h1 class="title" {
p class="subtitle" { "Create a game?" } "Mis-Interpreter"
// Flash target
div id="flash" {
@if let Some(msg) = flash {
(render_flash_inline(msg))
} }
p class="subtitle" {
"Create a game?"
} }
form method="post" action="/create" class="form" {
// Live form using htmx WebSocket label class="sr" for="game_name" {
div hx-ext="ws" ws-connect="/ws" { "Game Name"
form id="create-form" class="form" ws-send {
label class="sr" for="game_name" { "Game Name" }
input id="game_name" type="text" name="game_name"
placeholder="Game Name (Optional)" class="input";
label class="sr" for="password" { "Password" }
input id="password" type="password" name="password"
placeholder="Password (Optional)" class="input";
label class="sr" for="player_name" { "Player Name" }
input id="player_name" type="text" name="player_name"
value="John Wick" class="input";
button type="submit" class="btn" { "Create (Live)" }
} }
div id="ws-log" class="wslog" {} input id="game_name" type="text" name="game_name" placeholder="Game Name (Optional)" class="input";
label class="sr" for="password" {
"Password"
}
input id="password" type="password" name="password" placeholder="Password (Optional)" class="input"; label class="sr" for="player_name" {
"Player Name"
}
input id="player_name" type="text" name="player_name" value="John Wick" class="input"; button type="submit" class="btn" {
"Create"
} }
} }
} }
} }
} }
}).into_string() }
}.into_string())
} }
fn render_flash_inline(msg: &str) -> Markup { pub async fn lobby(jar: CookieJar) -> Html<String> {
html! { div class="flash cartoon" { (escape(msg)) } } let title = jar.get("game_name").and_then(|v| Some(v.value())).unwrap_or("Lobby");
Html::from(html! {
(maud::DOCTYPE)
html lang="en" {
head {
meta charset="utf-8";
meta name="viewport" content="width=device-width, initial-scale=1";
script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js" integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz" crossorigin="anonymous" {}
script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/ws.js" {}
title { "Mis-Interpreter - "(title) }
style { (PASTEL_CSS) }
}
body {
main class="shell" {
section class="card cartoon" {
h1 class="title" {
(title)
}
hr;
p class="subtitle" {
"Player List"
}
div {
}
}
}
}
}
}.into_string())
} }
fn render_flash_oob(msg: &str) -> String { const PASTEL_CSS: &str = r#" /* ===== Pastel Blues, Cartoon Borders ===== */ :root{ /* palette */ --sky-50:#eff6ff; --sky-100:#dbeafe; --sky-200:#bfdbfe; --sky-300:#93c5fd; --sky-400:#60a5fa; --sky-500:#3b82f6; --indigo-600:#4f46e5; --teal-300:#99f6e4; --bg-top: color-mix(in oklab, var(--sky-100) 70%, white); --bg-bot: color-mix(in oklab, var(--teal-300) 35%, white); --card: #f2f7ff; --ink: #0b1220; --muted:#44506a; /* cartoon outlines */ --outline: #133a84; /* deep blue stroke */ --shadow: #0e2250; /* offset shadow for sticker look */ --ring: var(--sky-400); --field-a: #e9f2ff; --field-b: #e6fffb; --btn-a: #a5b4ff; /* indigo pastel */ --btn-b: #7dd3fc; /* sky pastel */ --flash-bg:#c4f1f9; /* cyan-200 */ --flash-border:#0891b2; } *{box-sizing:border-box} html,body{height:100%} body{ margin:0; font:17px/1.55 ui-sans-serif, system-ui, -apple-system, Segoe UI, Inter, Roboto, Arial; color:var(--ink); background: radial-gradient(1000px 700px at 15% -10%, var(--sky-200) 0%, transparent 60%), radial-gradient(1100px 800px at 110% 0%, var(--teal-300) 0%, transparent 60%), linear-gradient(180deg, var(--bg-top), var(--bg-bot)); } /* ——— cartoon helpers ——— */ .cartoon{ border:4px solid var(--outline); box-shadow: 8px 8px 0 var(--shadow); border-radius: 26px; } /* layout */ .shell{min-height:100%; display:grid; place-items:center; padding:3rem 1rem} .card{ width:min(740px, 94vw); background: var(--card); padding:3rem 2.5rem; } .card.cartoon{} /* (keep class for specificity) */ .title{ text-align:center; margin:0 0 .5rem; font-weight:900; letter-spacing:.3px; font-size: clamp(2.1rem, 3.4vw + 1rem, 3.4rem); /* shiny blue gradient text */ background: linear-gradient(90deg, var(--sky-500), var(--indigo-600)); -webkit-background-clip: text; background-clip: text; color: transparent; text-shadow: 0 2px 0 #ffffff66; } .subtitle{ text-align:center; color:var(--muted); margin:0 0 2rem; font-weight:700 } .flash{ background: var(--flash-bg); border:4px solid var(--flash-border); color:#0c4a6e; padding:.85rem 1rem; border-radius:16px; margin:0 0 1.25rem; box-shadow: 6px 6px 0 var(--shadow); } /* form */ .form{ display:grid; grid-template-columns:1fr auto; gap:16px 16px; align-items:center } .input{ width:100%; padding:1rem 1.1rem; border:4px solid var(--outline); border-radius:16px; background: linear-gradient(0deg, #ffffff, #ffffff) padding-box, radial-gradient(120% 120% at 0% 0%, var(--field-a) 0%, var(--field-b) 100%) border-box; color:var(--ink); box-shadow: 4px 4px 0 var(--shadow); transition: transform .06s ease, box-shadow .15s ease, border-color .15s ease; } .input::placeholder{color:#6b7280} .input:focus{ outline:none; transform: translateY(-1px); border-color: var(--ring); box-shadow: 6px 6px 0 var(--shadow), 0 0 0 6px color-mix(in oklab, var(--ring) 40%, white); } /* button: chunky pill with dual-tone blue */ .btn{ padding:1rem 1.35rem; font-weight:900; border:4px solid var(--outline); border-radius:999px; color:#06223a; background: linear-gradient(180deg, color-mix(in oklab, var(--btn-b) 75%, white), color-mix(in oklab, var(--btn-a) 85%, white)); cursor:pointer; box-shadow: 6px 6px 0 var(--shadow); transition: transform .07s ease, filter .15s ease, box-shadow .15s ease; } .btn:hover{ filter:brightness(1.06) saturate(1.05) } .btn:active{ transform: translateY(2px); box-shadow: 4px 4px 0 var(--shadow) } /* accessibility */ .sr{ position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0 } /* responsive */ @media (max-width: 560px){ .form{ grid-template-columns: 1fr } .btn{ width:100% } } "#;
format!(
r#"<div id="flash" hx-swap-oob="true"><div class="flash cartoon">{}</div></div>"#,
escape(msg)
)
}
fn render_log_oob(text: &str) -> String {
format!(r#"<div id="ws-log" hx-swap-oob="true">{}</div>"#, escape(text))
}
/* ===================== Utilities ===================== */
fn flash_msg(form: &CreateForm) -> String {
let game = form.game_name.as_deref().unwrap_or("Untitled");
let player = form.player_name.as_deref().unwrap_or("Anonymous");
let priv_tag = if form.password.as_deref().unwrap_or("").is_empty() { "" } else { " (private)" };
format!("Created game '{}' for '{}'{priv_tag}", escape(game), escape(player))
}
// minimal safe escaping for text nodes
fn escape(s: &str) -> String {
s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
}
/* ===================== Theme ===================== */
const PASTEL_CSS: &str = r#"
/* ===== Pastel Blues, Cartoon Borders ===== */
:root{
/* palette */
--sky-50:#eff6ff; --sky-100:#dbeafe; --sky-200:#bfdbfe; --sky-300:#93c5fd;
--sky-400:#60a5fa; --sky-500:#3b82f6; --indigo-600:#4f46e5; --teal-300:#99f6e4;
--bg-top: color-mix(in oklab, var(--sky-100) 70%, white);
--bg-bot: color-mix(in oklab, var(--teal-300) 35%, white);
--card: #f2f7ff;
--ink: #0b1220;
--muted:#44506a;
/* cartoon outlines */
--outline: #133a84; /* deep blue stroke */
--shadow: #0e2250; /* offset shadow for sticker look */
--ring: var(--sky-400);
--field-a: #e9f2ff;
--field-b: #e6fffb;
--btn-a: #a5b4ff; /* indigo pastel */
--btn-b: #7dd3fc; /* sky pastel */
--flash-bg:#c4f1f9; /* cyan-200 */
--flash-border:#0891b2;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
font:17px/1.55 ui-sans-serif, system-ui, -apple-system, Segoe UI, Inter, Roboto, Arial;
color:var(--ink);
background:
radial-gradient(1000px 700px at 15% -10%, var(--sky-200) 0%, transparent 60%),
radial-gradient(1100px 800px at 110% 0%, var(--teal-300) 0%, transparent 60%),
linear-gradient(180deg, var(--bg-top), var(--bg-bot));
}
/* ——— cartoon helpers ——— */
.cartoon{
border:4px solid var(--outline);
box-shadow: 8px 8px 0 var(--shadow);
border-radius: 26px;
}
/* layout */
.shell{min-height:100%; display:grid; place-items:center; padding:3rem 1rem}
.card{
width:min(740px, 94vw);
background: var(--card);
padding:3rem 2.5rem;
}
.card.cartoon{} /* (keep class for specificity) */
.title{
text-align:center; margin:0 0 .5rem; font-weight:900; letter-spacing:.3px;
font-size: clamp(2.1rem, 3.4vw + 1rem, 3.4rem);
/* shiny blue gradient text */
background: linear-gradient(90deg, var(--sky-500), var(--indigo-600));
-webkit-background-clip: text; background-clip: text; color: transparent;
text-shadow: 0 2px 0 #ffffff66;
}
.subtitle{ text-align:center; color:var(--muted); margin:0 0 2rem; font-weight:700 }
.flash{
background: var(--flash-bg);
border:4px solid var(--flash-border);
color:#0c4a6e;
padding:.85rem 1rem; border-radius:16px; margin:0 0 1.25rem;
box-shadow: 6px 6px 0 var(--shadow);
}
/* form */
.form{ display:grid; grid-template-columns:1fr auto; gap:16px 16px; align-items:center }
.input{
width:100%;
padding:1rem 1.1rem;
border:4px solid var(--outline);
border-radius:16px;
background:
linear-gradient(0deg, #ffffff, #ffffff) padding-box,
radial-gradient(120% 120% at 0% 0%, var(--field-a) 0%, var(--field-b) 100%) border-box;
color:var(--ink);
box-shadow: 4px 4px 0 var(--shadow);
transition: transform .06s ease, box-shadow .15s ease, border-color .15s ease;
}
.input::placeholder{color:#6b7280}
.input:focus{
outline:none; transform: translateY(-1px);
border-color: var(--ring);
box-shadow: 6px 6px 0 var(--shadow), 0 0 0 6px color-mix(in oklab, var(--ring) 40%, white);
}
/* button: chunky pill with dual-tone blue */
.btn{
padding:1rem 1.35rem;
font-weight:900;
border:4px solid var(--outline);
border-radius:999px;
color:#06223a;
background: linear-gradient(180deg,
color-mix(in oklab, var(--btn-b) 75%, white),
color-mix(in oklab, var(--btn-a) 85%, white));
cursor:pointer;
box-shadow: 6px 6px 0 var(--shadow);
transition: transform .07s ease, filter .15s ease, box-shadow .15s ease;
}
.btn:hover{ filter:brightness(1.06) saturate(1.05) }
.btn:active{ transform: translateY(2px); box-shadow: 4px 4px 0 var(--shadow) }
/* accessibility */
.sr{ position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0 }
/* responsive */
@media (max-width: 560px){
.form{ grid-template-columns: 1fr }
.btn{ width:100% }
}
"#;

View File

@@ -1,7 +1,15 @@
name = "mis-interpreter" name = "mis-interpreter"
workers_dev = true
compatibility_date = "2025-10-27"
main = "build/worker/shim.mjs" main = "build/worker/shim.mjs"
compatibility_date = "2024-11-06"
[build] [build]
command = "cargo install -q worker-build && worker-build --release" # required command = "cargo install -q worker-build && worker-build --release"
[durable_objects]
bindings = [
{ name = "ROOM", class_name = "Room" }
]
[[migrations]]
tag = "v1"
new_classes = ["Room"]