Compare commits

...

18 Commits
4300 ... 4315

Author SHA1 Message Date
Greg Johnston
12a052eaaf chore: specify Tailwind version in Trunk.toml (closes #4315) 2025-09-21 13:47:46 -04:00
dependabot[bot]
d0295009cf chore(deps): bump tj-actions/changed-files from 46 to 47 (#4297)
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 46 to 47.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/v46...v47)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: '47'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-20 11:40:53 -04:00
dependabot[bot]
3e8b5c9805 chore(deps): bump actions/setup-node from 4 to 5 (#4283)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-20 11:40:41 -04:00
Greg Johnston
924efa8ac1 feat: minimal support for subsecond and an example (#4307) 2025-09-20 11:40:28 -04:00
Ægir Örn Símonarson
44bc4fbc31 Locking dependencies in cargo-leptos install example (#4295)
This limits dependencies errors on install
2025-09-19 11:35:51 -04:00
Greg Johnston
646cfc12ed leptos v0.8.9 2025-09-18 15:49:46 -04:00
Greg Johnston
62977a68b0 fix: support const generic static strs on nightly versions with conflicting feature names (closes #4300) (#4301) 2025-09-18 09:09:36 -04:00
Adam Doyle
e9ee90c78f chore: add referrerpolicy attribute to a element (#4299) 2025-09-18 09:05:27 -04:00
Greg Johnston
73e728f145 Merge pull request #4294 from leptos-rs/4285
fix: prevent double-rebuild and correctly navigate multiple times to same lazy route (closes #4285)
2025-09-17 10:05:49 -04:00
Greg Johnston
6f047a2271 test: add regression test for #4296 2025-09-16 16:22:42 -04:00
Greg Johnston
7c942b8b47 chore: correct name for test 2025-09-16 16:07:00 -04:00
Greg Johnston
d4bf6d9cb6 test: add regression test for #4285 2025-09-15 21:05:12 -04:00
Greg Johnston
9deb96ea01 fix: provide correct URL/query/params to preloaders (closes #4296) 2025-09-15 19:46:52 -04:00
Greg Johnston
d1899cde1c during SSR, don't dispose of preload owners until whole request is done 2025-09-15 18:54:11 -04:00
Greg Johnston
ee731d7a3a fix: create individual owners for each preload 2025-09-12 18:51:46 -04:00
Greg Johnston
59cbcfa0fb fix: prevent infinite rebuild loop 2025-09-12 18:00:13 -04:00
Greg Johnston
0939cf63ad Revert "fix: prevent double-rebuild and correctly navigate multiple times to same lazy route (closes #4285)"
This reverts commit d37512bebd.
2025-09-12 17:59:14 -04:00
Greg Johnston
d37512bebd fix: prevent double-rebuild and correctly navigate multiple times to same lazy route (closes #4285) 2025-09-12 17:20:52 -04:00
35 changed files with 1009 additions and 221 deletions

View File

@@ -24,7 +24,7 @@ jobs:
fetch-depth: 0
- name: Get example files that changed
id: changed-files
uses: tj-actions/changed-files@v46
uses: tj-actions/changed-files@v47
with:
files: |
examples/**

View File

@@ -18,7 +18,7 @@ jobs:
fetch-depth: 0
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v46
uses: tj-actions/changed-files@v47
with:
files_ignore: |
.*/**/*

View File

@@ -88,7 +88,7 @@ jobs:
run: trunk --version
- name: Install Node.js
if: contains(inputs.directory, 'examples')
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 20
- uses: pnpm/action-setup@v4

586
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -50,28 +50,28 @@ any_spawner = { path = "./any_spawner/", version = "0.3.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
either_of = { path = "./either_of/", version = "0.1.6" }
hydration_context = { path = "./hydration_context", version = "0.3.0" }
leptos = { path = "./leptos", version = "0.8.8" }
leptos = { path = "./leptos", version = "0.8.9" }
leptos_config = { path = "./leptos_config", version = "0.8.7" }
leptos_dom = { path = "./leptos_dom", version = "0.8.6" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.5" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.5" }
leptos_macro = { path = "./leptos_macro", version = "0.8.8" }
leptos_router = { path = "./router", version = "0.8.6" }
leptos_router = { path = "./router", version = "0.8.7" }
leptos_router_macro = { path = "./router_macro", version = "0.8.5" }
leptos_server = { path = "./leptos_server", version = "0.8.5" }
leptos_meta = { path = "./meta", version = "0.8.5" }
next_tuple = { path = "./next_tuple", version = "0.1.0" }
oco_ref = { path = "./oco", version = "0.2.1" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.2.6" }
reactive_graph = { path = "./reactive_graph", version = "0.2.7" }
reactive_stores = { path = "./reactive_stores", version = "0.2.5" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.6" }
server_fn = { path = "./server_fn", version = "0.8.6" }
server_fn = { path = "./server_fn", version = "0.8.7" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.7" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.5" }
tachys = { path = "./tachys", version = "0.2.7" }
tachys = { path = "./tachys", version = "0.2.8" }
wasm_split_helpers = { path = "./wasm_split", version = "0.1.2" }
wasm_split_macros = { path = "./wasm_split_macros", version = "0.1.2" }
wasm_split_macros = { path = "./wasm_split_macros", version = "0.1.3" }
# members deps
async-once-cell = { default-features = false, version = "0.5.3" }
@@ -170,6 +170,9 @@ async-lock = { default-features = false, version = "3.4.1" }
base16 = { default-features = false, version = "0.2.1" }
digest = { default-features = false, version = "0.10.7" }
sha2 = { default-features = false, version = "0.10.8" }
subsecond = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" }
dioxus-cli-config = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" }
dioxus-devtools = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" }
[profile.release]
codegen-units = 1

View File

@@ -95,7 +95,7 @@ Here are some resources for learning more about Leptos:
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
```bash
cargo install cargo-leptos
cargo install cargo-leptos --locked
cargo leptos new --git https://github.com/leptos-rs/start-axum
cd [your project name]
cargo leptos watch

View File

@@ -0,0 +1,9 @@
@check_issue_4285
Feature: Check that issue 4285 does not reappear
Scenario: Navigating several times to same lazy route does not cause issues.
Given I see the app
And I can access regression test 4285
And I can access regression test 4285
And I can access regression test 4285
Then I see the result is the string 42

View File

@@ -0,0 +1,18 @@
@check_issue_4296
Feature: Check that issue 4296 does not reappear
Scenario: Query param signals created in LazyRoute::data() are reactive in ::view().
Given I see the app
And I can access regression test 4296
Then I see the result is the string None
When I select the link abc
Then I see the result is the string Some("abc")
When I select the link def
Then I see the result is the string Some("def")
Scenario: Loading page with query signal works as well.
Given I see the app
And I can access regression test 4296
When I select the link abc
When I reload the page
Then I see the result is the string Some("abc")

View File

@@ -1,6 +1,7 @@
use crate::{
issue_4005::Routes4005, issue_4088::Routes4088, issue_4217::Routes4217,
pr_4015::Routes4015, pr_4091::Routes4091,
issue_4285::Routes4285, issue_4296::Routes4296, pr_4015::Routes4015,
pr_4091::Routes4091,
};
use leptos::prelude::*;
use leptos_meta::{MetaTags, *};
@@ -31,9 +32,11 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
pub fn App() -> impl IntoView {
provide_meta_context();
let fallback = || view! { "Page not found." }.into_view();
let (_, set_is_routing) = signal(false);
view! {
<Stylesheet id="leptos" href="/pkg/regression.css"/>
<Router>
<Router set_is_routing>
<main>
<Routes fallback>
<Route path=path!("") view=HomePage/>
@@ -42,6 +45,8 @@ pub fn App() -> impl IntoView {
<Routes4088/>
<Routes4217/>
<Routes4005/>
<Routes4285/>
<Routes4296/>
</Routes>
</main>
</Router>
@@ -66,6 +71,8 @@ fn HomePage() -> impl IntoView {
<li><a href="/4088/">"4088"</a></li>
<li><a href="/4217/">"4217"</a></li>
<li><a href="/4005/">"4005"</a></li>
<li><a href="/4285/">"4285"</a></li>
<li><a href="/4296/">"4296"</a></li>
</ul>
</nav>
}

View File

@@ -0,0 +1,49 @@
use leptos::prelude::*;
use leptos_router::LazyRoute;
#[allow(unused_imports)]
use leptos_router::{
components::Route, path, Lazy, MatchNestedRoutes, NavigateOptions,
};
#[component]
pub fn Routes4285() -> impl MatchNestedRoutes + Clone {
view! {
<Route path=path!("4285") view={Lazy::<Issue4285>::new()}/>
}
.into_inner()
}
struct Issue4285 {
data: Resource<Result<i32, ServerFnError>>,
}
impl LazyRoute for Issue4285 {
fn data() -> Self {
Self {
data: Resource::new(|| (), |_| slow_call()),
}
}
async fn view(this: Self) -> AnyView {
let Issue4285 { data } = this;
view! {
<Suspense>
{move || {
Suspend::new(async move {
let data = data.await;
view! {
<p id="result">{data}</p>
}
})
}}
</Suspense>
}
.into_any()
}
}
#[server]
async fn slow_call() -> Result<i32, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(42)
}

View File

@@ -0,0 +1,36 @@
use leptos::prelude::*;
#[allow(unused_imports)]
use leptos_router::{
components::Route, path, Lazy, MatchNestedRoutes, NavigateOptions,
};
use leptos_router::{hooks::use_query_map, LazyRoute};
#[component]
pub fn Routes4296() -> impl MatchNestedRoutes + Clone {
view! {
<Route path=path!("4296") view={Lazy::<Issue4296>::new()}/>
}
.into_inner()
}
struct Issue4296 {
query: Signal<Option<String>>,
}
impl LazyRoute for Issue4296 {
fn data() -> Self {
let query = use_query_map();
let query = Signal::derive(move || query.read().get("q"));
Self { query }
}
async fn view(this: Self) -> AnyView {
let Issue4296 { query } = this;
view! {
<a href="?q=abc">"abc"</a>
<a href="?q=def">"def"</a>
<p id="result">{move || format!("{:?}", query.get())}</p>
}
.into_any()
}
}

View File

@@ -2,6 +2,8 @@ pub mod app;
mod issue_4005;
mod issue_4088;
mod issue_4217;
mod issue_4285;
mod issue_4296;
mod pr_4015;
mod pr_4091;

View File

@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target
.DS_Store
# These are backup files generated by rustfmt
**/*.rs.bk

View File

@@ -0,0 +1,13 @@
[package]
name = "subsecond_hot_patch"
version = "0.1.0"
authors = ["Greg Johnston <greg.johnston@gmail.com>"]
edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["csr", "subsecond"] }
leptos_router = { path = "../../router" }
[features]
default = ["web"]
web = []

View File

@@ -0,0 +1,21 @@
[application]
[web.app]
# HTML title tag content
title = "ltest"
# include `assets` in web platform
[web.resource]
# Additional CSS style files
style = []
# Additional JavaScript files
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []

View File

@@ -0,0 +1 @@
extend = [{ path = "../cargo-make/main.toml" }]

View File

@@ -0,0 +1,31 @@
# Hot Patching with `dx`
This is an experimental example exploring how to combine Leptos with the binary hot-patching provided by Dioxus's `subsecond` library and `dx` cli.
### Serving Your App
This requires installing the Dioxus CLI version 0.7.0. At the time I'm writing this README, that does not yet have a stable release. Once `dioxus-cli` 0.7.0 has been released, you should use the latest stable release. Until then, I'd suggest installing from git:
```sh
cargo install dioxus-cli --git https://github.com/DioxusLabs/dioxus
```
Then you can run the example with `dx serve --hot-patch --platform web`.
### Hot Patching
Changes to the your application should be reflected in your app without a full rebuild and reload.
### Limitatations
Currently we only support hot-patching for reactive view functions. You probably want to use `AnyView` (via `.into_any()`) on any views that will be hot-patched, so they can be rebuilt correctly despite their types changing when the structure of the view tree changes.
If you are using `leptos_router` this actually works quite well, as every routes view is erased to `AnyView` and the router itself is a reactive view function: in other words, changes inside any route should be hot-patched in any case.
Note that any hot-patch will cause all render effects to run again. This means that some client-side state (like the values of signals) will be wiped out.
### Build Tooling
The preference of the Dioxus team is that all hot-patching work that uses their `subsecond` also use `dioxus-cli`. As this demo shows, it's completely possible to use `dioxus-cli` to build and run a Leptos project. We do not plan to build `subsecond` into our own build tooling at this time.
**This is an experiment/POC. It is being published because members of the community have found it useful and have asked for the support to be merged in its current state. Further development and bugfixes are a relatively low priority at this time.**

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,46 @@
/* App-wide styling */
body {
background-color: #0f1116;
color: #ffffff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
}
#hero {
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#links {
width: 400px;
text-align: left;
font-size: x-large;
color: white;
display: flex;
flex-direction: column;
}
#links a {
color: white;
text-decoration: none;
margin-top: 20px;
margin: 10px 0px;
border: white 1px solid;
border-radius: 5px;
padding: 10px;
}
#links a:hover {
background-color: #1f1f1f;
cursor: pointer;
}
#header {
max-width: 1200px;
}

View File

@@ -0,0 +1,44 @@
use leptos::{prelude::*, subsecond::connect_to_hot_patch_messages};
use leptos_router::{
components::{Route, Router, Routes},
path,
};
fn main() {
// connect to DX CLI and patch the WASM binary whenever we receive a message
connect_to_hot_patch_messages();
// wrapping App here in a closure so we can hot-reload it, because we only do that
// for reactive views right now. changing anything will re-run App and update the view
mount_to_body(|| App);
}
#[component]
fn App() -> impl IntoView {
view! {
<nav>
<a href="/">"Home"</a>
<a href="/about">"About"</a>
</nav>
<Router>
<Routes fallback=|| "Not found">
<Route path=path!("/") view=HomePage/>
<Route path=path!("/about") view=About/>
</Routes>
</Router>
}
}
#[component]
fn HomePage() -> impl IntoView {
view! {
<h1>"Home Page"</h1>
}
}
#[component]
fn About() -> impl IntoView {
view! {
<h1>"About"</h1>
}
}

View File

@@ -0,0 +1,3 @@
[tools]
tailwindcss = "4.1.13"

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos"
version = "0.8.8"
version = "0.8.9"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -58,6 +58,9 @@ slotmap = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
wasm_split_helpers.workspace = true
subsecond = { workspace = true, default-features = true, optional = true }
dioxus-cli-config = { workspace = true, default-features = true, optional = true }
dioxus-devtools = { workspace = true, default-features = true, optional = true }
[features]
hydration = [
@@ -102,6 +105,16 @@ trace-component-props = [
]
delegation = ["tachys/delegation"]
islands-router = ["tachys/mark_branches"]
subsecond = [
"reactive_graph/subsecond",
"dep:subsecond",
"dep:dioxus-cli-config",
"dep:dioxus-devtools",
"web-sys/Location",
"web-sys/MessageEvent",
"web-sys/WebSocket",
"web-sys/Window",
]
[dev-dependencies]
tokio = { features = [

View File

@@ -1,5 +1,4 @@
#![deny(missing_docs)]
#![forbid(unsafe_code)]
//! # About Leptos
//!
@@ -306,6 +305,10 @@ pub use tachys::mathml as math;
#[doc(inline)]
pub use tachys::svg;
#[cfg(feature = "subsecond")]
/// Utilities for using binary hot-patching with [`subsecond`].
pub mod subsecond;
/// Utilities for simple isomorphic logging to the console or terminal.
pub mod logging {
pub use leptos_dom::{debug_warn, error, log, warn};

62
leptos/src/subsecond.rs Normal file
View File

@@ -0,0 +1,62 @@
use dioxus_devtools::DevserverMsg;
use wasm_bindgen::{prelude::Closure, JsCast};
use web_sys::{js_sys::JsString, MessageEvent, WebSocket};
/// Sets up a websocket connect to the `dx` CLI, waiting for incoming hot-patching messages
/// and patching the WASM binary appropriately.
//
// Note: This is a stripped-down version of Dioxus's `make_ws` from `dioxus_web`
// It's essentially copy-pasted here because it's not pub there.
// Would love to just take a dependency on that to be able to use it and deduplicate.
//
// https://github.com/DioxusLabs/dioxus/blob/main/packages/web/src/devtools.rs#L36
pub fn connect_to_hot_patch_messages() {
// Get the location of the devserver, using the current location plus the /_dioxus path
// The idea here being that the devserver is always located on the /_dioxus behind a proxy
let location = web_sys::window().unwrap().location();
let url = format!(
"{protocol}//{host}/_dioxus?build_id={build_id}",
protocol = match location.protocol().unwrap() {
prot if prot == "https:" => "wss:",
_ => "ws:",
},
host = location.host().unwrap(),
build_id = dioxus_cli_config::build_id(),
);
let ws = WebSocket::new(&url).unwrap();
ws.set_onmessage(Some(
Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| {
let Ok(text) = e.data().dyn_into::<JsString>() else {
return;
};
// The devserver messages have some &'static strs in them, so we need to leak the source string
let string: String = text.into();
let string = Box::leak(string.into_boxed_str());
if let Ok(DevserverMsg::HotReload(msg)) =
serde_json::from_str::<DevserverMsg>(string)
{
if let Some(jump_table) = msg.jump_table.as_ref().cloned() {
if msg.for_build_id == Some(dioxus_cli_config::build_id()) {
let our_pid = if cfg!(target_family = "wasm") {
None
} else {
Some(std::process::id())
};
if msg.for_pid == our_pid {
unsafe { subsecond::apply_patch(jump_table) }
.unwrap();
}
}
}
}
})
.into_js_value()
.as_ref()
.unchecked_ref(),
));
}

View File

@@ -463,7 +463,7 @@ pub fn set_interval_with_handle(
#[inline(never)]
fn si(
cb: Box<dyn Fn()>,
cb: Box<dyn FnMut()>,
duration: Duration,
) -> Result<IntervalHandle, JsValue> {
let cb = Closure::wrap(cb).into_js_value();

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.2.6"
version = "0.2.7"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -27,6 +27,7 @@ async-lock = { workspace = true, default-features = true }
send_wrapper = { features = [
"futures",
], workspace = true, default-features = true }
subsecond = { workspace = true, default-features = true, optional = true }
indexmap = { workspace = true, default-features = true }
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
@@ -51,6 +52,7 @@ hydration = ["dep:hydration_context"]
effects = [
] # whether to run effects: should be disabled for something like server rendering
sandboxed-arenas = []
subsecond = ["dep:subsecond"]
[package.metadata.docs.rs]
all-features = true

View File

@@ -9,6 +9,8 @@ use crate::{
};
use futures::StreamExt;
use or_poisoned::OrPoisoned;
#[cfg(feature = "subsecond")]
use std::sync::Mutex;
use std::{
fmt::Debug,
future::{Future, IntoFuture},
@@ -49,13 +51,39 @@ impl<T> Debug for RenderEffect<T> {
}
}
#[cfg(feature = "subsecond")]
type CurrentHotPtr = Box<dyn Fn() -> Option<subsecond::HotFnPtr> + Send + Sync>;
impl<T> RenderEffect<T>
where
T: 'static,
{
/// Creates a new render effect, which immediately runs `fun`.
pub fn new(fun: impl FnMut(Option<T>) -> T + 'static) -> Self {
Self::new_with_value_erased(Box::new(fun), None)
#[cfg(feature = "subsecond")]
let (hot_fn_ptr, fun) = {
let fun = Arc::new(Mutex::new(subsecond::HotFn::current(fun)));
(
{
let fun = Arc::downgrade(&fun);
let wrapped = send_wrapper::SendWrapper::new(move || {
fun.upgrade()
.map(|n| n.lock().or_poisoned().ptr_address())
});
// it's not redundant, it's due to the SendWrapper deref
#[allow(clippy::redundant_closure)]
Box::new(move || wrapped())
},
move |prev| fun.lock().or_poisoned().call((prev,)),
)
};
Self::new_with_value_erased(
Box::new(fun),
None,
#[cfg(feature = "subsecond")]
hot_fn_ptr,
)
}
/// Creates a new render effect with an initial value.
@@ -63,7 +91,30 @@ where
fun: impl FnMut(Option<T>) -> T + 'static,
initial_value: Option<T>,
) -> Self {
Self::new_with_value_erased(Box::new(fun), initial_value)
#[cfg(feature = "subsecond")]
let (hot_fn_ptr, fun) = {
let fun = Arc::new(Mutex::new(subsecond::HotFn::current(fun)));
(
{
let fun = Arc::downgrade(&fun);
let wrapped = send_wrapper::SendWrapper::new(move || {
fun.upgrade()
.map(|n| n.lock().or_poisoned().ptr_address())
});
// it's not redundant, it's due to the SendWrapper deref
#[allow(clippy::redundant_closure)]
Box::new(move || wrapped())
},
move |prev| fun.lock().or_poisoned().call((prev,)),
)
};
Self::new_with_value_erased(
Box::new(fun),
initial_value,
#[cfg(feature = "subsecond")]
hot_fn_ptr,
)
}
/// Creates a new render effect, which immediately runs `fun`.
@@ -71,6 +122,11 @@ where
fun: impl FnMut(Option<T>) -> T + 'static,
value: impl IntoFuture<Output = T> + 'static,
) -> Self {
#[cfg(feature = "subsecond")]
let mut fun = subsecond::HotFn::current(fun);
#[cfg(feature = "subsecond")]
let fun = move |prev| fun.call((prev,));
Self::new_with_async_value_erased(
Box::new(fun),
Box::pin(value.into_future()),
@@ -79,8 +135,13 @@ where
}
fn new_with_value_erased(
mut fun: Box<dyn FnMut(Option<T>) -> T + 'static>,
#[allow(unused_mut)] mut fun: Box<dyn FnMut(Option<T>) -> T + 'static>,
initial_value: Option<T>,
// this argument can be used to invalidate individual effects in the future
// in present experiments, I have found that it is not actually granular enough to make a difference
#[allow(unused)]
#[cfg(feature = "subsecond")]
hot_fn_ptr: CurrentHotPtr,
) -> Self {
// codegen optimisation:
fn prep() -> (Owner, Arc<RwLock<EffectInner>>, crate::channel::Receiver)
@@ -104,12 +165,56 @@ where
let _ = initial_value;
let _ = owner;
let _ = &mut rx;
let _ = &mut fun;
let _ = fun;
}
#[cfg(feature = "effects")]
{
let subscriber = inner.to_any_subscriber();
#[cfg(all(feature = "subsecond", debug_assertions))]
let mut fun = {
use crate::graph::ReactiveNode;
use rustc_hash::FxHashMap;
use std::sync::{Arc, LazyLock, Mutex};
use subsecond::HotFnPtr;
static HOT_RELOAD_SUBSCRIBERS: LazyLock<
Mutex<FxHashMap<AnySubscriber, (HotFnPtr, CurrentHotPtr)>>,
> = LazyLock::new(|| {
subsecond::register_handler(Arc::new(|| {
HOT_RELOAD_SUBSCRIBERS.lock().or_poisoned().retain(
|subscriber, (prev_ptr, hot_fn_ptr)| {
match hot_fn_ptr() {
None => false,
Some(curr_hot_ptr) => {
if curr_hot_ptr != *prev_ptr {
crate::log_warning(format_args!(
"{prev_ptr:?} <> \
{curr_hot_ptr:?}",
));
*prev_ptr = curr_hot_ptr;
subscriber.mark_dirty();
}
true
}
}
},
);
}));
Default::default()
});
let mut fun = subsecond::HotFn::current(fun);
let initial_ptr = hot_fn_ptr().unwrap();
HOT_RELOAD_SUBSCRIBERS
.lock()
.or_poisoned()
.insert(subscriber.clone(), (initial_ptr, hot_fn_ptr));
move |prev| fun.call((prev,))
};
*value.write().or_poisoned() = Some(
owner.with(|| subscriber.with_observer(|| fun(initial_value))),
);
@@ -230,6 +335,11 @@ where
pub fn new_isomorphic(
fun: impl FnMut(Option<T>) -> T + Send + Sync + 'static,
) -> Self {
#[cfg(feature = "subsecond")]
let mut fun = subsecond::HotFn::current(fun);
#[cfg(feature = "subsecond")]
let fun = move |prev| fun.call((prev,));
fn erased<T: Send + Sync + 'static>(
mut fun: Box<dyn FnMut(Option<T>) -> T + Send + Sync + 'static>,
) -> RenderEffect<T> {

View File

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

View File

@@ -26,7 +26,7 @@ use reactive_graph::{
computed::{ArcMemo, ScopedFuture},
owner::{provide_context, use_context, Owner},
signal::{ArcRwSignal, ArcTrigger},
traits::{Get, GetUntracked, Notify, ReadUntracked, Set, Track},
traits::{Get, GetUntracked, Notify, ReadUntracked, Set, Track, Write},
transition::AsyncTransition,
wrappers::write::SignalSetter,
};
@@ -119,6 +119,7 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
drop(url);
@@ -159,13 +160,14 @@ where
}
return;
}
// since the path didn't match, we'll update the retained path for future diffing
state.path.clear();
state.path.push_str(url_snapshot.path());
let new_match = self.routes.match_route(url_snapshot.path());
state.current_url.set(url_snapshot);
*state.current_url.write_untracked() = url_snapshot;
match new_match {
None => {
@@ -192,6 +194,7 @@ where
&mut state.outlets,
self.set_is_routing.is_some(),
0,
&self.outer_owner,
);
let (abort_handle, abort_registration) =
@@ -369,6 +372,7 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
// outlets will not send their views if the loaders are never polled
@@ -422,8 +426,16 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
let preload_owners = outlets
.iter()
.map(|o| o.preload_owner.clone())
.collect::<Vec<_>>();
outer_owner
.with(|| Owner::on_cleanup(move || drop(preload_owners)));
// outlets will not send their views if the loaders are never polled
// the loaders are async so that they can lazy-load routes in the browser,
// but they should always be synchronously available on the server
@@ -475,6 +487,7 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
drop(url);
@@ -530,6 +543,7 @@ where
base,
&mut loaders,
&mut outlets,
&outer_owner,
);
drop(url);
@@ -566,6 +580,7 @@ pub(crate) struct RouteContext {
base: Option<Oco<'static, str>>,
view_fn: Arc<Mutex<OutletViewFn>>,
owner: Arc<Mutex<Option<Owner>>>,
preload_owner: Owner,
child: ChildRoute,
}
@@ -597,6 +612,7 @@ impl Clone for RouteContext {
view_fn: Arc::clone(&self.view_fn),
owner: Arc::clone(&self.owner),
child: self.child.clone(),
preload_owner: self.preload_owner.clone(),
}
}
}
@@ -608,6 +624,7 @@ trait AddNestedRoute {
base: Option<Oco<'static, str>>,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
outlets: &mut Vec<RouteContext>,
outer_owner: &Owner,
);
#[allow(clippy::too_many_arguments)]
@@ -621,6 +638,7 @@ trait AddNestedRoute {
outlets: &mut Vec<RouteContext>,
set_is_routing: bool,
level: u8,
outer_owner: &Owner,
) -> u8;
}
@@ -634,6 +652,7 @@ where
base: Option<Oco<'static, str>>,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
outlets: &mut Vec<RouteContext>,
outer_owner: &Owner,
) {
let orig_url = url;
@@ -701,6 +720,7 @@ where
base: base.clone(),
child: ChildRoute(Arc::new(Mutex::new(None))),
owner: Arc::new(Mutex::new(None)),
preload_owner: outer_owner.child(),
};
if !outlets.is_empty() {
let prev_index = outlets.len().saturating_sub(1);
@@ -725,7 +745,15 @@ where
provide_context(params.clone());
provide_context(url.clone());
provide_context(matched.clone());
view.preload().await;
outlet
.preload_owner
.with(|| {
provide_context(params.clone());
provide_context(url.clone());
provide_context(matched.clone());
ScopedFuture::new(view.preload())
})
.await;
let child = outlet.child.clone();
*view_fn.lock().or_poisoned() =
Box::new(move |owner_where_used| {
@@ -772,7 +800,13 @@ where
// this is important because to build the view, we need access to the outlet
// and the outlet will be returned from building this child
if let Some(child) = child {
child.build_nested_route(orig_url, base, loaders, outlets);
child.build_nested_route(
orig_url,
base,
loaders,
outlets,
outer_owner,
);
}
}
@@ -787,6 +821,7 @@ where
outlets: &mut Vec<RouteContext>,
set_is_routing: bool,
level: u8,
outer_owner: &Owner,
) -> u8 {
let (parent_params, parent_matches): (Vec<_>, Vec<_>) = outlets
.iter()
@@ -803,7 +838,13 @@ where
match current {
// if there's nothing currently in the routes at this point, build from here
None => {
self.build_nested_route(url, base, preloaders, outlets);
self.build_nested_route(
url,
base,
preloaders,
outlets,
outer_owner,
);
level
}
Some(current) => {
@@ -843,6 +884,10 @@ where
&mut current.matched,
ArcRwSignal::new(new_match),
);
let old_preload_owner = mem::replace(
&mut current.preload_owner,
outer_owner.child(),
);
let matched_including_parents = {
ArcMemo::new({
let matched = current.matched.clone();
@@ -885,11 +930,26 @@ where
let child = outlet.child.clone();
async move {
let child = child.clone();
if set_is_routing {
AsyncTransition::run(|| view.preload()).await;
} else {
view.preload().await;
}
outlet
.preload_owner
.with(|| {
provide_context(
params_including_parents.clone(),
);
provide_context(url.clone());
provide_context(matched.clone());
ScopedFuture::new(async {
if set_is_routing {
AsyncTransition::run(|| {
view.preload()
})
.await;
} else {
view.preload().await;
}
})
})
.await;
*view_fn.lock().or_poisoned() =
Box::new(move |owner_where_used| {
let prev_owner = route_owner
@@ -938,6 +998,7 @@ where
drop(old_params);
drop(old_url);
drop(old_matched);
drop(old_preload_owner);
trigger
}
})));
@@ -948,8 +1009,13 @@ where
// if this children has matches, then rebuild the lower section of the tree
if let Some(child) = child {
child
.build_nested_route(url, base, preloaders, outlets);
child.build_nested_route(
url,
base,
preloaders,
outlets,
outer_owner,
);
} else {
*outlets[*items].child.0.lock().or_poisoned() = None;
}
@@ -973,6 +1039,7 @@ where
outlets,
set_is_routing,
level + 1,
outer_owner,
)
} else {
*current.child.0.lock().or_poisoned() = None;

View File

@@ -5,7 +5,7 @@ license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "RPC for any web framework."
readme = "../README.md"
version = "0.8.6"
version = "0.8.7"
rust-version.workspace = true
edition.workspace = true

View File

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

View File

@@ -227,7 +227,7 @@ html_self_closing_elements! {
html_elements! {
/// The `<a>` HTML element (or anchor element), with its href attribute, creates a hyperlink to web pages, files, email addresses, locations in the same page, or anything else a URL can address.
a HtmlAnchorElement [download, href, hreflang, ping, rel, target, r#type ] true,
a HtmlAnchorElement [download, href, hreflang, ping, referrerpolicy, rel, target, r#type ] true,
/// The `<abbr>` HTML element represents an abbreviation or acronym; the optional title attribute can provide an expansion or description for the abbreviation. If present, title must contain this full description and nothing else.
abbr HtmlElement [] true,
/// The `<address>` HTML element indicates that the enclosed HTML provides contact information for a person or people, or for an organization.

View File

@@ -10,6 +10,9 @@
all(feature = "nightly", rustc_nightly),
feature(unsized_const_params)
)]
// support for const generic &'static str has now moved back and forth between
// these two features a couple times; we'll just enable both
#![cfg_attr(all(feature = "nightly", rustc_nightly), feature(adt_const_params))]
#![deny(missing_docs)]
/// Commonly-used traits.

View File

@@ -1,6 +1,6 @@
[package]
name = "wasm_split_macros"
version = "0.1.2"
version = "0.1.3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "README.md"