Compare commits

...

17 Commits

Author SHA1 Message Date
benwis
9052804ab4 v0.7.8 2025-03-20 08:21:11 -07:00
benwis
e95c903e85 v0.7.7 2025-03-19 18:19:37 -07:00
bimoadityar
8a179e6f45 examples: update example tailwind input css to v4 (#3702) 2025-03-19 21:01:03 -04:00
Greg Johnston
e765f99016 fix: matching optional params after an initial static param (closes #3730) (#3732) 2025-03-19 20:59:41 -04:00
Nicolas Cura
30548eca31 Merge pull request #3727 from NCura/patch-1
Fix typo
2025-03-18 14:30:23 -04:00
Greg Johnston
d04d4c77f9 Merge pull request #3720 from metatoaster/regression_tests_3502
test: regression from #3502
2025-03-16 14:16:05 -04:00
Tommy Yu
5c75928b5b test: avoiding testdriver browser contention
- The number of tests have increased sufficiently that the browser test
  instances spawned by the driver are choked out on CI.
2025-03-16 22:43:21 +13:00
Tommy Yu
abc5631654 test: regression from #3502
- Also as reported in #3719, which has an actual minimum example.
- The "quicker" test had a reset but that runs into timing issue with
  the way this reset is done too soon after resource usage, so leaving
  this out and we will just trust the bigger counters.
2025-03-16 22:43:21 +13:00
Tommy Yu
40e5288ac1 fix instrumented use_context
- It shouldn't be in on_cleanup, move into it from the component to
  avoid BorrowMut error.
2025-03-16 22:43:21 +13:00
Greg Johnston
6ee72f42e2 Merge pull request #3687 from leptos-rs/3671
Various issues related to setting signals and context in cleanups
2025-03-15 10:34:53 -04:00
Danik Vitek
5cfe7f6b5e fix: make tuple struct field locator in Patch impl Index instead of usize (#3700) 2025-03-15 10:19:41 -04:00
Greg Johnston
0404efd5c3 fix: ensure that store subfield mutations notify from the root down (closes #3704) (#3714) 2025-03-15 10:04:52 -04:00
Tommy Yu
cd2904f6a6 chore: prep common base to share example with 0.8 2025-03-15 14:35:55 +13:00
zakstucke
5633148047 feat: allow LocalResource sync methods to be used outside Suspense (#3708) 2025-03-13 09:28:00 -04:00
Greg Johnston
330920eae2 chore: clippy 2025-03-10 10:14:46 -04:00
Greg Johnston
a94bc0a6da fix: only store a weak reference to an Owner in the current thread (see #3671) 2025-03-10 10:14:46 -04:00
Greg Johnston
f85e01f4d6 fix: do not panic unnecessarily in try_ methods on Arena (closes #3671) 2025-03-10 10:14:46 -04:00
26 changed files with 453 additions and 209 deletions

42
Cargo.lock generated
View File

@@ -1734,7 +1734,7 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
[[package]]
name = "leptos"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"any_spawner",
"base64",
@@ -1784,7 +1784,7 @@ dependencies = [
[[package]]
name = "leptos_actix"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"actix-files",
"actix-http",
@@ -1809,7 +1809,7 @@ dependencies = [
[[package]]
name = "leptos_axum"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"any_spawner",
"axum",
@@ -1832,7 +1832,7 @@ dependencies = [
[[package]]
name = "leptos_config"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"config",
"regex",
@@ -1846,7 +1846,7 @@ dependencies = [
[[package]]
name = "leptos_dom"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"js-sys",
"leptos",
@@ -1863,7 +1863,7 @@ dependencies = [
[[package]]
name = "leptos_hot_reload"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"anyhow",
"camino",
@@ -1879,7 +1879,7 @@ dependencies = [
[[package]]
name = "leptos_integration_utils"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"futures",
"hydration_context",
@@ -1892,7 +1892,7 @@ dependencies = [
[[package]]
name = "leptos_macro"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"attribute-derive",
"cfg-if",
@@ -1911,7 +1911,7 @@ dependencies = [
"rstml",
"serde",
"server_fn",
"server_fn_macro 0.7.7",
"server_fn_macro 0.7.8",
"syn 2.0.98",
"tracing",
"trybuild",
@@ -1921,7 +1921,7 @@ dependencies = [
[[package]]
name = "leptos_meta"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"futures",
"indexmap",
@@ -1936,7 +1936,7 @@ dependencies = [
[[package]]
name = "leptos_router"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"any_spawner",
"either_of",
@@ -1960,7 +1960,7 @@ dependencies = [
[[package]]
name = "leptos_router_macro"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"leptos_macro",
"leptos_router",
@@ -1972,7 +1972,7 @@ dependencies = [
[[package]]
name = "leptos_server"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"any_spawner",
"base64",
@@ -2658,7 +2658,7 @@ dependencies = [
[[package]]
name = "reactive_graph"
version = "0.1.7"
version = "0.1.8"
dependencies = [
"any_spawner",
"async-lock",
@@ -2680,7 +2680,7 @@ dependencies = [
[[package]]
name = "reactive_stores"
version = "0.1.7"
version = "0.1.8"
dependencies = [
"any_spawner",
"guardian",
@@ -2697,7 +2697,7 @@ dependencies = [
[[package]]
name = "reactive_stores_macro"
version = "0.1.7"
version = "0.1.8"
dependencies = [
"convert_case 0.7.1",
"proc-macro-error2",
@@ -3155,7 +3155,7 @@ dependencies = [
[[package]]
name = "server_fn"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"actix-web",
"axum",
@@ -3211,7 +3211,7 @@ dependencies = [
[[package]]
name = "server_fn_macro"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"const_format",
"convert_case 0.6.0",
@@ -3223,9 +3223,9 @@ dependencies = [
[[package]]
name = "server_fn_macro_default"
version = "0.7.7"
version = "0.7.8"
dependencies = [
"server_fn_macro 0.7.7",
"server_fn_macro 0.7.8",
"syn 2.0.98",
]
@@ -3419,7 +3419,7 @@ dependencies = [
[[package]]
name = "tachys"
version = "0.1.7"
version = "0.1.8"
dependencies = [
"any_spawner",
"async-trait",

View File

@@ -40,7 +40,7 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.7.7"
version = "0.7.8"
edition = "2021"
rust-version = "1.76"
@@ -51,16 +51,16 @@ const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
either_of = { path = "./either_of/", version = "0.1.5" }
hydration_context = { path = "./hydration_context", version = "0.2.0" }
itertools = "0.14.0"
leptos = { path = "./leptos", version = "0.7.7" }
leptos_config = { path = "./leptos_config", version = "0.7.7" }
leptos_dom = { path = "./leptos_dom", version = "0.7.7" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.7" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.7" }
leptos_macro = { path = "./leptos_macro", version = "0.7.7" }
leptos_router = { path = "./router", version = "0.7.7" }
leptos_router_macro = { path = "./router_macro", version = "0.7.7" }
leptos_server = { path = "./leptos_server", version = "0.7.7" }
leptos_meta = { path = "./meta", version = "0.7.7" }
leptos = { path = "./leptos", version = "0.7.8" }
leptos_config = { path = "./leptos_config", version = "0.7.8" }
leptos_dom = { path = "./leptos_dom", version = "0.7.8" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.8" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.8" }
leptos_macro = { path = "./leptos_macro", version = "0.7.8" }
leptos_router = { path = "./router", version = "0.7.8" }
leptos_router_macro = { path = "./router_macro", version = "0.7.8" }
leptos_server = { path = "./leptos_server", version = "0.7.8" }
leptos_meta = { path = "./meta", version = "0.7.8" }
next_tuple = { path = "./next_tuple", version = "0.1.0" }
oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
@@ -68,9 +68,9 @@ reactive_graph = { path = "./reactive_graph", version = "0.1.7" }
reactive_stores = { path = "./reactive_stores", version = "0.1.7" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.7" }
serde_json = "1.0.0"
server_fn = { path = "./server_fn", version = "0.7.7" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.7" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.7" }
server_fn = { path = "./server_fn", version = "0.7.8" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.8" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.8" }
tachys = { path = "./tachys", version = "0.1.7" }
wasm-bindgen = { version = "0.2.100" }

View File

@@ -25,7 +25,7 @@ pub fn RouterExample() -> impl IntoView {
// contexts are passed down through the route tree
provide_context(ExampleContext(0));
// this signal will be ued to set whether we are allowed to access a protected route
// this signal will be used to set whether we are allowed to access a protected route
let (logged_in, set_logged_in) = signal(true);
let (is_routing, set_is_routing) = signal(false);

View File

@@ -0,0 +1,99 @@
@check_instrumented_issue_3719
Feature: Using instrumented counters to test regression from #3502.
Check that the suspend/suspense and the underlying resources are
called with the expected number of times. If this was already in
place by #3502 (5c43c18) it should have caught this regression.
For a better minimum demonstration see #3719.
Background:
Given I see the app
And I select the mode Instrumented
Scenario: follow all paths via CSR avoids #3502
Given I select the following links
| Item Listing |
| Item 1 |
| Inspect path2 |
| Inspect path2/field3 |
And I click on Reset CSR Counters
When I select the following links
| Inspect path2/field1 |
| Inspect path2/field2 |
And I go check the Counters
Then I see the following counters under section
| Suspend Calls | |
| item_listing | 0 |
| item_overview | 0 |
| item_inspect | 2 |
And the following counters under section
| Server Calls (CSR) | |
| list_items | 0 |
| get_item | 0 |
| inspect_item_root | 0 |
| inspect_item_field | 2 |
# To show that starting directly from within a param will simply
# cause the problem.
Scenario: Quicker way to demonstrate regression caused by #3502
Given I select the link Target 123
# And I click on Reset CSR Counters
When I select the following links
| Inspect path2/field1 |
| Inspect path2/field2 |
And I go check the Counters
Then I see the following counters under section
| Suspend Calls | |
| item_listing | 0 |
| item_overview | 0 |
| item_inspect | 3 |
And the following counters under section
| Server Calls (CSR) | |
| list_items | 1 |
| get_item | 1 |
| inspect_item_root | 0 |
| inspect_item_field | 4 |
Scenario: Follow paths ordinarily down to a target
Given I select the following links
| Item Listing |
| Item 1 |
And I click on Reset CSR Counters
When I select the following links
| Target 4## |
| Target 3## |
And I go check the Counters
Then I see the following counters under section
| Suspend Calls | |
| item_listing | 0 |
| item_overview | 2 |
| item_inspect | 0 |
And the following counters under section
| Server Calls (CSR) | |
| list_items | 0 |
| get_item | 2 |
| inspect_item_root | 0 |
| inspect_item_field | 0 |
Scenario: Same as above, but add a refresh to test hydration
Given I select the following links
| Item Listing |
| Item 1 |
And I refresh the page
And I click on Reset CSR Counters
When I select the following links
| Target 4## |
| Target 3## |
And I go check the Counters
Then I see the following counters under section
| Suspend Calls | |
| item_listing | 0 |
| item_overview | 2 |
| item_inspect | 0 |
And the following counters under section
| Server Calls (CSR) | |
| list_items | 0 |
| get_item | 2 |
| inspect_item_root | 0 |
| inspect_item_field | 0 |

View File

@@ -3,12 +3,28 @@ mod fixtures;
use anyhow::Result;
use cucumber::World;
use fixtures::world::AppWorld;
use std::{ffi::OsStr, fs::read_dir};
#[tokio::main]
async fn main() -> Result<()> {
AppWorld::cucumber()
.fail_on_skipped()
.run_and_exit("./features")
.await;
// Normally the below is done, but it's now gotten to the point of
// having a sufficient number of tests where the resource contention
// of the concurrently running browsers will cause failures on CI.
// AppWorld::cucumber()
// .fail_on_skipped()
// .run_and_exit("./features")
// .await;
// Mitigate the issue by manually stepping through each feature,
// rather than letting cucumber glob them and dispatch all at once.
for entry in read_dir("./features")? {
let path = entry?.path();
if path.extension() == Some(OsStr::new("feature")) {
AppWorld::cucumber()
.fail_on_skipped()
.run_and_exit(path)
.await;
}
}
Ok(())
}

View File

@@ -4,7 +4,7 @@ use leptos_router::{
hooks::use_params,
nested_router::Outlet,
params::Params,
MatchNestedRoutes, ParamSegment, SsrMode, StaticSegment, WildcardSegment,
ParamSegment, SsrMode, StaticSegment, WildcardSegment,
};
#[cfg(feature = "ssr")]
@@ -21,6 +21,7 @@ pub(super) mod counter {
pub struct Counter(AtomicU32);
impl Counter {
#[allow(dead_code)]
pub const fn new() -> Self {
Self(AtomicU32::new(0))
}
@@ -203,20 +204,20 @@ pub struct SuspenseCounters {
}
#[component]
pub fn InstrumentedRoutes() -> impl MatchNestedRoutes + Clone {
pub fn InstrumentedRoutes() -> impl leptos_router::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/>
<Route path=StaticSegment("/") view=InstrumentedTop />
<ParentRoute path=StaticSegment("item") view=ItemRoot>
<Route path=StaticSegment("/") view=ItemListing/>
<Route path=StaticSegment("/") view=ItemListing />
<ParentRoute path=ParamSegment("id") view=ItemTop>
<Route path=StaticSegment("/") view=ItemOverview/>
<Route path=WildcardSegment("path") view=ItemInspect/>
<Route path=StaticSegment("/") view=ItemOverview />
<Route path=WildcardSegment("path") view=ItemInspect />
</ParentRoute>
</ParentRoute>
<Route path=StaticSegment("counters") view=ShowCounters/>
<Route path=StaticSegment("counters") view=ShowCounters />
</ParentRoute>
}
.into_inner()
@@ -279,32 +280,41 @@ fn InstrumentedRoot() -> impl IntoView {
<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>
<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 {
<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>
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>
@@ -323,11 +333,17 @@ fn InstrumentedRoot() -> impl IntoView {
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>
<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>
<li>
<a href="item/">"Item Listing"</a>
</li>
<li>
<a href="item/4/path1/">"Target 41#"</a>
</li>
</ul>
}
}
@@ -342,7 +358,7 @@ fn ItemRoot() -> impl IntoView {
view! {
<h2>"<ItemRoot/>"</h2>
<Outlet/>
<Outlet />
}
}
@@ -360,7 +376,9 @@ fn ItemListing() -> impl IntoView {
// 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>
<li>
<a href=format!("/instrumented/item/{item}/")>"Item "{item}</a>
</li>
}
)
.collect_view()
@@ -373,9 +391,7 @@ fn ItemListing() -> impl IntoView {
view! {
<h3>"<ItemListing/>"</h3>
<ul>
<Suspense>
{item_listing}
</Suspense>
<Suspense>{item_listing}</Suspense>
</ul>
}
}
@@ -402,7 +418,7 @@ fn ItemTop() -> impl IntoView {
));
view! {
<h4>"<ItemTop/>"</h4>
<Outlet/>
<Outlet />
}
}
@@ -412,24 +428,29 @@ fn ItemOverview() -> impl IntoView {
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>
});
let result = resource.await.map(|GetItemResult(item, names)| {
view! {
<p>{format!("Viewing {item:?}")}</p>
<ul>
{names
.into_iter()
.map(|name| {
let id = item.id;
// 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>
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
})
@@ -437,9 +458,7 @@ fn ItemOverview() -> impl IntoView {
view! {
<h5>"<ItemOverview/>"</h5>
<Suspense>
{item_view}
</Suspense>
<Suspense>{item_view}</Suspense>
}
}
@@ -473,8 +492,9 @@ fn ItemInspect() -> impl IntoView {
// result
},
);
on_cleanup(|| {
if let Some(c) = use_context::<WriteSignal<Option<FieldNavCtx>>>() {
let ws = use_context::<WriteSignal<Option<FieldNavCtx>>>();
on_cleanup(move || {
if let Some(c) = ws {
c.set(None);
}
});
@@ -496,23 +516,26 @@ fn ItemInspect() -> impl IntoView {
));
view! {
<p>{format!("Inspecting {item:?}")}</p>
<ul>{
fields.iter()
<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>
// <li><A href=format!("{field}")>{text}</A></li>
// }
view! {
<li><a href=format!("/instrumented/item/{id}/{name}/{field}")>{
format!("Inspect {name}/{field}")
}</a></li>
<li>
<a href=format!(
"/instrumented/item/{id}/{name}/{field}",
)>{format!("Inspect {name}/{field}")}</a>
</li>
}
})
.collect_view()
}</ul>
.collect_view()}
</ul>
}
});
suspense_counters.update_untracked(|c| c.item_inspect += 1);
@@ -527,9 +550,7 @@ fn ItemInspect() -> impl IntoView {
view! {
<h5>"<ItemInspect/>"</h5>
<Suspense>
{inspect_view}
</Suspense>
<Suspense>{inspect_view}</Suspense>
}
}
@@ -590,7 +611,8 @@ fn ShowCounters() -> impl IntoView {
id="reset-counters"
type="submit"
value="Reset Counters"
on:click=clear_suspense_counters/>
on:click=clear_suspense_counters
/>
</ActionForm>
}
})
@@ -601,20 +623,23 @@ fn ShowCounters() -> impl IntoView {
<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>
})}
{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>
<Suspense>{counter_view}</Suspense>
}
}
@@ -642,17 +667,17 @@ pub fn FieldNavPortlet() -> impl IntoView {
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>
<nav>
{ctx
.0
.map(|ctx| {
ctx.into_iter()
.map(|FieldNavItem { href, text }| {
view! { <A href=href>{text}</A> }
})
.collect_view()
})}
</nav>
</div>
}
})

View File

@@ -1,3 +1 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";

View File

@@ -1,3 +1 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";

View File

@@ -172,12 +172,6 @@ where
fn try_read_untracked(&self) -> Option<Self::Value> {
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
notifier.notify();
} else if cfg!(feature = "ssr") {
panic!(
"Reading from a LocalResource outside Suspense in `ssr` mode \
will cause the response to hang, because LocalResources are \
always pending on the server."
);
}
self.data.try_read_untracked()
}
@@ -364,12 +358,6 @@ where
fn try_read_untracked(&self) -> Option<Self::Value> {
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
notifier.notify();
} else if cfg!(feature = "ssr") {
panic!(
"Reading from a LocalResource outside Suspense in `ssr` mode \
will cause the response to hang, because LocalResources are \
always pending on the server."
);
}
self.data.try_read_untracked()
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.7.7"
version = "0.7.8"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"

View File

@@ -8,9 +8,9 @@ codegen-units = 1
lto = true
[dependencies]
leptos = { version = "0.7.7", features = ["csr"] }
leptos_meta = { version = "0.7.7" }
leptos_router = { version = "0.7.7" }
leptos = { version = "0.7.8", features = ["csr"] }
leptos_meta = { version = "0.7.8" }
leptos_router = { version = "0.7.8" }
console_log = "1.0"
log = "0.4.22"
console_error_panic_hook = "0.1.7"

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.1.7"
version = "0.1.8"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -60,6 +60,38 @@ pub struct Owner {
pub(crate) shared_context: Option<Arc<dyn SharedContext + Send + Sync>>,
}
impl Owner {
fn downgrade(&self) -> WeakOwner {
WeakOwner {
inner: Arc::downgrade(&self.inner),
#[cfg(feature = "hydration")]
shared_context: self.shared_context.as_ref().map(Arc::downgrade),
}
}
}
#[derive(Clone)]
struct WeakOwner {
inner: Weak<RwLock<OwnerInner>>,
#[cfg(feature = "hydration")]
shared_context: Option<Weak<dyn SharedContext + Send + Sync>>,
}
impl WeakOwner {
fn upgrade(&self) -> Option<Owner> {
self.inner.upgrade().map(|inner| {
#[cfg(feature = "hydration")]
let shared_context =
self.shared_context.as_ref().and_then(|sc| sc.upgrade());
Owner {
inner,
#[cfg(feature = "hydration")]
shared_context,
}
})
}
}
impl PartialEq for Owner {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.inner, &other.inner)
@@ -67,7 +99,7 @@ impl PartialEq for Owner {
}
thread_local! {
static OWNER: RefCell<Option<Owner>> = Default::default();
static OWNER: RefCell<Option<WeakOwner>> = Default::default();
}
impl Owner {
@@ -107,12 +139,16 @@ impl Owner {
/// Creates a new `Owner` and registers it as a child of the current `Owner`, if there is one.
pub fn new() -> Self {
#[cfg(not(feature = "hydration"))]
let parent = OWNER
.with(|o| o.borrow().as_ref().map(|o| Arc::downgrade(&o.inner)));
let parent = OWNER.with(|o| {
o.borrow()
.as_ref()
.and_then(|o| o.upgrade())
.map(|o| Arc::downgrade(&o.inner))
});
#[cfg(feature = "hydration")]
let (parent, shared_context) = OWNER
.with(|o| {
o.borrow().as_ref().map(|o| {
o.borrow().as_ref().and_then(|o| o.upgrade()).map(|o| {
(Some(Arc::downgrade(&o.inner)), o.shared_context.clone())
})
})
@@ -200,7 +236,7 @@ impl Owner {
/// Sets this as the current `Owner`.
pub fn set(&self) {
OWNER.with_borrow_mut(|owner| *owner = Some(self.clone()));
OWNER.with_borrow_mut(|owner| *owner = Some(self.downgrade()));
#[cfg(feature = "sandboxed-arenas")]
Arena::set(&self.inner.read().or_poisoned().arena);
}
@@ -208,7 +244,9 @@ impl Owner {
/// Runs the given function with this as the current `Owner`.
pub fn with<T>(&self, fun: impl FnOnce() -> T) -> T {
let prev = {
OWNER.with(|o| Option::replace(&mut *o.borrow_mut(), self.clone()))
OWNER.with(|o| {
Option::replace(&mut *o.borrow_mut(), self.downgrade())
})
};
#[cfg(feature = "sandboxed-arenas")]
Arena::set(&self.inner.read().or_poisoned().arena);
@@ -255,7 +293,7 @@ impl Owner {
/// Returns the current `Owner`, if any.
pub fn current() -> Option<Owner> {
OWNER.with(|o| o.borrow().clone())
OWNER.with(|o| o.borrow().as_ref().and_then(|n| n.upgrade()))
}
/// Returns the [`SharedContext`] associated with this owner, if any.
@@ -269,7 +307,7 @@ impl Owner {
/// Removes this from its state as the thread-local owner and drops it.
pub fn unset(self) {
OWNER.with_borrow_mut(|owner| {
if owner.as_ref() == Some(&self) {
if owner.as_ref().and_then(|n| n.upgrade()) == Some(self) {
mem::take(owner);
}
})
@@ -282,6 +320,7 @@ impl Owner {
OWNER.with(|o| {
o.borrow()
.as_ref()
.and_then(|o| o.upgrade())
.and_then(|current| current.shared_context.clone())
})
}
@@ -295,6 +334,7 @@ impl Owner {
let sc = OWNER.with_borrow(|o| {
o.as_ref()
.and_then(|o| o.upgrade())
.and_then(|current| current.shared_context.clone())
});
match sc {
@@ -320,6 +360,7 @@ impl Owner {
let sc = OWNER.with_borrow(|o| {
o.as_ref()
.and_then(|o| o.upgrade())
.and_then(|current| current.shared_context.clone())
});
match sc {

View File

@@ -48,20 +48,30 @@ impl Arena {
fun(&MAP.get_or_init(Default::default).read().or_poisoned())
}
#[cfg(feature = "sandboxed-arenas")]
{
Arena::try_with(fun).unwrap_or_else(|| {
panic!(
"at {}, the `sandboxed-arenas` feature is active, but no \
Arena is active",
std::panic::Location::caller()
)
})
}
}
#[track_caller]
pub fn try_with<U>(fun: impl FnOnce(&ArenaMap) -> U) -> Option<U> {
#[cfg(not(feature = "sandboxed-arenas"))]
{
Some(fun(&MAP.get_or_init(Default::default).read().or_poisoned()))
}
#[cfg(feature = "sandboxed-arenas")]
{
MAP.with_borrow(|arena| {
fun(&arena
arena
.as_ref()
.and_then(Weak::upgrade)
.unwrap_or_else(|| {
panic!(
"at {}, the `sandboxed-arenas` feature is active, \
but no Arena is active",
std::panic::Location::caller()
)
})
.read()
.or_poisoned())
.map(|n| fun(&n.read().or_poisoned()))
})
}
}
@@ -74,20 +84,32 @@ impl Arena {
}
#[cfg(feature = "sandboxed-arenas")]
{
let caller = std::panic::Location::caller();
Arena::try_with_mut(fun).unwrap_or_else(|| {
panic!(
"at {}, the `sandboxed-arenas` feature is active, but no \
Arena is active",
std::panic::Location::caller()
)
})
}
}
#[track_caller]
pub fn try_with_mut<U>(fun: impl FnOnce(&mut ArenaMap) -> U) -> Option<U> {
#[cfg(not(feature = "sandboxed-arenas"))]
{
Some(fun(&mut MAP
.get_or_init(Default::default)
.write()
.or_poisoned()))
}
#[cfg(feature = "sandboxed-arenas")]
{
MAP.with_borrow(|arena| {
fun(&mut arena
arena
.as_ref()
.and_then(Weak::upgrade)
.unwrap_or_else(|| {
panic!(
"at {}, the `sandboxed-arenas` feature is active, \
but no Arena is active",
caller
)
})
.write()
.or_poisoned())
.map(|n| fun(&mut n.write().or_poisoned()))
})
}
}
@@ -126,6 +148,7 @@ pub mod sandboxed {
/// called.
///
/// [item]:[crate::owner::ArenaItem]
#[track_caller]
pub fn new(inner: T) -> Self {
let arena = MAP.with_borrow(|n| n.as_ref().and_then(Weak::upgrade));
Self { arena, inner }

View File

@@ -53,7 +53,7 @@ where
})
};
OWNER.with(|o| {
if let Some(owner) = &*o.borrow() {
if let Some(owner) = o.borrow().as_ref().and_then(|o| o.upgrade()) {
owner.register(node);
}
});

View File

@@ -76,24 +76,26 @@ where
}
fn try_with<U>(node: NodeId, fun: impl FnOnce(&T) -> U) -> Option<U> {
Arena::with(|arena| {
Arena::try_with(|arena| {
let m = arena.get(node);
m.and_then(|n| n.downcast_ref::<T>()).map(fun)
})
.flatten()
}
fn try_with_mut<U>(
node: NodeId,
fun: impl FnOnce(&mut T) -> U,
) -> Option<U> {
Arena::with_mut(|arena| {
Arena::try_with_mut(|arena| {
let m = arena.get_mut(node);
m.and_then(|n| n.downcast_mut::<T>()).map(fun)
})
.flatten()
}
fn try_set(node: NodeId, value: T) -> Option<T> {
Arena::with_mut(|arena| {
Arena::try_with_mut(|arena| {
let m = arena.get_mut(node);
match m.and_then(|n| n.downcast_mut::<T>()) {
Some(inner) => {
@@ -103,6 +105,7 @@ where
None => Some(value),
}
})
.flatten()
}
fn take(node: NodeId) -> Option<T> {

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores"
version = "0.1.7"
version = "0.1.8"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -100,6 +100,9 @@ where
let mut full_path = self.path().into_iter().collect::<StorePath>();
full_path.pop();
// build a list of triggers, starting with the full path to this node and ending with the root
// this will mean that the root is the final item, and this path is first
let mut triggers = Vec::with_capacity(full_path.len());
triggers.push(trigger.this.clone());
loop {
@@ -110,6 +113,17 @@ where
}
full_path.pop();
}
// when the WriteGuard is dropped, each trigger will be notified, in order
// reversing the list will cause the triggers to be notified starting from the root,
// then to each child down to this one
//
// notifying from the root down is important for things like OptionStoreExt::map()/unwrap(),
// where it's really important that any effects that subscribe to .is_some() run before effects
// that subscribe to the inner value, so that the inner effect can be canceled if the outer switches to `None`
// (see https://github.com/leptos-rs/leptos/issues/3704)
triggers.reverse();
let guard = WriteGuard::new(triggers, parent);
Some(MappedMut::new(guard, self.read, self.write))

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores_macro"
version = "0.1.7"
version = "0.1.8"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -604,9 +604,9 @@ impl ToTokens for PatchModel {
let Field {
attrs, ident, ..
} = &field;
let field_name = match &ident {
Some(ident) => quote! { #ident },
None => quote! { #idx },
let locator = match &ident {
Some(ident) => Either::Left(ident),
None => Either::Right(Index::from(idx)),
};
let closure = attrs
.iter()
@@ -639,9 +639,9 @@ impl ToTokens for PatchModel {
let params = closure.inputs;
let body = closure.body;
quote! {
if new.#field_name != self.#field_name {
if new.#locator != self.#locator {
_ = {
let (#params) = (&mut self.#field_name, new.#field_name);
let (#params) = (&mut self.#locator, new.#locator);
#body
};
notify(&new_path);
@@ -651,8 +651,8 @@ impl ToTokens for PatchModel {
} else {
quote! {
#library_path::PatchField::patch_field(
&mut self.#field_name,
new.#field_name,
&mut self.#locator,
new.#locator,
&new_path,
notify
);
@@ -684,3 +684,17 @@ impl ToTokens for PatchModel {
});
}
}
enum Either<A, B> {
Left(A),
Right(B),
}
impl<A: ToTokens, B: ToTokens> ToTokens for Either<A, B> {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Either::Left(a) => a.to_tokens(tokens),
Either::Right(b) => b.to_tokens(tokens),
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.7.7"
version = "0.7.8"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"

View File

@@ -318,6 +318,28 @@ mod tests {
assert!(params.is_empty());
}
#[test]
fn static_before_param() {
let path = "/foo/bar";
let def = (StaticSegment("foo"), ParamSegment("b"));
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/bar");
assert_eq!(matched.remaining(), "");
let params = matched.params();
assert_eq!(params[0], ("b".into(), "bar".into()));
}
#[test]
fn static_before_optional_param() {
let path = "/foo/bar";
let def = (StaticSegment("foo"), OptionalParamSegment("b"));
let matched = def.test(path).expect("couldn't match route");
assert_eq!(matched.matched(), "/foo/bar");
assert_eq!(matched.remaining(), "");
let params = matched.params();
assert_eq!(params[0], ("b".into(), "bar".into()));
}
#[test]
fn multiple_optional_params_match_first() {
let path = "/foo/bar";

View File

@@ -25,7 +25,10 @@ macro_rules! tuples {
let mut p = Vec::new();
let mut m = String::new();
if !$first::OPTIONAL || nth_field < include_optionals {
if $first::OPTIONAL {
nth_field += 1;
}
if !$first::OPTIONAL || nth_field <= include_optionals {
match $first.test(r) {
None => {
return None;
@@ -43,7 +46,7 @@ macro_rules! tuples {
if $ty::OPTIONAL {
nth_field += 1;
}
if !$ty::OPTIONAL || nth_field < include_optionals {
if !$ty::OPTIONAL || nth_field <= include_optionals {
let PartialPathMatch {
remaining,
matched,

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router_macro"
version = "0.7.7"
version = "0.7.8"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"

4
server_fn/Cargo.lock generated
View File

@@ -198,7 +198,7 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.7"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd"
dependencies = [
@@ -880,7 +880,7 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash 0.7.7",
"ahash 0.7.8",
]
[[package]]

View File

@@ -1,6 +1,6 @@
[package]
name = "tachys"
version = "0.1.7"
version = "0.1.8"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"