counter-isomorphic mostly works now

This commit is contained in:
Ben Wishovich 2022-11-14 14:52:05 -08:00
parent b9e0255016
commit e9c1846470
17 changed files with 14 additions and 457 deletions

View file

@ -14,10 +14,7 @@ members = [
# examples
"examples/counter",
"examples/counter-isomorphic/client",
"examples/counter-isomorphic/server",
"examples/counter-isomorphic/counter",
"examples/counter-isomorphic-sfa",
"examples/counter-isomorphic",
"examples/counters",
"examples/counters-stable",
"examples/fetch",

View file

@ -9,11 +9,13 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
broadcaster = "1"
console_log = "0.2"
console_error_panic_hook = "0.1"
serde = { version = "1", features = ["derive"] }
futures = "0.3"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
"serde",
] }
@ -21,16 +23,11 @@ leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
log = "0.4"
simple_logger = "2"
gloo = { git = "https://github.com/rustwasm/gloo" }
#counter = { path = "../counter", default-features = false}
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
ssr = ["dep:actix-files", "dep:actix-web", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr"]

View file

@ -1,16 +0,0 @@
[package]
name = "counter-client"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2"
leptos = { path = "../../../leptos", default-features = false, features = ["hydrate", "serde"] }
counter-isomorphic = { path = "../counter", default-features = false, features = ["hydrate"] }
log = "0.4"
wasm-bindgen = "0.2"
console_error_panic_hook = "0.1.7"

View file

@ -1 +0,0 @@
wasm-pack build --target=web --release

View file

@ -1,14 +0,0 @@
use counter_isomorphic::*;
use leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::hydrate(body().unwrap(), |cx| {
view! { cx, <Counters/> }
});
}

View file

@ -1,25 +0,0 @@
[package]
name = "counter-isomorphic"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../../leptos", default-features = false, features = ["serde"] }
leptos_router = { path = "../../../router", default-features = false }
broadcaster = "1"
console_log = "0.2"
futures = "0.3"
gloo = { git = "https://github.com/rustwasm/gloo" }
lazy_static = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
serde = { version = "1", features = ["derive"] }
[dependencies.web-sys]
version = "0.3"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_router/hydrate"]
ssr = ["leptos/ssr", "leptos_router/ssr"]

View file

@ -1,7 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
</head>
<body></body>
</html>

View file

