From 2db36f8d75d332eab472348832f03979c1632c12 Mon Sep 17 00:00:00 2001 From: codeitlikemiley Date: Fri, 29 May 2026 14:18:42 +0800 Subject: [PATCH 1/2] Upgrade leptos_ssr_axum example to latest leptos_wasi and WASIp3 trigger --- examples/leptos_ssr_axum/Cargo.toml | 33 +++- examples/leptos_ssr_axum/build.rs | 21 +++ examples/leptos_ssr_axum/spin.toml | 5 +- examples/leptos_ssr_axum/src/app.rs | 230 ++++++++++++------------- examples/leptos_ssr_axum/src/lib.rs | 2 +- examples/leptos_ssr_axum/src/server.rs | 111 +++++++----- examples/leptos_ssr_axum/src/types.rs | 4 + 7 files changed, 231 insertions(+), 175 deletions(-) create mode 100644 examples/leptos_ssr_axum/build.rs diff --git a/examples/leptos_ssr_axum/Cargo.toml b/examples/leptos_ssr_axum/Cargo.toml index 328403c..91c4b8c 100644 --- a/examples/leptos_ssr_axum/Cargo.toml +++ b/examples/leptos_ssr_axum/Cargo.toml @@ -3,20 +3,28 @@ name = "leptos_ssr_axum" authors = ["codeitlikemiley "] description = "Antigravity SDK Chat Interface — Leptos + Spin WASI Example" version = "0.1.0" -edition = "2021" +edition = "2024" +rust-version = "1.93.0" +build = "build.rs" [lib] crate-type = [ "cdylib" ] [dependencies] any_spawner = { version = "0.3.0", features = ["futures-executor"] } +bytes = "1.7.2" console_error_panic_hook = "0.1" +futures = "0.3.30" +hydration_context = { version = "0.3.0" } leptos = { version = "0.8.9" } leptos_meta = { version = "0.8.5" } leptos_router = { version = "0.8.7" } -leptos_wasi = { git = "https://github.com/codeitlikemiley/leptos_wasi", optional = true } -spin-sdk = { version = "5", optional = true } -wasi = { version = "=0.13.1", optional = true } +leptos_wasi = { git = "https://github.com/leptos-rs/leptos_wasi", rev = "216acc484b0d6fe4b18876f1c96b68272498592b", default-features = false, features = ["wasip3"], optional = true } +server_fn = { version = "0.8.7", features = ["axum-no-default"] } +spin-sdk = { version = "6.0.0", features = ["json"], optional = true } +wasip3 = { version = "0.6.0", features = ["http-compat"], optional = true } +http = { version = "1.1.0", optional = true } +http-body = { version = "1.0.0", optional = true } wasm-bindgen = { version = "=0.2.103", optional = true } wasm-bindgen-futures = { version = "0.4", optional = true } web-sys = { version = "0.3", features = ["Storage", "Window", "EventSource", "MessageEvent", "ErrorEvent", "Document", "Element", "HtmlMetaElement"], optional = true } @@ -38,13 +46,21 @@ ssr = [ "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", - "leptos/spin", "dep:spin-sdk", "dep:leptos_wasi", - "dep:wasi", + "dep:wasip3", "dep:antigravity-sdk-rust", + "dep:http", + "dep:http-body", ] +[profile.wasm-release] +inherits = "release" +opt-level = 'z' +lto = true +codegen-units = 1 +panic = "abort" + [package.metadata.leptos] output-name = "leptos_ssr_axum" tailwind-input-file = "input.css" @@ -53,3 +69,8 @@ bin-features = ["ssr"] bin-default-features = false lib-features = ["hydrate"] lib-default-features = false +lib-profile-release = "wasm-release" +bin-profile-release = "wasm-release" +bin-target-triple = "wasm32-wasip2" +bin-target-dir = "target/server" + diff --git a/examples/leptos_ssr_axum/build.rs b/examples/leptos_ssr_axum/build.rs new file mode 100644 index 0000000..3520d26 --- /dev/null +++ b/examples/leptos_ssr_axum/build.rs @@ -0,0 +1,21 @@ +use std::env; +fn main() { + println!("cargo::rustc-check-cfg=cfg(runtime_spin)"); + println!("cargo::rustc-check-cfg=cfg(runtime_wasmtime)"); + let runtime = env::var("WASI_RUNTIME").unwrap_or_else(|_| "wasmtime".to_string()); + println!("cargo:rustc-env=WASI_RUNTIME={}", runtime); + match runtime.as_str() { + "spin" => { + println!("cargo:rustc-cfg=runtime_spin"); + } + "wasmtime" => { + println!("cargo:rustc-cfg=runtime_wasmtime"); + } + _ => { + println!("cargo:rustc-cfg=runtime_wasmtime"); + } + } + if env::var("SPIN_BUILD").is_ok() { + println!("cargo:rustc-cfg=runtime_spin"); + } +} diff --git a/examples/leptos_ssr_axum/spin.toml b/examples/leptos_ssr_axum/spin.toml index 6383e7c..5917e66 100644 --- a/examples/leptos_ssr_axum/spin.toml +++ b/examples/leptos_ssr_axum/spin.toml @@ -13,16 +13,17 @@ agent_server_url = { required = false, default = "http://127.0.0.1:8080" } [[trigger.http]] route = "/..." component = "leptos-ssr-axum" +executor = { type = "http" } [component.leptos-ssr-axum] -source = "target/wasm32-wasip1/release/leptos_ssr_axum.wasm" +source = "target/wasm32-wasip2/release/leptos_ssr_axum.wasm" allowed_outbound_hosts = ["https://generativelanguage.googleapis.com", "http://127.0.0.1:8080"] key_value_stores = ["default"] [component.leptos-ssr-axum.variables] gemini_api_key = "{{ gemini_api_key }}" agent_server_url = "{{ agent_server_url }}" [component.leptos-ssr-axum.build] -command = "cargo leptos build --release && LEPTOS_OUTPUT_NAME=leptos_ssr_axum cargo build --lib --target wasm32-wasip1 --release --no-default-features --features ssr" +command = "WASI_RUNTIME=spin cargo leptos build --release && WASI_RUNTIME=spin LEPTOS_OUTPUT_NAME=leptos_ssr_axum cargo build --lib --target wasm32-wasip2 --release --no-default-features --features ssr" watch = ["src/**/*.rs", "Cargo.toml"] [[trigger.http]] diff --git a/examples/leptos_ssr_axum/src/app.rs b/examples/leptos_ssr_axum/src/app.rs index 4454dcd..5daa034 100644 --- a/examples/leptos_ssr_axum/src/app.rs +++ b/examples/leptos_ssr_axum/src/app.rs @@ -18,8 +18,9 @@ use crate::types::{ChatSession, SessionIndex}; #[cfg(feature = "ssr")] pub fn shell(options: LeptosOptions) -> impl IntoView { - let agent_url = spin_sdk::variables::get("agent_server_url") - .unwrap_or_else(|_| "http://127.0.0.1:8080".to_string()); + let agent_url = leptos::prelude::use_context::() + .map(|url| url.0) + .unwrap_or_else(|| "http://127.0.0.1:8080".to_string()); view! { @@ -211,6 +212,8 @@ fn ToolCallView( subagent_blocks: Vec, on_answer: Callback<(u64, Vec, bool)>, ) -> impl IntoView { + let _ = id; + let _ = call_id; let args_str = serde_json::to_string_pretty(&args).unwrap_or_else(|_| args.to_string()); // Prefer the human-readable label; fall back to raw tool name. @@ -1019,7 +1022,7 @@ fn collect_subagents_recursive(blocks: &[MessageBlock]) -> Vec { .. } = block { if name == "START_SUBAGENT" { - if let Some(ref traj_id) = subagent_trajectory_id { + if let Some(traj_id) = subagent_trajectory_id { let prompt = args["prompt"].as_str().unwrap_or("Unknown Subagent").to_string(); let mut step_count = 0; @@ -1389,7 +1392,7 @@ fn update_subagent_blocks_impl( subagent_blocks, .. } = block { - if let Some(ref tid) = subagent_trajectory_id { + if let Some(tid) = subagent_trajectory_id.as_ref() { if tid == traj_id { f(subagent_blocks); return true; @@ -1880,7 +1883,7 @@ fn ChatPage() -> impl IntoView { let _ = &responses; let _ = cancelled; set_blocks.update(|bs| { - if let Some(MessageBlock::Question { ref mut answered, .. }) = bs.iter_mut().find(|b| match b { + if let Some(MessageBlock::Question { answered, .. }) = bs.iter_mut().find(|b| match b { MessageBlock::Question { id, .. } => *id == block_id, _ => false, }) { @@ -2082,7 +2085,7 @@ fn ChatPage() -> impl IntoView { let text_to_push = data.text.clone(); set_blocks.update(|bs| { update_block_by_id_impl(bs, target_id, &mut |b| { - if let MessageBlock::AssistantMessage { ref mut content, .. } = b { + if let MessageBlock::AssistantMessage { content, .. } = b { content.push_str(&text_to_push); } }); @@ -2106,7 +2109,7 @@ fn ChatPage() -> impl IntoView { } set_stream_text_buf.update(|s| s.push_str(&data.text)); update_streaming_block(set_blocks, id_opt, |b| { - if let MessageBlock::AssistantMessage { ref mut content, .. } = b { + if let MessageBlock::AssistantMessage { content, .. } = b { content.push_str(&data.text); } }); @@ -2164,7 +2167,7 @@ fn ChatPage() -> impl IntoView { let text_to_push = data.text.clone(); set_blocks.update(|bs| { update_block_by_id_impl(bs, target_id, &mut |b| { - if let MessageBlock::Thinking { ref mut content, .. } = b { + if let MessageBlock::Thinking { content, .. } = b { content.push_str(&text_to_push); } }); @@ -2188,7 +2191,7 @@ fn ChatPage() -> impl IntoView { } set_stream_think_buf.update(|s| s.push_str(&data.text)); update_streaming_block(set_blocks, id_opt, |b| { - if let MessageBlock::Thinking { ref mut content, .. } = b { + if let MessageBlock::Thinking { content, .. } = b { content.push_str(&data.text); } }); @@ -2301,7 +2304,7 @@ fn ChatPage() -> impl IntoView { set_blocks.update(|bs| { fn update_status_recursive(blocks: &mut Vec, cid: &str, err: bool) -> bool { for b in blocks.iter_mut() { - if let MessageBlock::ToolCall { call_id, ref mut status, subagent_blocks, .. } = b { + if let MessageBlock::ToolCall { call_id, status, subagent_blocks, .. } = b { if call_id == cid { *status = if err { ToolCallStatus::Error } else { ToolCallStatus::Done }; return true; @@ -2334,7 +2337,7 @@ fn ChatPage() -> impl IntoView { }); } else { set_blocks.update(|bs| { - if let Some(MessageBlock::ToolCall { ref mut status, .. }) = bs.iter_mut().find(|b| match b { + if let Some(MessageBlock::ToolCall { status, .. }) = bs.iter_mut().find(|b| match b { MessageBlock::ToolCall { call_id, .. } => call_id == &data.id, _ => false, }) { @@ -2527,7 +2530,7 @@ fn ChatPage() -> impl IntoView { if data.status == "DONE" || data.status == "ERROR" { set_blocks.update(|bs| { for b in bs.iter_mut() { - if let MessageBlock::Thinking { ref mut is_streaming, .. } = b { + if let MessageBlock::Thinking { is_streaming, .. } = b { *is_streaming = false; } } @@ -3733,94 +3736,69 @@ fn NotFound() -> impl IntoView { view! {

