mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 13:43:01 -05:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a10ab613b8 | ||
|
|
0eebe9e289 | ||
|
|
2abbdb6594 | ||
|
|
8f8f3e23e4 | ||
|
|
aab952357e | ||
|
|
f1ebf77fa6 | ||
|
|
5cc2f3858d | ||
|
|
8252655959 | ||
|
|
14e47e87ba | ||
|
|
4a8cfad7c5 | ||
|
|
d9f52dad76 | ||
|
|
3a8508df6c | ||
|
|
865c6df483 | ||
|
|
c1d7f0f8d1 | ||
|
|
8c2dd73b70 | ||
|
|
d5894555cc | ||
|
|
2ef1723607 | ||
|
|
13f7387d45 | ||
|
|
0a13f7c08c | ||
|
|
7c83904aea | ||
|
|
6e13ff9787 | ||
|
|
234d138f03 | ||
|
|
97110cd5ac | ||
|
|
5acc1b1a5a | ||
|
|
f3987246cb | ||
|
|
e5149fb348 | ||
|
|
d67ff03568 | ||
|
|
1dbca3005d | ||
|
|
af61be0c72 | ||
|
|
76facf9539 | ||
|
|
0e73d18d7b | ||
|
|
d306a15f86 | ||
|
|
bf95648dc9 | ||
|
|
00edfc0e0a | ||
|
|
396327b667 | ||
|
|
a437289f81 | ||
|
|
58e7897db6 | ||
|
|
1be1f41fba | ||
|
|
7b8cd90a6e | ||
|
|
d0ef7b904d | ||
|
|
7904e0c395 | ||
|
|
7b4c470155 | ||
|
|
98eccc9eb8 | ||
|
|
70d06e2716 |
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
@@ -7,7 +7,6 @@ updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directories:
|
||||
- "/"
|
||||
- "/examples/*"
|
||||
- "/benchmarks"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
13
.github/workflows/run-cargo-make-task.yml
vendored
13
.github/workflows/run-cargo-make-task.yml
vendored
@@ -19,6 +19,19 @@ jobs:
|
||||
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
run: |
|
||||
echo "Disk space before cleanup:"
|
||||
df -h
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||
sudo rm -rf /usr/local/lib/android/sdk/ndk
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/share/boost
|
||||
sudo apt-get clean
|
||||
echo "Disk space after cleanup:"
|
||||
df -h
|
||||
# Setup environment
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Rust
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -3,7 +3,9 @@ dist
|
||||
pkg
|
||||
comparisons
|
||||
blob.rs
|
||||
Cargo.lock
|
||||
**/projects/**/Cargo.lock
|
||||
**/examples/**/Cargo.lock
|
||||
**/benchmarks/**/Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.idea
|
||||
@@ -11,4 +13,4 @@ Cargo.lock
|
||||
.envrc
|
||||
|
||||
.vscode
|
||||
vendor
|
||||
vendor
|
||||
|
||||
4474
Cargo.lock
generated
Normal file
4474
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
Cargo.toml
42
Cargo.toml
@@ -40,36 +40,36 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.0-gamma3"
|
||||
version = "0.7.0-rc1"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-gamma3" }
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-rc1" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
|
||||
either_of = { path = "./either_of/", version = "0.1.0" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-gamma3" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-gamma3" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-gamma3" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-gamma3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-gamma3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-gamma3" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-gamma3" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-gamma3" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-gamma3" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-gamma3" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-gamma3" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-gamma3" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-rc1" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-rc1" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-rc1" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-rc1" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-rc1" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-rc1" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-rc1" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-rc1" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-rc1" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-rc1" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-rc1" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-rc1" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-gamma3" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-gamma3" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-gamma3" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-gamma3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-gamma3" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-gamma3" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-gamma3" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-rc1" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-rc1" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-rc1" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-rc1" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-rc1" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-rc1" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-rc1" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "throw_error"
|
||||
version = "0.2.0-gamma3"
|
||||
version = "0.2.0-rc1"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -10,4 +10,4 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = "0.2.14"
|
||||
pin-project-lite = "0.2.15"
|
||||
|
||||
@@ -10,14 +10,14 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-executor = { version = "1.13.1", optional = true }
|
||||
futures = "0.3.30"
|
||||
glib = { version = "0.20.0", optional = true }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.39", optional = true, default-features = false, features = [
|
||||
futures = "0.3.31"
|
||||
glib = { version = "0.20.5", optional = true }
|
||||
thiserror = "2.0"
|
||||
tokio = { version = "1.41", optional = true, default-features = false, features = [
|
||||
"rt",
|
||||
] }
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.42", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.45", optional = true }
|
||||
|
||||
[features]
|
||||
async-executor = ["dep:async-executor"]
|
||||
|
||||
@@ -291,9 +291,10 @@ impl Executor {
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
pub fn init_custom_executor(
|
||||
custom_executor: impl CustomExecutor + 'static,
|
||||
custom_executor: impl CustomExecutor + Send + Sync + 'static,
|
||||
) -> Result<(), ExecutorError> {
|
||||
static EXECUTOR: OnceLock<Box<dyn CustomExecutor>> = OnceLock::new();
|
||||
static EXECUTOR: OnceLock<Box<dyn CustomExecutor + Send + Sync>> =
|
||||
OnceLock::new();
|
||||
EXECUTOR
|
||||
.set(Box::new(custom_executor))
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
@@ -311,13 +312,46 @@ impl Executor {
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Locally sets a custom executor as the executor used to spawn tasks
|
||||
/// in the current thread.
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
pub fn init_local_custom_executor(
|
||||
custom_executor: impl CustomExecutor + 'static,
|
||||
) -> Result<(), ExecutorError> {
|
||||
thread_local! {
|
||||
static EXECUTOR: OnceLock<Box<dyn CustomExecutor>> = OnceLock::new();
|
||||
}
|
||||
EXECUTOR.with(|this| {
|
||||
this.set(Box::new(custom_executor))
|
||||
.map_err(|_| ExecutorError::AlreadySet)
|
||||
})?;
|
||||
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
EXECUTOR.with(|this| this.get().unwrap().spawn(fut));
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
EXECUTOR.with(|this| this.get().unwrap().spawn_local(fut));
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| {
|
||||
EXECUTOR.with(|this| this.get().unwrap().poll_local());
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for custom executors.
|
||||
/// Custom executors can be used to integrate with any executor that supports spawning futures.
|
||||
///
|
||||
/// All methods can be called recursively.
|
||||
pub trait CustomExecutor: Send + Sync {
|
||||
pub trait CustomExecutor {
|
||||
/// Spawns a future, usually on a thread pool.
|
||||
fn spawn(&self, fut: PinnedFuture<()>);
|
||||
/// Spawns a local future. May require calling `poll_local` to make progress.
|
||||
|
||||
@@ -10,4 +10,4 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = "0.2.14"
|
||||
pin-project-lite = "0.2.15"
|
||||
|
||||
@@ -26,6 +26,7 @@ async fn main() {
|
||||
};
|
||||
use axum_js_ssr::app::*;
|
||||
use http_body_util::BodyExt;
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use counter::*;
|
||||
use leptos::mount::mount_to;
|
||||
use leptos::prelude::*;
|
||||
|
||||
@@ -63,7 +63,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use counter_without_macros::counter;
|
||||
use leptos::{prelude::*, task::tick};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use directives::App;
|
||||
use leptos::{prelude::*, task::tick};
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
@@ -6,9 +6,7 @@ use leptos_axum::ResponseOptions;
|
||||
// A basic function to display errors served by the error boundaries.
|
||||
// Feel free to do more complicated things here than just displaying them.
|
||||
#[component]
|
||||
pub fn ErrorTemplate(
|
||||
#[prop(into)] errors: MaybeSignal<Errors>,
|
||||
) -> impl IntoView {
|
||||
pub fn ErrorTemplate(#[prop(into)] errors: Signal<Errors>) -> impl IntoView {
|
||||
// Get Errors from Signal
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors = Memo::new(move |_| {
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, RoutingProgress},
|
||||
ParamSegment, StaticSegment,
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use std::time::Duration;
|
||||
@@ -28,9 +28,7 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -56,7 +56,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, RoutingProgress},
|
||||
ParamSegment, StaticSegment,
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use std::time::Duration;
|
||||
@@ -46,9 +46,7 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router},
|
||||
ParamSegment, StaticSegment,
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -42,9 +42,7 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, RoutingProgress},
|
||||
ParamSegment, StaticSegment,
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use std::time::Duration;
|
||||
@@ -46,9 +46,7 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -6,7 +6,7 @@ fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
let handle = mount_to(
|
||||
helpers::document()
|
||||
document()
|
||||
.get_element_by_id("app")
|
||||
.unwrap()
|
||||
.unchecked_into(),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use portal::App;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
@@ -10,7 +10,7 @@ struct Then {
|
||||
// the type with Option<...> and marking the option as #[prop(optional)].
|
||||
#[slot]
|
||||
struct ElseIf {
|
||||
cond: MaybeSignal<bool>,
|
||||
cond: Signal<bool>,
|
||||
children: ChildrenFn,
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ struct Fallback {
|
||||
// Slots are added to components like any other prop.
|
||||
#[component]
|
||||
fn SlotIf(
|
||||
cond: MaybeSignal<bool>,
|
||||
cond: Signal<bool>,
|
||||
then: Then,
|
||||
#[prop(default=vec![])] else_if: Vec<ElseIf>,
|
||||
#[prop(optional)] fallback: Option<Fallback>,
|
||||
@@ -43,9 +43,9 @@ fn SlotIf(
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let (count, set_count) = signal(0);
|
||||
let is_even = MaybeSignal::derive(move || count.get() % 2 == 0);
|
||||
let is_div5 = MaybeSignal::derive(move || count.get() % 5 == 0);
|
||||
let is_div7 = MaybeSignal::derive(move || count.get() % 7 == 0);
|
||||
let is_even = Signal::derive(move || count.get() % 2 == 0);
|
||||
let is_div5 = Signal::derive(move || count.get() % 5 == 0);
|
||||
let is_div7 = Signal::derive(move || count.get() % 7 == 0);
|
||||
|
||||
view! {
|
||||
<button on:click=move |_| set_count.update(|value| *value += 1)>"+1"</button>
|
||||
|
||||
@@ -39,7 +39,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::Router;
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use ssr_modes_axum::app::*;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::Router;
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list_with_ssg, LeptosRoutes};
|
||||
use static_routing::app::*;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use chrono::{Local, NaiveDate};
|
||||
use leptos::logging::warn;
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::{Field, Patch, Store};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -109,11 +110,7 @@ pub fn App() -> impl IntoView {
|
||||
// directly implements IntoIterator, so we can use it in <For/> and
|
||||
// it will manage reactivity for the store fields correctly
|
||||
<For
|
||||
each=move || {
|
||||
leptos::logging::log!("RERUNNING FOR CALCULATION");
|
||||
store.todos()
|
||||
}
|
||||
|
||||
each=move || store.todos()
|
||||
key=|row| row.id().get()
|
||||
let:todo
|
||||
>
|
||||
|
||||
@@ -10,6 +10,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
actix-files = { version = "0.6.6", optional = true }
|
||||
actix-web = { version = "4.8", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
js-sys = { version = "0.3.70", optional = true }
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
@@ -19,7 +20,10 @@ serde = "1.0"
|
||||
tokio = { version = "1.39", features = ["time", "rt"], optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
hydrate = [
|
||||
"dep:js-sys",
|
||||
"leptos/hydrate",
|
||||
]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
@check_instrumented
|
||||
Feature: Instrumented Counters showing the expected values
|
||||
|
||||
Scenario: I can fresh CSR instrumented counters
|
||||
Given I see the app
|
||||
When I access the instrumented counters via CSR
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 0 |
|
||||
| item_inspect | 0 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: I should see counter going up after viewing Item Listing
|
||||
Given I see the app
|
||||
When I select the following links
|
||||
| Instrumented |
|
||||
| Item Listing |
|
||||
| Counters |
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 1 |
|
||||
| item_overview | 0 |
|
||||
| item_inspect | 0 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 1 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
# the reload has happened in Item Listing, it follows a suspend
|
||||
# will be called as hydration happens.
|
||||
Scenario: Refreshing Item Listing should have only suspend counters
|
||||
Given I see the app
|
||||
When I access the instrumented counters via SSR
|
||||
And I select the component Item Listing
|
||||
And I reload the page
|
||||
And I select the component Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 1 |
|
||||
| item_overview | 0 |
|
||||
| item_inspect | 0 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Reset CSR Counters work as expected.
|
||||
Given I see the app
|
||||
When I access the instrumented counters via SSR
|
||||
And I select the component Item Listing
|
||||
And I click on Reset CSR Counters
|
||||
And I select the component Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 0 |
|
||||
| item_inspect | 0 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Standard usage of the instruments traversing down
|
||||
Given I see the app
|
||||
When I select the following links
|
||||
| Instrumented |
|
||||
| Item Listing |
|
||||
| Item 2 |
|
||||
| Inspect path3 |
|
||||
| Inspect path3/field1 |
|
||||
And I access the instrumented counters via CSR
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 1 |
|
||||
| item_overview | 1 |
|
||||
| item_inspect | 2 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 1 |
|
||||
| get_item | 1 |
|
||||
| inspect_item_root | 1 |
|
||||
| inspect_item_field | 1 |
|
||||
@@ -0,0 +1,187 @@
|
||||
@check_instrumented_suspense_resource
|
||||
Feature: Using instrumented counters for real
|
||||
Check that the suspend/suspense and the underlying resources are
|
||||
called with the expected number of times for CSR rendering.
|
||||
|
||||
Background:
|
||||
|
||||
Given I see the app
|
||||
And I select the mode Instrumented
|
||||
|
||||
Scenario: Emulate steps 1 to 5 of issue #2961
|
||||
Given I select the link Target 3##
|
||||
And I refresh the page
|
||||
When I select the following links
|
||||
| Item Listing |
|
||||
| Target 4## |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 1 |
|
||||
| item_overview | 2 |
|
||||
| item_inspect | 0 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 1 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Emulate step 6 of issue #2961
|
||||
Given I select the link Target 41#
|
||||
And I refresh the page
|
||||
When I select the following links
|
||||
| Target 4## |
|
||||
| Target 42# |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 1 |
|
||||
| item_inspect | 2 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 1 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Emulate step 7 of issue #2961
|
||||
Given I select the link Target 42#
|
||||
And I refresh the page
|
||||
When I select the following links
|
||||
| Target 4## |
|
||||
| Target 42# |
|
||||
| Target 41# |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 1 |
|
||||
| item_inspect | 3 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 2 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Emulate step 8, "not trigger double fetch".
|
||||
Given I select the link Target 3##
|
||||
And I refresh the page
|
||||
When I select the following links
|
||||
| Item Listing |
|
||||
| Target 4## |
|
||||
| Target 41# |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 1 |
|
||||
| item_overview | 2 |
|
||||
| item_inspect | 1 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 1 |
|
||||
| inspect_item_root | 1 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Like above, for the "double fetch" which shouldn't happen
|
||||
Given I select the link Target 3##
|
||||
And I refresh the page
|
||||
When I select the following links
|
||||
| Item Listing |
|
||||
| Target 4## |
|
||||
| Target 41# |
|
||||
| Target 3## |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 1 |
|
||||
| item_overview | 3 |
|
||||
| item_inspect | 1 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 2 |
|
||||
| inspect_item_root | 1 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Like above, but using 4## instead
|
||||
Given I select the link Target 3##
|
||||
And I refresh the page
|
||||
When I select the following links
|
||||
| Item Listing |
|
||||
| Target 4## |
|
||||
| Target 41# |
|
||||
| Target 4## |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 1 |
|
||||
| item_overview | 3 |
|
||||
| item_inspect | 1 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 1 |
|
||||
| inspect_item_root | 1 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
# The following tests previously showed the clear difference between
|
||||
# hydration and CSR, where hydration resulting in extra server API
|
||||
# calls via the resource while CSR did not suffer from the issue.
|
||||
# With #3182 merged the issue is corrected, going up to components
|
||||
# specified by the parent route should no longer result in the
|
||||
# superfluous fetches for resources needed by component about to be
|
||||
# unmounted.
|
||||
Scenario: Emulate part of step 8 of issue #2961
|
||||
Given I select the link Target 3##
|
||||
And I refresh the page
|
||||
When I select the link Item Listing
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Emulate above, instead of refresh page, reset csr counters
|
||||
Given I select the link Target 3##
|
||||
And I click on Reset CSR Counters
|
||||
When I select the link Item Listing
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
# Further two sets for good measure.
|
||||
Scenario: Start with hydration from Target 41# and go up
|
||||
Given I select the link Target 41#
|
||||
And I refresh the page
|
||||
When I select the link Target 4##
|
||||
And I select the link Item Listing
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Emulate the same csr counter reset, for Target 41#.
|
||||
Given I select the link Target 41#
|
||||
And I click on Reset CSR Counters
|
||||
When I select the link Target 4##
|
||||
And I select the link Item Listing
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
@@ -37,3 +37,19 @@ pub async fn click_second_button(client: &Client) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn click_reset_counters_button(client: &Client) -> Result<()> {
|
||||
let reset_counter = find::reset_counter(client).await?;
|
||||
|
||||
reset_counter.click().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn click_reset_csr_counters_button(client: &Client) -> Result<()> {
|
||||
let reset_counter = find::reset_csr_counter(client).await?;
|
||||
|
||||
reset_counter.click().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -63,3 +63,21 @@ pub async fn second_count_is(client: &Client, expected: u32) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn instrumented_counts(
|
||||
client: &Client,
|
||||
expected: &[(&str, u32)],
|
||||
) -> Result<()> {
|
||||
let mut actual = Vec::<(&str, u32)>::new();
|
||||
|
||||
for (selector, _) in expected.iter() {
|
||||
actual.push((
|
||||
selector,
|
||||
find::instrumented_count(client, selector).await?,
|
||||
))
|
||||
}
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -77,6 +77,43 @@ pub async fn second_button(client: &Client) -> Result<Element> {
|
||||
Ok(counter_button)
|
||||
}
|
||||
|
||||
pub async fn instrumented_count(
|
||||
client: &Client,
|
||||
selector: &str,
|
||||
) -> Result<u32> {
|
||||
let element = client
|
||||
.wait()
|
||||
.for_element(Locator::Id(selector))
|
||||
.await
|
||||
.expect(format!("Element #{selector} not found.")
|
||||
.as_str());
|
||||
let text = element.text().await?;
|
||||
let count = text.parse::<u32>()
|
||||
.expect(format!("Element #{selector} does not contain a number.")
|
||||
.as_str());
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub async fn reset_counter(client: &Client) -> Result<Element> {
|
||||
let reset_button = client
|
||||
.wait()
|
||||
.for_element(Locator::Id("reset-counters"))
|
||||
.await
|
||||
.expect("Reset counter input not found");
|
||||
|
||||
Ok(reset_button)
|
||||
}
|
||||
|
||||
pub async fn reset_csr_counter(client: &Client) -> Result<Element> {
|
||||
let reset_button = client
|
||||
.wait()
|
||||
.for_element(Locator::Id("reset-csr-counters"))
|
||||
.await
|
||||
.expect("Reset CSR counter input not found");
|
||||
|
||||
Ok(reset_button)
|
||||
}
|
||||
|
||||
async fn component_message(client: &Client, id: &str) -> Result<String> {
|
||||
let element =
|
||||
client.wait().for_element(Locator::Id(id)).await.expect(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::fixtures::{action, world::AppWorld};
|
||||
use anyhow::{Ok, Result};
|
||||
use cucumber::{given, when};
|
||||
use cucumber::{given, when, gherkin::Step};
|
||||
|
||||
#[given("I see the app")]
|
||||
#[when("I open the app")]
|
||||
@@ -12,19 +12,13 @@ async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
|
||||
}
|
||||
|
||||
#[given(regex = r"^I select the mode (.*)$")]
|
||||
async fn i_select_the_mode(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_link(client, &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(regex = r"^I select the component (.*)$")]
|
||||
#[when(regex = "^I select the component (.*)$")]
|
||||
async fn i_select_the_component(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
#[given(regex = "^I select the link (.*)$")]
|
||||
#[when(regex = "^I select the link (.*)$")]
|
||||
#[when(regex = "^I click on the link (.*)$")]
|
||||
#[when(regex = "^I go check the (.*)$")]
|
||||
async fn i_select_the_link(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_link(client, &text).await?;
|
||||
|
||||
@@ -59,3 +53,69 @@ async fn i_click_the_second_button_n_times(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(regex = "^I (refresh|reload) the (browser|page)$")]
|
||||
#[when(regex = "^I (refresh|reload) the (browser|page)$")]
|
||||
async fn i_refresh_the_browser(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
client.refresh().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[when(expr = "I click on Reset Counters")]
|
||||
async fn i_click_on_reset_counters(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_reset_counters_button(client).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(expr = "I click on Reset CSR Counters")]
|
||||
#[when(expr = "I click on Reset CSR Counters")]
|
||||
async fn i_click_on_reset_csr_counters(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_reset_csr_counters_button(client).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[when(expr = "I access the instrumented counters via SSR")]
|
||||
async fn i_access_the_instrumented_counters_page_via_ssr(
|
||||
world: &mut AppWorld,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_link(client, "Instrumented").await?;
|
||||
action::click_link(client, "Counters").await?;
|
||||
client.refresh().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[when(expr = "I access the instrumented counters via CSR")]
|
||||
async fn i_access_the_instrumented_counters_page_via_csr(
|
||||
world: &mut AppWorld,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_link(client, "Instrumented").await?;
|
||||
action::click_link(client, "Counters").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(expr = "I select the following links")]
|
||||
#[when(expr = "I select the following links")]
|
||||
async fn i_select_the_following_links(
|
||||
world: &mut AppWorld,
|
||||
step: &Step,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
|
||||
if let Some(table) = step.table.as_ref() {
|
||||
for row in table.rows.iter() {
|
||||
action::click_link(client, &row[0]).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::fixtures::{check, world::AppWorld};
|
||||
use anyhow::{Ok, Result};
|
||||
use cucumber::then;
|
||||
use cucumber::{then, gherkin::Step};
|
||||
|
||||
#[then(regex = r"^I see the page title is (.*)$")]
|
||||
async fn i_see_the_page_title_is(
|
||||
@@ -79,3 +79,23 @@ async fn i_see_the_second_count_is(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(expr = "I see the following counters under section")]
|
||||
#[then(expr = "the following counters under section")]
|
||||
async fn i_see_the_following_counters_under_section(
|
||||
world: &mut AppWorld,
|
||||
step: &Step,
|
||||
) -> Result<()> {
|
||||
// FIXME ideally check the mode; for now leave it because effort
|
||||
let client = &world.client;
|
||||
if let Some(table) = step.table.as_ref() {
|
||||
let expected = table.rows
|
||||
.iter()
|
||||
.skip(1)
|
||||
.map(|row| (row[0].as_str(), row[1].parse::<u32>().unwrap()))
|
||||
.collect::<Vec<_>>();
|
||||
check::instrumented_counts(client, &expected).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::instrumented::InstrumentedRoutes;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::{
|
||||
components::{Outlet, ParentRoute, Redirect, Route, Router, Routes, A},
|
||||
@@ -41,6 +42,7 @@ pub fn App() -> impl IntoView {
|
||||
<A href="/out-of-order">"Out-of-Order"</A>
|
||||
<A href="/in-order">"In-Order"</A>
|
||||
<A href="/async">"Async"</A>
|
||||
<A href="/instrumented/">"Instrumented"</A>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes fallback=|| "Page not found.">
|
||||
@@ -110,6 +112,7 @@ pub fn App() -> impl IntoView {
|
||||
<Route path=StaticSegment("local") view=LocalResource/>
|
||||
<Route path=StaticSegment("none") view=None/>
|
||||
</ParentRoute>
|
||||
<InstrumentedRoutes/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
667
examples/suspense_tests/src/instrumented.rs
Normal file
667
examples/suspense_tests/src/instrumented.rs
Normal file
@@ -0,0 +1,667 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::{
|
||||
components::{ParentRoute, Route, A},
|
||||
hooks::use_params,
|
||||
nested_router::Outlet,
|
||||
params::Params,
|
||||
MatchNestedRoutes, ParamSegment, SsrMode, StaticSegment, WildcardSegment,
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub(super) mod counter {
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
atomic::{AtomicU32, Ordering},
|
||||
LazyLock, Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Counter(AtomicU32);
|
||||
|
||||
impl Counter {
|
||||
pub const fn new() -> Self {
|
||||
Self(AtomicU32::new(0))
|
||||
}
|
||||
|
||||
pub fn get(&self) -> u32 {
|
||||
self.0.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn inc(&self) -> u32 {
|
||||
self.0.fetch_add(1, Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn reset(&self) {
|
||||
self.0.store(0, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Counters {
|
||||
pub list_items: Counter,
|
||||
pub get_item: Counter,
|
||||
pub inspect_item_root: Counter,
|
||||
pub inspect_item_field: Counter,
|
||||
}
|
||||
|
||||
impl From<&mut Counters> for super::Counters {
|
||||
fn from(counter: &mut Counters) -> Self {
|
||||
Self {
|
||||
get_item: counter.get_item.get(),
|
||||
inspect_item_root: counter.inspect_item_root.get(),
|
||||
inspect_item_field: counter.inspect_item_field.get(),
|
||||
list_items: counter.list_items.get(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Counters {
|
||||
pub fn reset(&self) {
|
||||
self.get_item.reset();
|
||||
self.inspect_item_root.reset();
|
||||
self.inspect_item_field.reset();
|
||||
self.list_items.reset();
|
||||
}
|
||||
}
|
||||
|
||||
pub static COUNTERS: LazyLock<Mutex<HashMap<u64, Counters>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||
pub struct Item {
|
||||
id: i64,
|
||||
name: Option<String>,
|
||||
field: Option<String>,
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn list_items(ticket: u64) -> Result<Vec<i64>, ServerFnError> {
|
||||
// emulate database query overhead
|
||||
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
|
||||
(*counter::COUNTERS)
|
||||
.lock()
|
||||
.expect("somehow panicked elsewhere")
|
||||
.entry(ticket)
|
||||
.or_default()
|
||||
.list_items
|
||||
.inc();
|
||||
Ok(vec![1, 2, 3, 4])
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||
pub struct GetItemResult(pub Item, pub Vec<String>);
|
||||
|
||||
#[server]
|
||||
async fn get_item(
|
||||
ticket: u64,
|
||||
id: i64,
|
||||
) -> Result<GetItemResult, ServerFnError> {
|
||||
// emulate database query overhead
|
||||
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
|
||||
(*counter::COUNTERS)
|
||||
.lock()
|
||||
.expect("somehow panicked elsewhere")
|
||||
.entry(ticket)
|
||||
.or_default()
|
||||
.get_item
|
||||
.inc();
|
||||
let name = None::<String>;
|
||||
let field = None::<String>;
|
||||
Ok(GetItemResult(
|
||||
Item { id, name, field },
|
||||
["path1", "path2", "path3"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||
pub struct InspectItemResult(pub Item, pub String, pub Vec<String>);
|
||||
|
||||
#[server]
|
||||
async fn inspect_item(
|
||||
ticket: u64,
|
||||
id: i64,
|
||||
path: String,
|
||||
) -> Result<InspectItemResult, ServerFnError> {
|
||||
// emulate database query overhead
|
||||
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
|
||||
let mut split = path.split('/');
|
||||
let name = split.next().map(str::to_string);
|
||||
let path = name
|
||||
.clone()
|
||||
.expect("name should have been defined at this point");
|
||||
let field = split
|
||||
.next()
|
||||
.and_then(|s| (!s.is_empty()).then(|| s.to_string()));
|
||||
if field.is_none() {
|
||||
(*counter::COUNTERS)
|
||||
.lock()
|
||||
.expect("somehow panicked elsewhere")
|
||||
.entry(ticket)
|
||||
.or_default()
|
||||
.inspect_item_root
|
||||
.inc();
|
||||
} else {
|
||||
(*counter::COUNTERS)
|
||||
.lock()
|
||||
.expect("somehow panicked elsewhere")
|
||||
.entry(ticket)
|
||||
.or_default()
|
||||
.inspect_item_field
|
||||
.inc();
|
||||
}
|
||||
Ok(InspectItemResult(
|
||||
Item { id, name, field },
|
||||
path,
|
||||
["field1", "field2", "field3"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||
pub struct Counters {
|
||||
pub get_item: u32,
|
||||
pub inspect_item_root: u32,
|
||||
pub inspect_item_field: u32,
|
||||
pub list_items: u32,
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn get_counters(ticket: u64) -> Result<Counters, ServerFnError> {
|
||||
Ok((*counter::COUNTERS)
|
||||
.lock()
|
||||
.expect("somehow panicked elsewhere")
|
||||
.entry(ticket)
|
||||
.or_default()
|
||||
.into())
|
||||
}
|
||||
|
||||
#[server(ResetCounters)]
|
||||
async fn reset_counters(ticket: u64) -> Result<(), ServerFnError> {
|
||||
(*counter::COUNTERS)
|
||||
.lock()
|
||||
.expect("somehow panicked elsewhere")
|
||||
.entry(ticket)
|
||||
.or_default()
|
||||
.reset();
|
||||
// leptos::logging::log!("counters for ticket {ticket} have been reset");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SuspenseCounters {
|
||||
item_overview: u32,
|
||||
item_inspect: u32,
|
||||
item_listing: u32,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn InstrumentedRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
// TODO should make this mode configurable via feature flag?
|
||||
let ssr = SsrMode::Async;
|
||||
view! {
|
||||
<ParentRoute path=StaticSegment("instrumented") view=InstrumentedRoot ssr>
|
||||
<Route path=StaticSegment("/") view=InstrumentedTop/>
|
||||
<ParentRoute path=StaticSegment("item") view=ItemRoot>
|
||||
<Route path=StaticSegment("/") view=ItemListing/>
|
||||
<ParentRoute path=ParamSegment("id") view=ItemTop>
|
||||
<Route path=StaticSegment("/") view=ItemOverview/>
|
||||
<Route path=WildcardSegment("path") view=ItemInspect/>
|
||||
</ParentRoute>
|
||||
</ParentRoute>
|
||||
<Route path=StaticSegment("counters") view=ShowCounters/>
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Ticket(pub u64);
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct CSRTicket(pub u64);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn inst_ticket() -> u64 {
|
||||
// SSR will always use 0 for the ticket
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn inst_ticket() -> u64 {
|
||||
// CSR will use a random number for the ticket
|
||||
(js_sys::Math::random() * ((u64::MAX - 1) as f64) + 1f64) as u64
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn InstrumentedRoot() -> impl IntoView {
|
||||
let counters = RwSignal::new(SuspenseCounters::default());
|
||||
provide_context(counters);
|
||||
provide_field_nav_portlet_context();
|
||||
|
||||
// Generate a ID directly on this component. Rather than relying on
|
||||
// additional server functions, doing it this way emulates more
|
||||
// standard workflows better and to avoid having to add another
|
||||
// thing to instrument/interfere with the typical use case.
|
||||
// Downside is that randomness has a chance to conflict.
|
||||
//
|
||||
// Furthermore, this approach **will** result in unintuitive
|
||||
// behavior when it isn't accounted for - specifically, the reason
|
||||
// for this design is that when SSR it will guarantee usage of `0`
|
||||
// as the ticket, while CSR it will be of some other value as the
|
||||
// version it uses will be random. However, when trying to get back
|
||||
// the counters associated with the ticket, rendering using SSR will
|
||||
// always produce the SSR version and this quirk will need to be
|
||||
// accounted for.
|
||||
let ticket = inst_ticket();
|
||||
// leptos::logging::log!(
|
||||
// "Ticket for this InstrumentedRoot instance: {ticket}"
|
||||
// );
|
||||
provide_context(Ticket(ticket));
|
||||
|
||||
let csr_ticket = RwSignal::<Option<CSRTicket>>::new(None);
|
||||
|
||||
let reset_counters = ServerAction::<ResetCounters>::new();
|
||||
|
||||
Effect::new(move |_| {
|
||||
let ticket = expect_context::<Ticket>().0;
|
||||
csr_ticket.set(Some(CSRTicket(ticket)));
|
||||
});
|
||||
|
||||
view! {
|
||||
<section id="instrumented">
|
||||
<nav>
|
||||
<a href="/">"Site Root"</a>
|
||||
<A href="./" exact=true>"Instrumented Root"</A>
|
||||
<A href="item/" strict_trailing_slash=true>"Item Listing"</A>
|
||||
<A href="counters" strict_trailing_slash=true>"Counters"</A>
|
||||
</nav>
|
||||
<FieldNavPortlet/>
|
||||
<Outlet/>
|
||||
<Suspense>{
|
||||
move || Suspend::new(async move {
|
||||
let clear_suspense_counters = move |_| {
|
||||
counters.update(|c| *c = SuspenseCounters::default());
|
||||
};
|
||||
csr_ticket.get().map(|ticket| {
|
||||
let ticket = ticket.0;
|
||||
view! {
|
||||
<ActionForm action=reset_counters>
|
||||
<input type="hidden" name="ticket" value=format!("{ticket}") />
|
||||
<input
|
||||
id="reset-csr-counters"
|
||||
type="submit"
|
||||
value="Reset CSR Counters"
|
||||
on:click=clear_suspense_counters/>
|
||||
</ActionForm>
|
||||
}
|
||||
})
|
||||
})
|
||||
}</Suspense>
|
||||
<footer>
|
||||
<nav>
|
||||
<A href="item/3/">"Target 3##"</A>
|
||||
<A href="item/4/">"Target 4##"</A>
|
||||
<A href="item/4/path1/">"Target 41#"</A>
|
||||
<A href="item/4/path2/">"Target 42#"</A>
|
||||
<A href="item/4/path2/field1">"Target 421"</A>
|
||||
<A href="item/1/path2/field3">"Target 123"</A>
|
||||
</nav>
|
||||
</footer>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn InstrumentedTop() -> impl IntoView {
|
||||
view! {
|
||||
<h1>"Instrumented Tests"</h1>
|
||||
<p>"These tests validates the number of invocations of server functions and suspenses per access."</p>
|
||||
<ul>
|
||||
// not using `A` because currently some bugs with artix
|
||||
<li><a href="item/">"Item Listing"</a></li>
|
||||
<li><a href="item/4/path1/">"Target 41#"</a></li>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ItemRoot() -> impl IntoView {
|
||||
let ticket = expect_context::<Ticket>().0;
|
||||
provide_context(Resource::new_blocking(
|
||||
move || (),
|
||||
move |_| async move { list_items(ticket).await },
|
||||
));
|
||||
|
||||
view! {
|
||||
<h2>"<ItemRoot/>"</h2>
|
||||
<Outlet/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ItemListing() -> impl IntoView {
|
||||
let suspense_counters = expect_context::<RwSignal<SuspenseCounters>>();
|
||||
let resource =
|
||||
expect_context::<Resource<Result<Vec<i64>, ServerFnError>>>();
|
||||
let item_listing = move || {
|
||||
Suspend::new(async move {
|
||||
let result = resource.await.map(|items| items
|
||||
.into_iter()
|
||||
.map(move |item|
|
||||
// FIXME seems like relative link isn't working, it is currently
|
||||
// adding an extra `/` in artix; manually construct `a` instead.
|
||||
// <li><A href=format!("./{item}/")>"Item "{item}</A></li>
|
||||
view! {
|
||||
<li><a href=format!("/instrumented/item/{item}/")>"Item "{item}</a></li>
|
||||
}
|
||||
)
|
||||
.collect_view()
|
||||
);
|
||||
suspense_counters.update_untracked(|c| c.item_listing += 1);
|
||||
result
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<h3>"<ItemListing/>"</h3>
|
||||
<ul>
|
||||
<Suspense>
|
||||
{item_listing}
|
||||
</Suspense>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Params, PartialEq, Clone, Debug)]
|
||||
struct ItemTopParams {
|
||||
id: Option<i64>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ItemTop() -> impl IntoView {
|
||||
let ticket = expect_context::<Ticket>().0;
|
||||
let params = use_params::<ItemTopParams>();
|
||||
// map result to an option as the focus isn't error rendering
|
||||
provide_context(Resource::new_blocking(
|
||||
move || params.get().map(|p| p.id),
|
||||
move |id| async move {
|
||||
match id {
|
||||
Err(_) => None,
|
||||
Ok(Some(id)) => get_item(ticket, id).await.ok(),
|
||||
_ => None,
|
||||
}
|
||||
},
|
||||
));
|
||||
view! {
|
||||
<h4>"<ItemTop/>"</h4>
|
||||
<Outlet/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ItemOverview() -> impl IntoView {
|
||||
let suspense_counters = expect_context::<RwSignal<SuspenseCounters>>();
|
||||
let resource = expect_context::<Resource<Option<GetItemResult>>>();
|
||||
let item_view = move || {
|
||||
Suspend::new(async move {
|
||||
let result = resource.await.map(|GetItemResult(item, names)| view! {
|
||||
<p>{format!("Viewing {item:?}")}</p>
|
||||
<ul>{
|
||||
names.into_iter()
|
||||
.map(|name| {
|
||||
// FIXME seems like relative link isn't working, it is currently
|
||||
// adding an extra `/` in artix; manually construct `a` instead.
|
||||
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
|
||||
let id = item.id;
|
||||
view! {
|
||||
<li><a href=format!("/instrumented/item/{id}/{name}/")>
|
||||
"Inspect "{name.clone()}
|
||||
</a></li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}</ul>
|
||||
});
|
||||
suspense_counters.update_untracked(|c| c.item_overview += 1);
|
||||
result
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<h5>"<ItemOverview/>"</h5>
|
||||
<Suspense>
|
||||
{item_view}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Params, PartialEq, Clone, Debug)]
|
||||
struct ItemInspectParams {
|
||||
path: Option<String>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ItemInspect() -> impl IntoView {
|
||||
let ticket = expect_context::<Ticket>().0;
|
||||
let suspense_counters = expect_context::<RwSignal<SuspenseCounters>>();
|
||||
let params = use_params::<ItemInspectParams>();
|
||||
let res_overview = expect_context::<Resource<Option<GetItemResult>>>();
|
||||
let res_inspect = Resource::new_blocking(
|
||||
move || params.get().map(|p| p.path),
|
||||
move |p| async move {
|
||||
// leptos::logging::log!("res_inspect: res_overview.await");
|
||||
let overview = res_overview.await;
|
||||
// leptos::logging::log!("res_inspect: resolved res_overview.await");
|
||||
// let result =
|
||||
match (overview, p) {
|
||||
(Some(item), Ok(Some(path))) => {
|
||||
// leptos::logging::log!("res_inspect: inspect_item().await");
|
||||
inspect_item(ticket, item.0.id, path.clone()).await.ok()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
// ;
|
||||
// leptos::logging::log!("res_inspect: resolved inspect_item().await");
|
||||
// result
|
||||
},
|
||||
);
|
||||
on_cleanup(|| {
|
||||
if let Some(c) = use_context::<WriteSignal<Option<FieldNavCtx>>>() {
|
||||
c.set(None);
|
||||
}
|
||||
});
|
||||
let inspect_view = move || {
|
||||
// leptos::logging::log!("inspect_view closure invoked");
|
||||
Suspend::new(async move {
|
||||
// leptos::logging::log!("inspect_view Suspend::new() called");
|
||||
let result = res_inspect.await.map(|InspectItemResult(item, name, fields)| {
|
||||
// leptos::logging::log!("inspect_view res_inspect awaited");
|
||||
let id = item.id;
|
||||
expect_context::<WriteSignal<Option<FieldNavCtx>>>().set(Some(
|
||||
fields.iter()
|
||||
.map(|field| FieldNavItem {
|
||||
href: format!("/instrumented/item/{id}/{name}/{field}"),
|
||||
text: field.to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into()
|
||||
));
|
||||
view! {
|
||||
<p>{format!("Inspecting {item:?}")}</p>
|
||||
<ul>{
|
||||
fields.iter()
|
||||
.map(|field| {
|
||||
// FIXME seems like relative link to root for a wildcard isn't
|
||||
// working as expected, so manually construct `a` instead.
|
||||
// let text = format!("Inspect {name}/{field}");
|
||||
// view! {
|
||||
// <li><A href=format!("{field}")>{text}</A></li>
|
||||
// }
|
||||
view! {
|
||||
<li><a href=format!("/instrumented/item/{id}/{name}/{field}")>{
|
||||
format!("Inspect {name}/{field}")
|
||||
}</a></li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}</ul>
|
||||
}
|
||||
});
|
||||
suspense_counters.update_untracked(|c| c.item_inspect += 1);
|
||||
// leptos::logging::log!(
|
||||
// "returning result, result.is_some() = {}, count = {}",
|
||||
// result.is_some(),
|
||||
// suspense_counters.get().item_inspect,
|
||||
// );
|
||||
result
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<h5>"<ItemInspect/>"</h5>
|
||||
<Suspense>
|
||||
{inspect_view}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ShowCounters() -> impl IntoView {
|
||||
// There is _weirdness_ in this view. The `Server Calls` counters
|
||||
// will be acquired via the expected mode and be rendered as such.
|
||||
//
|
||||
// However, upon `Reset Counters`, the mode from which the reset
|
||||
// was issued will result in the rendering be reflected as such, so
|
||||
// if the intial state was SSR, resetting under CSR will result in
|
||||
// the CSR counters be rendered after. However for the intents and
|
||||
// purpose for the testing only the CSR is cared for.
|
||||
//
|
||||
// At the end of the day, it is possible to have both these be
|
||||
// separated out, but for the purpose of this test the focus is not
|
||||
// on the SSR side of things (at least until further regression is
|
||||
// discovered that affects SSR directly).
|
||||
let ticket = expect_context::<Ticket>().0;
|
||||
let suspense_counters = expect_context::<RwSignal<SuspenseCounters>>();
|
||||
let reset_counters = ServerAction::<ResetCounters>::new();
|
||||
let res_counter = Resource::new(
|
||||
move || reset_counters.version().get(),
|
||||
move |_| async move {
|
||||
(
|
||||
get_counters(ticket).await,
|
||||
if ticket == 0 { "SSR" } else { "CSR" }.to_string(),
|
||||
ticket,
|
||||
)
|
||||
},
|
||||
);
|
||||
let counter_view = move || {
|
||||
Suspend::new(async move {
|
||||
// ensure current mode and ticket are both updated
|
||||
let (counters, mode, ticket) = res_counter.await;
|
||||
counters.map(|counters| {
|
||||
let clear_suspense_counters = move |_| {
|
||||
suspense_counters.update(|c| {
|
||||
// leptos::logging::log!("resetting suspense counters");
|
||||
*c = SuspenseCounters::default();
|
||||
});
|
||||
};
|
||||
view! {
|
||||
<h3 id="server-calls">"Server Calls ("{mode}")"</h3>
|
||||
<dl>
|
||||
<dt>"list_items"</dt>
|
||||
<dd id="list_items">{counters.list_items}</dd>
|
||||
<dt>"get_item"</dt>
|
||||
<dd id="get_item">{counters.get_item}</dd>
|
||||
<dt>"inspect_item_root"</dt>
|
||||
<dd id="inspect_item_root">{counters.inspect_item_root}</dd>
|
||||
<dt>"inspect_item_field"</dt>
|
||||
<dd id="inspect_item_field">{counters.inspect_item_field}</dd>
|
||||
</dl>
|
||||
<ActionForm action=reset_counters>
|
||||
<input type="hidden" name="ticket" value=format!("{ticket}") />
|
||||
<input
|
||||
id="reset-counters"
|
||||
type="submit"
|
||||
value="Reset Counters"
|
||||
on:click=clear_suspense_counters/>
|
||||
</ActionForm>
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<h2>"Counters"</h2>
|
||||
|
||||
<h3 id="suspend-calls">"Suspend Calls"</h3>
|
||||
{move || suspense_counters.with(|c| view! {
|
||||
<dl>
|
||||
<dt>"item_listing"</dt>
|
||||
<dd id="item_listing">{c.item_listing}</dd>
|
||||
<dt>"item_overview"</dt>
|
||||
<dd id="item_overview">{c.item_overview}</dd>
|
||||
<dt>"item_inspect"</dt>
|
||||
<dd id="item_inspect">{c.item_inspect}</dd>
|
||||
</dl>
|
||||
})}
|
||||
|
||||
<Suspense>
|
||||
{counter_view}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct FieldNavItem {
|
||||
pub href: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct FieldNavCtx(pub Option<Vec<FieldNavItem>>);
|
||||
|
||||
impl From<Vec<FieldNavItem>> for FieldNavCtx {
|
||||
fn from(item: Vec<FieldNavItem>) -> Self {
|
||||
Self(Some(item))
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn FieldNavPortlet() -> impl IntoView {
|
||||
let ctx = expect_context::<ReadSignal<Option<FieldNavCtx>>>();
|
||||
move || {
|
||||
let ctx = ctx.get();
|
||||
ctx.map(|ctx| {
|
||||
view! {
|
||||
<div id="FieldNavPortlet">
|
||||
<span>"FieldNavPortlet:"</span>
|
||||
<nav>{
|
||||
ctx.0.map(|ctx| {
|
||||
ctx.into_iter()
|
||||
.map(|FieldNavItem { href, text }| {
|
||||
view! {
|
||||
<A href=href>{text}</A>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
})
|
||||
}</nav>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provide_field_nav_portlet_context() {
|
||||
// wrapping the Ctx in an Option allows better ergonomics whenever it isn't needed
|
||||
let (ctx, set_ctx) = signal(None::<FieldNavCtx>);
|
||||
provide_context(ctx);
|
||||
provide_context(set_ctx);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod app;
|
||||
mod instrumented;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
|
||||
@@ -41,7 +41,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
})
|
||||
.bind(addr)?
|
||||
.workers(1)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("should see the welcome message", async ({ page }) => {
|
||||
test("homepage has title 'Leptos + Tailwindcss'", async ({ page }) => {
|
||||
await page.goto("http://localhost:3000/");
|
||||
|
||||
await expect(page.locator("h2")).toHaveText("Welcome to Leptos with Tailwind");
|
||||
await expect(page).toHaveTitle("Leptos + Tailwindcss");
|
||||
});
|
||||
|
||||
@@ -22,24 +22,29 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
fn Home() -> impl IntoView {
|
||||
let (count, set_count) = signal(0);
|
||||
let (value, set_value) = signal(0);
|
||||
|
||||
// thanks to https://tailwindcomponents.com/component/blue-buttons-example for the showcase layout
|
||||
view! {
|
||||
<main class="my-0 mx-auto max-w-3xl text-center">
|
||||
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
|
||||
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
|
||||
<button
|
||||
class="bg-amber-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
|
||||
on:click=move |_| set_count.update(|count| *count += 1)
|
||||
>
|
||||
"Something's here | "
|
||||
{move || if count.get() == 0 {
|
||||
"Click me!".to_string()
|
||||
} else {
|
||||
count.get().to_string()
|
||||
}}
|
||||
" | Some more text"
|
||||
</button>
|
||||
<Title text="Leptos + Tailwindcss"/>
|
||||
<main>
|
||||
<div class="bg-gradient-to-tl from-blue-800 to-blue-500 text-white font-mono flex flex-col min-h-screen">
|
||||
<div class="flex flex-row-reverse flex-wrap m-auto">
|
||||
<button on:click=move |_| set_value.update(|value| *value += 1) class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white">
|
||||
"+"
|
||||
</button>
|
||||
<button class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-800 border-blue-900 text-white">
|
||||
{value}
|
||||
</button>
|
||||
<button
|
||||
on:click=move |_| set_value.update(|value| *value -= 1)
|
||||
class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white"
|
||||
class:invisible=move || {value.get() < 1}
|
||||
>
|
||||
"-"
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: {
|
||||
content: {
|
||||
files: ["*.html", "./src/**/*.rs"],
|
||||
transform: {
|
||||
rs: (content) => content.replace(/(?:^|\s)class:/g, ' '),
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("homepage has title and links to intro page", async ({ page }) => {
|
||||
test("homepage has title 'Leptos + Tailwindcss'", async ({ page }) => {
|
||||
await page.goto("http://localhost:3000/");
|
||||
|
||||
await expect(page).toHaveTitle("Welcome to Leptos");
|
||||
|
||||
await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
|
||||
await expect(page).toHaveTitle("Leptos + Tailwindcss");
|
||||
});
|
||||
|
||||
@@ -54,7 +54,11 @@ fn Home() -> impl IntoView {
|
||||
<button class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-800 border-blue-900 text-white">
|
||||
{value}
|
||||
</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1) class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white">
|
||||
<button
|
||||
on:click=move |_| set_value.update(|value| *value -= 1)
|
||||
class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white"
|
||||
class:invisible=move || {value.get() < 1}
|
||||
>
|
||||
"-"
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["*.html", "./src/**/*.rs",],
|
||||
content: {
|
||||
files: ["*.html", "./src/**/*.rs"],
|
||||
transform: {
|
||||
rs: (content) => content.replace(/(?:^|\s)class:/g, ' '),
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("homepage has title and links to intro page", async ({ page }) => {
|
||||
test("homepage has title 'Leptos + Tailwindcss'", async ({ page }) => {
|
||||
await page.goto("http://localhost:8080/");
|
||||
|
||||
await expect(page).toHaveTitle("Leptos • Counter with Tailwind");
|
||||
|
||||
await expect(page.locator("h2")).toHaveText("Welcome to Leptos with Tailwind");
|
||||
await expect(page).toHaveTitle("Leptos + Tailwindcss");
|
||||
});
|
||||
|
||||
@@ -22,24 +22,29 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
fn Home() -> impl IntoView {
|
||||
let (count, set_count) = signal(0);
|
||||
let (value, set_value) = signal(0);
|
||||
|
||||
// thanks to https://tailwindcomponents.com/component/blue-buttons-example for the showcase layout
|
||||
view! {
|
||||
<div class="my-0 mx-auto max-w-3xl text-center">
|
||||
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
|
||||
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
|
||||
<button
|
||||
class="bg-amber-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
|
||||
on:click=move |_| set_count.update(|count| *count += 1)
|
||||
>
|
||||
"Something's here | "
|
||||
{move || if count.get() == 0 {
|
||||
"Click me!".to_string()
|
||||
} else {
|
||||
count.get().to_string()
|
||||
}}
|
||||
" | Some more text"
|
||||
</button>
|
||||
</div>
|
||||
<Title text="Leptos + Tailwindcss"/>
|
||||
<main>
|
||||
<div class="bg-gradient-to-tl from-blue-800 to-blue-500 text-white font-mono flex flex-col min-h-screen">
|
||||
<div class="flex flex-row-reverse flex-wrap m-auto">
|
||||
<button on:click=move |_| set_value.update(|value| *value += 1) class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white">
|
||||
"+"
|
||||
</button>
|
||||
<button class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-800 border-blue-900 text-white">
|
||||
{value}
|
||||
</button>
|
||||
<button
|
||||
on:click=move |_| set_value.update(|value| *value -= 1)
|
||||
class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white"
|
||||
class:invisible=move || {value.get() < 1}
|
||||
>
|
||||
"-"
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: {
|
||||
content: {
|
||||
files: ["*.html", "./src/**/*.rs"],
|
||||
transform: {
|
||||
rs: (content) => content.replace(/(?:^|\s)class:/g, ' '),
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn TimerDemo() -> impl IntoView {
|
||||
pub fn use_interval<T, F>(interval_millis: T, f: F)
|
||||
where
|
||||
F: Fn() + Clone + 'static,
|
||||
T: Into<MaybeSignal<u64>> + 'static,
|
||||
T: Into<Signal<u64>> + 'static,
|
||||
{
|
||||
let interval_millis = interval_millis.into();
|
||||
Effect::new(move |prev_handle: Option<IntervalHandle>| {
|
||||
|
||||
@@ -59,7 +59,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -16,15 +16,15 @@ leptos_router = { path = "../../router" }
|
||||
leptos_integration_utils = { path = "../../integrations/utils", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tower = { version = "0.5.1", features = ["util"], optional = true }
|
||||
tower-http = { version = "0.6.1", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
http = { version = "1.1" }
|
||||
sqlx = { version = "0.8.0", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0"
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -9,7 +9,7 @@ use leptos::{
|
||||
hydration::{AutoReload, HydrationScripts},
|
||||
prelude::*,
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
use tower::util::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub async fn file_or_index_handler(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hydration_context"
|
||||
version = "0.2.0-gamma3"
|
||||
version = "0.2.0-rc1"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -12,12 +12,12 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
throw_error = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
futures = "0.3.30"
|
||||
futures = "0.3.31"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
wasm-bindgen = { version = "0.2.93", optional = true }
|
||||
js-sys = { version = "0.3.69", optional = true }
|
||||
once_cell = "1.19"
|
||||
pin-project-lite = "0.2.14"
|
||||
wasm-bindgen = { version = "0.2.95", optional = true }
|
||||
js-sys = { version = "0.3.72", optional = true }
|
||||
once_cell = "1.20"
|
||||
pin-project-lite = "0.2.15"
|
||||
|
||||
[features]
|
||||
browser = ["dep:wasm-bindgen", "dep:js-sys"]
|
||||
|
||||
@@ -44,6 +44,18 @@ pub type PinnedStream<T> = Pin<Box<dyn Stream<Item = T> + Send + Sync>>;
|
||||
/// from the server to the client.
|
||||
pub struct SerializedDataId(usize);
|
||||
|
||||
impl SerializedDataId {
|
||||
/// Create a new instance of [`SerializedDataId`].
|
||||
pub fn new(id: usize) -> Self {
|
||||
SerializedDataId(id)
|
||||
}
|
||||
|
||||
/// Consume into the inner usize identifier.
|
||||
pub fn into_inner(self) -> usize {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SerializedDataId> for ErrorId {
|
||||
fn from(value: SerializedDataId) -> Self {
|
||||
value.0.into()
|
||||
|
||||
@@ -58,6 +58,27 @@ impl SsrSharedContext {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume the data buffers, awaiting all async resources,
|
||||
/// returning both sync and async buffers.
|
||||
/// Useful to implement custom hydration contexts.
|
||||
///
|
||||
/// WARNING: this will clear the internal buffers, it should only be called once.
|
||||
/// A second call would return an empty `vec![]`.
|
||||
pub async fn consume_buffers(&self) -> Vec<(SerializedDataId, String)> {
|
||||
let sync_data = mem::take(&mut *self.sync_buf.write().or_poisoned());
|
||||
let async_data = mem::take(&mut *self.async_buf.write().or_poisoned());
|
||||
|
||||
let mut all_data = Vec::new();
|
||||
for resolved in sync_data {
|
||||
all_data.push((resolved.0, resolved.1));
|
||||
}
|
||||
for (id, fut) in async_data {
|
||||
let data = fut.await;
|
||||
all_data.push((id, data));
|
||||
}
|
||||
all_data
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SsrSharedContext {
|
||||
|
||||
@@ -9,10 +9,10 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3.8"
|
||||
actix-http = "3.9"
|
||||
actix-files = "0.6"
|
||||
actix-web = "4.8"
|
||||
futures = "0.3.30"
|
||||
actix-web = "4.9"
|
||||
futures = "0.3.31"
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
leptos = { workspace = true, features = ["nonce", "ssr"] }
|
||||
@@ -24,7 +24,7 @@ server_fn = { workspace = true, features = ["actix"] }
|
||||
serde_json = "1.0"
|
||||
parking_lot = "0.12.3"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
tokio = { version = "1.39", features = ["rt", "fs"] }
|
||||
tokio = { version = "1.41", features = ["rt", "fs"] }
|
||||
send_wrapper = "0.6.0"
|
||||
dashmap = "6"
|
||||
once_cell = "1"
|
||||
|
||||
@@ -35,7 +35,7 @@ use leptos_router::{
|
||||
components::provide_server_redirect,
|
||||
location::RequestUrl,
|
||||
static_routes::{RegenerationFn, ResolvedStaticPath},
|
||||
Method, PathSegment, RouteList, RouteListing, SsrMode,
|
||||
ExpandOptionals, Method, PathSegment, RouteList, RouteListing, SsrMode,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
@@ -44,6 +44,7 @@ use server_fn::{
|
||||
redirect::REDIRECT_HEADER, request::actix::ActixRequest, ServerFnError,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::{Debug, Display},
|
||||
future::Future,
|
||||
ops::{Deref, DerefMut},
|
||||
@@ -900,7 +901,7 @@ trait ActixPath {
|
||||
fn to_actix_path(&self) -> String;
|
||||
}
|
||||
|
||||
impl ActixPath for &[PathSegment] {
|
||||
impl ActixPath for Vec<PathSegment> {
|
||||
fn to_actix_path(&self) -> String {
|
||||
let mut path = String::new();
|
||||
for segment in self.iter() {
|
||||
@@ -922,6 +923,14 @@ impl ActixPath for &[PathSegment] {
|
||||
path.push_str(":.*}");
|
||||
}
|
||||
PathSegment::Unit => {}
|
||||
PathSegment::OptionalParam(_) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!(
|
||||
"to_axum_path should only be called on expanded \
|
||||
paths, which do not have OptionalParam any longer"
|
||||
);
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
path
|
||||
@@ -935,25 +944,38 @@ pub struct ActixRouteListing {
|
||||
mode: SsrMode,
|
||||
methods: Vec<leptos_router::Method>,
|
||||
regenerate: Vec<RegenerationFn>,
|
||||
exclude: bool,
|
||||
}
|
||||
|
||||
impl From<RouteListing> for ActixRouteListing {
|
||||
fn from(value: RouteListing) -> Self {
|
||||
let path = value.path().to_actix_path();
|
||||
let path = if path.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let mode = value.mode();
|
||||
let methods = value.methods().collect();
|
||||
let regenerate = value.regenerate().into();
|
||||
Self {
|
||||
path,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
regenerate,
|
||||
}
|
||||
trait IntoRouteListing: Sized {
|
||||
fn into_route_listing(self) -> Vec<ActixRouteListing>;
|
||||
}
|
||||
|
||||
impl IntoRouteListing for RouteListing {
|
||||
fn into_route_listing(self) -> Vec<ActixRouteListing> {
|
||||
self.path()
|
||||
.to_vec()
|
||||
.expand_optionals()
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
let path = path.to_actix_path();
|
||||
let path = if path.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let mode = self.mode();
|
||||
let methods = self.methods().collect();
|
||||
let regenerate = self.regenerate().into();
|
||||
ActixRouteListing {
|
||||
path,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
regenerate,
|
||||
exclude: false,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -970,6 +992,7 @@ impl ActixRouteListing {
|
||||
mode,
|
||||
methods: methods.into_iter().collect(),
|
||||
regenerate: regenerate.into(),
|
||||
exclude: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1027,27 +1050,37 @@ where
|
||||
let mut routes = routes
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.map(ActixRouteListing::from)
|
||||
.flat_map(IntoRouteListing::into_route_listing)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(
|
||||
if routes.is_empty() {
|
||||
vec![ActixRouteListing::new(
|
||||
"/".to_string(),
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
vec![],
|
||||
)]
|
||||
} else {
|
||||
// Routes to exclude from auto generation
|
||||
if let Some(excluded_routes) = excluded_routes {
|
||||
routes
|
||||
.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
|
||||
}
|
||||
routes
|
||||
},
|
||||
generator,
|
||||
)
|
||||
let routes = if routes.is_empty() {
|
||||
vec![ActixRouteListing::new(
|
||||
"/".to_string(),
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
vec![],
|
||||
)]
|
||||
} else {
|
||||
// Routes to exclude from auto generation
|
||||
if let Some(excluded_routes) = &excluded_routes {
|
||||
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
|
||||
}
|
||||
routes
|
||||
};
|
||||
|
||||
let excluded =
|
||||
excluded_routes
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|path| ActixRouteListing {
|
||||
path,
|
||||
mode: Default::default(),
|
||||
methods: Vec::new(),
|
||||
regenerate: Vec::new(),
|
||||
exclude: true,
|
||||
});
|
||||
|
||||
(routes.into_iter().chain(excluded).collect(), generator)
|
||||
}
|
||||
|
||||
/// Allows generating any prerendered routes.
|
||||
@@ -1353,15 +1386,24 @@ where
|
||||
{
|
||||
let mut router = self;
|
||||
|
||||
let excluded = paths
|
||||
.iter()
|
||||
.filter(|&p| p.exclude)
|
||||
.map(|p| p.path.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// register server functions first to allow for wildcard route in Leptos's Router
|
||||
for (path, _) in server_fn::actix::server_fn_paths() {
|
||||
let additional_context = additional_context.clone();
|
||||
let handler = handle_server_fns_with_context(additional_context);
|
||||
router = router.route(path, handler);
|
||||
if !excluded.contains(path) {
|
||||
let additional_context = additional_context.clone();
|
||||
let handler =
|
||||
handle_server_fns_with_context(additional_context);
|
||||
router = router.route(path, handler);
|
||||
}
|
||||
}
|
||||
|
||||
// register routes defined in Leptos's Router
|
||||
for listing in paths.iter() {
|
||||
for listing in paths.iter().filter(|p| !p.exclude) {
|
||||
let path = listing.path();
|
||||
let mode = listing.mode();
|
||||
|
||||
@@ -1457,15 +1499,24 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||
{
|
||||
let mut router = self;
|
||||
|
||||
let excluded = paths
|
||||
.iter()
|
||||
.filter(|&p| p.exclude)
|
||||
.map(|p| p.path.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// register server functions first to allow for wildcard route in Leptos's Router
|
||||
for (path, _) in server_fn::actix::server_fn_paths() {
|
||||
let additional_context = additional_context.clone();
|
||||
let handler = handle_server_fns_with_context(additional_context);
|
||||
router = router.route(path, handler);
|
||||
if !excluded.contains(path) {
|
||||
let additional_context = additional_context.clone();
|
||||
let handler =
|
||||
handle_server_fns_with_context(additional_context);
|
||||
router = router.route(path, handler);
|
||||
}
|
||||
}
|
||||
|
||||
// register routes defined in Leptos's Router
|
||||
for listing in paths.iter() {
|
||||
for listing in paths.iter().filter(|p| !p.exclude) {
|
||||
let path = listing.path();
|
||||
let mode = listing.mode();
|
||||
|
||||
@@ -1554,7 +1605,10 @@ where
|
||||
ServerFnError::new("HttpRequest should have been provided via context")
|
||||
})?;
|
||||
|
||||
T::extract(&req)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
SendWrapper::new(async move {
|
||||
T::extract(&req)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
axum = { version = "0.7.5", default-features = false, features = [
|
||||
axum = { version = "0.7.7", default-features = false, features = [
|
||||
"matched-path",
|
||||
] }
|
||||
dashmap = "6"
|
||||
futures = "0.3.30"
|
||||
futures = "0.3.31"
|
||||
leptos = { workspace = true, features = ["nonce", "ssr"] }
|
||||
server_fn = { workspace = true, features = ["axum-no-default"] }
|
||||
leptos_macro = { workspace = true, features = ["axum"] }
|
||||
@@ -24,14 +24,14 @@ leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12.3"
|
||||
tokio = { version = "1.39", default-features = false }
|
||||
tower = { version = "0.4.13", features = ["util"] }
|
||||
tower-http = "0.5.2"
|
||||
tokio = { version = "1.41", default-features = false }
|
||||
tower = { version = "0.5.1", features = ["util"] }
|
||||
tower-http = "0.6.1"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = "0.7.5"
|
||||
tokio = { version = "1.39", features = ["net", "rt-multi-thread"] }
|
||||
axum = "0.7.7"
|
||||
tokio = { version = "1.41", features = ["net", "rt-multi-thread"] }
|
||||
|
||||
[features]
|
||||
wasm = []
|
||||
|
||||
@@ -66,7 +66,7 @@ use leptos_router::{
|
||||
components::provide_server_redirect,
|
||||
location::RequestUrl,
|
||||
static_routes::{RegenerationFn, StaticParamsMap},
|
||||
PathSegment, RouteList, RouteListing, SsrMode,
|
||||
ExpandOptionals, PathSegment, RouteList, RouteListing, SsrMode,
|
||||
};
|
||||
#[cfg(feature = "default")]
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -74,7 +74,7 @@ use parking_lot::RwLock;
|
||||
use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
|
||||
#[cfg(feature = "default")]
|
||||
use std::path::Path;
|
||||
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
|
||||
use std::{collections::HashSet, fmt::Debug, io, pin::Pin, sync::Arc};
|
||||
#[cfg(feature = "default")]
|
||||
use tower::util::ServiceExt;
|
||||
#[cfg(feature = "default")]
|
||||
@@ -1263,25 +1263,38 @@ pub struct AxumRouteListing {
|
||||
methods: Vec<leptos_router::Method>,
|
||||
#[allow(unused)]
|
||||
regenerate: Vec<RegenerationFn>,
|
||||
exclude: bool,
|
||||
}
|
||||
|
||||
impl From<RouteListing> for AxumRouteListing {
|
||||
fn from(value: RouteListing) -> Self {
|
||||
let path = value.path().to_axum_path();
|
||||
let path = if path.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let mode = value.mode();
|
||||
let methods = value.methods().collect();
|
||||
let regenerate = value.regenerate().into();
|
||||
Self {
|
||||
path,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
regenerate,
|
||||
}
|
||||
trait IntoRouteListing: Sized {
|
||||
fn into_route_listing(self) -> Vec<AxumRouteListing>;
|
||||
}
|
||||
|
||||
impl IntoRouteListing for RouteListing {
|
||||
fn into_route_listing(self) -> Vec<AxumRouteListing> {
|
||||
self.path()
|
||||
.to_vec()
|
||||
.expand_optionals()
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
let path = path.to_axum_path();
|
||||
let path = if path.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let mode = self.mode();
|
||||
let methods = self.methods().collect();
|
||||
let regenerate = self.regenerate().into();
|
||||
AxumRouteListing {
|
||||
path,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
regenerate,
|
||||
exclude: false,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1298,6 +1311,7 @@ impl AxumRouteListing {
|
||||
mode,
|
||||
methods: methods.into_iter().collect(),
|
||||
regenerate: regenerate.into(),
|
||||
exclude: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1360,27 +1374,36 @@ where
|
||||
let mut routes = routes
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.map(AxumRouteListing::from)
|
||||
.flat_map(IntoRouteListing::into_route_listing)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(
|
||||
if routes.is_empty() {
|
||||
vec![AxumRouteListing::new(
|
||||
"/".to_string(),
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
vec![],
|
||||
)]
|
||||
} else {
|
||||
// Routes to exclude from auto generation
|
||||
if let Some(excluded_routes) = excluded_routes {
|
||||
routes
|
||||
.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
|
||||
}
|
||||
routes
|
||||
},
|
||||
generator,
|
||||
)
|
||||
let routes = if routes.is_empty() {
|
||||
vec![AxumRouteListing::new(
|
||||
"/".to_string(),
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
vec![],
|
||||
)]
|
||||
} else {
|
||||
// Routes to exclude from auto generation
|
||||
if let Some(excluded_routes) = &excluded_routes {
|
||||
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
|
||||
}
|
||||
routes
|
||||
};
|
||||
let excluded =
|
||||
excluded_routes
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|path| AxumRouteListing {
|
||||
path,
|
||||
mode: Default::default(),
|
||||
methods: Vec::new(),
|
||||
regenerate: Vec::new(),
|
||||
exclude: true,
|
||||
});
|
||||
|
||||
(routes.into_iter().chain(excluded).collect(), generator)
|
||||
}
|
||||
|
||||
/// Allows generating any prerendered routes.
|
||||
@@ -1693,7 +1716,7 @@ trait AxumPath {
|
||||
fn to_axum_path(&self) -> String;
|
||||
}
|
||||
|
||||
impl AxumPath for &[PathSegment] {
|
||||
impl AxumPath for Vec<PathSegment> {
|
||||
fn to_axum_path(&self) -> String {
|
||||
let mut path = String::new();
|
||||
for segment in self.iter() {
|
||||
@@ -1713,6 +1736,14 @@ impl AxumPath for &[PathSegment] {
|
||||
path.push_str(s);
|
||||
}
|
||||
PathSegment::Unit => {}
|
||||
PathSegment::OptionalParam(_) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!(
|
||||
"to_axum_path should only be called on expanded \
|
||||
paths, which do not have OptionalParam any longer"
|
||||
);
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
path
|
||||
@@ -1768,32 +1799,41 @@ where
|
||||
|
||||
let mut router = self;
|
||||
|
||||
let excluded = paths
|
||||
.iter()
|
||||
.filter(|&p| p.exclude)
|
||||
.map(|p| p.path.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// register server functions
|
||||
for (path, method) in server_fn::axum::server_fn_paths() {
|
||||
let cx_with_state = cx_with_state.clone();
|
||||
let handler = move |req: Request<Body>| async move {
|
||||
handle_server_fns_with_context(cx_with_state, req).await
|
||||
};
|
||||
router = router.route(
|
||||
path,
|
||||
match method {
|
||||
Method::GET => get(handler),
|
||||
Method::POST => post(handler),
|
||||
Method::PUT => put(handler),
|
||||
Method::DELETE => delete(handler),
|
||||
Method::PATCH => patch(handler),
|
||||
_ => {
|
||||
panic!(
|
||||
"Unsupported server function HTTP method: \
|
||||
{method:?}"
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if !excluded.contains(path) {
|
||||
router = router.route(
|
||||
path,
|
||||
match method {
|
||||
Method::GET => get(handler),
|
||||
Method::POST => post(handler),
|
||||
Method::PUT => put(handler),
|
||||
Method::DELETE => delete(handler),
|
||||
Method::PATCH => patch(handler),
|
||||
_ => {
|
||||
panic!(
|
||||
"Unsupported server function HTTP method: \
|
||||
{method:?}"
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// register router paths
|
||||
for listing in paths.iter() {
|
||||
for listing in paths.iter().filter(|p| !p.exclude) {
|
||||
let path = listing.path();
|
||||
|
||||
for method in listing.methods() {
|
||||
@@ -1902,7 +1942,7 @@ where
|
||||
T: 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
for listing in paths.iter() {
|
||||
for listing in paths.iter().filter(|p| !p.exclude) {
|
||||
for method in listing.methods() {
|
||||
router = router.route(
|
||||
listing.path(),
|
||||
|
||||
@@ -9,7 +9,7 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.30"
|
||||
futures = "0.3.31"
|
||||
hydration_context = { workspace = true }
|
||||
leptos = { workspace = true, features = ["nonce"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
|
||||
@@ -11,7 +11,7 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
throw_error = { workspace = true }
|
||||
any_spawner = { workspace = true, features = ["wasm-bindgen"] }
|
||||
any_spawner = { workspace = true, features = ["wasm-bindgen", "futures-executor"] }
|
||||
base64 = { version = "0.22.1", optional = true }
|
||||
cfg-if = "1.0"
|
||||
hydration_context = { workspace = true }
|
||||
@@ -29,10 +29,10 @@ rand = { version = "0.8.5", optional = true }
|
||||
reactive_graph = { workspace = true, features = ["serde"] }
|
||||
rustc-hash = "2.0"
|
||||
tachys = { workspace = true, features = ["reactive_graph", "reactive_stores", "oco"] }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
typed-builder = "0.19.1"
|
||||
typed-builder-macro = "0.19.1"
|
||||
typed-builder = "0.20.0"
|
||||
typed-builder-macro = "0.20.0"
|
||||
serde = "1.0"
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
server_fn = { workspace = true, features = [
|
||||
@@ -40,15 +40,15 @@ server_fn = { workspace = true, features = [
|
||||
"browser",
|
||||
"url",
|
||||
] }
|
||||
web-sys = { version = "0.3.70", features = [
|
||||
web-sys = { version = "0.3.72", features = [
|
||||
"ShadowRoot",
|
||||
"ShadowRootInit",
|
||||
"ShadowRootMode",
|
||||
] }
|
||||
wasm-bindgen = "=0.2.93"
|
||||
wasm-bindgen = "0.2.95"
|
||||
serde_qs = "0.13.0"
|
||||
slotmap = "1.0"
|
||||
futures = "0.3.30"
|
||||
futures = "0.3.31"
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -50,7 +50,7 @@ pub fn HydrationScripts(
|
||||
path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.join(&options.hash_file);
|
||||
.join(options.hash_file.as_ref());
|
||||
if hash_path.exists() {
|
||||
let hashes = std::fs::read_to_string(&hash_path)
|
||||
.expect("failed to read hash file");
|
||||
|
||||
@@ -163,7 +163,7 @@ pub mod prelude {
|
||||
form::*, hydration::*, into_view::*, mount::*, suspense::*,
|
||||
};
|
||||
pub use leptos_config::*;
|
||||
pub use leptos_dom::{helpers::*, *};
|
||||
pub use leptos_dom::helpers::*;
|
||||
pub use leptos_macro::*;
|
||||
pub use leptos_server::*;
|
||||
pub use oco_ref::*;
|
||||
|
||||
@@ -10,18 +10,18 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
config = { version = "0.14.0", default-features = false, features = [
|
||||
config = { version = "0.14.1", default-features = false, features = [
|
||||
"toml",
|
||||
"convert-case",
|
||||
] }
|
||||
regex = "1.10"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
typed-builder = "0.19.1"
|
||||
regex = "1.11"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
thiserror = "2.0"
|
||||
typed-builder = "0.20.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.39", features = ["rt", "macros"] }
|
||||
tempfile = "3.12"
|
||||
tokio = { version = "1.41", features = ["rt", "macros"] }
|
||||
tempfile = "3.13"
|
||||
temp-env = { version = "0.3.6", features = ["async_closure"] }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
@@ -5,7 +5,9 @@ pub mod errors;
|
||||
use crate::errors::LeptosConfigError;
|
||||
use config::{Case, Config, File, FileFormat};
|
||||
use regex::Regex;
|
||||
use std::{env::VarError, fs, net::SocketAddr, path::Path, str::FromStr};
|
||||
use std::{
|
||||
env::VarError, fs, net::SocketAddr, path::Path, str::FromStr, sync::Arc,
|
||||
};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
|
||||
@@ -25,17 +27,17 @@ pub struct ConfFile {
|
||||
pub struct LeptosOptions {
|
||||
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
|
||||
#[builder(setter(into), default=default_output_name())]
|
||||
pub output_name: String,
|
||||
pub output_name: Arc<str>,
|
||||
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
|
||||
/// tools.
|
||||
#[builder(setter(into), default=default_site_root())]
|
||||
#[serde(default = "default_site_root")]
|
||||
pub site_root: String,
|
||||
pub site_root: Arc<str>,
|
||||
/// The path of the WASM and JS files generated by wasm-bindgen from the root of your app
|
||||
/// By default, wasm-bindgen puts them in `pkg`.
|
||||
#[builder(setter(into), default=default_site_pkg_dir())]
|
||||
#[serde(default = "default_site_pkg_dir")]
|
||||
pub site_pkg_dir: String,
|
||||
pub site_pkg_dir: Arc<str>,
|
||||
/// Used to configure the running environment of Leptos. Can be used to load dev constants and keys v prod, or change
|
||||
/// things based on the deployment environment
|
||||
/// I recommend passing in the result of `env::var("LEPTOS_ENV")`
|
||||
@@ -66,11 +68,11 @@ pub struct LeptosOptions {
|
||||
/// The path of a custom 404 Not Found page to display when statically serving content, defaults to `site_root/404.html`
|
||||
#[builder(default = default_not_found_path())]
|
||||
#[serde(default = "default_not_found_path")]
|
||||
pub not_found_path: String,
|
||||
pub not_found_path: Arc<str>,
|
||||
/// The file name of the hash text file generated by cargo-leptos. Defaults to `hash.txt`.
|
||||
#[builder(default = default_hash_file_name())]
|
||||
#[serde(default = "default_hash_file_name")]
|
||||
pub hash_file: String,
|
||||
pub hash_file: Arc<str>,
|
||||
/// If true, hashes will be generated for all files in the site_root and added to their file names.
|
||||
/// Defaults to `true`.
|
||||
#[builder(default = default_hash_files())]
|
||||
@@ -96,9 +98,9 @@ impl LeptosOptions {
|
||||
);
|
||||
}
|
||||
Ok(LeptosOptions {
|
||||
output_name,
|
||||
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?,
|
||||
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?,
|
||||
output_name: output_name.into(),
|
||||
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?.into(),
|
||||
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?.into(),
|
||||
env: env_from_str(env_w_default("LEPTOS_ENV", "DEV")?.as_str())?,
|
||||
site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?
|
||||
.parse()?,
|
||||
@@ -113,8 +115,10 @@ impl LeptosOptions {
|
||||
reload_ws_protocol: ws_from_str(
|
||||
env_w_default("LEPTOS_RELOAD_WS_PROTOCOL", "ws")?.as_str(),
|
||||
)?,
|
||||
not_found_path: env_w_default("LEPTOS_NOT_FOUND_PATH", "/404")?,
|
||||
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?,
|
||||
not_found_path: env_w_default("LEPTOS_NOT_FOUND_PATH", "/404")?
|
||||
.into(),
|
||||
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?
|
||||
.into(),
|
||||
hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?,
|
||||
})
|
||||
}
|
||||
@@ -126,16 +130,16 @@ impl Default for LeptosOptions {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_output_name() -> String {
|
||||
env!("CARGO_CRATE_NAME").replace('-', "_")
|
||||
fn default_output_name() -> Arc<str> {
|
||||
env!("CARGO_CRATE_NAME").replace('-', "_").into()
|
||||
}
|
||||
|
||||
fn default_site_root() -> String {
|
||||
".".to_string()
|
||||
fn default_site_root() -> Arc<str> {
|
||||
".".into()
|
||||
}
|
||||
|
||||
fn default_site_pkg_dir() -> String {
|
||||
"pkg".to_string()
|
||||
fn default_site_pkg_dir() -> Arc<str> {
|
||||
"pkg".into()
|
||||
}
|
||||
|
||||
fn default_env() -> Env {
|
||||
@@ -150,12 +154,12 @@ fn default_reload_port() -> u32 {
|
||||
3001
|
||||
}
|
||||
|
||||
fn default_not_found_path() -> String {
|
||||
"/404".to_string()
|
||||
fn default_not_found_path() -> Arc<str> {
|
||||
"/404".into()
|
||||
}
|
||||
|
||||
fn default_hash_file_name() -> String {
|
||||
"hash.txt".to_string()
|
||||
fn default_hash_file_name() -> Arc<str> {
|
||||
"hash.txt".into()
|
||||
}
|
||||
|
||||
fn default_hash_files() -> bool {
|
||||
|
||||
@@ -76,9 +76,9 @@ fn try_from_env_test() {
|
||||
|| LeptosOptions::try_from_env().unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(config.output_name.as_ref(), "app_test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
|
||||
@@ -50,9 +50,9 @@ async fn get_configuration_from_file_ok() {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -106,9 +106,9 @@ async fn get_config_from_file_ok() {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -151,9 +151,9 @@ fn get_config_from_str_content() {
|
||||
|| get_config_from_str(CARGO_TOML_CONTENT_OK).unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -178,9 +178,9 @@ async fn get_config_from_env() {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -202,8 +202,8 @@ async fn get_config_from_env() {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.site_root, "target/site");
|
||||
assert_eq!(config.site_pkg_dir, "pkg");
|
||||
assert_eq!(config.site_root.as_ref(), "target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("127.0.0.1:3000").unwrap()
|
||||
@@ -215,10 +215,10 @@ async fn get_config_from_env() {
|
||||
#[test]
|
||||
fn leptos_options_builder_default() {
|
||||
let conf = LeptosOptions::builder().output_name("app-test").build();
|
||||
assert_eq!(conf.output_name, "app-test");
|
||||
assert_eq!(conf.output_name.as_ref(), "app-test");
|
||||
assert!(matches!(conf.env, Env::DEV));
|
||||
assert_eq!(conf.site_pkg_dir, "pkg");
|
||||
assert_eq!(conf.site_root, ".");
|
||||
assert_eq!(conf.site_pkg_dir.as_ref(), "pkg");
|
||||
assert_eq!(conf.site_root.as_ref(), ".");
|
||||
assert_eq!(
|
||||
conf.site_addr,
|
||||
SocketAddr::from_str("127.0.0.1:3000").unwrap()
|
||||
@@ -242,9 +242,9 @@ fn environment_variable_override() {
|
||||
|| get_config_from_str(CARGO_TOML_CONTENT_OK).unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -265,9 +265,9 @@ fn environment_variable_override() {
|
||||
|| get_config_from_str(CARGO_TOML_CONTENT_OK).unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(config.output_name, "app-test2");
|
||||
assert_eq!(config.site_root, "my_target/site2");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg2");
|
||||
assert_eq!(config.output_name.as_ref(), "app-test2");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site2");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg2");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:82").unwrap()
|
||||
|
||||
@@ -12,10 +12,10 @@ edition.workspace = true
|
||||
tachys = { workspace = true }
|
||||
reactive_graph = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
js-sys = "0.3.69"
|
||||
js-sys = "0.3.72"
|
||||
send_wrapper = "0.6.0"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
wasm-bindgen = "0.2.93"
|
||||
wasm-bindgen = "0.2.95"
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
serde = { version = "1.0", optional = true }
|
||||
|
||||
@@ -23,7 +23,7 @@ serde = { version = "1.0", optional = true }
|
||||
leptos = { path = "../leptos" }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.70"
|
||||
version = "0.3.72"
|
||||
features = ["Location"]
|
||||
|
||||
[features]
|
||||
|
||||
@@ -4,11 +4,11 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
gloo = { version = "0.11.0", features = ["futures"] }
|
||||
leptos = { path = "../../../leptos", features = ["nightly", "csr", "tracing"] }
|
||||
tracing = "0.1.0"
|
||||
tracing-subscriber = "0.3.0"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
tracing-subscriber-wasm = "0.1.0"
|
||||
|
||||
[workspace]
|
||||
|
||||
@@ -8,5 +8,4 @@ pub mod helpers;
|
||||
pub mod macro_helpers;
|
||||
|
||||
/// Utilities for simple isomorphic logging to the console or terminal.
|
||||
#[macro_use]
|
||||
pub mod logging;
|
||||
|
||||
@@ -25,4 +25,4 @@ proc-macro2 = { version = "1.0", features = ["span-locations", "nightly"] }
|
||||
parking_lot = "0.12.3"
|
||||
walkdir = "2.5"
|
||||
camino = "1.1"
|
||||
indexmap = "2.3"
|
||||
indexmap = "2.6"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_macro"
|
||||
version = "0.7.0-gamma3"
|
||||
version = "0.7.0-rc1"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -13,11 +13,11 @@ edition.workspace = true
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
attribute-derive = { version = "0.9.2", features = ["syn-full"] }
|
||||
attribute-derive = { version = "0.10.2", features = ["syn-full"] }
|
||||
cfg-if = "1.0"
|
||||
html-escape = "0.2.13"
|
||||
itertools = "0.13.0"
|
||||
prettyplease = "0.2.20"
|
||||
prettyplease = "0.2.25"
|
||||
proc-macro-error2 = { version = "2.0", default-features = false }
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
@@ -26,16 +26,16 @@ rstml = "0.12.0"
|
||||
leptos_hot_reload = { workspace = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
convert_case = "0.6.0"
|
||||
uuid = { version = "1.10", features = ["v4"] }
|
||||
uuid = { version = "1.11", features = ["v4"] }
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
log = "0.4.22"
|
||||
typed-builder = "0.19.1"
|
||||
typed-builder = "0.20.0"
|
||||
trybuild = "1.0"
|
||||
leptos = { path = "../leptos" }
|
||||
server_fn = { path = "../server_fn", features = ["cbor"] }
|
||||
insta = "1.39"
|
||||
insta = "1.41"
|
||||
serde = "1.0"
|
||||
|
||||
[features]
|
||||
@@ -48,6 +48,7 @@ experimental-islands = []
|
||||
trace-component-props = []
|
||||
actix = ["server_fn_macro/actix"]
|
||||
axum = ["server_fn_macro/axum"]
|
||||
generic = ["server_fn_macro/generic"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly", "tracing", "trace-component-props"]
|
||||
@@ -68,6 +69,14 @@ skip_feature_sets = [
|
||||
"actix",
|
||||
"axum",
|
||||
],
|
||||
[
|
||||
"actix",
|
||||
"generic",
|
||||
],
|
||||
[
|
||||
"generic",
|
||||
"axum",
|
||||
],
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
@@ -23,8 +23,11 @@ use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
};
|
||||
use syn::{
|
||||
spanned::Spanned, Expr, Expr::Tuple, ExprLit, ExprRange, Lit, LitStr,
|
||||
RangeLimits, Stmt,
|
||||
punctuated::Pair::{End, Punctuated},
|
||||
spanned::Spanned,
|
||||
Expr,
|
||||
Expr::Tuple,
|
||||
ExprArray, ExprLit, ExprRange, Lit, LitStr, RangeLimits, Stmt,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
@@ -650,6 +653,9 @@ pub(crate) fn element_to_tokens(
|
||||
_ => None,
|
||||
};
|
||||
match (key_a.as_deref(), key_b.as_deref()) {
|
||||
(Some("class"), Some("class")) | (Some("style"), Some("style")) => {
|
||||
Ordering::Equal
|
||||
}
|
||||
(Some("class"), _) | (Some("style"), _) => Ordering::Less,
|
||||
(_, Some("class")) | (_, Some("style")) => Ordering::Greater,
|
||||
_ => Ordering::Equal,
|
||||
@@ -661,9 +667,18 @@ pub(crate) fn element_to_tokens(
|
||||
for attr in node.attributes() {
|
||||
if let NodeAttribute::Attribute(attr) = attr {
|
||||
let mut name = attr.key.to_string();
|
||||
if let Some(tuple_name) = tuple_name(&name, attr) {
|
||||
name.push(':');
|
||||
name.push_str(&tuple_name);
|
||||
match tuple_name(&name, attr) {
|
||||
TupleName::None => {}
|
||||
TupleName::Str(tuple_name) => {
|
||||
name.push(':');
|
||||
name.push_str(&tuple_name);
|
||||
}
|
||||
TupleName::Array(names) => {
|
||||
for tuple_name in names {
|
||||
name.push(':');
|
||||
name.push_str(&tuple_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
if names.contains(&name) {
|
||||
proc_macro_error2::emit_error!(
|
||||
@@ -986,10 +1001,10 @@ pub(crate) fn attribute_absolute(
|
||||
) -> Option<TokenStream> {
|
||||
let key = node.key.to_string();
|
||||
let contains_dash = key.contains('-');
|
||||
let attr_aira = key.starts_with("attr:aria-");
|
||||
let attr_colon = key.starts_with("attr:");
|
||||
// anything that follows the x:y pattern
|
||||
match &node.key {
|
||||
NodeName::Punctuated(parts) if !contains_dash || attr_aira => {
|
||||
NodeName::Punctuated(parts) if !contains_dash || attr_colon => {
|
||||
if parts.len() >= 2 {
|
||||
let id = &parts[0];
|
||||
match id {
|
||||
@@ -998,7 +1013,8 @@ pub(crate) fn attribute_absolute(
|
||||
if id == "let" || id == "clone" {
|
||||
None
|
||||
} else if id == "attr" {
|
||||
let value = attribute_value(node, true);
|
||||
let value = attribute_value(node, true);
|
||||
let multipart = parts.len() > 2;
|
||||
let key = &parts[1];
|
||||
let key_name = key.to_string();
|
||||
if key_name == "class" || key_name == "style" {
|
||||
@@ -1014,6 +1030,15 @@ pub(crate) fn attribute_absolute(
|
||||
Some(
|
||||
quote! { ::leptos::tachys::html::attribute::#key(#value) },
|
||||
)
|
||||
} else if multipart {
|
||||
// e.g., attr:data-foo="bar"
|
||||
let key_name = parts.pairs().skip(1).map(|p| match p {
|
||||
Punctuated(n, p) => format!("{n}{p}"),
|
||||
End(n) => n.to_string(),
|
||||
}).collect::<String>();
|
||||
Some(
|
||||
quote! { ::leptos::tachys::html::attribute::custom::custom_attribute(#key_name, #value) },
|
||||
)
|
||||
} else {
|
||||
Some(
|
||||
quote! { ::leptos::tachys::html::attribute::#key(#value) },
|
||||
@@ -1183,6 +1208,32 @@ fn class_to_tokens(
|
||||
class: TokenStream,
|
||||
class_name: Option<&str>,
|
||||
) -> TokenStream {
|
||||
// case of class=(["foo", "bar"], /* something */)
|
||||
// just expands to multiple uses of class:
|
||||
if let Some(Tuple(tuple)) = node.value() {
|
||||
if tuple.elems.len() == 2 {
|
||||
let name = &tuple.elems[0];
|
||||
let value = &tuple.elems[1];
|
||||
if let Expr::Array(ExprArray { elems, .. }) = name {
|
||||
return elems
|
||||
.iter()
|
||||
.map(|elem| match elem {
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s), ..
|
||||
}) => quote! {
|
||||
.#class((#s, #value))
|
||||
},
|
||||
_ => proc_macro_error2::abort!(
|
||||
elem.span(),
|
||||
"invalid name"
|
||||
),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// default case
|
||||
let value = attribute_value(node, false);
|
||||
if let Some(class_name) = class_name {
|
||||
quote! {
|
||||
@@ -1617,7 +1668,7 @@ pub(crate) fn directive_call_from_attribute_node(
|
||||
quote! { .directive(#handler, #[allow(clippy::useless_conversion)] #param) }
|
||||
}
|
||||
|
||||
fn tuple_name(name: &str, node: &KeyedAttribute) -> Option<String> {
|
||||
fn tuple_name(name: &str, node: &KeyedAttribute) -> TupleName {
|
||||
if name == "style" || name == "class" {
|
||||
if let Some(Tuple(tuple)) = node.value() {
|
||||
{
|
||||
@@ -1627,12 +1678,37 @@ fn tuple_name(name: &str, node: &KeyedAttribute) -> Option<String> {
|
||||
lit: Lit::Str(s), ..
|
||||
}) = style_name
|
||||
{
|
||||
return Some(s.value());
|
||||
return TupleName::Str(s.value());
|
||||
} else if let Expr::Array(ExprArray { elems, .. }) =
|
||||
style_name
|
||||
{
|
||||
return TupleName::Array(
|
||||
elems
|
||||
.iter()
|
||||
.filter_map(|elem| match elem {
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s),
|
||||
..
|
||||
}) => Some(s.value()),
|
||||
_ => proc_macro_error2::abort!(
|
||||
elem.span(),
|
||||
"invalid name"
|
||||
),
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
TupleName::None
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TupleName {
|
||||
None,
|
||||
Str(String),
|
||||
Array(Vec<String>),
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ hydration_context = { workspace = true }
|
||||
reactive_graph = { workspace = true, features = ["hydration"] }
|
||||
server_fn = { workspace = true }
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
futures = "0.3.30"
|
||||
futures = "0.3.31"
|
||||
|
||||
any_spawner = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
@@ -25,8 +25,8 @@ send_wrapper = "0.6"
|
||||
|
||||
# serialization formats
|
||||
serde = { version = "1.0" }
|
||||
js-sys = { version = "0.3.69", optional = true }
|
||||
wasm-bindgen = { version = "0.2.93", optional = true }
|
||||
js-sys = { version = "0.3.72", optional = true }
|
||||
wasm-bindgen = { version = "0.2.95", optional = true }
|
||||
serde_json = { version = "1.0" }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -18,7 +18,7 @@ use std::{
|
||||
};
|
||||
|
||||
pub struct ArcLocalResource<T> {
|
||||
data: ArcAsyncDerived<T>,
|
||||
data: ArcAsyncDerived<SendWrapper<T>>,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
@@ -59,8 +59,12 @@ impl<T> ArcLocalResource<T> {
|
||||
}
|
||||
}
|
||||
};
|
||||
let fetcher = SendWrapper::new(fetcher);
|
||||
Self {
|
||||
data: ArcAsyncDerived::new_unsync(fetcher),
|
||||
data: ArcAsyncDerived::new(move || {
|
||||
let fut = fetcher();
|
||||
SendWrapper::new(async move { SendWrapper::new(fut.await) })
|
||||
}),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
@@ -72,9 +76,14 @@ where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
type Output = T;
|
||||
type IntoFuture = AsyncDerivedFuture<T>;
|
||||
type IntoFuture = futures::future::Map<
|
||||
AsyncDerivedFuture<SendWrapper<T>>,
|
||||
fn(SendWrapper<T>) -> T,
|
||||
>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
use futures::FutureExt;
|
||||
|
||||
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||
notifier.notify();
|
||||
} else if cfg!(feature = "ssr") {
|
||||
@@ -84,7 +93,7 @@ where
|
||||
always pending on the server."
|
||||
);
|
||||
}
|
||||
self.data.into_future()
|
||||
self.data.into_future().map(|value| (*value).clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +114,8 @@ impl<T> ReadUntracked for ArcLocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value = ReadGuard<Option<T>, AsyncPlain<Option<T>>>;
|
||||
type Value =
|
||||
ReadGuard<Option<SendWrapper<T>>, AsyncPlain<Option<SendWrapper<T>>>>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||
@@ -373,3 +383,23 @@ where
|
||||
self.data.clear_sources(subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> From<ArcLocalResource<T>> for LocalResource<T> {
|
||||
fn from(arc: ArcLocalResource<T>) -> Self {
|
||||
Self {
|
||||
data: arc.data.into(),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: arc.defined_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> From<LocalResource<T>> for ArcLocalResource<T> {
|
||||
fn from(local: LocalResource<T>) -> Self {
|
||||
Self {
|
||||
data: local.data.into(),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: local.defined_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.7.0-gamma3"
|
||||
version = "0.7.0-rc1"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -10,16 +10,16 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { workspace = true }
|
||||
once_cell = "1.19"
|
||||
once_cell = "1.20"
|
||||
or_poisoned = { workspace = true }
|
||||
indexmap = "2.3"
|
||||
indexmap = "2.6"
|
||||
send_wrapper = "0.6.0"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
wasm-bindgen = "0.2.93"
|
||||
futures = "0.3.30"
|
||||
wasm-bindgen = "0.2.95"
|
||||
futures = "0.3.31"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.70"
|
||||
version = "0.3.72"
|
||||
features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
|
||||
|
||||
[features]
|
||||
|
||||
@@ -54,7 +54,7 @@ pub fn HashedStylesheet(
|
||||
path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.join(&options.hash_file);
|
||||
.join(options.hash_file.as_ref());
|
||||
if hash_path.exists() {
|
||||
let hashes = std::fs::read_to_string(&hash_path)
|
||||
.expect("failed to read hash file");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "next_tuple"
|
||||
version = "0.1.0-gamma3"
|
||||
version = "0.1.0-rc1"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -10,7 +10,7 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0"
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.1.0-gamma3"
|
||||
version = "0.1.0-rc1"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -12,23 +12,23 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
any_spawner = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
futures = "0.3.30"
|
||||
futures = "0.3.31"
|
||||
hydration_context = { workspace = true, optional = true }
|
||||
pin-project-lite = "0.2.14"
|
||||
pin-project-lite = "0.2.15"
|
||||
rustc-hash = "2.0"
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
slotmap = "1.0"
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
guardian = "1.2"
|
||||
async-lock = "3.4.0"
|
||||
send_wrapper = { version = "0.6.0", features = ["futures"] }
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
web-sys = "0.3.70"
|
||||
web-sys = "0.3.72"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.39", features = ["rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.41", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-test = { version = "0.4.4" }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
SyncStorage,
|
||||
},
|
||||
signal::{ArcRwSignal, RwSignal},
|
||||
traits::{DefinedAt, Dispose, Get, GetUntracked, GetValue, Update},
|
||||
traits::{DefinedAt, Dispose, Get, GetUntracked, GetValue, Set, Update},
|
||||
unwrap_signal,
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
@@ -202,6 +202,15 @@ where
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the value of the action, setting its current value to `None`.
|
||||
///
|
||||
/// This has no other effect: i.e., it will not cancel in-flight actions, set the
|
||||
/// input, etc.
|
||||
#[track_caller]
|
||||
pub fn clear(&self) {
|
||||
self.value.set(None);
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle that allows aborting an in-flight action. It is returned from [`Action::dispatch`] or
|
||||
@@ -678,6 +687,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O, S> Action<I, O, S>
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
S: Storage<ArcAction<I, O>>,
|
||||
{
|
||||
/// Clears the value of the action, setting its current value to `None`.
|
||||
///
|
||||
/// This has no other effect: i.e., it will not cancel in-flight actions, set the
|
||||
/// input, etc.
|
||||
#[track_caller]
|
||||
pub fn clear(&self) {
|
||||
self.inner.try_with_value(|inner| inner.value.set(None));
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O> Action<I, O, LocalStorage>
|
||||
where
|
||||
I: 'static,
|
||||
|
||||
@@ -286,10 +286,15 @@ macro_rules! spawn_derived {
|
||||
|
||||
let mut first_run = {
|
||||
let (ready_tx, ready_rx) = oneshot::channel();
|
||||
AsyncTransition::register(ready_rx);
|
||||
if !was_ready {
|
||||
AsyncTransition::register(ready_rx);
|
||||
}
|
||||
Some(ready_tx)
|
||||
};
|
||||
|
||||
if was_ready {
|
||||
first_run.take();
|
||||
}
|
||||
// begin loading eagerly but asynchronously, if not already loaded
|
||||
if !was_ready {
|
||||
any_subscriber.mark_dirty();
|
||||
@@ -339,7 +344,9 @@ macro_rules! spawn_derived {
|
||||
// register with global transition listener, if any
|
||||
let ready_tx = first_run.take().unwrap_or_else(|| {
|
||||
let (ready_tx, ready_rx) = oneshot::channel();
|
||||
AsyncTransition::register(ready_rx);
|
||||
if !was_ready {
|
||||
AsyncTransition::register(ready_rx);
|
||||
}
|
||||
ready_tx
|
||||
});
|
||||
|
||||
|
||||
@@ -130,8 +130,9 @@ where
|
||||
|
||||
changed
|
||||
} else {
|
||||
let mut lock = self.write().or_poisoned();
|
||||
lock.state = ReactiveNodeState::Clean;
|
||||
if let Ok(mut lock) = self.try_write() {
|
||||
lock.state = ReactiveNodeState::Clean;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -142,7 +143,9 @@ where
|
||||
S: Storage<T>,
|
||||
{
|
||||
fn add_subscriber(&self, subscriber: AnySubscriber) {
|
||||
self.write().or_poisoned().subscribers.subscribe(subscriber);
|
||||
if let Ok(mut lock) = self.try_write() {
|
||||
lock.subscribers.subscribe(subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_subscriber(&self, subscriber: &AnySubscriber) {
|
||||
|
||||
@@ -17,6 +17,7 @@ use std::{
|
||||
};
|
||||
|
||||
/// Effects run a certain chunk of code whenever the signals they depend on change.
|
||||
///
|
||||
/// Creating an effect runs the given function once after any current synchronous work is done.
|
||||
/// This tracks its reactive values read within it, and reruns the function whenever the value
|
||||
/// of a dependency changes.
|
||||
@@ -169,10 +170,9 @@ impl Effect<LocalStorage> {
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if first_run
|
||||
|| subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
})
|
||||
if subscriber
|
||||
.with_observer(|| subscriber.update_if_necessary())
|
||||
|| first_run
|
||||
{
|
||||
first_run = false;
|
||||
subscriber.clear_sources(&subscriber);
|
||||
@@ -321,10 +321,9 @@ impl Effect<LocalStorage> {
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if first_run
|
||||
|| subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
})
|
||||
if subscriber
|
||||
.with_observer(|| subscriber.update_if_necessary())
|
||||
|| first_run
|
||||
{
|
||||
subscriber.clear_sources(&subscriber);
|
||||
|
||||
@@ -389,10 +388,9 @@ impl Effect<SyncStorage> {
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if first_run
|
||||
|| subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
})
|
||||
if subscriber
|
||||
.with_observer(|| subscriber.update_if_necessary())
|
||||
|| first_run
|
||||
{
|
||||
first_run = false;
|
||||
subscriber.clear_sources(&subscriber);
|
||||
@@ -436,9 +434,9 @@ impl Effect<SyncStorage> {
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if first_run
|
||||
|| subscriber
|
||||
.with_observer(|| subscriber.update_if_necessary())
|
||||
if subscriber
|
||||
.with_observer(|| subscriber.update_if_necessary())
|
||||
|| first_run
|
||||
{
|
||||
first_run = false;
|
||||
subscriber.clear_sources(&subscriber);
|
||||
@@ -489,10 +487,9 @@ impl Effect<SyncStorage> {
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if first_run
|
||||
|| subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
})
|
||||
if subscriber
|
||||
.with_observer(|| subscriber.update_if_necessary())
|
||||
|| first_run
|
||||
{
|
||||
subscriber.clear_sources(&subscriber);
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ pub mod owner;
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde;
|
||||
pub mod signal;
|
||||
mod trait_options;
|
||||
pub mod traits;
|
||||
pub mod transition;
|
||||
pub mod wrappers;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#[allow(deprecated)]
|
||||
use crate::wrappers::read::{MaybeProp, MaybeSignal};
|
||||
use crate::{
|
||||
computed::{ArcMemo, Memo},
|
||||
owner::Storage,
|
||||
@@ -7,7 +9,7 @@ use crate::{
|
||||
},
|
||||
traits::{Get, Set},
|
||||
wrappers::{
|
||||
read::{ArcSignal, MaybeProp, MaybeSignal, Signal},
|
||||
read::{ArcSignal, Signal, SignalTypes},
|
||||
write::SignalSetter,
|
||||
},
|
||||
};
|
||||
@@ -112,7 +114,8 @@ macro_rules! impl_get_fn_traits_get_arena {
|
||||
($($ty:ident),*) => {
|
||||
$(
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<T, S> FnOnce<()> for $ty<T, S> where $ty<T, S>: Get, S: Storage<T> + Storage<Option<T>> {
|
||||
#[allow(deprecated)]
|
||||
impl<T, S> FnOnce<()> for $ty<T, S> where $ty<T, S>: Get, S: Storage<T> + Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>> {
|
||||
type Output = <Self as Get>::Value;
|
||||
|
||||
#[inline(always)]
|
||||
@@ -122,7 +125,8 @@ macro_rules! impl_get_fn_traits_get_arena {
|
||||
}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<T, S> FnMut<()> for $ty<T, S> where $ty<T, S>: Get, S: Storage<T> + Storage<Option<T>> {
|
||||
#[allow(deprecated)]
|
||||
impl<T, S> FnMut<()> for $ty<T, S> where $ty<T, S>: Get, S: Storage<T> + Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
@@ -130,7 +134,8 @@ macro_rules! impl_get_fn_traits_get_arena {
|
||||
}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<T, S> Fn<()> for $ty<T, S> where $ty<T, S>: Get, S: Storage<T> + Storage<Option<T>> {
|
||||
#[allow(deprecated)]
|
||||
impl<T, S> Fn<()> for $ty<T, S> where $ty<T, S>: Get, S: Storage<T> + Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
|
||||
@@ -168,9 +168,10 @@ pub fn provide_context<T: Send + Sync + 'static>(value: T) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts a context value of type `T` from the reactive system by traversing
|
||||
/// it upwards, beginning from the current reactive [`Owner`] and iterating
|
||||
/// through its parents, if any. When the value is found, it is cloned.
|
||||
/// Extracts a context value of type `T` from the reactive system.
|
||||
///
|
||||
/// This traverses the reactive ownership graph, beginning from the current reactive
|
||||
/// [`Owner`] and iterating through its parents, if any. When the value is found, it is cloned.
|
||||
///
|
||||
/// The context value should have been provided elsewhere using
|
||||
/// [`provide_context`](provide_context).
|
||||
@@ -212,9 +213,11 @@ pub fn use_context<T: Clone + 'static>() -> Option<T> {
|
||||
Owner::current().and_then(|owner| owner.use_context())
|
||||
}
|
||||
|
||||
/// Extracts a context value of type `T` from the reactive system by traversing
|
||||
/// it upwards, beginning from the current reactive [`Owner`] and iterating
|
||||
/// through its parents, if any. When the value is found, it is cloned.
|
||||
/// Extracts a context value of type `T` from the reactive system, and
|
||||
/// panics if it can't be found.
|
||||
///
|
||||
/// This traverses the reactive ownership graph, beginning from the current reactive
|
||||
/// [`Owner`] and iterating through its parents, if any. When the value is found, it is cloned.
|
||||
///
|
||||
/// Panics if no value is found.
|
||||
///
|
||||
@@ -270,9 +273,11 @@ pub fn expect_context<T: Clone + 'static>() -> T {
|
||||
})
|
||||
}
|
||||
|
||||
/// Extracts a context value of type `T` from the reactive system by traversing
|
||||
/// it upwards, beginning from the current reactive [`Owner`] and iterating
|
||||
/// through its parents, if any. When the value is found, it is removed from the context,
|
||||
/// Extracts a context value of type `T` from the reactive system, and takes ownership,
|
||||
/// removing it from the context system.
|
||||
///
|
||||
/// This traverses the reactive ownership graph, beginning from the current reactive
|
||||
/// [`Owner`] and iterating through its parents, if any. When the value is found, it is removed,
|
||||
/// and is not available to any other [`use_context`] or [`take_context`] calls.
|
||||
///
|
||||
/// If the value is `Clone`, use [`use_context`] instead.
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#[allow(deprecated)]
|
||||
use crate::wrappers::read::{MaybeProp, MaybeSignal};
|
||||
use crate::{
|
||||
computed::{ArcMemo, Memo},
|
||||
owner::Storage,
|
||||
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
|
||||
traits::With,
|
||||
wrappers::read::{MaybeProp, MaybeSignal, Signal, SignalTypes},
|
||||
wrappers::read::{Signal, SignalTypes},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -73,6 +75,7 @@ impl<T: Serialize + 'static, St: Storage<T>> Serialize for ArcMemo<T, St> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T, St> Serialize for MaybeSignal<T, St>
|
||||
where
|
||||
T: Clone + Send + Sync + Serialize,
|
||||
@@ -96,15 +99,8 @@ where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match &self.0 {
|
||||
None | Some(MaybeSignal::Static(None)) => {
|
||||
None::<T>.serialize(serializer)
|
||||
}
|
||||
Some(MaybeSignal::Static(Some(value))) => {
|
||||
value.serialize(serializer)
|
||||
}
|
||||
Some(MaybeSignal::Dynamic(signal)) => {
|
||||
signal.with(|value| value.serialize(serializer))
|
||||
}
|
||||
None => None::<T>.serialize(serializer),
|
||||
Some(signal) => signal.with(|value| value.serialize(serializer)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,6 +142,7 @@ impl<'de, T: Deserialize<'de>> Deserialize<'de> for ArcRwSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<'de, T: Deserialize<'de>, St> Deserialize<'de> for MaybeSignal<T, St>
|
||||
where
|
||||
St: Storage<T>,
|
||||
|
||||
238
reactive_graph/src/trait_options.rs
Normal file
238
reactive_graph/src/trait_options.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
use crate::{
|
||||
traits::{
|
||||
DefinedAt, Get, GetUntracked, Read, ReadUntracked, Track, With,
|
||||
WithUntracked,
|
||||
},
|
||||
unwrap_signal,
|
||||
};
|
||||
use std::panic::Location;
|
||||
|
||||
impl<T> DefinedAt for Option<T>
|
||||
where
|
||||
T: DefinedAt,
|
||||
{
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
self.as_ref().map(DefinedAt::defined_at).unwrap_or(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Track for Option<T>
|
||||
where
|
||||
T: Track,
|
||||
{
|
||||
fn track(&self) {
|
||||
if let Some(signal) = self {
|
||||
signal.track();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An alternative [`ReadUntracked`](crate) trait that works with `Option<Readable>` types.
|
||||
pub trait ReadUntrackedOptional: Sized + DefinedAt {
|
||||
/// The guard type that will be returned, which can be dereferenced to the value.
|
||||
type Value;
|
||||
|
||||
/// Returns the guard, or `None` if the signal has already been disposed.
|
||||
#[track_caller]
|
||||
fn try_read_untracked(&self) -> Option<Self::Value>;
|
||||
|
||||
/// Returns the guard.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that has been disposed.
|
||||
#[track_caller]
|
||||
fn read_untracked(&self) -> Self::Value {
|
||||
self.try_read_untracked()
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ReadUntrackedOptional for Option<T>
|
||||
where
|
||||
Self: DefinedAt,
|
||||
T: ReadUntracked,
|
||||
{
|
||||
type Value = Option<<T as ReadUntracked>::Value>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
Some(if let Some(signal) = self {
|
||||
Some(signal.try_read_untracked()?)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An alternative [`Read`](crate) trait that works with `Option<Readable>` types.
|
||||
pub trait ReadOptional: DefinedAt {
|
||||
/// The guard type that will be returned, which can be dereferenced to the value.
|
||||
type Value;
|
||||
|
||||
/// Subscribes to the signal, and returns the guard, or `None` if the signal has already been disposed.
|
||||
#[track_caller]
|
||||
fn try_read(&self) -> Option<Self::Value>;
|
||||
|
||||
/// Subscribes to the signal, and returns the guard.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that has been disposed.
|
||||
#[track_caller]
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ReadOptional for Option<T>
|
||||
where
|
||||
Self: DefinedAt,
|
||||
T: Read,
|
||||
{
|
||||
type Value = Option<<T as Read>::Value>;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
Some(if let Some(readable) = self {
|
||||
Some(readable.try_read()?)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An alternative [`WithUntracked`](crate) trait that works with `Option<Withable>` types.
|
||||
pub trait WithUntrackedOptional: DefinedAt {
|
||||
/// The type of the value contained in the signal.
|
||||
type Value: ?Sized;
|
||||
|
||||
/// Applies the closure to the value, and returns the result,
|
||||
/// or `None` if the signal has already been disposed.
|
||||
#[track_caller]
|
||||
fn try_with_untracked<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(Option<&Self::Value>) -> U,
|
||||
) -> Option<U>;
|
||||
|
||||
/// Applies the closure to the value, and returns the result.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that has been disposed.
|
||||
#[track_caller]
|
||||
fn with_untracked<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(Option<&Self::Value>) -> U,
|
||||
) -> U {
|
||||
self.try_with_untracked(fun)
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> WithUntrackedOptional for Option<T>
|
||||
where
|
||||
Self: DefinedAt,
|
||||
T: WithUntracked,
|
||||
<T as WithUntracked>::Value: Sized,
|
||||
{
|
||||
type Value = <T as WithUntracked>::Value;
|
||||
|
||||
fn try_with_untracked<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(Option<&Self::Value>) -> U,
|
||||
) -> Option<U> {
|
||||
if let Some(signal) = self {
|
||||
Some(signal.try_with_untracked(|val| fun(Some(val)))?)
|
||||
} else {
|
||||
Some(fun(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An alternative [`With`](crate) trait that works with `Option<Withable>` types.
|
||||
pub trait WithOptional: DefinedAt {
|
||||
/// The type of the value contained in the signal.
|
||||
type Value: ?Sized;
|
||||
|
||||
/// Subscribes to the signal, applies the closure to the value, and returns the result,
|
||||
/// or `None` if the signal has already been disposed.
|
||||
#[track_caller]
|
||||
fn try_with<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(Option<&Self::Value>) -> U,
|
||||
) -> Option<U>;
|
||||
|
||||
/// Subscribes to the signal, applies the closure to the value, and returns the result.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that has been disposed.
|
||||
#[track_caller]
|
||||
fn with<U>(&self, fun: impl FnOnce(Option<&Self::Value>) -> U) -> U {
|
||||
self.try_with(fun).unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> WithOptional for Option<T>
|
||||
where
|
||||
Self: DefinedAt,
|
||||
T: With,
|
||||
<T as With>::Value: Sized,
|
||||
{
|
||||
type Value = <T as With>::Value;
|
||||
|
||||
fn try_with<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(Option<&Self::Value>) -> U,
|
||||
) -> Option<U> {
|
||||
if let Some(signal) = self {
|
||||
Some(signal.try_with(|val| fun(Some(val)))?)
|
||||
} else {
|
||||
Some(fun(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> GetUntracked for Option<T>
|
||||
where
|
||||
Self: DefinedAt,
|
||||
T: GetUntracked,
|
||||
{
|
||||
type Value = Option<<T as GetUntracked>::Value>;
|
||||
|
||||
fn try_get_untracked(&self) -> Option<Self::Value> {
|
||||
Some(if let Some(signal) = self {
|
||||
Some(signal.try_get_untracked()?)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Get for Option<T>
|
||||
where
|
||||
Self: DefinedAt,
|
||||
T: Get,
|
||||
{
|
||||
type Value = Option<<T as Get>::Value>;
|
||||
|
||||
fn try_get(&self) -> Option<Self::Value> {
|
||||
Some(if let Some(signal) = self {
|
||||
Some(signal.try_get()?)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper trait to implement flatten() on Option<&Option<T>>.
|
||||
pub trait FlattenOptionRefOption {
|
||||
/// The type of the value contained in the double option.
|
||||
type Value;
|
||||
|
||||
/// Converts from `Option<&Option<T>>` to `Option<&T>`.
|
||||
fn flatten(&self) -> Option<&Self::Value>;
|
||||
}
|
||||
|
||||
impl<'a, T> FlattenOptionRefOption for Option<&'a Option<T>> {
|
||||
type Value = T;
|
||||
|
||||
fn flatten(&self) -> Option<&'a T> {
|
||||
self.map(Option::as_ref).flatten()
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@
|
||||
//! there isn't an `RwLock` so you can't wrap in a [`ReadGuard`](crate::signal::guards::ReadGuard),
|
||||
//! but you can still implement [`WithUntracked`] and [`Track`], the same traits will still be implemented.
|
||||
|
||||
pub use crate::trait_options::*;
|
||||
use crate::{
|
||||
effect::Effect,
|
||||
graph::{Observer, Source, Subscriber, ToAnySource},
|
||||
|
||||
@@ -708,6 +708,34 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ArcReadSignal<T>> for Signal<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: ArcReadSignal<T>) -> Self {
|
||||
Self {
|
||||
inner: ArenaItem::new(SignalTypes::ReadSignal(value)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ArcReadSignal<T>> for Signal<T, LocalStorage>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: ArcReadSignal<T>) -> Self {
|
||||
Self {
|
||||
inner: ArenaItem::new_local(SignalTypes::ReadSignal(value)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RwSignal<T>> for Signal<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
@@ -740,6 +768,38 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ArcRwSignal<T>> for Signal<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: ArcRwSignal<T>) -> Self {
|
||||
Self {
|
||||
inner: ArenaItem::new(SignalTypes::ReadSignal(
|
||||
value.read_only(),
|
||||
)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ArcRwSignal<T>> for Signal<T, LocalStorage>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: ArcRwSignal<T>) -> Self {
|
||||
Self {
|
||||
inner: ArenaItem::new_local(SignalTypes::ReadSignal(
|
||||
value.read_only(),
|
||||
)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Memo<T>> for Signal<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
@@ -768,6 +828,74 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ArcMemo<T>> for Signal<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: ArcMemo<T>) -> Self {
|
||||
Self {
|
||||
inner: ArenaItem::new(SignalTypes::Memo(value)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ArcMemo<T, LocalStorage>> for Signal<T, LocalStorage>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: ArcMemo<T, LocalStorage>) -> Self {
|
||||
Self {
|
||||
inner: ArenaItem::new_local(SignalTypes::Memo(value)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Signal<Option<T>>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: T) -> Self {
|
||||
Signal::stored(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Signal<Option<T>, LocalStorage>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: T) -> Self {
|
||||
Signal::stored_local(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Signal<T>> for Signal<Option<T>>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: Signal<T>) -> Self {
|
||||
Signal::derive(move || Some(value.get()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Signal<T, LocalStorage>> for Signal<Option<T>, LocalStorage>
|
||||
where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: Signal<T, LocalStorage>) -> Self {
|
||||
Signal::derive_local(move || Some(value.get()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Signal<String> {
|
||||
#[track_caller]
|
||||
fn from(value: &str) -> Self {
|
||||
@@ -782,6 +910,145 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Signal<Option<String>> {
|
||||
#[track_caller]
|
||||
fn from(value: &str) -> Self {
|
||||
Signal::stored(Some(value.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Signal<Option<String>, LocalStorage> {
|
||||
#[track_caller]
|
||||
fn from(value: &str) -> Self {
|
||||
Signal::stored_local(Some(value.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signal<&'static str>> for Signal<String> {
|
||||
#[track_caller]
|
||||
fn from(value: Signal<&'static str>) -> Self {
|
||||
Signal::derive(move || value.read().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signal<&'static str>> for Signal<String, LocalStorage> {
|
||||
#[track_caller]
|
||||
fn from(value: Signal<&'static str>) -> Self {
|
||||
Signal::derive_local(move || value.read().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signal<&'static str>> for Signal<Option<String>> {
|
||||
#[track_caller]
|
||||
fn from(value: Signal<&'static str>) -> Self {
|
||||
Signal::derive(move || Some(value.read().to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signal<&'static str>> for Signal<Option<String>, LocalStorage> {
|
||||
#[track_caller]
|
||||
fn from(value: Signal<&'static str>) -> Self {
|
||||
Signal::derive_local(move || Some(value.read().to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signal<Option<&'static str>>> for Signal<Option<String>> {
|
||||
#[track_caller]
|
||||
fn from(value: Signal<Option<&'static str>>) -> Self {
|
||||
Signal::derive(move || value.read().map(str::to_string))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signal<Option<&'static str>>>
|
||||
for Signal<Option<String>, LocalStorage>
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: Signal<Option<&'static str>>) -> Self {
|
||||
Signal::derive_local(move || value.read().map(str::to_string))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<MaybeSignal<T>> for Signal<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: MaybeSignal<T>) -> Self {
|
||||
match value {
|
||||
MaybeSignal::Static(value) => Signal::stored(value),
|
||||
MaybeSignal::Dynamic(signal) => signal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<MaybeSignal<T, LocalStorage>> for Signal<T, LocalStorage>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: MaybeSignal<T, LocalStorage>) -> Self {
|
||||
match value {
|
||||
MaybeSignal::Static(value) => Signal::stored_local(value),
|
||||
MaybeSignal::Dynamic(signal) => signal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<MaybeSignal<T>> for Signal<Option<T>>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: MaybeSignal<T>) -> Self {
|
||||
match value {
|
||||
MaybeSignal::Static(value) => Signal::stored(Some(value)),
|
||||
MaybeSignal::Dynamic(signal) => {
|
||||
Signal::derive(move || Some(signal.get()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<MaybeSignal<T, LocalStorage>> for Signal<Option<T>, LocalStorage>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: MaybeSignal<T, LocalStorage>) -> Self {
|
||||
match value {
|
||||
MaybeSignal::Static(value) => Signal::stored_local(Some(value)),
|
||||
MaybeSignal::Dynamic(signal) => {
|
||||
Signal::derive_local(move || Some(signal.get()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<MaybeProp<T>> for Option<Signal<Option<T>>>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: MaybeProp<T>) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<MaybeProp<T, LocalStorage>>
|
||||
for Option<Signal<Option<T>, LocalStorage>>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: MaybeProp<T, LocalStorage>) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for a value that is *either* `T` or [`Signal<T>`].
|
||||
///
|
||||
/// This allows you to create APIs that take either a reactive or a non-reactive value
|
||||
@@ -810,6 +1077,12 @@ pub mod read {
|
||||
/// assert_eq!(above_3(&memoized_double_count.into()), true);
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[deprecated(
|
||||
since = "0.7.0-rc1",
|
||||
note = "`MaybeSignal<T>` is deprecated in favour of `Signal<T>` which \
|
||||
is `Copy`, now has a more efficient From<T> implementation \
|
||||
and other benefits in 0.7."
|
||||
)]
|
||||
pub enum MaybeSignal<T, S = SyncStorage>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -821,6 +1094,7 @@ pub mod read {
|
||||
Dynamic(Signal<T, S>),
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T: Clone, S> Clone for MaybeSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
@@ -833,8 +1107,10 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T: Copy, S> Copy for MaybeSignal<T, S> where S: Storage<T> {}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T: Default, S> Default for MaybeSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
@@ -844,6 +1120,7 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T, S> DefinedAt for MaybeSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
@@ -855,6 +1132,7 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T, S> Track for MaybeSignal<T, S>
|
||||
where
|
||||
S: Storage<T> + Storage<SignalTypes<T, S>>,
|
||||
@@ -867,6 +1145,7 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T, S> ReadUntracked for MaybeSignal<T, S>
|
||||
where
|
||||
T: Clone,
|
||||
@@ -891,6 +1170,7 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> MaybeSignal<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
@@ -904,6 +1184,7 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> MaybeSignal<T, LocalStorage> {
|
||||
/// Wraps a derived signal, i.e., any computation that accesses one or more
|
||||
/// reactive signals.
|
||||
@@ -912,6 +1193,7 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<T> for MaybeSignal<T, SyncStorage>
|
||||
where
|
||||
SyncStorage: Storage<T>,
|
||||
@@ -921,6 +1203,7 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> FromLocal<T> for MaybeSignal<T, LocalStorage>
|
||||
where
|
||||
LocalStorage: Storage<T>,
|
||||
@@ -930,6 +1213,7 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<ReadSignal<T>> for MaybeSignal<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
@@ -939,12 +1223,14 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<ReadSignal<T, LocalStorage>> for MaybeSignal<T, LocalStorage> {
|
||||
fn from(value: ReadSignal<T, LocalStorage>) -> Self {
|
||||
Self::Dynamic(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<RwSignal<T>> for MaybeSignal<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
@@ -954,12 +1240,14 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<RwSignal<T, LocalStorage>> for MaybeSignal<T, LocalStorage> {
|
||||
fn from(value: RwSignal<T, LocalStorage>) -> Self {
|
||||
Self::Dynamic(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<Memo<T>> for MaybeSignal<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
@@ -969,12 +1257,14 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<Memo<T, LocalStorage>> for MaybeSignal<T, LocalStorage> {
|
||||
fn from(value: Memo<T, LocalStorage>) -> Self {
|
||||
Self::Dynamic(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<ArcReadSignal<T>> for MaybeSignal<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
@@ -984,12 +1274,14 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> FromLocal<ArcReadSignal<T>> for MaybeSignal<T, LocalStorage> {
|
||||
fn from_local(value: ArcReadSignal<T>) -> Self {
|
||||
ReadSignal::from_local(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<ArcRwSignal<T>> for MaybeSignal<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
@@ -999,6 +1291,7 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> FromLocal<ArcRwSignal<T>> for MaybeSignal<T, LocalStorage>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -1008,6 +1301,7 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<ArcMemo<T>> for MaybeSignal<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
@@ -1017,12 +1311,14 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> FromLocal<ArcMemo<T, LocalStorage>> for MaybeSignal<T, LocalStorage> {
|
||||
fn from_local(value: ArcMemo<T, LocalStorage>) -> Self {
|
||||
Memo::from_local(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T, S> From<Signal<T, S>> for MaybeSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
@@ -1032,6 +1328,7 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<S> From<&str> for MaybeSignal<String, S>
|
||||
where
|
||||
S: Storage<String> + Storage<Arc<RwLock<String>>>,
|
||||
@@ -1041,9 +1338,10 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapping type for an optional component prop, which can either be a signal or a
|
||||
/// non-reactive value, and which may or may not have a value. In other words, this is
|
||||
/// an `Option<MaybeSignal<Option<T>>>` that automatically flattens its getters.
|
||||
/// A wrapping type for an optional component prop.
|
||||
///
|
||||
/// This can either be a signal or a non-reactive value, and may or may not have a value.
|
||||
/// In other words, this is an `Option<Signal<Option<T>>>`, but automatically flattens its getters.
|
||||
///
|
||||
/// This creates an extremely flexible type for component libraries, etc.
|
||||
///
|
||||
@@ -1074,25 +1372,28 @@ pub mod read {
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct MaybeProp<T: 'static, S = SyncStorage>(
|
||||
pub(crate) Option<MaybeSignal<Option<T>, S>>,
|
||||
pub(crate) Option<Signal<Option<T>, S>>,
|
||||
)
|
||||
where
|
||||
S: Storage<Option<T>>;
|
||||
S: Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>>;
|
||||
|
||||
impl<T: Clone, S> Clone for MaybeProp<T, S>
|
||||
impl<T, S> Clone for MaybeProp<T, S>
|
||||
where
|
||||
S: Storage<Option<T>>,
|
||||
S: Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>>,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy, S> Copy for MaybeProp<T, S> where S: Storage<Option<T>> {}
|
||||
impl<T, S> Copy for MaybeProp<T, S> where
|
||||
S: Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>>
|
||||
{
|
||||
}
|
||||
|
||||
impl<T, S> Default for MaybeProp<T, S>
|
||||
where
|
||||
S: Storage<Option<T>>,
|
||||
S: Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>>,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self(None)
|
||||
@@ -1101,7 +1402,7 @@ pub mod read {
|
||||
|
||||
impl<T, S> DefinedAt for MaybeProp<T, S>
|
||||
where
|
||||
S: Storage<Option<T>>,
|
||||
S: Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>>,
|
||||
{
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
// TODO this can be improved by adding a defined_at field
|
||||
@@ -1124,7 +1425,7 @@ pub mod read {
|
||||
impl<T, S> ReadUntracked for MaybeProp<T, S>
|
||||
where
|
||||
T: Clone,
|
||||
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
|
||||
S: Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>>,
|
||||
{
|
||||
type Value = ReadGuard<Option<T>, SignalReadGuard<Option<T>, S>>;
|
||||
|
||||
@@ -1152,43 +1453,49 @@ pub mod read {
|
||||
pub fn derive(
|
||||
derived_signal: impl Fn() -> Option<T> + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
Self(Some(MaybeSignal::derive(derived_signal)))
|
||||
Self(Some(Signal::derive(derived_signal)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for MaybeProp<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
SyncStorage: Storage<Option<T>>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self(Some(MaybeSignal::from(Some(value))))
|
||||
Self(Some(Signal::stored(Some(value))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Option<T>> for MaybeProp<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
SyncStorage: Storage<Option<T>>,
|
||||
{
|
||||
fn from(value: Option<T>) -> Self {
|
||||
Self(Some(MaybeSignal::from(value)))
|
||||
Self(Some(Signal::stored(value)))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<MaybeSignal<Option<T>>> for MaybeProp<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
SyncStorage: Storage<Option<T>>,
|
||||
{
|
||||
fn from(value: MaybeSignal<Option<T>>) -> Self {
|
||||
Self(Some(value))
|
||||
Self(Some(value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<Option<MaybeSignal<Option<T>>>> for MaybeProp<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
SyncStorage: Storage<Option<T>>,
|
||||
{
|
||||
fn from(value: Option<MaybeSignal<Option<T>>>) -> Self {
|
||||
Self(value)
|
||||
Self(value.map(Into::into))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1221,10 +1528,11 @@ pub mod read {
|
||||
|
||||
impl<T> From<Signal<Option<T>>> for MaybeProp<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
SyncStorage: Storage<Option<T>>,
|
||||
{
|
||||
fn from(value: Signal<Option<T>>) -> Self {
|
||||
Self(Some(value.into()))
|
||||
Self(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1233,7 +1541,7 @@ pub mod read {
|
||||
T: Send + Sync + Clone,
|
||||
{
|
||||
fn from(value: ReadSignal<T>) -> Self {
|
||||
Self(Some(MaybeSignal::derive(move || Some(value.get()))))
|
||||
Self(Some(Signal::derive(move || Some(value.get()))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1242,7 +1550,7 @@ pub mod read {
|
||||
T: Send + Sync + Clone,
|
||||
{
|
||||
fn from(value: RwSignal<T>) -> Self {
|
||||
Self(Some(MaybeSignal::derive(move || Some(value.get()))))
|
||||
Self(Some(Signal::derive(move || Some(value.get()))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1251,7 +1559,7 @@ pub mod read {
|
||||
T: Send + Sync + Clone,
|
||||
{
|
||||
fn from(value: Memo<T>) -> Self {
|
||||
Self(Some(MaybeSignal::derive(move || Some(value.get()))))
|
||||
Self(Some(Signal::derive(move || Some(value.get()))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1260,13 +1568,13 @@ pub mod read {
|
||||
T: Send + Sync + Clone,
|
||||
{
|
||||
fn from(value: Signal<T>) -> Self {
|
||||
Self(Some(MaybeSignal::derive(move || Some(value.get()))))
|
||||
Self(Some(Signal::derive(move || Some(value.get()))))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for MaybeProp<String> {
|
||||
fn from(value: &str) -> Self {
|
||||
Self(Some(MaybeSignal::from(Some(value.to_string()))))
|
||||
Self(Some(Signal::from(Some(value.to_string()))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1276,35 +1584,41 @@ pub mod read {
|
||||
pub fn derive_local(
|
||||
derived_signal: impl Fn() -> Option<T> + 'static,
|
||||
) -> Self {
|
||||
Self(Some(MaybeSignal::derive_local(derived_signal)))
|
||||
Self(Some(Signal::derive_local(derived_signal)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromLocal<T> for MaybeProp<T, LocalStorage> {
|
||||
fn from_local(value: T) -> Self {
|
||||
Self(Some(MaybeSignal::from_local(Some(value))))
|
||||
Self(Some(Signal::stored_local(Some(value))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromLocal<Option<T>> for MaybeProp<T, LocalStorage> {
|
||||
fn from_local(value: Option<T>) -> Self {
|
||||
Self(Some(MaybeSignal::from_local(value)))
|
||||
Self(Some(Signal::stored_local(value)))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<MaybeSignal<Option<T>, LocalStorage>>
|
||||
for MaybeProp<T, LocalStorage>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
fn from(value: MaybeSignal<Option<T>, LocalStorage>) -> Self {
|
||||
Self(Some(value))
|
||||
Self(Some(value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<T> From<Option<MaybeSignal<Option<T>, LocalStorage>>>
|
||||
for MaybeProp<T, LocalStorage>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
fn from(value: Option<MaybeSignal<Option<T>, LocalStorage>>) -> Self {
|
||||
Self(value)
|
||||
Self(value.map(Into::into))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1337,7 +1651,7 @@ pub mod read {
|
||||
|
||||
impl<T> From<Signal<Option<T>, LocalStorage>> for MaybeProp<T, LocalStorage> {
|
||||
fn from(value: Signal<Option<T>, LocalStorage>) -> Self {
|
||||
Self(Some(value.into()))
|
||||
Self(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1346,7 +1660,7 @@ pub mod read {
|
||||
T: Send + Sync + Clone,
|
||||
{
|
||||
fn from(value: ReadSignal<T, LocalStorage>) -> Self {
|
||||
Self(Some(MaybeSignal::derive_local(move || Some(value.get()))))
|
||||
Self(Some(Signal::derive_local(move || Some(value.get()))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1355,7 +1669,7 @@ pub mod read {
|
||||
T: Send + Sync + Clone,
|
||||
{
|
||||
fn from(value: RwSignal<T, LocalStorage>) -> Self {
|
||||
Self(Some(MaybeSignal::derive_local(move || Some(value.get()))))
|
||||
Self(Some(Signal::derive_local(move || Some(value.get()))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1364,7 +1678,7 @@ pub mod read {
|
||||
T: Send + Sync + Clone,
|
||||
{
|
||||
fn from(value: Memo<T, LocalStorage>) -> Self {
|
||||
Self(Some(MaybeSignal::derive_local(move || Some(value.get()))))
|
||||
Self(Some(Signal::derive_local(move || Some(value.get()))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1373,13 +1687,13 @@ pub mod read {
|
||||
T: Send + Sync + Clone,
|
||||
{
|
||||
fn from(value: Signal<T, LocalStorage>) -> Self {
|
||||
Self(Some(MaybeSignal::derive_local(move || Some(value.get()))))
|
||||
Self(Some(Signal::derive_local(move || Some(value.get()))))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for MaybeProp<String, LocalStorage> {
|
||||
fn from(value: &str) -> Self {
|
||||
Self(Some(MaybeSignal::from_local(Some(value.to_string()))))
|
||||
Self(Some(Signal::stored_local(Some(value.to_string()))))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_stores"
|
||||
version = "0.1.0-gamma3"
|
||||
version = "0.1.0-rc1"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -19,7 +19,7 @@ rustc-hash = "2.0"
|
||||
reactive_stores_macro = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.39", features = ["rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.41", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-test = { version = "0.4.4" }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
reactive_graph = { workspace = true, features = ["effects"] }
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use crate::{
|
||||
path::{StorePath, StorePathSegment},
|
||||
AtIndex, AtKeyed, KeyMap, KeyedSubfield, StoreField, StoreFieldTrigger,
|
||||
Subfield,
|
||||
ArcStore, AtIndex, AtKeyed, KeyMap, KeyedSubfield, Store, StoreField,
|
||||
StoreFieldTrigger, Subfield,
|
||||
};
|
||||
use reactive_graph::traits::{
|
||||
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
|
||||
use reactive_graph::{
|
||||
owner::Storage,
|
||||
traits::{
|
||||
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
@@ -26,6 +29,7 @@ where
|
||||
read: Arc<dyn Fn() -> Option<StoreFieldReader<T>> + Send + Sync>,
|
||||
write: Arc<dyn Fn() -> Option<StoreFieldWriter<T>> + Send + Sync>,
|
||||
keys: Arc<dyn Fn() -> Option<KeyMap> + Send + Sync>,
|
||||
track_field: Arc<dyn Fn() + Send + Sync>,
|
||||
}
|
||||
|
||||
pub struct StoreFieldReader<T>(Box<dyn Deref<Target = T>>);
|
||||
@@ -98,6 +102,62 @@ impl<T> StoreField for ArcField<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<Store<T, S>> for ArcField<T>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<ArcStore<T>>,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: Store<T, S>) -> Self {
|
||||
ArcField {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
path: value.path().into_iter().collect(),
|
||||
trigger: value.get_trigger(value.path().into_iter().collect()),
|
||||
get_trigger: Arc::new(move |path| value.get_trigger(path)),
|
||||
read: Arc::new(move || value.reader().map(StoreFieldReader::new)),
|
||||
write: Arc::new(move || value.writer().map(StoreFieldWriter::new)),
|
||||
keys: Arc::new(move || value.keys()),
|
||||
track_field: Arc::new(move || value.track_field()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ArcStore<T>> for ArcField<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: ArcStore<T>) -> Self {
|
||||
ArcField {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
path: value.path().into_iter().collect(),
|
||||
trigger: value.get_trigger(value.path().into_iter().collect()),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
}),
|
||||
read: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.reader().map(StoreFieldReader::new)
|
||||
}),
|
||||
write: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.writer().map(StoreFieldWriter::new)
|
||||
}),
|
||||
keys: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.keys()
|
||||
}),
|
||||
track_field: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.track_field()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, Prev, T> From<Subfield<Inner, Prev, T>> for ArcField<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
@@ -128,6 +188,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.keys()
|
||||
}),
|
||||
track_field: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.track_field()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +226,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.keys()
|
||||
}),
|
||||
track_field: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.track_field()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,6 +268,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.keys()
|
||||
}),
|
||||
track_field: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.track_field()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,6 +287,7 @@ impl<T> Clone for ArcField<T> {
|
||||
read: Arc::clone(&self.read),
|
||||
write: Arc::clone(&self.write),
|
||||
keys: Arc::clone(&self.keys),
|
||||
track_field: Arc::clone(&self.track_field),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,8 +313,7 @@ impl<T> Notify for ArcField<T> {
|
||||
|
||||
impl<T> Track for ArcField<T> {
|
||||
fn track(&self) {
|
||||
self.trigger.this.track();
|
||||
self.trigger.children.track();
|
||||
(self.track_field)();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
arc_field::{StoreFieldReader, StoreFieldWriter},
|
||||
path::{StorePath, StorePathSegment},
|
||||
ArcField, AtIndex, AtKeyed, KeyMap, KeyedSubfield, StoreField,
|
||||
StoreFieldTrigger, Subfield,
|
||||
ArcField, ArcStore, AtIndex, AtKeyed, KeyMap, KeyedSubfield, Store,
|
||||
StoreField, StoreFieldTrigger, Subfield,
|
||||
};
|
||||
use reactive_graph::{
|
||||
owner::{ArenaItem, Storage, SyncStorage},
|
||||
@@ -55,6 +55,36 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<Store<T, S>> for Field<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<ArcStore<T>> + Storage<ArcField<T>>,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: Store<T, S>) -> Self {
|
||||
Field {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: ArenaItem::new_with_storage(value.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<ArcStore<T>> for Field<T, S>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
S: Storage<ArcStore<T>> + Storage<ArcField<T>>,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: ArcStore<T>) -> Self {
|
||||
Field {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: ArenaItem::new_with_storage(value.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, Prev, T, S> From<Subfield<Inner, Prev, T>> for Field<T, S>
|
||||
where
|
||||
T: Send + Sync,
|
||||
|
||||
@@ -152,9 +152,7 @@ where
|
||||
Prev::Output: Sized + 'static,
|
||||
{
|
||||
fn track(&self) {
|
||||
let trigger = self.get_trigger(self.path().into_iter().collect());
|
||||
trigger.this.track();
|
||||
trigger.children.track();
|
||||
self.track_field();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,9 +195,9 @@ pub trait StoreFieldIterator<Prev>
|
||||
where
|
||||
Self: StoreField<Value = Prev>,
|
||||
{
|
||||
fn at(self, index: usize) -> AtIndex<Self, Prev>;
|
||||
fn at_unkeyed(self, index: usize) -> AtIndex<Self, Prev>;
|
||||
|
||||
fn iter(self) -> StoreFieldIter<Self, Prev>;
|
||||
fn iter_unkeyed(self) -> StoreFieldIter<Self, Prev>;
|
||||
}
|
||||
|
||||
impl<Inner, Prev> StoreFieldIterator<Prev> for Inner
|
||||
@@ -209,12 +207,12 @@ where
|
||||
Prev: IndexMut<usize> + AsRef<[Prev::Output]>,
|
||||
{
|
||||
#[track_caller]
|
||||
fn at(self, index: usize) -> AtIndex<Inner, Prev> {
|
||||
fn at_unkeyed(self, index: usize) -> AtIndex<Inner, Prev> {
|
||||
AtIndex::new(self.clone(), index)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn iter(self) -> StoreFieldIter<Inner, Prev> {
|
||||
fn iter_unkeyed(self) -> StoreFieldIter<Inner, Prev> {
|
||||
// reactively track changes to this field
|
||||
let trigger = self.get_trigger(self.path().into_iter().collect());
|
||||
trigger.this.track();
|
||||
|
||||
@@ -124,6 +124,16 @@ where
|
||||
fn keys(&self) -> Option<KeyMap> {
|
||||
self.inner.keys()
|
||||
}
|
||||
|
||||
fn track_field(&self) {
|
||||
let inner = self
|
||||
.inner
|
||||
.get_trigger(self.inner.path().into_iter().collect());
|
||||
inner.this.track();
|
||||
let trigger = self.get_trigger(self.path().into_iter().collect());
|
||||
trigger.this.track();
|
||||
trigger.children.track();
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, Prev, K, T> KeyedSubfield<Inner, Prev, K, T>
|
||||
@@ -287,13 +297,7 @@ where
|
||||
K: Debug + Send + Sync + PartialEq + Eq + Hash + 'static,
|
||||
{
|
||||
fn track(&self) {
|
||||
let inner = self
|
||||
.inner
|
||||
.get_trigger(self.inner.path().into_iter().collect());
|
||||
inner.this.track();
|
||||
let trigger = self.get_trigger(self.path().into_iter().collect());
|
||||
trigger.this.track();
|
||||
trigger.children.track();
|
||||
self.track_field();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,9 +555,7 @@ where
|
||||
T::Output: Sized,
|
||||
{
|
||||
fn track(&self) {
|
||||
let trigger = self.get_trigger(self.path().into_iter().collect());
|
||||
trigger.this.track();
|
||||
trigger.children.track();
|
||||
self.track_field();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ pub use keyed::*;
|
||||
pub use option::*;
|
||||
pub use patch::*;
|
||||
pub use path::{StorePath, StorePathSegment};
|
||||
pub use store_field::{StoreField, Then};
|
||||
pub use store_field::StoreField;
|
||||
pub use subfield::Subfield;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -291,9 +291,7 @@ where
|
||||
|
||||
impl<T: 'static> Track for ArcStore<T> {
|
||||
fn track(&self) {
|
||||
let trigger = self.get_trigger(Default::default());
|
||||
trigger.this.track();
|
||||
trigger.children.track();
|
||||
self.track_field();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,7 +636,10 @@ mod tests {
|
||||
} else {
|
||||
println!("next run");
|
||||
}
|
||||
println!("{:?}", store.todos().iter().collect::<Vec<_>>());
|
||||
println!(
|
||||
"{:?}",
|
||||
store.todos().iter_unkeyed().collect::<Vec<_>>()
|
||||
);
|
||||
combined_count.store(1, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,21 +6,13 @@ use or_poisoned::OrPoisoned;
|
||||
use reactive_graph::{
|
||||
owner::Storage,
|
||||
signal::{
|
||||
guards::{Mapped, MappedMut, Plain, UntrackedWriteGuard, WriteGuard},
|
||||
guards::{Plain, UntrackedWriteGuard, WriteGuard},
|
||||
ArcTrigger,
|
||||
},
|
||||
traits::{
|
||||
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
|
||||
Write,
|
||||
},
|
||||
traits::{DefinedAt, Track, UntrackableGuard},
|
||||
unwrap_signal,
|
||||
};
|
||||
use std::{
|
||||
iter,
|
||||
ops::{Deref, DerefMut},
|
||||
panic::Location,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{iter, ops::Deref, sync::Arc};
|
||||
|
||||
pub trait StoreField: Sized {
|
||||
type Value;
|
||||
@@ -43,21 +35,6 @@ pub trait StoreField: Sized {
|
||||
fn writer(&self) -> Option<Self::Writer>;
|
||||
|
||||
fn keys(&self) -> Option<KeyMap>;
|
||||
|
||||
#[track_caller]
|
||||
fn then<T>(
|
||||
self,
|
||||
map_fn: fn(&Self::Value) -> &T,
|
||||
map_fn_mut: fn(&mut Self::Value) -> &mut T,
|
||||
) -> Then<T, Self> {
|
||||
Then {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: self,
|
||||
map_fn,
|
||||
map_fn_mut,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> StoreField for ArcStore<T>
|
||||
@@ -128,146 +105,3 @@ where
|
||||
self.inner.try_get_value().and_then(|inner| inner.keys())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Then<T, S>
|
||||
where
|
||||
S: StoreField,
|
||||
{
|
||||
inner: S,
|
||||
map_fn: fn(&S::Value) -> &T,
|
||||
map_fn_mut: fn(&mut S::Value) -> &mut T,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl<T, S> Then<T, S>
|
||||
where
|
||||
S: StoreField,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new(
|
||||
inner: S,
|
||||
map_fn: fn(&S::Value) -> &T,
|
||||
map_fn_mut: fn(&mut S::Value) -> &mut T,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
map_fn,
|
||||
map_fn_mut,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> StoreField for Then<T, S>
|
||||
where
|
||||
S: StoreField,
|
||||
{
|
||||
type Value = T;
|
||||
type Reader = Mapped<S::Reader, T>;
|
||||
type Writer = MappedMut<S::Writer, T>;
|
||||
|
||||
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
|
||||
self.inner.get_trigger(path)
|
||||
}
|
||||
|
||||
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner.path()
|
||||
}
|
||||
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
let inner = self.inner.reader()?;
|
||||
Some(Mapped::new_with_guard(inner, self.map_fn))
|
||||
}
|
||||
|
||||
fn writer(&self) -> Option<Self::Writer> {
|
||||
let inner = self.inner.writer()?;
|
||||
Some(MappedMut::new(inner, self.map_fn, self.map_fn_mut))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn keys(&self) -> Option<KeyMap> {
|
||||
self.inner.keys()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> DefinedAt for Then<T, S>
|
||||
where
|
||||
S: StoreField,
|
||||
{
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> IsDisposed for Then<T, S>
|
||||
where
|
||||
S: StoreField + IsDisposed,
|
||||
{
|
||||
fn is_disposed(&self) -> bool {
|
||||
self.inner.is_disposed()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Notify for Then<T, S>
|
||||
where
|
||||
S: StoreField,
|
||||
{
|
||||
fn notify(&self) {
|
||||
let trigger = self.get_trigger(self.path().into_iter().collect());
|
||||
trigger.this.notify();
|
||||
trigger.children.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Track for Then<T, S>
|
||||
where
|
||||
S: StoreField,
|
||||
{
|
||||
fn track(&self) {
|
||||
let trigger = self.get_trigger(self.path().into_iter().collect());
|
||||
trigger.this.track();
|
||||
trigger.children.track();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> ReadUntracked for Then<T, S>
|
||||
where
|
||||
S: StoreField,
|
||||
{
|
||||
type Value = <Self as StoreField>::Reader;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
self.reader()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Write for Then<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: StoreField,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
self.writer()
|
||||
}
|
||||
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
self.writer().map(|mut writer| {
|
||||
writer.untrack();
|
||||
writer
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user