@ -1,242 +0,0 @@
use leptos::*;
use leptos_router::*;
use std::fmt::Debug;
#[cfg(feature = "ssr")]
use std::sync::atomic::{AtomicI32, Ordering};
#[cfg(feature = "ssr")]
use broadcaster::BroadcastChannel;
use serde::{Deserialize, Serialize};
#[cfg(feature = "ssr")]
pub fn register_server_functions() {
GetServerCount::register();
AdjustServerCount::register();
ClearServerCount::register();
}
#[cfg(feature = "ssr")]
static COUNT: AtomicI32 = AtomicI32::new(0);
#[cfg(feature = "ssr")]
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
#[server(GetServerCount, "/api")]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
Ok(COUNT.load(Ordering::Relaxed))
}
#[server(AdjustServerCount, "/api")]
pub async fn adjust_server_count(delta: i32, msg: String) -> Result<i32, ServerFnError> {
let new = COUNT.load(Ordering::Relaxed) + delta;
COUNT.store(new, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&new).await;
println!("message = {:?}", msg);
Ok(new)
}
#[server(ClearServerCount, "/api")]
pub async fn clear_server_count() -> Result<i32, ServerFnError> {
COUNT.store(0, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&0).await;
Ok(0)
}
#[component]
pub fn Counters(cx: Scope) -> Element {
view! {
cx,
<div>
<Router>
<header>
<h1>"Server-Side Counters"</h1>
<p>"Each of these counters stores its data in the same variable on the server."</p>
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
</header>
<nav>
<ul>
<li><A href="">"Simple"</A></li>
<li><A href="form">"Form-Based"</A></li>
<li><A href="multi">"Multi-User"</A></li>
</ul>
</nav>
<main>
<Routes>
<Route path="" element=|cx| view! {
cx,
<Counter/>
}/>
<Route path="form" element=|cx| view! {
cx,
<FormCounter/>
}/>
<Route path="multi" element=|cx| view! {
cx,
<MultiuserCounter/>
}/>
</Routes>
</main>
</Router>
</div>
}
}
// This is an example of "single-user" server functions
// The counter value is loaded from the server, and re-fetches whenever
// it's invalidated by one of the user's own actions
// This is the typical pattern for a CRUD app
#[component]
pub fn Counter(cx: Scope) -> Element {
let dec = create_action(cx, |_| adjust_server_count(-1, "decing".into()));
let inc = create_action(cx, |_| adjust_server_count(1, "incing".into()));
let clear = create_action(cx, |_| clear_server_count());
let counter = create_resource(
cx,
move || (dec.version.get(), inc.version.get(), clear.version.get()),
|_| get_server_count(),
);
let value = move || counter.read().map(|count| count.unwrap_or(0)).unwrap_or(0);
let error_msg = move || {
counter
.read()
.map(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
})
.flatten()
};
view! {
cx,
<div>
<h2>"Simple Counter"</h2>
<p>"This counter sets the value on the server and automatically reloads the new value."</p>
<div>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<span>"Value: " {move || value().to_string()} "!"</span>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
</div>
</div>
}
}
// This is the <Form/> counter
// It uses the same invalidation pattern as the plain counter,
// but uses HTML forms to submit the actions
#[component]
pub fn FormCounter(cx: Scope) -> Element {
let adjust = create_server_action::<AdjustServerCount>(cx);
let clear = create_server_action::<ClearServerCount>(cx);
let counter = create_resource(
cx,
{
let adjust = adjust.version;
let clear = clear.version;
move || (adjust.get(), clear.get())
},
|_| {
log::debug!("FormCounter running fetcher");
get_server_count()
},
);
let value = move || {
log::debug!("FormCounter looking for value");
counter
.read()
.map(|n| n.ok())
.flatten()
.map(|n| n)
.unwrap_or(0)
};
let adjust2 = adjust.clone();
view! {
cx,
<div>
<h2>"Form Counter"</h2>
<p>"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"</p>
<div>
// calling a server function is the same as POSTing to its API URL
// so we can just do that with a form and button
<ActionForm action=clear>
<input type="submit" value="Clear"/>
</ActionForm>
// We can submit named arguments to the server functions
// by including them as input values with the same name
<ActionForm action=adjust>
<input type="hidden" name="delta" value="-1"/>
<input type="hidden" name="msg" value="\"form value down\""/>
<input type="submit" value="-1"/>
</ActionForm>
<span>"Value: " {move || value().to_string()} "!"</span>
<ActionForm action=adjust2>
<input type="hidden" name="delta" value="1"/>
<input type="hidden" name="msg" value="\"form value up\""/>
<input type="submit" value="+1"/>
</ActionForm>
</div>
</div>
}
}
// This is a kind of "multi-user" counter
// It relies on a stream of server-sent events (SSE) for the counter's value
// Whenever another user updates the value, it will update here
// This is the primitive pattern for live chat, collaborative editing, etc.
#[component]
pub fn MultiuserCounter(cx: Scope) -> Element {
let dec = create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
let inc = create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
let clear = create_action(cx, |_| clear_server_count());
#[cfg(not(feature = "ssr"))]
let multiplayer_value = {
use futures::StreamExt;
let mut source = gloo::net::eventsource::futures::EventSource::new("/api/events")
.expect_throw("couldn't connect to SSE stream");
let s = create_signal_from_stream(
cx,
source.subscribe("message").unwrap().map(|value| {
value
.expect_throw("no message event")
.1
.data()
.as_string()
.expect_throw("expected string value")
}),
);
on_cleanup(cx, move || source.close());
s
};
#[cfg(feature = "ssr")]
let multiplayer_value =
create_signal_from_stream(cx, futures::stream::once(Box::pin(async { 0.to_string() })));
view! {
cx,
<div>
<h2>"Multi-User Counter"</h2>
<p>"This one uses server-sent events (SSE) to live-update when other users make changes."</p>
<div>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<span>"Multiplayer Value: " {move || multiplayer_value().unwrap_or_default().to_string()}</span>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
</div>
</div>
}
}

View file

@ -1,8 +0,0 @@
pub use counter_isomorphic::*;
use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <Counter/> });
}

View file