"Not Found"

} } -/// Helper: send an HTTP POST request using wasi:http outgoing handler. -/// -/// Generic transport function — sends to any HTTP endpoint via `wasi::http/outgoing-handler`. #[cfg(feature = "ssr")] -fn send_wasi_http_post( +async fn send_wasi_http_post( scheme: &str, authority: &str, path: &str, headers: &[(String, Vec)], body: &[u8], ) -> Result<(u16, Vec), String> { - use wasi::http::types::{Fields, Method, OutgoingBody, OutgoingRequest, Scheme}; - - let wasi_headers = Fields::from_list( - &headers - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect::>(), - ) - .map_err(|e| format!("Failed to create headers: {e:?}"))?; - - let outgoing_req = OutgoingRequest::new(wasi_headers); - outgoing_req - .set_method(&Method::Post) - .map_err(|_| "Failed to set method".to_string())?; - - let wasi_scheme = if scheme == "https" { - Scheme::Https - } else { - Scheme::Http - }; - outgoing_req - .set_scheme(Some(&wasi_scheme)) - .map_err(|_| "Failed to set scheme".to_string())?; - outgoing_req - .set_authority(Some(authority)) - .map_err(|_| "Failed to set authority".to_string())?; - outgoing_req - .set_path_with_query(Some(path)) - .map_err(|_| "Failed to set path".to_string())?; - - // Write body - let out_body = outgoing_req - .body() - .map_err(|_| "Failed to get body handle".to_string())?; - { - let stream = out_body - .write() - .map_err(|_| "Failed to get write stream".to_string())?; - stream - .blocking_write_and_flush(body) - .map_err(|e| format!("Failed to write body: {e:?}"))?; + struct SimpleBody(Option); + + impl http_body::Body for SimpleBody { + type Data = bytes::Bytes; + type Error = std::io::Error; + + fn poll_frame( + mut self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll, Self::Error>>> { + std::task::Poll::Ready(self.0.take().map(|data| Ok(http_body::Frame::data(data)))) + } + } + + let mut builder = http::Request::builder() + .method(http::Method::POST) + .uri(format!("{}://{}{}", scheme, authority, path)); + + for (k, v) in headers { + builder = builder.header(k, &v[..]); } - OutgoingBody::finish(out_body, None) - .map_err(|e| format!("Failed to finish body: {e:?}"))?; + let http_req = builder + .body(SimpleBody(Some(bytes::Bytes::copy_from_slice(body)))) + .map_err(|e| format!("Failed to build request: {e:?}"))?; + + let wasi_req = wasip3::http_compat::http_into_wasi_request(http_req) + .map_err(|e| format!("Failed to convert request: {e:?}"))?; - let future_response = wasi::http::outgoing_handler::handle(outgoing_req, None) + let wasi_resp = wasip3::http::client::send(wasi_req) + .await .map_err(|e| format!("Failed to send request: {e:?}"))?; - // Block until response - let incoming_resp = loop { - if let Some(result) = future_response.get() { - break result - .map_err(|_| "Response already consumed".to_string())? - .map_err(|e| format!("HTTP error: {e:?}"))?; - } - future_response.subscribe().block(); - }; + let status = wasi_resp.get_status_code(); - let status = incoming_resp.status(); - let resp_body_handle = incoming_resp - .consume() - .map_err(|_| "Failed to consume body".to_string())?; - let resp_stream = resp_body_handle - .stream() - .map_err(|_| "Failed to get stream".to_string())?; - - let mut resp_bytes = Vec::new(); - loop { - match resp_stream.blocking_read(65536) { - Ok(chunk) => resp_bytes.extend_from_slice(&chunk), - Err(wasi::io::streams::StreamError::Closed) => break, - Err(e) => return Err(format!("Failed to read response: {e:?}")), + let http_resp = wasip3::http_compat::http_from_wasi_response(wasi_resp) + .map_err(|e| format!("Failed to convert response: {e:?}"))?; + + use futures::future::poll_fn; + use http_body::Body; + + let mut incoming_body = std::pin::pin!(http_resp.into_body()); + let mut body_bytes = Vec::new(); + while let Some(frame) = poll_fn(|cx| incoming_body.as_mut().poll_frame(cx)).await { + match frame { + Ok(f) => { + if let Some(data) = f.data_ref() { + body_bytes.extend_from_slice(data); + } + } + Err(e) => return Err(format!("Failed to read response frame: {e:?}")), } } - Ok((status, resp_bytes)) + Ok((status, body_bytes)) } /// Send a message via the antigravity-sdk-rust Agent sidecar server (full SDK) /// @@ -3829,11 +3807,12 @@ fn send_wasi_http_post( #[server(prefix = "/api")] pub async fn send_message(message: String) -> Result> { let agent_server_url = spin_sdk::variables::get("agent_server_url") + .await .unwrap_or_else(|_| "http://127.0.0.1:8080".to_string()); // Load chat history from KV store - let store = spin_sdk::key_value::Store::open_default().map_err(|e| e.to_string())?; - let history: Vec = match store.get_json::>("chat_messages") { + let store = spin_sdk::key_value::Store::open_default().await.map_err(|e| e.to_string())?; + let history: Vec = match store.get_json::>("chat_messages").await { Ok(Some(msgs)) => msgs, _ => Vec::new(), }; @@ -3848,7 +3827,7 @@ pub async fn send_message(message: String) -> Result Result (String, String) { /// Get all chat messages from the KV store. #[server(prefix = "/api")] pub async fn get_messages() -> Result, ServerFnError> { - let store = spin_sdk::key_value::Store::open_default().map_err(|e| e.to_string())?; - match store.get_json::>("chat_messages") { + let store = spin_sdk::key_value::Store::open_default().await.map_err(|e| e.to_string())?; + match store.get_json::>("chat_messages").await { Ok(Some(msgs)) => Ok(msgs), Ok(None) => Ok(Vec::new()), Err(e) => { @@ -3938,8 +3918,8 @@ pub async fn get_messages() -> Result, ServerFnError> { /// Clear all chat messages from the KV store. #[server(prefix = "/api")] pub async fn clear_messages() -> Result<(), ServerFnError> { - let store = spin_sdk::key_value::Store::open_default().map_err(|e| e.to_string())?; - let _ = store.delete("chat_messages"); + let store = spin_sdk::key_value::Store::open_default().await.map_err(|e| e.to_string())?; + let _ = store.delete("chat_messages").await; Ok(()) } @@ -4039,8 +4019,8 @@ pub async fn save_chat_turn( thinking: Option, tool_calls: Option>, ) -> Result<(), ServerFnError> { - let store = spin_sdk::key_value::Store::open_default().map_err(|e| e.to_string())?; - let mut history: Vec = match store.get_json::>("chat_messages") { + let store = spin_sdk::key_value::Store::open_default().await.map_err(|e| e.to_string())?; + let mut history: Vec = match store.get_json::>("chat_messages").await { Ok(Some(msgs)) => msgs, _ => Vec::new(), }; @@ -4071,6 +4051,7 @@ pub async fn save_chat_turn( store .set_json("chat_messages", &history) + .await .map_err(|e| ServerFnError::ServerError(e.to_string()))?; Ok(()) @@ -4079,31 +4060,31 @@ pub async fn save_chat_turn( /// List all chat sessions. #[server(prefix = "/api")] pub async fn list_sessions() -> Result, ServerFnError> { - let store = spin_sdk::key_value::Store::open_default().map_err(|e| e.to_string())?; + let store = spin_sdk::key_value::Store::open_default().await.map_err(|e| e.to_string())?; // Check if session_index exists, if not, migrate legacy - let index_exists = store.exists("session_index").unwrap_or(false); + let index_exists = store.exists("session_index").await.unwrap_or(false); if !index_exists { - let legacy_exists = store.exists("chat_messages").unwrap_or(false); + let legacy_exists = store.exists("chat_messages").await.unwrap_or(false); if legacy_exists { - migrate_legacy_messages(&store)?; + migrate_legacy_messages(&store).await?; } else { let empty = SessionIndex { sessions: Vec::new() }; - store.set_json("session_index", &empty).map_err(|e| ServerFnError::ServerError(e.to_string()))?; + store.set_json("session_index", &empty).await.map_err(|e| ServerFnError::ServerError(e.to_string()))?; } } - match store.get_json::("session_index") { + match store.get_json::("session_index").await { Ok(Some(idx)) => Ok(idx.sessions), _ => Ok(Vec::new()), } } #[cfg(feature = "ssr")] -fn migrate_legacy_messages( +async fn migrate_legacy_messages( store: &spin_sdk::key_value::Store, ) -> Result<(), ServerFnError> { - let legacy: Vec = match store.get_json::>("chat_messages") { + let legacy: Vec = match store.get_json::>("chat_messages").await { Ok(Some(msgs)) => msgs, _ => Vec::new(), }; @@ -4137,6 +4118,7 @@ fn migrate_legacy_messages( store .set_json(format!("session_{}", &session.id), &session) + .await .map_err(|e| ServerFnError::ServerError(e.to_string()))?; let index = SessionIndex { @@ -4144,17 +4126,18 @@ fn migrate_legacy_messages( }; store .set_json("session_index", &index) + .await .map_err(|e| ServerFnError::ServerError(e.to_string()))?; - let _ = store.delete("chat_messages"); + let _ = store.delete("chat_messages").await; Ok(()) } /// Create a new session. #[server(prefix = "/api")] pub async fn create_session(title: Option) -> Result> { - let store = spin_sdk::key_value::Store::open_default().map_err(|e| e.to_string())?; - let mut idx = match store.get_json::("session_index") { + let store = spin_sdk::key_value::Store::open_default().await.map_err(|e| e.to_string())?; + let mut idx = match store.get_json::("session_index").await { Ok(Some(idx)) => idx, _ => SessionIndex { sessions: Vec::new() }, }; @@ -4177,10 +4160,10 @@ pub async fn create_session(title: Option) -> Result) -> Result Result<(), ServerFnError> { - let store = spin_sdk::key_value::Store::open_default().map_err(|e| e.to_string())?; - let mut idx = match store.get_json::("session_index") { + let store = spin_sdk::key_value::Store::open_default().await.map_err(|e| e.to_string())?; + let mut idx = match store.get_json::("session_index").await { Ok(Some(idx)) => idx, _ => SessionIndex { sessions: Vec::new() }, }; idx.sessions.retain(|s| s.id != session_id); - store.set_json("session_index", &idx).map_err(|e| ServerFnError::ServerError(e.to_string()))?; + store.set_json("session_index", &idx).await.map_err(|e| ServerFnError::ServerError(e.to_string()))?; - let _ = store.delete(&format!("session_{}", session_id)); + let _ = store.delete(&format!("session_{}", session_id)).await; Ok(()) } /// Rename a session. #[server(prefix = "/api")] pub async fn rename_session(session_id: String, new_title: String) -> Result<(), ServerFnError> { - let store = spin_sdk::key_value::Store::open_default().map_err(|e| e.to_string())?; + let store = spin_sdk::key_value::Store::open_default().await.map_err(|e| e.to_string())?; // Update the session detail key let session_key = format!("session_{}", session_id); - if let Ok(Some(mut sess)) = store.get_json::(&session_key) { + if let Ok(Some(mut sess)) = store.get_json::(&session_key).await { sess.title = new_title.clone(); let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -4217,11 +4200,12 @@ pub async fn rename_session(session_id: String, new_title: String) -> Result<(), sess.updated_at = now; store .set_json(&session_key, &sess) + .await .map_err(|e| ServerFnError::ServerError(e.to_string()))?; } // Update the session meta inside the index - let mut idx = match store.get_json::("session_index") { + let mut idx = match store.get_json::("session_index").await { Ok(Some(idx)) => idx, _ => SessionIndex { sessions: Vec::new() }, }; @@ -4235,6 +4219,7 @@ pub async fn rename_session(session_id: String, new_title: String) -> Result<(), idx.sessions[pos].updated_at = now; store .set_json("session_index", &idx) + .await .map_err(|e| ServerFnError::ServerError(e.to_string()))?; } @@ -4245,8 +4230,8 @@ pub async fn rename_session(session_id: String, new_title: String) -> Result<(), /// Get all message blocks for a session. #[server(prefix = "/api")] pub async fn get_session_blocks(session_id: String) -> Result, ServerFnError> { - let store = spin_sdk::key_value::Store::open_default().map_err(|e| e.to_string())?; - match store.get_json::(&format!("session_{}", session_id)) { + let store = spin_sdk::key_value::Store::open_default().await.map_err(|e| e.to_string())?; + match store.get_json::(&format!("session_{}", session_id)).await { Ok(Some(sess)) => Ok(sess.blocks), _ => Ok(Vec::new()), } @@ -4255,8 +4240,8 @@ pub async fn get_session_blocks(session_id: String) -> Result, /// Save/update all blocks for a session, and auto-update title. #[server(prefix = "/api", input = leptos::server_fn::codec::Json)] pub async fn save_turn_blocks(session_id: String, blocks: Vec) -> Result<(), ServerFnError> { - let store = spin_sdk::key_value::Store::open_default().map_err(|e| e.to_string())?; - let mut sess = match store.get_json::(&format!("session_{}", session_id)) { + let store = spin_sdk::key_value::Store::open_default().await.map_err(|e| e.to_string())?; + let mut sess = match store.get_json::(&format!("session_{}", session_id)).await { Ok(Some(s)) => s, _ => ChatSession::new(session_id.clone()), }; @@ -4296,10 +4281,10 @@ pub async fn save_turn_blocks(session_id: String, blocks: Vec) -> } } - store.set_json(format!("session_{}", session_id), &sess).map_err(|e| ServerFnError::ServerError(e.to_string()))?; + store.set_json(format!("session_{}", session_id), &sess).await.map_err(|e| ServerFnError::ServerError(e.to_string()))?; // Also update session_index - let mut idx = match store.get_json::("session_index") { + let mut idx = match store.get_json::("session_index").await { Ok(Some(idx)) => idx, _ => SessionIndex { sessions: Vec::new() }, }; @@ -4322,7 +4307,7 @@ pub async fn save_turn_blocks(session_id: String, blocks: Vec) -> }); } - store.set_json("session_index", &idx).map_err(|e| ServerFnError::ServerError(e.to_string()))?; + store.set_json("session_index", &idx).await.map_err(|e| ServerFnError::ServerError(e.to_string()))?; Ok(()) } @@ -4351,8 +4336,9 @@ fn get_agent_server_url_encoded(val: &str) -> String { fn get_agent_server_url_any() -> String { #[cfg(feature = "ssr")] { - spin_sdk::variables::get("agent_server_url") - .unwrap_or_else(|_| "http://127.0.0.1:8080".to_string()) + leptos::prelude::use_context::() + .map(|url| url.0) + .unwrap_or_else(|| "http://127.0.0.1:8080".to_string()) } #[cfg(not(feature = "ssr"))] { diff --git a/examples/leptos_ssr_axum/src/lib.rs b/examples/leptos_ssr_axum/src/lib.rs index 1aa96db..deaa238 100644 --- a/examples/leptos_ssr_axum/src/lib.rs +++ b/examples/leptos_ssr_axum/src/lib.rs @@ -1,7 +1,7 @@ #![recursion_limit = "512"] mod app; -mod types; +pub mod types; #[cfg(feature = "ssr")] mod server; diff --git a/examples/leptos_ssr_axum/src/server.rs b/examples/leptos_ssr_axum/src/server.rs index 12db8ed..26f98b3 100644 --- a/examples/leptos_ssr_axum/src/server.rs +++ b/examples/leptos_ssr_axum/src/server.rs @@ -1,59 +1,82 @@ -use any_spawner::Executor as LeptosExecutor; use leptos::config::get_configuration; -use leptos_wasi::{ - handler::HandlerError, - prelude::{IncomingRequest, ResponseOutparam, WasiExecutor}, -}; -use wasi::exports::http::incoming_handler::Guest; -use wasi::http::proxy::export; +use leptos_wasi::executor::init_wasip3_spawner; +use leptos_wasi::prelude::Handler; +use wasip3::http::types::{Request, Response, ErrorCode}; use crate::app::{ shell, App, ClearMessages, GetMessages, SendMessage, SaveChatTurn, ListSessions, CreateSession, GetSessionBlocks, SaveTurnBlocks, DeleteSession, RenameSession, }; +use crate::types::AgentServerUrl; struct LeptosServer; -impl Guest for LeptosServer { - fn handle(request: IncomingRequest, response_out: ResponseOutparam) { - let executor = WasiExecutor::new(leptos_wasi::executor::Mode::Stalled); - if let Err(e) = LeptosExecutor::init_local_custom_executor(executor.clone()) { - eprintln!("Got error while initializing leptos_wasi executor: {e:?}"); - return; - } - executor.run_until(async { - if let Err(e) = handle_request(request, response_out).await { - eprintln!("Got error while handling request: {e:?}"); - } - }) +impl wasip3::exports::http::handler::Guest for LeptosServer { + async fn handle(request: Request) -> Result { + // 1. Initialize host async task scheduling + let _ = init_wasip3_spawner(); + + let conf = get_configuration(None).unwrap(); + let leptos_options = conf.leptos_options; + + // Convert the WASI request to http::Request + let req = wasip3::http_compat::http_from_wasi_request(request)?; + + // Query the variable agent_server_url asynchronously using .await + let agent_url_val = spin_sdk::variables::get("agent_server_url") + .await + .unwrap_or_else(|_| "http://127.0.0.1:8080".to_string()); + let agent_url = AgentServerUrl(agent_url_val); + + // 2. Build and handle request natively + let wasi_res = Handler::build(req).await + .map_err(|e| { + eprintln!("Error building handler: {:?}", e); + ErrorCode::InternalError(None) + })? + .static_files_handler("/pkg", serve_static_files) + .with_server_fn::() + .with_server_fn::() + .with_server_fn::() + .with_server_fn::() + .with_server_fn::() + .with_server_fn::() + .with_server_fn::() + .with_server_fn::() + .with_server_fn::() + .with_server_fn::() + .generate_routes(App) + .handle_with_context( + move || shell(leptos_options.clone()), + move || { + leptos::prelude::provide_context(agent_url.clone()); + } + ) + .await + .map_err(|e| { + eprintln!("Error handling request: {:?}", e); + ErrorCode::InternalError(None) + })?; + + Ok(wasi_res) } } -async fn handle_request( - request: IncomingRequest, - response_out: ResponseOutparam, -) -> Result<(), HandlerError> { - use leptos_wasi::prelude::Handler; - - let conf = get_configuration(None).unwrap(); - let leptos_options = conf.leptos_options; - - Handler::build(request, response_out)? - .with_server_fn::() - .with_server_fn::() - .with_server_fn::() - .with_server_fn::() - .with_server_fn::() - .with_server_fn::() - .with_server_fn::() - .with_server_fn::() - .with_server_fn::() - .with_server_fn::() - .generate_routes(App) - .handle_with_context(move || shell(leptos_options.clone()), || {}) - .await?; - Ok(()) +fn serve_static_files(path: String) -> Option { + use std::fs; + let path = path.strip_prefix("/").unwrap_or(&path); + // Wasmtime mounts site directory at root, so look at /path directly + let file_path = format!("/{}", path); + println!("serving static file: {}", file_path); + + if let Ok(bytes) = fs::read(&file_path) { + Some(leptos_wasi::response::Body::Sync(bytes.into())) + } else { + println!("Could not read file at {}", file_path); + None + } } -export!(LeptosServer with_types_in wasi); +// Export the server for standard WASIp3 http trigger +wasip3::http::service::export!(LeptosServer); diff --git a/examples/leptos_ssr_axum/src/types.rs b/examples/leptos_ssr_axum/src/types.rs index 21aed1f..2b36cd5 100644 --- a/examples/leptos_ssr_axum/src/types.rs +++ b/examples/leptos_ssr_axum/src/types.rs @@ -251,3 +251,7 @@ pub struct ConfirmPayload { pub tool_name: Option, } +#[derive(Clone, Debug)] +pub struct AgentServerUrl(pub String); + + From 47db6bb042864399e92bbd70092eaee71d98b177 Mon Sep 17 00:00:00 2001 From: codeitlikemiley Date: Fri, 29 May 2026 14:24:41 +0800 Subject: [PATCH 2/2] docs: update target to wasm32-wasip2 in README --- examples/leptos_ssr_axum/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/leptos_ssr_axum/README.md b/examples/leptos_ssr_axum/README.md index 99c0df9..245c362 100644 --- a/examples/leptos_ssr_axum/README.md +++ b/examples/leptos_ssr_axum/README.md @@ -24,7 +24,7 @@ Browser ──[HTTP/SSE]──→ Spin Component (WASI Guest) ## Prerequisites Ensure you have the following installed on your system: -- **Rust** (with `wasm32-wasip1` and `wasm32-unknown-unknown` targets added). +- **Rust** (with `wasm32-wasip2` and `wasm32-unknown-unknown` targets added). - **Spin CLI**: Install Spin by following the [Spin Installation Guide](https://developer.fermyon.com/spin/v2/install). - **cargo-leptos**: Install cargo-leptos using cargo: ```sh @@ -73,7 +73,7 @@ spin build --up This will: 1. Run `cargo leptos build --release` to compile client-side hydrate WASM assets and output-style CSS. -2. Compile the server-side logic to the `wasm32-wasip1` target. +2. Compile the server-side logic to the `wasm32-wasip2` target. 3. Start the Spin local web server. Open `http://localhost:3000` (or the port outputted by Spin) in your web browser.