@ -1,20 +0,0 @@
[package]
name = "counter-server"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-files = "0.6"
actix-web = { version = "4" }
futures = "0.3"
leptos = { path = "../../../leptos", default-features = false, features = [
"ssr",
"serde",
] }
leptos_router = { path = "../../../router", default-features = false, features = [
"ssr",
] }
counter-isomorphic = { path = "../counter", default-features = false, features = [
"ssr",
] }
lazy_static = "1"

View file

@ -1,107 +0,0 @@
use actix_files::Files;
use actix_web::*;
use counter_isomorphic::*;
use leptos::*;
use leptos_router::*;
#[get("{tail:.*}")]
async fn render(req: HttpRequest) -> impl Responder {
let path = req.path();
let path = "http://leptos".to_string() + path;
println!("path = {path}");
HttpResponse::Ok().content_type("text/html").body(format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Isomorphic Counter</title>
</head>
<body>
{}
</body>
<script type="module">import init, {{ main }} from './pkg/counter_client.js'; init().then(main);</script>
</html>"#,
run_scope({
move |cx| {
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
view! { cx, <Counters/>}
}
})
))
}
#[post("/api/{tail:.*}")]
async fn handle_server_fns(
req: HttpRequest,
params: web::Path<String>,
body: web::Bytes,
) -> impl Responder {
let path = params.into_inner();
let accept_header = req
.headers()
.get("Accept")
.and_then(|value| value.to_str().ok());
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
let body: &[u8] = &body;
match server_fn(&body).await {
Ok(serialized) => {
// if this is Accept: application/json then send a serialized JSON response
if let Some("application/json") = accept_header {
HttpResponse::Ok().body(serialized)
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
else {
HttpResponse::SeeOther()
.insert_header(("Location", "/"))
.content_type("application/json")
.body(serialized)
}
}
Err(e) => {
eprintln!("server function error: {e:#?}");
HttpResponse::InternalServerError().body(e.to_string())
}
}
} else {
HttpResponse::BadRequest().body(format!("Could not find a server function at that route."))
}
}
#[get("/api/events")]
async fn counter_events() -> impl Responder {
use futures::StreamExt;
let stream =
futures::stream::once(async { counter_isomorphic::get_server_count().await.unwrap_or(0) })
.chain(COUNT_CHANNEL.clone())
.map(|value| {
Ok(web::Bytes::from(format!(
"event: message\ndata: {value}\n\n"
))) as Result<web::Bytes>
});
HttpResponse::Ok()
.insert_header(("Content-Type", "text/event-stream"))
.streaming(stream)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
counter_isomorphic::register_server_functions();
HttpServer::new(|| {
App::new()
.service(Files::new("/pkg", "../client/pkg"))
.service(counter_events)
.service(handle_server_fns)
.service(render)
//.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8081))?
.run()
.await
}

View file

@ -1,6 +1,5 @@
use leptos::*;
use leptos_router::*;
use serde::{Deserialize, Serialize};
#[cfg(feature = "ssr")]
use std::sync::atomic::{AtomicI32, Ordering};

View file

@ -20,6 +20,8 @@ pub fn App(cx: Scope) -> Element {
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn main() {
console_error_panic_hook::set_once();

View file

@ -1,6 +1,5 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
mod counters;
@ -8,6 +7,9 @@ mod counters;
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use actix_files::{Files};
use actix_web::*;
use crate::counters::*;
#[get("{tail:.*}")]
async fn render(req: HttpRequest) -> impl Responder {
@ -26,7 +28,7 @@ cfg_if! {
<body>
{}
</body>
<script type="module">import init, {{ main }} from './pkg/counter_client.js'; init().then(main);</script>
<script type="module">import init, {{ main }} from './pkg/leptos_counter_isomorphic.js'; init().then(main);</script>
</html>"#,
run_scope({
move |cx| {
@ -82,7 +84,7 @@ cfg_if! {
use futures::StreamExt;
let stream =
futures::stream::once(async { counter_isomorphic::get_server_count().await.unwrap_or(0) })
futures::stream::once(async { crate::counters::get_server_count().await.unwrap_or(0) })
.chain(COUNT_CHANNEL.clone())
.map(|value| {
Ok(web::Bytes::from(format!(
@ -96,11 +98,11 @@ cfg_if! {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
counter_isomorphic::register_server_functions();
crate::counters::register_server_functions();
HttpServer::new(|| {
App::new()
.service(Files::new("/pkg", "../client/pkg"))
.service(Files::new("/pkg", "./pkg"))
.service(counter_events)
.service(handle_server_fns)
.service(render)