Compare commits

..

2 Commits

Author SHA1 Message Date
Greg Johnston
f97fd557c3 fix: maintain send/syncness of the handle 2025-09-21 14:11:04 -04:00
Greg Johnston
d642806e5e fix: remove event listeners correctly when dropping handles, as documented (closes #4313) 2025-09-20 12:43:09 -04:00
115 changed files with 1209 additions and 2959 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@v47
uses: tj-actions/changed-files@v46
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@v47
uses: tj-actions/changed-files@v46
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@v6
uses: actions/setup-node@v4
with:
node-version: 20
- uses: pnpm/action-setup@v4

934
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
resolver = "2"
members = [
# utilities
"oco",
"any_spawner",
"const_str_slice_concat",
"either_of",
@@ -44,31 +45,33 @@ rust-version = "1.88"
[workspace.dependencies]
# members
throw_error = { path = "./any_error/", version = "0.3.1" }
throw_error = { path = "./any_error/", version = "0.3.0" }
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.14" }
leptos_config = { path = "./leptos_config", version = "0.8.8" }
leptos_dom = { path = "./leptos_dom", version = "0.8.7" }
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.7" }
leptos_macro = { path = "./leptos_macro", version = "0.8.12" }
leptos_router = { path = "./router", version = "0.8.10" }
leptos_router_macro = { path = "./router_macro", version = "0.8.6" }
leptos_server = { path = "./leptos_server", version = "0.8.6" }
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.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.11" }
reactive_stores = { path = "./reactive_stores", version = "0.3.0" }
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.8" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.8" }
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.11" }
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.3" }
# members deps
async-once-cell = { default-features = false, version = "0.5.3" }
@@ -76,9 +79,8 @@ itertools = { default-features = false, version = "0.14.0" }
convert_case = { default-features = false, version = "0.8.0" }
serde_json = { default-features = false, version = "1.0.143" }
trybuild = { default-features = false, version = "1.0.110" }
typed-builder = { default-features = false, version = "0.22.0" }
typed-builder-macro = { default-features = false, version = "0.22.0" }
thiserror = { default-features = false, version = "2.0.17" }
typed-builder = { default-features = false, version = "0.21.2" }
thiserror = { default-features = false, version = "2.0.16" }
wasm-bindgen = { default-features = false, version = "0.2.100" }
indexmap = { default-features = false, version = "2.11.0" }
rstml = { default-features = false, version = "0.12.1" }
@@ -95,7 +97,7 @@ send_wrapper = { default-features = false, version = "0.6.0" }
tokio-test = { default-features = false, version = "0.4.4" }
html-escape = { default-features = false, version = "0.2.13" }
proc-macro-error2 = { default-features = false, version = "2.0.1" }
const_format = { default-features = false, version = "0.2.35" }
const_format = { default-features = false, version = "0.2.34" }
gloo-net = { default-features = false, version = "0.6.0" }
url = { default-features = false, version = "2.5.4" }
tokio = { default-features = false, version = "1.47.1" }
@@ -105,46 +107,46 @@ wasm-bindgen-futures = { default-features = false, version = "0.4.50" }
tower = { default-features = false, version = "0.5.2" }
proc-macro2 = { default-features = false, version = "1.0.101" }
serde = { default-features = false, version = "1.0.219" }
parking_lot = { default-features = false, version = "0.12.5" }
axum = { default-features = false, version = "0.8.6" }
parking_lot = { default-features = false, version = "0.12.4" }
axum = { default-features = false, version = "0.8.4" }
serde_qs = { default-features = false, version = "0.15.0" }
syn = { default-features = false, version = "2.0.106" }
xxhash-rust = { default-features = false, version = "0.8.15" }
paste = { default-features = false, version = "1.0.15" }
quote = { default-features = false, version = "1.0.41" }
quote = { default-features = false, version = "1.0.40" }
web-sys = { default-features = false, version = "0.3.77" }
js-sys = { default-features = false, version = "0.3.77" }
rand = { default-features = false, version = "0.9.1" }
serde-lite = { default-features = false, version = "0.5.0" }
tokio-tungstenite = { default-features = false, version = "0.28.0" }
tokio-tungstenite = { default-features = false, version = "0.27.0" }
serial_test = { default-features = false, version = "3.2.0" }
erased = { default-features = false, version = "0.1.2" }
glib = { default-features = false, version = "0.20.12" }
async-trait = { default-features = false, version = "0.1.89" }
typed-builder-macro = { default-features = false, version = "0.21.0" }
linear-map = { default-features = false, version = "1.2.0" }
anyhow = { default-features = false, version = "1.0.100" }
anyhow = { default-features = false, version = "1.0.99" }
walkdir = { default-features = false, version = "2.5.0" }
actix-ws = { default-features = false, version = "0.3.0" }
tower-http = { default-features = false, version = "0.6.4" }
prettyplease = { default-features = false, version = "0.2.37" }
inventory = { default-features = false, version = "0.3.21" }
config = { default-features = false, version = "0.15.14" }
camino = { default-features = false, version = "1.2.1" }
camino = { default-features = false, version = "1.1.11" }
ciborium = { default-features = false, version = "0.2.2" }
bitcode = { default-features = false, version = "0.6.6" }
multer = { default-features = false, version = "3.1.0" }
leptos-spin-macro = { default-features = false, version = "0.2.0" }
sledgehammer_utils = { default-features = false, version = "0.3.1" }
sledgehammer_bindgen = { default-features = false, version = "0.6.0" }
wasm-streams = { default-features = false, version = "0.4.2" }
rkyv = { default-features = false, version = "0.8.12" }
rkyv = { default-features = false, version = "0.8.11" }
temp-env = { default-features = false, version = "0.3.6" }
uuid = { default-features = false, version = "1.18.0" }
bytes = { default-features = false, version = "1.10.1" }
http = { default-features = false, version = "1.3.1" }
regex = { default-features = false, version = "1.11.3" }
regex = { default-features = false, version = "1.11.2" }
drain_filter_polyfill = { default-features = false, version = "0.1.3" }
tempfile = { default-features = false, version = "3.23.0" }
tempfile = { default-features = false, version = "3.21.0" }
futures-lite = { default-features = false, version = "2.6.1" }
log = { default-features = false, version = "0.4.27" }
percent-encoding = { default-features = false, version = "2.3.2" }
@@ -156,10 +158,10 @@ postcard = { default-features = false, version = "1.1.3" }
rmp-serde = { default-features = false, version = "1.3.0" }
reqwest = { default-features = false, version = "0.12.23" }
tower-layer = { default-features = false, version = "0.3.3" }
attribute-derive = { default-features = false, version = "0.10.5" }
attribute-derive = { default-features = false, version = "0.10.3" }
insta = { default-features = false, version = "1.43.1" }
codee = { default-features = false, version = "0.3.0" }
actix-http = { default-features = false, version = "3.11.2" }
actix-http = { default-features = false, version = "3.11.1" }
wasm-bindgen-test = { default-features = false, version = "0.3.50" }
rustversion = { default-features = false, version = "1.0.22" }
getrandom = { default-features = false, version = "0.3.3" }
@@ -168,10 +170,6 @@ 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, version = "0.7.0-rc.0" }
dioxus-cli-config = { default-features = false, version = "0.7.0-rc.0" }
dioxus-devtools = { default-features = false, version = "0.7.0-rc.0" }
wasm_split_helpers = { default-features = false, version = "0.2.0" }
[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 --locked
cargo install cargo-leptos
cargo leptos new --git https://github.com/leptos-rs/start-axum
cd [your project name]
cargo leptos watch

View File

@@ -1,6 +1,6 @@
[package]
name = "throw_error"
version = "0.3.1"
version = "0.3.0"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -11,6 +11,3 @@ edition.workspace = true
[dependencies]
pin-project-lite = { workspace = true, default-features = true }
[dev-dependencies]
anyhow.workspace = true

View File

@@ -45,10 +45,10 @@ impl fmt::Display for Error {
impl<T> From<T> for Error
where
T: Into<Box<dyn error::Error + Send + Sync + 'static>>,
T: error::Error + Send + Sync + 'static,
{
fn from(value: T) -> Self {
Error(Arc::from(value.into()))
Error(Arc::new(value))
}
}
@@ -158,32 +158,3 @@ where
this.inner.poll(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as StdError;
#[derive(Debug)]
struct MyError;
impl Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MyError")
}
}
impl StdError for MyError {}
#[test]
fn test_from() {
let e = MyError;
let _le = Error::from(e);
let e = "some error".to_string();
let _le = Error::from(e);
let e = anyhow::anyhow!("anyhow error");
let _le = Error::from(e);
}
}

View File

@@ -9,3 +9,6 @@ routing when you use islands.
This uses *only* server rendering, with no actual islands, but still maintains client-side state across page navigations.
It does this by building on the fact that we now have a statically-typed view tree to do pretty smart updates with
new HTML from the client, with extremely minimal diffing.
The demo itself works, but the feature that supports it is incomplete. A couple people have accidentally
used it and broken their applications in ways they don't understand, so I've renamed the feature to `dont-use-islands-router`.

View File

@@ -5,4 +5,4 @@ test cases that typically happens at integration.
## Quick Start
Run `cargo leptos watch --split` to run this example.
Run `cargo leptos watch` to run this example.

View File

@@ -1,13 +0,0 @@
@check_issue_4251
Feature: Check that issue 4251 does not reappear
Scenario: Clicking a link to the same page youre currently on should not add the page to the history stack.
Given I see the app
And I can access regression test 4324
When I select the link This page
And I select the link This page
And I select the link This page
Then I see the result is the string Issue4324
When I press the back button
And I select the link 4324
Then I see the result is the string Issue4324

View File

@@ -1,11 +0,0 @@
@check_issue_4324
Feature: Check that issue 4324 does not reappear
Scenario: Navigating to the same page after clicking "Back" should set the URL correctly
Given I see the app
And I can access regression test 4324
Then I see the path is /4324/
When I press the back button
Then I see the path is /
When I select the link 4324
Then I see the path is /4324/

View File

@@ -43,13 +43,3 @@ pub async fn element_value_is(
assert_eq!(value.as_deref(), Some(expected));
Ok(())
}
pub async fn path_is(client: &Client, expected_path: &str) -> Result<()> {
let url = client
.current_url()
.await
.expect("could not access current URL");
let path = url.path();
assert_eq!(expected_path, path);
Ok(())
}

View File

@@ -45,12 +45,3 @@ async fn i_refresh_the_browser(world: &mut AppWorld) -> Result<()> {
Ok(())
}
#[given(regex = "^I press the back button$")]
#[when(regex = "^I press the back button$")]
async fn i_go_back(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
client.back().await?;
Ok(())
}

View File

@@ -43,10 +43,3 @@ async fn i_see_the_value(
check::element_value_is(client, &id, &value).await?;
Ok(())
}
#[then(regex = r"^I see the path is (.*)$")]
async fn i_see_the_path(world: &mut AppWorld, path: String) -> Result<()> {
let client = &world.client;
check::path_is(client, &path).await?;
Ok(())
}

View File

@@ -1,7 +1,7 @@
use crate::{
issue_4005::Routes4005, issue_4088::Routes4088, issue_4217::Routes4217,
issue_4285::Routes4285, issue_4296::Routes4296, issue_4324::Routes4324,
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, *};
@@ -47,7 +47,6 @@ pub fn App() -> impl IntoView {
<Routes4005/>
<Routes4285/>
<Routes4296/>
<Routes4324/>
</Routes>
</main>
</Router>
@@ -74,7 +73,6 @@ fn HomePage() -> impl IntoView {
<li><a href="/4005/">"4005"</a></li>
<li><a href="/4285/">"4285"</a></li>
<li><a href="/4296/">"4296"</a></li>
<li><a href="/4324/">"4324"</a></li>
</ul>
</nav>
}

View File

@@ -1,21 +0,0 @@
use leptos::prelude::*;
#[allow(unused_imports)]
use leptos_router::{
components::Route, path, Lazy, MatchNestedRoutes, NavigateOptions,
};
#[component]
pub fn Routes4324() -> impl MatchNestedRoutes + Clone {
view! {
<Route path=path!("4324") view=Issue4324/>
}
.into_inner()
}
#[component]
pub fn Issue4324() -> impl IntoView {
view! {
<a href="/4324/">"This page"</a>
<p id="result">"Issue4324"</p>
}
}

View File

@@ -4,7 +4,6 @@ mod issue_4088;
mod issue_4217;
mod issue_4285;
mod issue_4296;
mod issue_4324;
mod pr_4015;
mod pr_4091;

View File

@@ -440,14 +440,7 @@ pub fn FileUploadWithProgress() -> impl IntoView {
let mut entry =
FILES.entry(filename.to_string()).or_insert_with(|| {
println!("[{filename}]\tinserting channel");
// NOTE: this channel capacity is set arbitrarily for this demo code.
// it allows for up to exactly 1048 chunks to be sent, which sets an upper cap
// on upload size (the precise details vary by client)
// in a real system, you will want to create some more reasonable ways of
// sending and sharing notifications
//
// see https://github.com/leptos-rs/leptos/issues/4397 for related discussion
let (tx, rx) = broadcast(1048);
let (tx, rx) = broadcast(128);
File { total: 0, tx, rx }
});
entry.total += len;

View File

@@ -1,10 +1,7 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{
http::{HeaderName, HeaderValue},
Router,
};
use axum::Router;
use leptos::{logging::log, prelude::*};
use leptos_axum::{generate_route_list, LeptosRoutes};
use ssr_modes_axum::app::*;
@@ -20,24 +17,7 @@ async fn main() {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler_with_context(
move || {
// if you want to add custom headers to the static file handler response,
// you can do that by providing `ResponseOptions` via context
let opts = use_context::<leptos_axum::ResponseOptions>()
.unwrap_or_default();
opts.insert_header(
HeaderName::from_static("cross-origin-opener-policy"),
HeaderValue::from_static("same-origin"),
);
opts.insert_header(
HeaderName::from_static("cross-origin-embedder-policy"),
HeaderValue::from_static("require-corp"),
);
provide_context(opts);
},
shell,
))
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
// run our app with hyper

View File

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

View File

@@ -1,13 +0,0 @@
[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

@@ -1,21 +0,0 @@
[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

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

View File

@@ -1,31 +0,0 @@
# 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.

Before

Width:  |  Height:  |  Size: 130 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -1,46 +0,0 @@
/* 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

@@ -1,44 +0,0 @@
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

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

View File

@@ -4,7 +4,7 @@ authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Actix integrations for the Leptos web framework."
version = "0.8.6"
version = "0.8.5"
rust-version.workspace = true
edition.workspace = true

View File

@@ -4,7 +4,7 @@ authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Axum integrations for the Leptos web framework."
version = "0.8.7"
version = "0.8.6"
rust-version.workspace = true
edition.workspace = true

View File

@@ -2050,20 +2050,7 @@ where
let res = res.await.unwrap();
if res.status() == StatusCode::OK {
let owner = Owner::new();
owner.with(|| {
additional_context();
let res = res.into_response();
if let Some(response_options) =
use_context::<ResponseOptions>()
{
let mut res = AxumResponse(res);
res.extend_response(&response_options);
res.0
} else {
res
}
})
res.into_response()
} else {
let mut res = handle_response_inner(
move || {

View File

@@ -4,7 +4,7 @@ authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Utilities to help build server integrations for the Leptos web framework."
version = "0.8.7"
version = "0.8.5"
rust-version.workspace = true
edition.workspace = true

View File

@@ -68,8 +68,7 @@ pub trait ExtendResponse: Sized {
let nonce =
use_nonce().map(|n| n.to_string()).unwrap_or_default();
if let Some(manifest) = use_context::<WasmSplitManifest>() {
let (pkg_path, manifest, wasm_split_file) =
&*manifest.0.read_value();
let (pkg_path, manifest) = &*manifest.0.read_value();
let prefetches = prefetches.0.read_value();
let all_prefetches = prefetches.iter().flat_map(|key| {
@@ -91,7 +90,7 @@ pub trait ExtendResponse: Sized {
.to_html();
}
_ = view! {
<Link rel="modulepreload" href=format!("{pkg_path}/{wasm_split_file}") crossorigin=nonce/>
<Link rel="modulepreload" href=format!("{pkg_path}/__wasm_split.js") crossorigin=nonce/>
}
.to_html();
}
@@ -121,7 +120,7 @@ pub trait ExtendResponse: Sized {
// drop the owner, cleaning up the reactive runtime,
// once the stream is over
.chain(once(async move {
owner.unset_with_forced_cleanup();
owner.unset();
Default::default()
})),
));

View File

@@ -1,10 +1,9 @@
[package]
name = "leptos"
version = "0.8.14"
version = "0.8.9"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
homepage = "https://leptos.dev/"
description = "Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces."
readme = "../README.md"
rust-version.workspace = true
@@ -58,10 +57,7 @@ serde_qs = { workspace = true, default-features = true }
slotmap = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
wasm_split_helpers = { workspace = true, default-features = 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 }
wasm_split_helpers.workspace = true
[features]
hydration = [
@@ -89,12 +85,6 @@ ssr = [
]
nightly = ["leptos_macro/nightly", "reactive_graph/nightly", "tachys/nightly"]
rkyv = ["server_fn/rkyv", "leptos_server/rkyv"]
bitcode = ["server_fn/bitcode"]
serde-lite = ["server_fn/serde-lite", "leptos_server/serde-lite"]
cbor = ["server_fn/cbor"]
msgpack = ["server_fn/msgpack"]
postcard = ["server_fn/postcard"]
multipart = ["server_fn/multipart"]
tracing = [
"dep:tracing",
"reactive_graph/tracing",
@@ -112,16 +102,6 @@ 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 = [
@@ -154,18 +134,13 @@ denylist = [
"trace-component-props",
"spin",
"islands",
"bitcode",
"serde-lite",
"cbor",
"msgpack",
"postcard",
"multipart",
]
skip_feature_sets = [
["csr", "ssr"],
["csr", "hydrate"],
["ssr", "hydrate"],
["serde", "serde-lite"],
["serde-lite", "miniserde"],
["serde", "miniserde"],
["serde", "rkyv"],
["miniserde", "rkyv"],

View File

@@ -2,19 +2,48 @@
//! for component properties, because they can be used to define optional callback functions,
//! which generic props dont support.
//!
//! The callback types implement [`Copy`], so they can easily be moved into and out of other closures, just like signals.
//! # Usage
//! Callbacks can be created manually from any function or closure, but the easiest way
//! to create them is to use `#[prop(into)]]` when defining a component.
//! ```
//! use leptos::prelude::*;
//!
//! #[component]
//! fn MyComponent(
//! #[prop(into)] render_number: Callback<(i32,), String>,
//! ) -> impl IntoView {
//! view! {
//! <div>
//! {render_number.run((1,))}
//! // callbacks can be called multiple times
//! {render_number.run((42,))}
//! </div>
//! }
//! }
//! // you can pass a closure directly as `render_number`
//! fn test() -> impl IntoView {
//! view! {
//! <MyComponent render_number=|x: i32| x.to_string()/>
//! }
//! }
//! ```
//!
//! *Notes*:
//! - The `render_number` prop can receive any type that implements `Fn(i32) -> String`.
//! - Callbacks are most useful when you want optional generic props.
//! - All callbacks implement the [`Callable`](leptos::callback::Callable) trait, and can be invoked with `my_callback.run(input)`.
//! - The callback types implement [`Copy`], so they can easily be moved into and out of other closures, just like signals.
//!
//! # Types
//! This modules implements 2 callback types:
//! - [`Callback`](reactive_graph::callback::Callback)
//! - [`UnsyncCallback`](reactive_graph::callback::UnsyncCallback)
//! - [`Callback`](leptos::callback::Callback)
//! - [`UnsyncCallback`](leptos::callback::UnsyncCallback)
//!
//! Use `SyncCallback` if the function is not `Sync` and `Send`.
use crate::{
use reactive_graph::{
owner::{LocalStorage, StoredValue},
traits::{Dispose, WithValue},
IntoReactiveValue,
};
use std::{fmt, rc::Rc, sync::Arc};
@@ -31,16 +60,7 @@ pub trait Callable<In: 'static, Out: 'static = ()> {
fn run(&self, input: In) -> Out;
}
/// A callback type that is not required to be [`Send`] or [`Sync`].
///
/// # Example
/// ```
/// # use reactive_graph::prelude::*; use reactive_graph::callback::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// let _: UnsyncCallback<()> = UnsyncCallback::new(|_| {});
/// let _: UnsyncCallback<(i32, i32)> = (|_x: i32, _y: i32| {}).into();
/// let cb: UnsyncCallback<i32, String> = UnsyncCallback::new(|x: i32| x.to_string());
/// assert_eq!(cb.run(42), "42".to_string());
/// ```
/// A callback type that is not required to be `Send + Sync`.
pub struct UnsyncCallback<In: 'static, Out: 'static = ()>(
StoredValue<Rc<dyn Fn(In) -> Out>, LocalStorage>,
);
@@ -128,15 +148,28 @@ impl_unsync_callable_from_fn!(
P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12
);
/// A callback type that is [`Send`] + [`Sync`].
/// Callbacks define a standard way to store functions and closures.
///
/// # Example
/// ```
/// # use reactive_graph::prelude::*; use reactive_graph::callback::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// let _: Callback<()> = Callback::new(|_| {});
/// let _: Callback<(i32, i32)> = (|_x: i32, _y: i32| {}).into();
/// let cb: Callback<i32, String> = Callback::new(|x: i32| x.to_string());
/// assert_eq!(cb.run(42), "42".to_string());
/// # use leptos::prelude::*;
/// # use leptos::callback::{Callable, Callback};
/// #[component]
/// fn MyComponent(
/// #[prop(into)] render_number: Callback<(i32,), String>,
/// ) -> impl IntoView {
/// view! {
/// <div>
/// {render_number.run((42,))}
/// </div>
/// }
/// }
///
/// fn test() -> impl IntoView {
/// view! {
/// <MyComponent render_number=move |x: i32| x.to_string()/>
/// }
/// }
/// ```
pub struct Callback<In, Out = ()>(
StoredValue<Arc<dyn Fn(In) -> Out + Send + Sync>>,
@@ -208,7 +241,6 @@ impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12);
impl<In: 'static, Out: 'static> Callback<In, Out> {
/// Creates a new callback from the given function.
#[track_caller]
pub fn new<F>(fun: F) -> Self
where
F: Fn(In) -> Out + Send + Sync + 'static,
@@ -230,94 +262,22 @@ impl<In: 'static, Out: 'static> Callback<In, Out> {
}
}
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerCallbackSingleParam;
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerCallbackStrOutputToString;
impl<I, O, F>
IntoReactiveValue<
Callback<I, O>,
__IntoReactiveValueMarkerCallbackSingleParam,
> for F
where
F: Fn(I) -> O + Send + Sync + 'static,
{
#[track_caller]
fn into_reactive_value(self) -> Callback<I, O> {
Callback::new(self)
}
}
impl<I, O, F>
IntoReactiveValue<
UnsyncCallback<I, O>,
__IntoReactiveValueMarkerCallbackSingleParam,
> for F
where
F: Fn(I) -> O + 'static,
{
#[track_caller]
fn into_reactive_value(self) -> UnsyncCallback<I, O> {
UnsyncCallback::new(self)
}
}
impl<I, F>
IntoReactiveValue<
Callback<I, String>,
__IntoReactiveValueMarkerCallbackStrOutputToString,
> for F
where
F: Fn(I) -> &'static str + Send + Sync + 'static,
{
#[track_caller]
fn into_reactive_value(self) -> Callback<I, String> {
Callback::new(move |i| self(i).to_string())
}
}
impl<I, F>
IntoReactiveValue<
UnsyncCallback<I, String>,
__IntoReactiveValueMarkerCallbackStrOutputToString,
> for F
where
F: Fn(I) -> &'static str + 'static,
{
#[track_caller]
fn into_reactive_value(self) -> UnsyncCallback<I, String> {
UnsyncCallback::new(move |i| self(i).to_string())
}
}
#[cfg(test)]
mod tests {
use super::Callable;
use crate::{
callback::{Callback, UnsyncCallback},
owner::Owner,
traits::Dispose,
IntoReactiveValue,
};
use crate::callback::{Callback, UnsyncCallback};
use reactive_graph::traits::Dispose;
struct NoClone {}
#[test]
fn clone_callback() {
let owner = Owner::new();
owner.set();
let callback = Callback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback;
}
#[test]
fn clone_unsync_callback() {
let owner = Owner::new();
owner.set();
let callback =
UnsyncCallback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback;
@@ -325,39 +285,20 @@ mod tests {
#[test]
fn runback_from() {
let owner = Owner::new();
owner.set();
let _callback: Callback<(), String> = (|| "test").into();
let _callback: Callback<(i32, String), String> =
(|num, s| format!("{num} {s}")).into();
// Single params should work without needing the (foo,) tuple using IntoReactiveValue:
let _callback: Callback<usize, &'static str> =
(|_usize| "test").into_reactive_value();
let _callback: Callback<usize, String> =
(|_usize| "test").into_reactive_value();
}
#[test]
fn sync_callback_from() {
let owner = Owner::new();
owner.set();
let _callback: UnsyncCallback<(), String> = (|| "test").into();
let _callback: UnsyncCallback<(i32, String), String> =
(|num, s| format!("{num} {s}")).into();
// Single params should work without needing the (foo,) tuple using IntoReactiveValue:
let _callback: UnsyncCallback<usize, &'static str> =
(|_usize| "test").into_reactive_value();
let _callback: UnsyncCallback<usize, String> =
(|_usize| "test").into_reactive_value();
}
#[test]
fn sync_callback_try_run() {
let owner = Owner::new();
owner.set();
let callback = Callback::new(move |arg| arg);
assert_eq!(callback.try_run((0,)), Some((0,)));
callback.dispose();
@@ -366,9 +307,6 @@ mod tests {
#[test]
fn unsync_callback_try_run() {
let owner = Owner::new();
owner.set();
let callback = UnsyncCallback::new(move |arg| arg);
assert_eq!(callback.try_run((0,)), Some((0,)));
callback.dispose();
@@ -377,9 +315,6 @@ mod tests {
#[test]
fn callback_matches_same() {
let owner = Owner::new();
owner.set();
let callback1 = Callback::new(|x: i32| x * 2);
let callback2 = callback1;
assert!(callback1.matches(&callback2));
@@ -387,9 +322,6 @@ mod tests {
#[test]
fn callback_matches_different() {
let owner = Owner::new();
owner.set();
let callback1 = Callback::new(|x: i32| x * 2);
let callback2 = Callback::new(|x: i32| x + 1);
assert!(!callback1.matches(&callback2));
@@ -397,9 +329,6 @@ mod tests {
#[test]
fn unsync_callback_matches_same() {
let owner = Owner::new();
owner.set();
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
let callback2 = callback1;
assert!(callback1.matches(&callback2));
@@ -407,9 +336,6 @@ mod tests {
#[test]
fn unsync_callback_matches_different() {
let owner = Owner::new();
owner.set();
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
let callback2 = UnsyncCallback::new(|x: i32| x + 1);
assert!(!callback1.matches(&callback2));

View File

@@ -65,56 +65,16 @@ pub fn HydrationScripts(
if let Some(splits) = SPLIT_MANIFEST.get_or_init(|| {
let root = root.clone().unwrap_or_default();
let (wasm_split_js, wasm_split_manifest) = if options.hash_files {
let hash_path = std::env::current_exe()
.map(|path| {
path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
})
.unwrap_or_default()
.join(options.hash_file.as_ref());
let hashes = std::fs::read_to_string(&hash_path)
.expect("failed to read hash file");
let mut split =
"__wasm_split.______________________.js".to_string();
let mut manifest = "__wasm_split_manifest.json".to_string();
for line in hashes.lines() {
let line = line.trim();
if !line.is_empty() {
if let Some((file, hash)) = line.split_once(':') {
if file == "manifest" {
manifest.clear();
manifest.push_str("__wasm_split_manifest.");
manifest.push_str(hash.trim());
manifest.push_str(".json");
}
if file == "split" {
split.clear();
split.push_str("__wasm_split.");
split.push_str(hash.trim());
split.push_str(".js");
}
}
}
}
(split, manifest)
} else {
(
"__wasm_split.______________________.js".to_string(),
"__wasm_split_manifest.json".to_string(),
)
};
let site_dir = &options.site_root;
let pkg_dir = &options.site_pkg_dir;
let path = PathBuf::from(site_dir.to_string());
let path = path.join(pkg_dir.to_string()).join(wasm_split_manifest);
let path = path
.join(pkg_dir.to_string())
.join("__wasm_split_manifest.json");
let file = std::fs::read_to_string(path).ok()?;
let manifest = WasmSplitManifest(ArcStoredValue::new((
format!("{root}/{pkg_dir}"),
serde_json::from_str(&file).expect("could not read manifest file"),
wasm_split_js,
)));
Some(manifest)

View File

@@ -1,4 +1,5 @@
#![deny(missing_docs)]
#![forbid(unsafe_code)]
//! # About Leptos
//!
@@ -203,7 +204,7 @@ pub mod prelude {
pub mod form;
/// A standard way to wrap functions and closures to pass them to components.
pub use reactive_graph::callback;
pub mod callback;
/// Types that can be passed as the `children` prop of a component.
pub mod children;
@@ -305,15 +306,9 @@ 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_error, debug_log, debug_warn, error, log, warn,
};
pub use leptos_dom::{debug_warn, error, log, warn};
}
/// Utilities for working with asynchronous tasks.
@@ -365,8 +360,7 @@ pub use serde_json;
pub use tracing;
#[doc(hidden)]
pub use wasm_bindgen;
#[doc(hidden)]
pub use wasm_split_helpers as wasm_split;
pub use wasm_split_helpers;
#[doc(hidden)]
pub use web_sys;
@@ -398,8 +392,7 @@ pub fn prefetch_lazy_fn_on_server(id: &'static str) {
#[derive(Clone, Debug, Default)]
pub struct WasmSplitManifest(
pub reactive_graph::owner::ArcStoredValue<(
String, // the pkg root
std::collections::HashMap<String, Vec<String>>, // preloads
String, // the name of the __wasm_split.js file
String,
std::collections::HashMap<String, Vec<String>>,
)>,
);

View File

@@ -1,62 +0,0 @@
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

@@ -6,7 +6,6 @@ use crate::{
use futures::{channel::oneshot, select, FutureExt};
use hydration_context::SerializedDataId;
use leptos_macro::component;
use or_poisoned::OrPoisoned;
use reactive_graph::{
computed::{
suspense::{LocalResourceNotifier, SuspenseContext},
@@ -15,10 +14,10 @@ use reactive_graph::{
effect::RenderEffect,
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
traits::{Dispose, Get, Read, ReadUntracked, Track, With, WriteValue},
traits::{Dispose, Get, Read, Track, With, WriteValue},
};
use slotmap::{DefaultKey, SlotMap};
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use tachys::{
either::Either,
html::attribute::{any_attribute::AnyAttribute, Attribute},
@@ -321,66 +320,23 @@ where
// walk over the tree of children once to make sure that all resource loads are registered
self.children.dry_resolve();
let children = Arc::new(Mutex::new(Some(self.children)));
// check the set of tasks to see if it is empty, now or later
let eff = reactive_graph::effect::Effect::new_isomorphic({
let children = Arc::clone(&children);
move |double_checking: Option<bool>| {
// on the first run, always track the tasks
if double_checking.is_none() {
tasks.track();
}
if let Some(curr_tasks) = tasks.try_read_untracked() {
if curr_tasks.is_empty() {
if double_checking == Some(true) {
// we have finished loading, and checking the children again told us there are
// no more pending tasks. so we can render both the children and the error boundary
if let Some(tx) = tasks_tx.take() {
// If the receiver has dropped, it means the ScopedFuture has already
// dropped, so it doesn't matter if we manage to send this.
_ = tx.send(());
}
if let Some(tx) = notify_error_boundary.take() {
_ = tx.send(());
}
} else {
// release the read guard on tasks, as we'll be updating it again
drop(curr_tasks);
// check the children for additional pending tasks
// the will catch additional resource reads nested inside a conditional depending on initial resource reads
if let Some(children) =
children.lock().or_poisoned().as_mut()
{
children.dry_resolve();
}
if tasks
.try_read()
.map(|n| n.is_empty())
.unwrap_or(false)
{
// there are no additional pending tasks, and we can simply return
if let Some(tx) = tasks_tx.take() {
// If the receiver has dropped, it means the ScopedFuture has already
// dropped, so it doesn't matter if we manage to send this.
_ = tx.send(());
}
if let Some(tx) = notify_error_boundary.take() {
_ = tx.send(());
}
}
// tell ourselves that we're just double-checking
return true;
move |_| {
tasks.track();
if let Some(tasks) = tasks.try_read() {
if tasks.is_empty() {
if let Some(tx) = tasks_tx.take() {
// If the receiver has dropped, it means the ScopedFuture has already
// dropped, so it doesn't matter if we manage to send this.
_ = tx.send(());
}
if let Some(tx) = notify_error_boundary.take() {
_ = tx.send(());
}
} else {
tasks.track();
}
}
false
}
});
@@ -406,17 +362,12 @@ where
None
}
_ = tasks_rx => {
let children = {
let mut children_lock = children.lock().or_poisoned();
children_lock.take().expect("children should not be removed until we render here")
};
// if we ran this earlier, reactive reads would always be registered as None
// this is fine in the case where we want to use Suspend and .await on some future
// but in situations like a <For each=|| some_resource.snapshot()/> we actually
// want to be able to 1) synchronously read a resource's value, but still 2) wait
// for it to load before we render everything
let mut children = Box::pin(children.resolve().fuse());
let mut children = Box::pin(self.children.resolve().fuse());
// we continue racing the children against the "do we have any local
// resources?" Future

View File

@@ -103,76 +103,6 @@ fn test_classes() {
assert_eq!(rendered.to_html(), "<div class=\"my big red car\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_class_with_class_directive_merge() {
use leptos::prelude::*;
// class= followed by class: should merge
let rendered: View<HtmlElement<_, _, _>> = view! {
<div class="foo" class:bar=true></div>
};
assert_eq!(rendered.to_html(), "<div class=\"foo bar\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_solo_class_directive() {
use leptos::prelude::*;
// Solo class: directive should work without class attribute
let rendered: View<HtmlElement<_, _, _>> = view! {
<div class:foo=true></div>
};
assert_eq!(rendered.to_html(), "<div class=\"foo\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_class_directive_with_static_class() {
use leptos::prelude::*;
// class:foo comes after class= due to macro sorting
// The class= clears buffer, then class:foo appends
let rendered: View<HtmlElement<_, _, _>> = view! {
<div class:foo=true class="bar"></div>
};
// After macro sorting: class="bar" class:foo=true
// Expected: "bar foo"
assert_eq!(rendered.to_html(), "<div class=\"bar foo\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_global_class_applied() {
use leptos::prelude::*;
// Test that a global class is properly applied
let rendered: View<HtmlElement<_, _, _>> = view! { class="global",
<div></div>
};
assert_eq!(rendered.to_html(), "<div class=\"global\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn test_multiple_class_attributes_overwrite() {
use leptos::prelude::*;
// When multiple class attributes are applied, the last one should win (browser behavior)
// This simulates what happens when attributes are combined programmatically
let el = leptos::html::div().class("first").class("second");
let html = el.to_html();
// The second class attribute should overwrite the first
assert_eq!(html, "<div class=\"second\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_with_styles() {

View File

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

View File

@@ -221,15 +221,18 @@ fn env_w_default(
/// An enum that can be used to define the environment Leptos is running in.
/// Setting this to the `PROD` variant will not include the WebSocket code for `cargo-leptos` watch mode.
/// Defaults to `DEV`.
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default,
)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub enum Env {
PROD,
#[default]
DEV,
}
impl Default for Env {
fn default() -> Self {
Self::DEV
}
}
fn env_from_str(input: &str) -> Result<Env, LeptosConfigError> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
@@ -276,15 +279,18 @@ impl TryFrom<String> for Env {
/// An enum that can be used to define the websocket protocol Leptos uses for hotreloading
/// Defaults to `ws`.
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default,
)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub enum ReloadWSProtocol {
#[default]
WS,
WSS,
}
impl Default for ReloadWSProtocol {
fn default() -> Self {
Self::WS
}
}
fn ws_from_str(input: &str) -> Result<ReloadWSProtocol, LeptosConfigError> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {

View File

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

View File

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

View File

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

View File

@@ -1016,27 +1016,25 @@ struct PropOpt {
name: Option<String>,
}
struct TypedBuilderOpts<'a> {
struct TypedBuilderOpts {
default: bool,
default_with_value: Option<syn::Expr>,
strip_option: bool,
into: bool,
ty: &'a Type,
}
impl<'a> TypedBuilderOpts<'a> {
fn from_opts(opts: &PropOpt, ty: &'a Type) -> Self {
impl TypedBuilderOpts {
fn from_opts(opts: &PropOpt, is_ty_option: bool) -> Self {
Self {
default: opts.optional || opts.optional_no_strip || opts.attrs,
default_with_value: opts.default.clone(),
strip_option: opts.strip_option || opts.optional && is_option(ty),
strip_option: opts.strip_option || opts.optional && is_ty_option,
into: opts.into,
ty,
}
}
}
impl TypedBuilderOpts<'_> {
impl TypedBuilderOpts {
fn to_serde_tokens(&self) -> TokenStream {
let default = if let Some(v) = &self.default_with_value {
let v = v.to_token_stream().to_string();
@@ -1055,7 +1053,7 @@ impl TypedBuilderOpts<'_> {
}
}
impl ToTokens for TypedBuilderOpts<'_> {
impl ToTokens for TypedBuilderOpts {
fn to_tokens(&self, tokens: &mut TokenStream) {
let default = if let Some(v) = &self.default_with_value {
let v = v.to_token_stream().to_string();
@@ -1066,29 +1064,14 @@ impl ToTokens for TypedBuilderOpts<'_> {
quote! {}
};
// If self.strip_option && self.into, then the strip_option will be represented as part of the transform closure.
let strip_option = if self.strip_option && !self.into {
let strip_option = if self.strip_option {
quote! { strip_option, }
} else {
quote! {}
};
let into = if self.into {
if !self.strip_option {
let ty = &self.ty;
quote! {
fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> #ty {
value.into_reactive_value()
},
}
} else {
let ty = unwrap_option(self.ty);
quote! {
fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> Option<#ty> {
Some(value.into_reactive_value())
},
}
}
quote! { into, }
} else {
quote! {}
};
@@ -1124,7 +1107,8 @@ fn prop_builder_fields(
ty,
} = prop;
let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, ty);
let builder_attrs =
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
let builder_docs = prop_to_doc(prop, PropDocStyle::Inline);
@@ -1169,7 +1153,8 @@ fn prop_serializer_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
ty,
} = prop;
let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, ty);
let builder_attrs =
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
let serde_attrs = builder_attrs.to_serde_tokens();
let PatIdent { ident, by_ref, .. } = &name;

View File

@@ -2,12 +2,12 @@ use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use proc_macro2::Ident;
use proc_macro_error2::abort;
use quote::{format_ident, quote};
use quote::quote;
use std::{
hash::{DefaultHasher, Hash, Hasher},
mem,
};
use syn::{parse_macro_input, parse_quote, ItemFn, ReturnType, Stmt};
use syn::{parse_macro_input, ItemFn};
pub fn lazy_impl(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let name = if !args.is_empty() {
@@ -16,7 +16,7 @@ pub fn lazy_impl(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
None
};
let fun = syn::parse::<ItemFn>(s).unwrap_or_else(|e| {
let mut fun = syn::parse::<ItemFn>(s).unwrap_or_else(|e| {
abort!(e.span(), "`lazy` can only be used on a function")
});
@@ -47,50 +47,29 @@ pub fn lazy_impl(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let is_wasm = cfg!(feature = "csr") || cfg!(feature = "hydrate");
if is_wasm {
let mut fun = fun;
let mut return_wrapper = None;
if was_async {
fun.sig.asyncness = None;
let ty = match &fun.sig.output {
ReturnType::Default => quote! { () },
ReturnType::Type(_, ty) => quote! { #ty },
};
let sync_output: ReturnType = parse_quote! {
-> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = #ty> + ::std::marker::Send + ::std::marker::Sync>>
};
let async_output = mem::replace(&mut fun.sig.output, sync_output);
let stmts = mem::take(&mut fun.block.stmts);
fun.block.stmts.push(Stmt::Expr(parse_quote! {
::std::boxed::Box::pin(::leptos::__reexports::send_wrapper::SendWrapper::new(async move {
#( #stmts )*
}))
}, None));
return_wrapper = Some(quote! {
return_wrapper(let future = _; { future.await } #async_output),
});
}
let preload_name = format_ident!("__preload_{}", fun.sig.ident);
quote! {
#[::leptos::wasm_split::wasm_split(
#[::leptos::wasm_split_helpers::wasm_split(
#unique_name,
wasm_split_path = ::leptos::wasm_split,
preload(#[doc(hidden)] #[allow(non_snake_case)] #preload_name),
#return_wrapper
::leptos::__reexports::send_wrapper
)]
#fun
}
} else {
let mut fun = fun;
if !was_async {
fun.sig.asyncness = Some(Default::default());
}
let statements = &mut fun.block.stmts;
let old_statements = mem::take(statements);
statements.push(parse_quote! {
::leptos::prefetch_lazy_fn_on_server(#unique_name_str);
});
statements.push(
syn::parse(
quote! {
::leptos::prefetch_lazy_fn_on_server(#unique_name_str);
}
.into(),
)
.unwrap(),
);
statements.extend(old_statements);
quote! { #fun }
}

View File

@@ -159,27 +159,25 @@ struct PropOpt {
pub attrs: bool,
}
struct TypedBuilderOpts<'a> {
struct TypedBuilderOpts {
default: bool,
default_with_value: Option<syn::Expr>,
strip_option: bool,
into: bool,
ty: &'a Type,
}
impl<'a> TypedBuilderOpts<'a> {
pub fn from_opts(opts: &PropOpt, ty: &'a Type) -> Self {
impl TypedBuilderOpts {
pub fn from_opts(opts: &PropOpt, is_ty_option: bool) -> Self {
Self {
default: opts.optional || opts.optional_no_strip || opts.attrs,
default_with_value: opts.default.clone(),
strip_option: opts.strip_option || opts.optional && is_option(ty),
strip_option: opts.strip_option || opts.optional && is_ty_option,
into: opts.into,
ty,
}
}
}
impl ToTokens for TypedBuilderOpts<'_> {
impl ToTokens for TypedBuilderOpts {
fn to_tokens(&self, tokens: &mut TokenStream) {
let default = if let Some(v) = &self.default_with_value {
let v = v.to_token_stream().to_string();
@@ -190,29 +188,14 @@ impl ToTokens for TypedBuilderOpts<'_> {
quote! {}
};
// If self.strip_option && self.into, then the strip_option will be represented as part of the transform closure.
let strip_option = if self.strip_option && !self.into {
let strip_option = if self.strip_option {
quote! { strip_option, }
} else {
quote! {}
};
let into = if self.into {
if !self.strip_option {
let ty = &self.ty;
quote! {
fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> #ty {
value.into_reactive_value()
},
}
} else {
let ty = unwrap_option(self.ty);
quote! {
fn transform<__IntoReactiveValueMarker>(value: impl ::leptos::prelude::IntoReactiveValue<#ty, __IntoReactiveValueMarker>) -> Option<#ty> {
Some(value.into_reactive_value())
},
}
}
quote! { into, }
} else {
quote! {}
};
@@ -244,7 +227,8 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
ty,
} = prop;
let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, ty);
let builder_attrs =
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
let builder_docs = prop_to_doc(prop, PropDocStyle::Inline);

View File

@@ -90,7 +90,7 @@ pub(crate) fn component_to_tokens(
if optional {
optional_props.push(quote! {
props.#name = { #value }.map(::leptos::prelude::IntoReactiveValue::into_reactive_value);
props.#name = { #value }.map(Into::into);
})
} else {
required_props.push(quote! {

View File

@@ -25,8 +25,9 @@ use std::{
use syn::{
punctuated::Pair::{End, Punctuated},
spanned::Spanned,
Expr::{self, Tuple},
ExprArray, ExprLit, ExprPath, ExprRange, Lit, LitStr, RangeLimits, Stmt,
Expr,
Expr::Tuple,
ExprArray, ExprLit, ExprRange, Lit, LitStr, RangeLimits, Stmt,
};
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -1678,7 +1679,7 @@ fn attribute_value(
}
// Keep list alphabetized for binary search
const TYPED_EVENTS: [&str; 127] = [
const TYPED_EVENTS: [&str; 126] = [
"DOMContentLoaded",
"abort",
"afterprint",
@@ -1774,7 +1775,6 @@ const TYPED_EVENTS: [&str; 127] = [
"reset",
"resize",
"scroll",
"scrollend",
"securitypolicyviolation",
"seeked",
"seeking",
@@ -1871,28 +1871,6 @@ pub(crate) fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
}
}
pub(crate) fn full_path_from_tag_name(tag_name: &NodeName) -> Option<ExprPath> {
match tag_name {
NodeName::Path(path) => Some(path.clone()),
NodeName::Block(_) => {
let span = tag_name.span();
proc_macro_error2::emit_error!(
span,
"blocks not allowed in tag-name position"
);
None
}
_ => {
let span = tag_name.span();
proc_macro_error2::emit_error!(
span,
"punctuated names not allowed in slots"
);
None
}
}
}
pub(crate) fn directive_call_from_attribute_node(
attr: &KeyedAttribute,
directive_name: &str,

View File

@@ -1,6 +1,6 @@
use super::{
component_builder::maybe_optimised_component_children,
convert_to_snake_case, full_path_from_tag_name,
convert_to_snake_case, ident_from_tag_name,
};
use crate::view::{fragment_to_tokens, utils::filter_prefixed_attrs, TagType};
use proc_macro2::{Ident, TokenStream, TokenTree};
@@ -24,7 +24,7 @@ pub(crate) fn slot_to_tokens(
node.name().to_string()
});
let component_path = full_path_from_tag_name(node.name());
let component_name = ident_from_tag_name(node.name());
let Some(parent_slots) = parent_slots else {
proc_macro_error2::emit_error!(
@@ -190,7 +190,7 @@ pub(crate) fn slot_to_tokens(
let slot = quote_spanned! {node.span()=>
{
let slot = #component_path::builder()
let slot = #component_name::builder()
#(#props)*
#(#slots)*
#children

View File

@@ -120,124 +120,6 @@ fn returns_static_lifetime() {
}
}
#[cfg(not(feature = "nightly"))]
#[component]
pub fn IntoReactiveValueTestComponentSignal(
#[prop(into)] arg1: Signal<String>,
#[prop(into)] arg2: Signal<String>,
#[prop(into)] arg3: Signal<String>,
#[prop(into)] arg4: Signal<usize>,
#[prop(into)] arg5: Signal<usize>,
#[prop(into)] arg6: Signal<usize>,
#[prop(into)] arg7: Signal<Option<usize>>,
#[prop(into)] arg8: ArcSignal<String>,
#[prop(into)] arg9: ArcSignal<String>,
#[prop(into)] arg10: ArcSignal<String>,
#[prop(into)] arg11: ArcSignal<usize>,
#[prop(into)] arg12: ArcSignal<usize>,
#[prop(into)] arg13: ArcSignal<usize>,
#[prop(into)] arg14: ArcSignal<Option<usize>>,
// Optionals:
#[prop(into, optional)] arg15: Option<Signal<usize>>,
#[prop(into, optional)] arg16_purposely_omitted: Option<Signal<usize>>,
#[prop(into, optional)] arg17: Option<Signal<usize>>,
#[prop(into, strip_option)] arg18: Option<Signal<usize>>,
) -> impl IntoView {
move || {
view! {
<div>
<p>{arg1.get()}</p>
<p>{arg2.get()}</p>
<p>{arg3.get()}</p>
<p>{arg4.get()}</p>
<p>{arg5.get()}</p>
<p>{arg6.get()}</p>
<p>{arg7.get()}</p>
<p>{arg8.get()}</p>
<p>{arg9.get()}</p>
<p>{arg10.get()}</p>
<p>{arg11.get()}</p>
<p>{arg12.get()}</p>
<p>{arg13.get()}</p>
<p>{arg14.get()}</p>
<p>{arg15.get()}</p>
<p>{arg16_purposely_omitted.get()}</p>
<p>{arg17.get()}</p>
<p>{arg18.get()}</p>
</div>
}
}
}
#[component]
pub fn IntoReactiveValueTestComponentCallback(
#[prop(into)] arg1: Callback<(), String>,
#[prop(into)] arg2: Callback<usize, String>,
#[prop(into)] arg3: Callback<(usize,), String>,
#[prop(into)] arg4: Callback<(usize, String), String>,
#[prop(into)] arg5: UnsyncCallback<(), String>,
#[prop(into)] arg6: UnsyncCallback<usize, String>,
#[prop(into)] arg7: UnsyncCallback<(usize,), String>,
#[prop(into)] arg8: UnsyncCallback<(usize, String), String>,
) -> impl IntoView {
move || {
view! {
<div>
<p>{arg1.run(())}</p>
<p>{arg2.run(1)}</p>
<p>{arg3.run((2,))}</p>
<p>{arg4.run((3, "three".into()))}</p>
<p>{arg5.run(())}</p>
<p>{arg6.run(1)}</p>
<p>{arg7.run((2,))}</p>
<p>{arg8.run((3, "three".into()))}</p>
</div>
}
}
}
#[cfg(not(feature = "nightly"))]
#[test]
fn test_into_reactive_value_signal() {
let _ = view! {
<IntoReactiveValueTestComponentSignal
arg1=move || "I was a reactive closure!"
arg2="I was a basic str!"
arg3=Signal::stored("I was already a signal!")
arg4=move || 2
arg5=3
arg6=Signal::stored(4)
arg7=|| 2
arg8=move || "I was a reactive closure!"
arg9="I was a basic str!"
arg10=ArcSignal::stored("I was already a signal!".to_string())
arg11=move || 2
arg12=3
arg13=ArcSignal::stored(4)
arg14=|| 2
arg15=|| 2
nostrip:arg17=Some(|| 2)
arg18=|| 2
/>
};
}
#[test]
fn test_into_reactive_value_callback() {
let _ = view! {
<IntoReactiveValueTestComponentCallback
arg1=|| "I was a callback static str!"
arg2=|_n| "I was a callback static str!"
arg3=|(_n,)| "I was a callback static str!"
arg4=|(_n, _s)| "I was a callback static str!"
arg5=|| "I was a callback static str!"
arg6=|_n| "I was a callback static str!"
arg7=|(_n,)| "I was a callback static str!"
arg8=|(_n, _s)| "I was a callback static str!"
/>
};
}
// an attempt to catch unhygienic macros regression
mod macro_hygiene {
// To ensure no relative module path to leptos inside macros.
@@ -270,7 +152,12 @@ mod macro_hygiene {
#[component]
fn Component() -> impl IntoView {
view! { <div>{().into_any()} {()}</div> }
view! {
<div>
{().into_any()}
{()}
</div>
}
}
}
}

View File

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

View File

@@ -188,39 +188,6 @@ where
}
}
#[cfg(debug_assertions)]
thread_local! {
static RESOURCE_SOURCE_SIGNAL_ACTIVE: AtomicBool = const { AtomicBool::new(false) };
}
#[cfg(debug_assertions)]
/// Returns whether the current thread is currently running a resource source signal.
pub fn in_resource_source_signal() -> bool {
RESOURCE_SOURCE_SIGNAL_ACTIVE
.with(|scope| scope.load(std::sync::atomic::Ordering::Relaxed))
}
/// Set a static to true whilst running the given function.
/// [`is_in_effect_scope`] will return true whilst the function is running.
fn run_in_resource_source_signal<T>(fun: impl FnOnce() -> T) -> T {
#[cfg(debug_assertions)]
{
// For the theoretical nested case, set back to initial value rather than false:
let initial = RESOURCE_SOURCE_SIGNAL_ACTIVE.with(|scope| {
scope.swap(true, std::sync::atomic::Ordering::Relaxed)
});
let result = fun();
RESOURCE_SOURCE_SIGNAL_ACTIVE.with(|scope| {
scope.store(initial, std::sync::atomic::Ordering::Relaxed)
});
result
}
#[cfg(not(debug_assertions))]
{
fun()
}
}
impl<T, Ser> ReadUntracked for ArcResource<T, Ser>
where
T: 'static,
@@ -235,9 +202,7 @@ where
computed::suspense::SuspenseContext, effect::in_effect_scope,
owner::use_context,
};
if !in_effect_scope()
&& !in_resource_source_signal()
&& use_context::<SuspenseContext>().is_none()
if !in_effect_scope() && use_context::<SuspenseContext>().is_none()
{
let location = std::panic::Location::caller();
reactive_graph::log_warning(format_args!(
@@ -306,7 +271,7 @@ where
let refetch = ArcRwSignal::new(0);
let source = ArcMemo::new({
let refetch = refetch.clone();
move |_| (refetch.get(), run_in_resource_source_signal(&source))
move |_| (refetch.get(), source())
});
let fun = {
let source = source.clone();
@@ -944,9 +909,7 @@ where
computed::suspense::SuspenseContext, effect::in_effect_scope,
owner::use_context,
};
if !in_effect_scope()
&& !in_resource_source_signal()
&& use_context::<SuspenseContext>().is_none()
if !in_effect_scope() && use_context::<SuspenseContext>().is_none()
{
let location = std::panic::Location::caller();
reactive_graph::log_warning(format_args!(

View File

@@ -15,18 +15,19 @@
}
},
"node_modules/@playwright/test": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.56.1"
"playwright": "1.44.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
"node": ">=16"
}
},
"node_modules/@types/node": {
@@ -45,6 +46,7 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -54,33 +56,35 @@
}
},
"node_modules/playwright": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.56.1"
"playwright-core": "1.44.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
"node": ">=16"
}
},
"node_modules/typescript": {
@@ -107,12 +111,12 @@
},
"dependencies": {
"@playwright/test": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
"requires": {
"playwright": "1.56.1"
"playwright": "1.44.1"
}
},
"@types/node": {
@@ -132,19 +136,19 @@
"optional": true
},
"playwright": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
"playwright-core": "1.56.1"
"playwright-core": "1.44.1"
}
},
"playwright-core": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true
},
"typescript": {

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.2.11"
version = "0.2.7"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -27,9 +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 }
paste = { workspace = true, default-features = true }
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
web-sys = { version = "0.3.77", features = ["console"] }
@@ -41,7 +39,6 @@ tokio = { features = [
], workspace = true, default-features = true }
tokio-test = { workspace = true, default-features = true }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
typed-builder.workspace = true
[build-dependencies]
rustc_version = { workspace = true, default-features = true }
@@ -54,7 +51,6 @@ 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

@@ -110,12 +110,10 @@ fn effect_base() -> (Receiver, Owner, Arc<RwLock<EffectInner>>) {
(rx, owner, inner)
}
#[cfg(debug_assertions)]
thread_local! {
static EFFECT_SCOPE_ACTIVE: AtomicBool = const { AtomicBool::new(false) };
}
#[cfg(debug_assertions)]
/// Returns whether the current thread is currently running an effect.
pub fn in_effect_scope() -> bool {
EFFECT_SCOPE_ACTIVE
@@ -125,22 +123,14 @@ pub fn in_effect_scope() -> bool {
/// Set a static to true whilst running the given function.
/// [`is_in_effect_scope`] will return true whilst the function is running.
fn run_in_effect_scope<T>(fun: impl FnOnce() -> T) -> T {
#[cfg(debug_assertions)]
{
// For the theoretical nested case, set back to initial value rather than false:
let initial = EFFECT_SCOPE_ACTIVE.with(|scope| {
scope.swap(true, std::sync::atomic::Ordering::Relaxed)
});
let result = fun();
EFFECT_SCOPE_ACTIVE.with(|scope| {
scope.store(initial, std::sync::atomic::Ordering::Relaxed)
});
result
}
#[cfg(not(debug_assertions))]
{
fun()
}
// For the theoretical nested case, set back to initial value rather than false:
let initial = EFFECT_SCOPE_ACTIVE
.with(|scope| scope.swap(true, std::sync::atomic::Ordering::Relaxed));
let result = fun();
EFFECT_SCOPE_ACTIVE.with(|scope| {
scope.store(initial, std::sync::atomic::Ordering::Relaxed)
});
result
}
impl<S> Effect<S>

View File

@@ -65,7 +65,6 @@ impl Dispose for ImmediateEffect {
impl ImmediateEffect {
/// Creates a new effect which runs immediately, then again as soon as any tracked signal changes.
/// (Unless [batch] is used.)
///
/// NOTE: this requires a `Fn` function because it might recurse.
/// Use [Self::new_mut] to pass a `FnMut` function, it'll panic on recursion.
@@ -83,7 +82,6 @@ impl ImmediateEffect {
Self { inner: Some(inner) }
}
/// Creates a new effect which runs immediately, then again as soon as any tracked signal changes.
/// (Unless [batch] is used.)
///
/// # Panics
/// Panics on recursion or if triggered in parallel. Also see [Self::new]
@@ -95,10 +93,8 @@ impl ImmediateEffect {
Self::new(move || fun.try_lock().expect(MSG)())
}
/// Creates a new effect which runs immediately, then again as soon as any tracked signal changes.
/// (Unless [batch] is used.)
///
/// NOTE: this requires a `Fn` function because it might recurse.
/// Use [Self::new_mut_scoped] to pass a `FnMut` function, it'll panic on recursion.
/// NOTE: this effect is automatically cleaned up when the current owner is cleared or disposed.
#[track_caller]
pub fn new_scoped(fun: impl Fn() + Send + Sync + 'static) {
@@ -106,19 +102,6 @@ impl ImmediateEffect {
on_cleanup(move || effect.dispose());
}
/// Creates a new effect which runs immediately, then again as soon as any tracked signal changes.
/// (Unless [batch] is used.)
///
/// NOTE: this effect is automatically cleaned up when the current owner is cleared or disposed.
///
/// # Panics
/// Panics on recursion or if triggered in parallel. Also see [Self::new_scoped]
#[track_caller]
pub fn new_mut_scoped(fun: impl FnMut() + Send + Sync + 'static) {
let effect = Self::new_mut(fun);
on_cleanup(move || effect.dispose());
}
/// Creates a new effect which runs immediately, then again as soon as any tracked signal changes.
///
@@ -147,41 +130,6 @@ impl DefinedAt for ImmediateEffect {
}
}
/// Defers any [ImmediateEffect]s from running until the end of the function.
///
/// NOTE: this affects only [ImmediateEffect]s, not other effects.
///
/// NOTE: this is rarely needed, but it is useful for example when multiple signals
/// need to be updated atomically (for example a double-bound signal tree).
pub fn batch<T>(f: impl FnOnce() -> T) -> T {
struct ExecuteOnDrop;
impl Drop for ExecuteOnDrop {
fn drop(&mut self) {
let effects = {
let mut batch = inner::BATCH.write().or_poisoned();
batch.take().unwrap().into_inner().expect("lock poisoned")
};
// TODO: Should we skip the effects if it's panicking?
for effect in effects {
effect.update_if_necessary();
}
}
}
let mut execute_on_drop = None;
{
let mut batch = inner::BATCH.write().or_poisoned();
if batch.is_none() {
execute_on_drop = Some(ExecuteOnDrop);
} else {
// Nested batching has no effect.
}
*batch = Some(batch.take().unwrap_or_default());
}
let ret = f();
drop(execute_on_drop);
ret
}
mod inner {
use crate::{
graph::{
@@ -192,7 +140,6 @@ mod inner {
owner::Owner,
traits::DefinedAt,
};
use indexmap::IndexSet;
use or_poisoned::OrPoisoned;
use std::{
panic::Location,
@@ -200,11 +147,6 @@ mod inner {
thread::{self, ThreadId},
};
/// Only the [super::batch] function ever writes to the outer RwLock.
/// While the effects will write to the inner one.
pub(super) static BATCH: RwLock<Option<RwLock<IndexSet<AnySubscriber>>>> =
RwLock::new(None);
/// Handles subscription logic for effects.
///
/// To handle parallelism and recursion we assign ordered (1..) ids to each run.
@@ -260,8 +202,6 @@ mod inner {
fun: impl Fn() + Send + Sync + 'static,
) -> Arc<RwLock<EffectInner>> {
let owner = Owner::new();
#[cfg(any(debug_assertions, leptos_debuginfo))]
let defined_at = Location::caller();
Arc::new_cyclic(|weak| {
let any_subscriber = AnySubscriber(
@@ -271,7 +211,7 @@ mod inner {
RwLock::new(EffectInner {
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at,
defined_at: Location::caller(),
owner,
state: ReactiveNodeState::Dirty,
run_count_start: 0,
@@ -320,17 +260,6 @@ mod inner {
ReactiveNodeState::Dirty => true,
};
{
if let Some(batch) = &*BATCH.read().or_poisoned() {
let mut batch = batch.write().or_poisoned();
let subscriber =
self.read().or_poisoned().any_subscriber.clone();
batch.insert(subscriber);
return needs_update;
}
}
if needs_update {
let mut guard = self.write().or_poisoned();

View File

@@ -9,8 +9,6 @@ use crate::{
};
use futures::StreamExt;
use or_poisoned::OrPoisoned;
#[cfg(feature = "subsecond")]
use std::sync::Mutex;
use std::{
fmt::Debug,
future::{Future, IntoFuture},
@@ -51,39 +49,13 @@ 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 {
#[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,
)
Self::new_with_value_erased(Box::new(fun), None)
}
/// Creates a new render effect with an initial value.
@@ -91,30 +63,7 @@ where
fun: impl FnMut(Option<T>) -> T + 'static,
initial_value: Option<T>,
) -> Self {
#[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,
)
Self::new_with_value_erased(Box::new(fun), initial_value)
}
/// Creates a new render effect, which immediately runs `fun`.
@@ -122,11 +71,6 @@ 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()),
@@ -135,13 +79,8 @@ where
}
fn new_with_value_erased(
#[allow(unused_mut)] mut fun: Box<dyn FnMut(Option<T>) -> T + 'static>,
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)
@@ -165,56 +104,12 @@ where
let _ = initial_value;
let _ = owner;
let _ = &mut rx;
let _ = fun;
let _ = &mut 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))),
);
@@ -335,11 +230,6 @@ 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,67 +0,0 @@
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerBaseCase;
/// A helper trait that works like `Into<T>` but uses a marker generic
/// to allow more `From` implementations than would be allowed with just `Into<T>`.
pub trait IntoReactiveValue<T, M> {
/// Converts `self` into a `T`.
fn into_reactive_value(self) -> T;
}
// The base case, which allows anything which implements .into() to work:
impl<T, I> IntoReactiveValue<T, __IntoReactiveValueMarkerBaseCase> for I
where
I: Into<T>,
{
fn into_reactive_value(self) -> T {
self.into()
}
}
#[cfg(test)]
mod tests {
use crate::{
into_reactive_value::IntoReactiveValue,
owner::{LocalStorage, Owner},
traits::GetUntracked,
wrappers::read::Signal,
};
use typed_builder::TypedBuilder;
#[test]
fn test_into_signal_compiles() {
let owner = Owner::new();
owner.set();
#[cfg(not(feature = "nightly"))]
let _: Signal<usize> = (|| 2).into_reactive_value();
let _: Signal<usize, LocalStorage> = 2.into_reactive_value();
#[cfg(not(feature = "nightly"))]
let _: Signal<usize, LocalStorage> = (|| 2).into_reactive_value();
let _: Signal<String> = "str".into_reactive_value();
let _: Signal<String, LocalStorage> = "str".into_reactive_value();
#[derive(TypedBuilder)]
struct Foo {
#[builder(setter(
fn transform<M>(value: impl IntoReactiveValue<Signal<usize>, M>) {
value.into_reactive_value()
}
))]
sig: Signal<usize>,
}
assert_eq!(Foo::builder().sig(2).build().sig.get_untracked(), 2);
#[cfg(not(feature = "nightly"))]
assert_eq!(Foo::builder().sig(|| 2).build().sig.get_untracked(), 2);
assert_eq!(
Foo::builder()
.sig(Signal::stored(2))
.build()
.sig
.get_untracked(),
2
);
}
}

View File

@@ -90,12 +90,6 @@ pub mod traits;
pub mod transition;
pub mod wrappers;
mod into_reactive_value;
pub use into_reactive_value::*;
/// A standard way to wrap functions and closures to pass them to components.
pub mod callback;
use computed::ScopedFuture;
#[cfg(all(feature = "nightly", rustc_nightly))]
@@ -103,9 +97,7 @@ mod nightly;
/// Reexports frequently-used traits.
pub mod prelude {
pub use crate::{
into_reactive_value::IntoReactiveValue, owner::FromLocal, traits::*,
};
pub use crate::{owner::FromLocal, traits::*};
}
// TODO remove this, it's just useful while developing

View File

@@ -209,25 +209,6 @@ impl Owner {
this
}
/// Returns the parent of this `Owner`, if any.
///
/// None when:
/// - This is a root owner
/// - The parent has been dropped
pub fn parent(&self) -> Option<Owner> {
self.inner
.read()
.or_poisoned()
.parent
.as_ref()
.and_then(|p| p.upgrade())
.map(|inner| Owner {
inner,
#[cfg(feature = "hydration")]
shared_context: self.shared_context.clone(),
})
}
/// Creates a new `Owner` that is the child of the current `Owner`, if any.
pub fn child(&self) -> Self {
let parent = Some(Arc::downgrade(&self.inner));
@@ -340,8 +321,6 @@ impl Owner {
}
/// Removes this from its state as the thread-local owner and drops it.
/// If there are other holders of this owner, it may not cleanup, if always cleaning up is required,
/// see [`Owner::unset_with_forced_cleanup`].
pub fn unset(self) {
OWNER.with_borrow_mut(|owner| {
if owner.as_ref().and_then(|n| n.upgrade()) == Some(self) {
@@ -350,23 +329,6 @@ impl Owner {
})
}
/// Removes this from its state as the thread-local owner and drops it.
/// Unlike [`Owner::unset`], this will always run cleanup on this owner,
/// even if there are other holders of this owner.
pub fn unset_with_forced_cleanup(self) {
OWNER.with_borrow_mut(|owner| {
if owner
.as_ref()
.and_then(|n| n.upgrade())
.map(|o| o == self)
.unwrap_or(false)
{
mem::take(owner);
}
});
self.cleanup();
}
/// Returns the current [`SharedContext`], if any.
#[cfg(feature = "hydration")]
pub fn current_shared_context(

View File

@@ -324,22 +324,6 @@ pub mod read {
}
}
impl<S> From<&'static str> for ArcSignal<String, S>
where
S: Storage<&'static str> + Storage<String>,
{
#[track_caller]
fn from(value: &'static str) -> Self {
Self {
inner: SignalTypes::Stored(ArcStoredValue::new(
value.to_string(),
)),
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
}
impl<T, S> DefinedAt for ArcSignal<T, S>
where
S: Storage<T>,
@@ -1065,13 +1049,6 @@ pub mod read {
}
}
impl From<Signal<&'static str, LocalStorage>> for Signal<String, LocalStorage> {
#[track_caller]
fn from(value: Signal<&'static str, LocalStorage>) -> Self {
Signal::derive_local(move || value.read().to_string())
}
}
impl From<Signal<&'static str>> for Signal<String, LocalStorage> {
#[track_caller]
fn from(value: Signal<&'static str>) -> Self {
@@ -1100,15 +1077,6 @@ pub mod read {
}
}
impl From<Signal<Option<&'static str>, LocalStorage>>
for Signal<Option<String>, LocalStorage>
{
#[track_caller]
fn from(value: Signal<Option<&'static str>, LocalStorage>) -> Self {
Signal::derive_local(move || value.read().map(str::to_string))
}
}
impl From<Signal<Option<&'static str>>>
for Signal<Option<String>, LocalStorage>
{
@@ -1118,192 +1086,6 @@ pub mod read {
}
}
#[cfg(not(feature = "nightly"))]
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerSignalFromReactiveClosure;
#[cfg(not(feature = "nightly"))]
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerSignalStrOutputToString;
#[cfg(not(feature = "nightly"))]
#[doc(hidden)]
pub struct __IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways;
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
Signal<T, SyncStorage>,
__IntoReactiveValueMarkerSignalFromReactiveClosure,
> for F
where
T: Send + Sync + 'static,
F: Fn() -> T + Send + Sync + 'static,
{
fn into_reactive_value(self) -> Signal<T, SyncStorage> {
Signal::derive(self)
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
ArcSignal<T, SyncStorage>,
__IntoReactiveValueMarkerSignalFromReactiveClosure,
> for F
where
T: Send + Sync + 'static,
F: Fn() -> T + Send + Sync + 'static,
{
fn into_reactive_value(self) -> ArcSignal<T, SyncStorage> {
ArcSignal::derive(self)
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
Signal<T, LocalStorage>,
__IntoReactiveValueMarkerSignalFromReactiveClosure,
> for F
where
T: 'static,
F: Fn() -> T + 'static,
{
fn into_reactive_value(self) -> Signal<T, LocalStorage> {
Signal::derive_local(self)
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
ArcSignal<T, LocalStorage>,
__IntoReactiveValueMarkerSignalFromReactiveClosure,
> for F
where
T: 'static,
F: Fn() -> T + 'static,
{
fn into_reactive_value(self) -> ArcSignal<T, LocalStorage> {
ArcSignal::derive_local(self)
}
}
#[cfg(not(feature = "nightly"))]
impl<F>
crate::IntoReactiveValue<
Signal<String, SyncStorage>,
__IntoReactiveValueMarkerSignalStrOutputToString,
> for F
where
F: Fn() -> &'static str + Send + Sync + 'static,
{
fn into_reactive_value(self) -> Signal<String, SyncStorage> {
Signal::derive(move || self().to_string())
}
}
#[cfg(not(feature = "nightly"))]
impl<F>
crate::IntoReactiveValue<
ArcSignal<String, SyncStorage>,
__IntoReactiveValueMarkerSignalStrOutputToString,
> for F
where
F: Fn() -> &'static str + Send + Sync + 'static,
{
fn into_reactive_value(self) -> ArcSignal<String, SyncStorage> {
ArcSignal::derive(move || self().to_string())
}
}
#[cfg(not(feature = "nightly"))]
impl<F>
crate::IntoReactiveValue<
Signal<String, LocalStorage>,
__IntoReactiveValueMarkerSignalStrOutputToString,
> for F
where
F: Fn() -> &'static str + 'static,
{
fn into_reactive_value(self) -> Signal<String, LocalStorage> {
Signal::derive_local(move || self().to_string())
}
}
#[cfg(not(feature = "nightly"))]
impl<F>
crate::IntoReactiveValue<
ArcSignal<String, LocalStorage>,
__IntoReactiveValueMarkerSignalStrOutputToString,
> for F
where
F: Fn() -> &'static str + 'static,
{
fn into_reactive_value(self) -> ArcSignal<String, LocalStorage> {
ArcSignal::derive_local(move || self().to_string())
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
Signal<Option<T>, SyncStorage>,
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
> for F
where
T: Send + Sync + 'static,
F: Fn() -> T + Send + Sync + 'static,
{
fn into_reactive_value(self) -> Signal<Option<T>, SyncStorage> {
Signal::derive(move || Some(self()))
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
ArcSignal<Option<T>, SyncStorage>,
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
> for F
where
T: Send + Sync + 'static,
F: Fn() -> T + Send + Sync + 'static,
{
fn into_reactive_value(self) -> ArcSignal<Option<T>, SyncStorage> {
ArcSignal::derive(move || Some(self()))
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
Signal<Option<T>, LocalStorage>,
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
> for F
where
T: 'static,
F: Fn() -> T + 'static,
{
fn into_reactive_value(self) -> Signal<Option<T>, LocalStorage> {
Signal::derive_local(move || Some(self()))
}
}
#[cfg(not(feature = "nightly"))]
impl<T, F>
crate::IntoReactiveValue<
ArcSignal<Option<T>, LocalStorage>,
__IntoReactiveValueMarkerOptionalSignalFromReactiveClosureAlways,
> for F
where
T: 'static,
F: Fn() -> T + 'static,
{
fn into_reactive_value(self) -> ArcSignal<Option<T>, LocalStorage> {
ArcSignal::derive_local(move || Some(self()))
}
}
#[allow(deprecated)]
impl<T> From<MaybeSignal<T>> for Signal<T>
where
@@ -1760,6 +1542,7 @@ pub mod read {
impl<T, S> ReadUntracked for MaybeProp<T, S>
where
T: Clone,
S: Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>>,
{
type Value = ReadGuard<Option<T>, SignalReadGuard<Option<T>, S>>;

View File

@@ -225,38 +225,3 @@ fn threaded_chaos_effect() {
let values: Vec<_> = signals.iter().map(|s| s.get_untracked()).collect();
println!("FINAL: {values:?}");
}
#[cfg(feature = "effects")]
#[test]
fn test_batch() {
use imports::*;
use reactive_graph::{effect::batch, owner::StoredValue};
let owner = Owner::new();
owner.set();
let a = RwSignal::new(0);
let b = RwSignal::new(0);
let values = StoredValue::new(Vec::new());
ImmediateEffect::new_scoped(move || {
println!("{} = {}", a.get(), b.get());
values.write_value().push((a.get(), b.get()));
});
a.set(1);
b.set(1);
batch(move || {
a.set(2);
b.set(2);
batch(move || {
a.set(3);
b.set(3);
});
});
assert_eq!(values.get_value(), vec![(0, 0), (1, 0), (1, 1), (3, 3)]);
}

View File

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

View File

@@ -30,8 +30,6 @@ where
defined_at: &'static Location<'static>,
path: Arc<dyn Fn() -> StorePath + Send + Sync>,
get_trigger: Arc<dyn Fn(StorePath) -> StoreFieldTrigger + Send + Sync>,
get_trigger_unkeyed:
Arc<dyn Fn(StorePath) -> StoreFieldTrigger + Send + Sync>,
read: Arc<dyn Fn() -> Option<StoreFieldReader<T>> + Send + Sync>,
pub(crate) write:
Arc<dyn Fn() -> Option<StoreFieldWriter<T>> + Send + Sync>,
@@ -105,10 +103,6 @@ impl<T> StoreField for ArcField<T> {
(self.get_trigger)(path)
}
fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
(self.get_trigger_unkeyed)(path)
}
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
(self.path)()
}
@@ -138,9 +132,6 @@ where
defined_at: Location::caller(),
path: Arc::new(move || value.path().into_iter().collect()),
get_trigger: Arc::new(move |path| value.get_trigger(path)),
get_trigger_unkeyed: Arc::new(move |path| {
value.get_trigger_unkeyed(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()),
@@ -167,10 +158,6 @@ where
let value = value.clone();
move |path| value.get_trigger(path)
}),
get_trigger_unkeyed: Arc::new({
let value = value.clone();
move |path| value.get_trigger_unkeyed(path)
}),
read: Arc::new({
let value = value.clone();
move || value.reader().map(StoreFieldReader::new)
@@ -215,10 +202,6 @@ where
let value = value.clone();
move |path| value.get_trigger(path)
}),
get_trigger_unkeyed: Arc::new({
let value = value.clone();
move |path| value.get_trigger_unkeyed(path)
}),
read: Arc::new({
let value = value.clone();
move || value.reader().map(StoreFieldReader::new)
@@ -262,10 +245,6 @@ where
let value = value.clone();
move |path| value.get_trigger(path)
}),
get_trigger_unkeyed: Arc::new({
let value = value.clone();
move |path| value.get_trigger_unkeyed(path)
}),
read: Arc::new({
let value = value.clone();
move || value.reader().map(StoreFieldReader::new)
@@ -310,10 +289,6 @@ where
let value = value.clone();
move |path| value.get_trigger(path)
}),
get_trigger_unkeyed: Arc::new({
let value = value.clone();
move |path| value.get_trigger_unkeyed(path)
}),
read: Arc::new({
let value = value.clone();
move || value.reader().map(StoreFieldReader::new)
@@ -362,10 +337,6 @@ where
let value = value.clone();
move |path| value.get_trigger(path)
}),
get_trigger_unkeyed: Arc::new({
let value = value.clone();
move |path| value.get_trigger_unkeyed(path)
}),
read: Arc::new({
let value = value.clone();
move || value.reader().map(StoreFieldReader::new)
@@ -397,7 +368,6 @@ impl<T> Clone for ArcField<T> {
defined_at: self.defined_at,
path: self.path.clone(),
get_trigger: Arc::clone(&self.get_trigger),
get_trigger_unkeyed: Arc::clone(&self.get_trigger_unkeyed),
read: Arc::clone(&self.read),
write: Arc::clone(&self.write),
keys: Arc::clone(&self.keys),

View File

@@ -68,11 +68,6 @@ where
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger(path)
}
fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger_unkeyed(path)
}
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner.path()
}

View File

@@ -59,13 +59,6 @@ where
.unwrap_or_default()
}
fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
self.inner
.try_get_value()
.map(|inner| inner.get_trigger_unkeyed(path))
.unwrap_or_default()
}
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
.try_get_value()

View File

@@ -84,10 +84,6 @@ where
self.inner.get_trigger(path)
}
fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger_unkeyed(path)
}
fn reader(&self) -> Option<Self::Reader> {
let inner = self.inner.reader()?;
let index = self.index;
@@ -113,23 +109,6 @@ where
fn keys(&self) -> Option<KeyMap> {
self.inner.keys()
}
fn track_field(&self) {
let mut full_path = self.path().into_iter().collect::<StorePath>();
let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.this.track();
trigger.children.track();
// tracks `this` for all ancestors: i.e., it will track any change that is made
// directly to one of its ancestors, but not a change made to a *child* of an ancestor
// (which would end up with every subfield tracking its own siblings, because they are
// children of its parent)
while !full_path.is_empty() {
full_path.pop();
let inner = self.get_trigger(full_path.clone());
inner.this.track();
}
}
}
impl<Inner, Prev> DefinedAt for AtIndex<Inner, Prev>

View File

@@ -110,10 +110,6 @@ where
self.inner.get_trigger(path)
}
fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger_unkeyed(path)
}
fn reader(&self) -> Option<Self::Reader> {
let inner = self.inner.reader()?;
Some(Mapped::new_with_guard(inner, self.read))
@@ -436,7 +432,7 @@ where
let this = keys
.with_field_keys(
inner.clone(),
|keys| (keys.get(&self.key), vec![]),
|keys| keys.get(&self.key),
|| self.inner.latest_keys(),
)
.flatten()
@@ -448,10 +444,6 @@ where
self.inner.get_trigger(path)
}
fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger_unkeyed(path)
}
fn reader(&self) -> Option<Self::Reader> {
let inner = self.inner.reader()?;
@@ -460,7 +452,7 @@ where
let index = keys
.with_field_keys(
inner_path,
|keys| (keys.get(&self.key), vec![]),
|keys| keys.get(&self.key),
|| self.inner.latest_keys(),
)
.flatten()
@@ -484,7 +476,7 @@ where
let index = keys
.with_field_keys(
inner_path.clone(),
|keys| (keys.get(&self.key), vec![]),
|keys| keys.get(&self.key),
|| self.inner.latest_keys(),
)
.flatten()
@@ -632,7 +624,9 @@ where
let latest = self.latest_keys();
keys.with_field_keys(
inner_path,
|keys| ((), keys.update(latest)),
|keys| {
keys.update(latest);
},
|| self.latest_keys(),
);
}

View File

@@ -364,18 +364,13 @@ where
})
}
fn update(
&mut self,
iter: impl IntoIterator<Item = K>,
) -> Vec<(usize, StorePathSegment)> {
fn update(&mut self, iter: impl IntoIterator<Item = K>) {
let new_keys = iter
.into_iter()
.enumerate()
.map(|(idx, key)| (key, idx))
.collect::<FxHashMap<K, usize>>();
let mut index_keys = Vec::with_capacity(new_keys.len());
// remove old keys and recycle the slots
self.keys.retain(|key, old_entry| match new_keys.get(key) {
Some(idx) => {
@@ -390,17 +385,14 @@ where
// add new keys
for (key, idx) in new_keys {
match self.keys.get(&key) {
Some((segment, idx)) => index_keys.push((*idx, *segment)),
None => {
let path = self.next_key();
self.keys.insert(key, (path, idx));
index_keys.push((idx, path));
}
// the suggestion doesn't compile because we need &mut for self.next_key(),
// and we don't want to call that until after the check
#[allow(clippy::map_entry)]
if !self.keys.contains_key(&key) {
let path = self.next_key();
self.keys.insert(key, (path, idx));
}
}
index_keys
}
}
@@ -423,20 +415,14 @@ type HashMap<K, V> = send_wrapper::SendWrapper<
/// A map of the keys for a keyed subfield.
#[derive(Clone)]
pub struct KeyMap(
HashMap<StorePath, Box<dyn Any + Send + Sync>>,
HashMap<(StorePath, usize), StorePathSegment>,
);
pub struct KeyMap(HashMap<StorePath, Box<dyn Any + Send + Sync>>);
impl Default for KeyMap {
fn default() -> Self {
#[cfg(not(target_arch = "wasm32"))]
return Self(Default::default(), Default::default());
return Self(Default::default());
#[cfg(target_arch = "wasm32")]
return Self(
send_wrapper::SendWrapper::new(Default::default()),
send_wrapper::SendWrapper::new(Default::default()),
);
return Self(send_wrapper::SendWrapper::new(Default::default()));
}
}
@@ -444,70 +430,31 @@ impl KeyMap {
fn with_field_keys<K, T>(
&self,
path: StorePath,
fun: impl FnOnce(&mut FieldKeys<K>) -> (T, Vec<(usize, StorePathSegment)>),
fun: impl FnOnce(&mut FieldKeys<K>) -> T,
initialize: impl FnOnce() -> Vec<K>,
) -> Option<T>
where
K: Debug + Hash + PartialEq + Eq + Send + Sync + 'static,
{
let initial_keys = initialize();
#[cfg(not(target_arch = "wasm32"))]
let mut entry = self
.0
.entry(path.clone())
.or_insert_with(|| Box::new(FieldKeys::new(initial_keys)));
.entry(path)
.or_insert_with(|| Box::new(FieldKeys::new(initialize())));
#[cfg(target_arch = "wasm32")]
let entry = if !self.0.borrow().contains_key(&path) {
Some(Box::new(FieldKeys::new(initial_keys)))
Some(Box::new(FieldKeys::new(initialize())))
} else {
None
};
#[cfg(target_arch = "wasm32")]
let mut map = self.0.borrow_mut();
#[cfg(target_arch = "wasm32")]
let entry = map.entry(path.clone()).or_insert_with(|| entry.unwrap());
let entry = map.entry(path).or_insert_with(|| entry.unwrap());
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
let (result, new_keys) = fun(entry);
if !new_keys.is_empty() {
for (idx, segment) in new_keys {
#[cfg(not(target_arch = "wasm32"))]
self.1.insert((path.clone(), idx), segment);
#[cfg(target_arch = "wasm32")]
self.1.borrow_mut().insert((path.clone(), idx), segment);
}
}
Some(result)
}
fn contains_key(&self, key: &StorePath) -> bool {
#[cfg(not(target_arch = "wasm32"))]
{
self.0.contains_key(key)
}
#[cfg(target_arch = "wasm32")]
{
self.0.borrow_mut().contains_key(key)
}
}
fn get_key_for_index(
&self,
key: &(StorePath, usize),
) -> Option<StorePathSegment> {
#[cfg(not(target_arch = "wasm32"))]
{
self.1.get(key).as_deref().copied()
}
#[cfg(target_arch = "wasm32")]
{
self.1.borrow().get(key).as_deref().copied()
}
Some(fun(entry))
}
}
@@ -885,30 +832,6 @@ mod tests {
}
}
#[derive(Debug, Clone, Store, Patch, Default)]
struct Foo {
id: i32,
bar: Bar,
}
#[derive(Debug, Clone, Store, Patch, Default)]
struct Bar {
bar_signature: i32,
baz: Baz,
}
#[derive(Debug, Clone, Store, Patch, Default)]
struct Baz {
more_data: i32,
baw: Baw,
}
#[derive(Debug, Clone, Store, Patch, Default)]
struct Baw {
more_data: i32,
end: i32,
}
#[tokio::test]
async fn mutating_field_triggers_effect() {
_ = any_spawner::Executor::init_tokio();
@@ -1189,6 +1112,30 @@ mod tests {
_ = any_spawner::Executor::init_tokio();
#[derive(Debug, Clone, Store, Patch, Default)]
struct Foo {
id: i32,
bar: Bar,
}
#[derive(Debug, Clone, Store, Patch, Default)]
struct Bar {
bar_signature: i32,
baz: Baz,
}
#[derive(Debug, Clone, Store, Patch, Default)]
struct Baz {
more_data: i32,
baw: Baw,
}
#[derive(Debug, Clone, Store, Patch, Default)]
struct Baw {
more_data: i32,
end: i32,
}
let store = Store::new(Foo {
id: 42,
bar: Bar {
@@ -1272,107 +1219,4 @@ mod tests {
assert_eq!(more_data_runs.get_value(), 3);
assert_eq!(baz_baw_end_runs.get_value(), 3);
}
#[tokio::test]
async fn changing_parent_notifies_subfield() {
_ = any_spawner::Executor::init_tokio();
let combined_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(Foo {
id: 42,
bar: Bar {
bar_signature: 69,
baz: Baz {
more_data: 9999,
baw: Baw {
more_data: 22,
end: 1112,
},
},
},
});
let tracked_field = store.bar().baz().more_data();
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
// we only track `more`, but this should still be notified
// when its parent fields `bar` or `baz` change
println!("{:?}", *tracked_field.read());
combined_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
tick().await;
store.bar().baz().set(Baz {
more_data: 42,
baw: Baw {
more_data: 11,
end: 31,
},
});
tick().await;
store.bar().set(Bar {
bar_signature: 23,
baz: Baz {
more_data: 32,
baw: Baw {
more_data: 432,
end: 423,
},
},
});
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
}
#[tokio::test]
async fn changing_parent_notifies_unkeyed_child() {
_ = any_spawner::Executor::init_tokio();
let combined_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(data());
let tracked_field = store.todos().at_unkeyed(0);
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
// we only track `more`, but this should still be notified
// when its parent fields `bar` or `baz` change
println!("{:?}", *tracked_field.read());
combined_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
tick().await;
store.todos().write().pop();
tick().await;
store.todos().write().push(Todo {
label: "another one".into(),
completed: false,
});
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
}
}

View File

@@ -35,7 +35,7 @@ where
// don't track the writer for the whole store
writer.untrack();
let mut notify = |path: &StorePath| {
self.triggers_for_path_unkeyed(path.to_owned()).notify();
self.triggers_for_path(path.to_owned()).notify();
};
writer.patch_field(new, &path, &mut notify);
}

View File

@@ -11,15 +11,6 @@ impl IntoIterator for StorePath {
}
}
impl<'a> IntoIterator for &'a StorePath {
type Item = &'a StorePathSegment;
type IntoIter = std::slice::Iter<'a, StorePathSegment>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl From<Vec<StorePathSegment>> for StorePath {
fn from(value: Vec<StorePathSegment>) -> Self {
Self(value)
@@ -27,16 +18,6 @@ impl From<Vec<StorePathSegment>> for StorePath {
}
impl StorePath {
/// Creates a new path.
pub fn new() -> Self {
Self(Vec::new())
}
/// Creates a new path with storage capacity for `capacity` segments.
pub fn with_capacity(capacity: usize) -> Self {
Self(Vec::with_capacity(capacity))
}
/// Adds a new segment to the path.
pub fn push(&mut self, segment: impl Into<StorePathSegment>) {
self.0.push(segment.into());

View File

@@ -26,14 +26,6 @@ pub trait StoreField: Sized {
#[track_caller]
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger;
/// Returns the trigger that tracks access and updates for this field.
///
/// This uses *unkeyed* paths: i.e., if any field in the path is keyed, it will
/// try to look up the key for the item at the index given in the path, rather than
/// the keyed item.
#[track_caller]
fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger;
/// The path of this field (see [`StorePath`]).
#[track_caller]
fn path(&self) -> impl IntoIterator<Item = StorePathSegment>;
@@ -92,26 +84,6 @@ pub trait StoreField: Sized {
triggers
}
/// Returns triggers for the field at the given path, and all parent fields
fn triggers_for_path_unkeyed(&self, path: StorePath) -> Vec<ArcTrigger> {
// see notes on triggers_for_path() for additional comments on implementation
let trigger = self.get_trigger_unkeyed(path.clone());
let mut full_path = path;
let mut triggers = Vec::with_capacity(full_path.len() + 2);
triggers.push(trigger.this.clone());
triggers.push(trigger.children.clone());
while !full_path.is_empty() {
full_path.pop();
let inner = self.get_trigger_unkeyed(full_path.clone());
triggers.push(inner.children.clone());
}
triggers.reverse();
triggers
}
}
impl<T> StoreField for ArcStore<T>
@@ -129,26 +101,6 @@ where
trigger
}
fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
let orig_path = path.clone();
let mut path = StorePath::with_capacity(orig_path.len());
for segment in &orig_path {
let parent_is_keyed = self.keys.contains_key(&path);
if parent_is_keyed {
let key = self
.keys
.get_key_for_index(&(path.clone(), segment.0))
.expect("could not find key for index");
path.push(key);
} else {
path.push(*segment);
}
}
self.get_trigger(path)
}
#[track_caller]
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
iter::empty()
@@ -189,14 +141,6 @@ where
.unwrap_or_default()
}
#[track_caller]
fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
self.inner
.try_get_value()
.map(|n| n.get_trigger_unkeyed(path))
.unwrap_or_default()
}
#[track_caller]
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner

View File

@@ -88,10 +88,6 @@ where
self.inner.get_trigger(path)
}
fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger_unkeyed(path)
}
fn reader(&self) -> Option<Self::Reader> {
let inner = self.inner.reader()?;
Some(Mapped::new_with_guard(inner, self.read))

View File

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

View File

@@ -364,12 +364,6 @@ where
#[derive(Debug)]
pub(crate) struct MatchedRoute(pub String, pub AnyView);
impl MatchedRoute {
fn branch_name(&self) -> String {
format!("{:?}", self.1.as_type_id())
}
}
impl Render for MatchedRoute {
type State = <AnyView as Render>::State;
@@ -420,9 +414,8 @@ impl RenderHtml for MatchedRoute {
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
let branch_name = (mark_branches && escape).then(|| self.branch_name());
if let Some(bn) = &branch_name {
buf.open_branch(bn);
if mark_branches && escape {
buf.open_branch(&self.0);
}
self.1.to_html_with_buf(
buf,
@@ -431,8 +424,8 @@ impl RenderHtml for MatchedRoute {
mark_branches,
extra_attrs,
);
if let Some(bn) = &branch_name {
buf.close_branch(bn);
if mark_branches && escape {
buf.close_branch(&self.0);
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}
@@ -449,9 +442,8 @@ impl RenderHtml for MatchedRoute {
) where
Self: Sized,
{
let branch_name = (mark_branches && escape).then(|| self.branch_name());
if let Some(bn) = &branch_name {
buf.open_branch(bn);
if mark_branches && escape {
buf.open_branch(&self.0);
}
self.1.to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
@@ -460,8 +452,8 @@ impl RenderHtml for MatchedRoute {
mark_branches,
extra_attrs,
);
if let Some(bn) = &branch_name {
buf.close_branch(bn);
if mark_branches && escape {
buf.close_branch(&self.0);
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}

View File

@@ -3,7 +3,7 @@ use crate::{hooks::use_navigate, params::ParamsMap};
use core::fmt;
use futures::channel::oneshot;
use js_sys::{try_iter, Array, JsString};
use leptos::{ev, prelude::*};
use leptos::prelude::*;
use or_poisoned::OrPoisoned;
use reactive_graph::{
signal::ArcRwSignal,
@@ -11,12 +11,13 @@ use reactive_graph::{
};
use std::{
borrow::Cow,
boxed::Box,
string::String,
sync::{Arc, Mutex},
};
use tachys::dom::{document, window};
use wasm_bindgen::{JsCast, JsValue};
use web_sys::UrlSearchParams;
use wasm_bindgen::{closure::Closure, JsCast, JsValue};
use web_sys::{Event, UrlSearchParams};
#[derive(Clone)]
pub struct BrowserUrl {
@@ -115,6 +116,7 @@ impl LocationProvider for BrowserUrl {
}
fn init(&self, base: Option<Cow<'static, str>>) {
let window = window();
let navigate = {
let url = self.url.clone();
let pending = Arc::clone(&self.pending_navigation);
@@ -157,32 +159,37 @@ impl LocationProvider for BrowserUrl {
let handle_anchor_click =
handle_anchor_click(base, Self::parse_with_base, navigate);
let click_handle = window_event_listener(ev::click, move |ev| {
let closure = Closure::wrap(Box::new(move |ev: Event| {
if let Err(e) = handle_anchor_click(ev) {
#[cfg(feature = "tracing")]
tracing::error!("{e:?}");
#[cfg(not(feature = "tracing"))]
web_sys::console::error_1(&e);
}
});
}) as Box<dyn FnMut(Event)>)
.into_js_value();
window
.add_event_listener_with_callback(
"click",
closure.as_ref().unchecked_ref(),
)
.expect(
"couldn't add `click` listener to `window` to handle `<a>` \
clicks",
);
// handle popstate event (forward/back navigation)
let popstate_cb = {
let cb = {
let url = self.url.clone();
let path_stack = self.path_stack.clone();
let is_back = self.is_back.clone();
move || match Self::current() {
Ok(new_url) => {
let mut stack = path_stack.write_value();
let stack = path_stack.read_value();
let is_navigating_back = stack.len() == 1
|| (stack.len() >= 2
&& stack.get(stack.len() - 2) == Some(&new_url));
if is_navigating_back {
stack.pop();
}
is_back.set(is_navigating_back);
url.set(new_url);
@@ -195,14 +202,14 @@ impl LocationProvider for BrowserUrl {
}
}
};
let popstate_handle =
window_event_listener(ev::popstate, move |_| popstate_cb());
on_cleanup(|| {
click_handle.remove();
popstate_handle.remove();
});
let closure =
Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
window
.add_event_listener_with_callback(
"popstate",
closure.as_ref().unchecked_ref(),
)
.expect("couldn't add `popstate` listener to `window`");
}
fn ready_to_complete(&self) {

View File

@@ -14,7 +14,7 @@ use send_wrapper::SendWrapper;
use std::{borrow::Cow, future::Future};
use tachys::dom::window;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{HtmlAnchorElement, MouseEvent};
use web_sys::{Event, HtmlAnchorElement, MouseEvent};
mod history;
mod server;
@@ -300,14 +300,15 @@ pub(crate) fn handle_anchor_click<NavFn, NavFut>(
router_base: Option<Cow<'static, str>>,
parse_with_base: fn(&str, &str) -> Result<Url, JsValue>,
navigate: NavFn,
) -> Box<dyn Fn(MouseEvent) -> Result<(), JsValue>>
) -> Box<dyn Fn(Event) -> Result<(), JsValue>>
where
NavFn: Fn(Url, LocationChange) -> NavFut + 'static,
NavFut: Future<Output = ()> + 'static,
{
let router_base = router_base.unwrap_or_default();
Box::new(move |ev: MouseEvent| {
Box::new(move |ev: Event| {
let ev = ev.unchecked_into::<MouseEvent>();
let origin = window().location().origin()?;
if ev.default_prevented()
|| ev.button() != 0
@@ -368,8 +369,8 @@ where
ev.prevent_default();
let to = path_name
+ if url.search.is_empty() { "" } else { "?" }
+ &url.search
+ &url.hash;
+ &Url::unescape(&url.search)
+ &Url::unescape(&url.hash);
let state = Reflect::get(&a, &JsValue::from_str("state"))
.ok()
.and_then(|value| {

View File

@@ -4,6 +4,7 @@ macro_rules! tuples {
($first:ident => $($ty:ident),*) => {
impl<$first, $($ty),*> PossibleRouteMatch for ($first, $($ty,)*)
where
Self: core::fmt::Debug,
$first: PossibleRouteMatch,
$($ty: PossibleRouteMatch),*,
{

View File

@@ -369,7 +369,7 @@ impl ResolvedStaticPath {
eprintln!("{e}");
}
}
owner.unset_with_forced_cleanup();
owner.unset();
}
}
});

View File

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

View File

@@ -7,7 +7,7 @@
use proc_macro::{TokenStream, TokenTree};
use proc_macro2::Span;
use proc_macro_error2::{abort, proc_macro_error, set_dummy};
use quote::{format_ident, quote, ToTokens};
use quote::{quote, ToTokens};
use syn::{
spanned::Spanned, FnArg, Ident, ImplItem, ItemImpl, Path, Type, TypePath,
};
@@ -267,7 +267,10 @@ fn lazy_route_impl(
};
let lazy_view_ident =
Ident::new(&format!("__{ty_name_to_snake}_View"), im.self_ty.span());
let preload_ident = format_ident!("__preload_{lazy_view_ident}");
let preload_lazy_view_ident = Ident::new(
&format!("__preload_{lazy_view_ident}"),
lazy_view_ident.span(),
);
im.items.push(
syn::parse::<ImplItem>(
@@ -277,7 +280,7 @@ fn lazy_route_impl(
// we don't split routes for wasm32 ssr
// but we don't require a `hydrate`/`csr` feature on leptos_router
#[cfg(target_arch = "wasm32")]
#preload_ident().await;
#preload_lazy_view_ident().await;
}
}
.into(),

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.8"
version = "0.8.7"
rust-version.workspace = true
edition.workspace = true
@@ -64,7 +64,6 @@ http-body-util = { optional = true, workspace = true, default-features = true }
rkyv = { optional = true, workspace = true, default-features = true }
rmp-serde = { optional = true, workspace = true, default-features = true }
base64 = { workspace = true, default-features = true }
bitcode = { optional = true, workspace = true, default-features = true }
# client
gloo-net = { optional = true, workspace = true, default-features = true }
@@ -127,7 +126,6 @@ cbor = ["dep:ciborium"]
rkyv = ["dep:rkyv"]
msgpack = ["dep:rmp-serde"]
postcard = ["dep:postcard"]
bitcode = ["dep:bitcode"]
default-tls = ["reqwest?/default-tls"]
rustls = ["reqwest?/rustls-tls", "tokio-tungstenite?/rustls"]
reqwest = ["dep:reqwest", "dep:tokio-tungstenite", "dep:tokio"]

View File

@@ -1,49 +0,0 @@
use super::{Patch, Post, Put};
use crate::{ContentType, Decodes, Encodes, Format, FormatType};
use bytes::Bytes;
/// Serializes and deserializes with [`bitcode`].
pub struct BitcodeEncoding;
impl ContentType for BitcodeEncoding {
const CONTENT_TYPE: &'static str = "application/bitcode";
}
impl FormatType for BitcodeEncoding {
const FORMAT_TYPE: Format = Format::Binary;
}
impl<T> Encodes<T> for BitcodeEncoding
where
T: bitcode::Encode,
{
type Error = std::convert::Infallible;
fn encode(value: &T) -> Result<Bytes, Self::Error> {
Ok(Bytes::from(bitcode::encode(value)))
}
}
impl<T> Decodes<T> for BitcodeEncoding
where
T: bitcode::DecodeOwned,
{
type Error = bitcode::Error;
fn decode(bytes: Bytes) -> Result<T, Self::Error> {
bitcode::decode(bytes.as_ref())
}
}
/// Pass arguments and receive responses using `bitcode` in a `POST` request.
pub type Bitcode = Post<BitcodeEncoding>;
/// Pass arguments and receive responses using `bitcode` in the body of a `PATCH` request.
/// **Note**: Browser support for `PATCH` requests without JS/WASM may be poor.
/// Consider using a `POST` request if functionality without JS/WASM is required.
pub type PatchBitcode = Patch<BitcodeEncoding>;
/// Pass arguments and receive responses using `bitcode` in the body of a `PUT` request.
/// **Note**: Browser support for `PUT` requests without JS/WASM may be poor.
/// Consider using a `POST` request if functionality without JS/WASM is required.
pub type PutBitcode = Put<BitcodeEncoding>;

View File

@@ -50,11 +50,6 @@ mod postcard;
#[cfg(feature = "postcard")]
pub use postcard::*;
#[cfg(feature = "bitcode")]
mod bitcode;
#[cfg(feature = "bitcode")]
pub use bitcode::*;
mod patch;
pub use patch::*;
mod post;

View File

@@ -133,8 +133,6 @@ pub use ::bytes as bytes_export;
#[doc(hidden)]
pub use ::http as http_export;
use base64::{engine::general_purpose::STANDARD_NO_PAD, DecodeError, Engine};
#[cfg(feature = "bitcode")]
pub use bitcode;
// re-exported to make it possible to implement a custom Client without adding a separate
// dependency on `bytes`
pub use bytes::Bytes;

View File

@@ -10,8 +10,7 @@
feature = "multipart",
feature = "serde-lite",
feature = "cbor",
feature = "msgpack",
feature = "bitcode",
feature = "msgpack"
))
))]

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.8"
version = "0.8.7"
edition.workspace = true
[dependencies]

View File

@@ -331,7 +331,6 @@ impl ServerFnCall {
enum PathInfo {
Serde,
Rkyv,
Bitcode,
None,
}
@@ -342,12 +341,6 @@ impl ServerFnCall {
Clone, #server_fn_path::rkyv::Archive, #server_fn_path::rkyv::Serialize, #server_fn_path::rkyv::Deserialize
},
),
Some("Bitcode") => (
PathInfo::Bitcode,
quote! {
Clone, #server_fn_path::bitcode::Encode, #server_fn_path::bitcode::Decode
},
),
Some("MultipartFormData")
| Some("Streaming")
| Some("StreamingText") => (PathInfo::None, quote! {}),
@@ -383,7 +376,6 @@ impl ServerFnCall {
#[serde(crate = #serde_path)]
}
}
PathInfo::Bitcode => quote! {},
PathInfo::Rkyv => quote! {},
PathInfo::None => quote! {},
};

View File

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

View File

@@ -205,14 +205,6 @@ where
self.add_any_attr(enterkeyhint(value))
}
/// The `exportparts` attribute enables the sharing of parts of an element's shadow DOM with a containing document.
fn exportparts(
self,
value: V,
) -> <Self as AddAnyAttr>::Output<Attr<Exportparts, V>> {
self.add_any_attr(exportparts(value))
}
/// The `hidden` global attribute is a Boolean attribute indicating that the element is not yet, or is no longer, relevant.
fn hidden(self, value: V) -> <Self as AddAnyAttr>::Output<Attr<Hidden, V>> {
self.add_any_attr(hidden(value))

View File

@@ -57,10 +57,6 @@ where
_style: &mut String,
_inner_html: &mut String,
) {
// If this is a class="..." attribute (not class:name=value), clear previous value
if self.class.should_overwrite() {
class.clear();
}
class.push(' ');
self.class.to_html(class);
}
@@ -160,12 +156,6 @@ pub trait IntoClass: Send {
/// Renders the class to HTML.
fn to_html(self, class: &mut String);
/// Whether this class attribute should overwrite previous class values.
/// Returns `true` for `class="..."` attributes, `false` for `class:name=value` directives.
fn should_overwrite(&self) -> bool {
false
}
/// Renders the class to HTML for a `<template>`.
#[allow(unused)] // it's used with `nightly` feature
fn to_template(class: &mut String) {}
@@ -299,10 +289,6 @@ impl IntoClass for &str {
class.push_str(self);
}
fn should_overwrite(&self) -> bool {
true
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &crate::renderer::types::Element,
@@ -360,10 +346,6 @@ impl IntoClass for Cow<'_, str> {
IntoClass::to_html(&*self, class);
}
fn should_overwrite(&self) -> bool {
true
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &crate::renderer::types::Element,
@@ -421,10 +403,6 @@ impl IntoClass for String {
IntoClass::to_html(self.as_str(), class);
}
fn should_overwrite(&self) -> bool {
true
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &crate::renderer::types::Element,
@@ -482,10 +460,6 @@ impl IntoClass for Arc<str> {
IntoClass::to_html(self.as_ref(), class);
}
fn should_overwrite(&self) -> bool {
true
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &crate::renderer::types::Element,

View File

@@ -47,13 +47,11 @@ pub fn directive<T, P, D>(handler: D, param: P) -> Directive<T, D, P>
where
D: IntoDirective<T, P>,
{
Directive((!cfg!(feature = "ssr")).then(|| {
SendWrapper::new(DirectiveInner {
handler,
param,
t: PhantomData,
})
}))
Directive(Some(SendWrapper::new(DirectiveInner {
handler,
param,
t: PhantomData,
})))
}
/// Custom logic that runs in the browser when the element is created or hydrated.
@@ -153,7 +151,13 @@ where
Directive(inner)
}
fn dry_resolve(&mut self) {}
fn dry_resolve(&mut self) {
// dry_resolve() only runs during SSR, and we should use it to
// synchronously remove and drop the SendWrapper value
// we don't need this value during SSR and leaving it here could drop it
// from a different thread
self.0.take();
}
async fn resolve(self) -> Self::AsyncOutput {
self

View File

@@ -212,7 +212,7 @@ html_self_closing_elements! {
/// The `<img>` HTML element embeds an image into the document.
img HtmlImageElement [alt, attributionsrc, crossorigin, decoding, elementtiming, fetchpriority, height, ismap, loading, referrerpolicy, sizes, src, srcset, usemap, width] true,
/// The `<input>` HTML element is used to create interactive controls for web-based forms in order to accept data from the user; a wide variety of types of input data and control widgets are available, depending on the device and user agent. The `<input>` element is one of the most powerful and complex in all of HTML due to the sheer number of combinations of input types and attributes.
input HtmlInputElement [accept, alt, autocomplete, capture, checked, dirname, disabled, form, formaction, formenctype, formmethod, formnovalidate, formtarget, height, list, max, maxlength, min, minlength, multiple, name, pattern, placeholder, popovertarget, popovertargetaction, readonly, required, size, src, step, r#type, value, width] true,
input HtmlInputElement [accept, alt, autocomplete, capture, checked, disabled, form, formaction, formenctype, formmethod, formnovalidate, formtarget, height, list, max, maxlength, min, minlength, multiple, name, pattern, placeholder, popovertarget, popovertargetaction, readonly, required, size, src, step, r#type, value, width] true,
/// The `<link>` HTML element specifies relationships between the current document and an external resource. This element is most commonly used to link to CSS, but is also used to establish site icons (both "favicon" style icons and icons for the home screen and apps on mobile devices) among other things.
link HtmlLinkElement [r#as, blocking, crossorigin, fetchpriority, href, hreflang, imagesizes, imagesrcset, integrity, media, rel, referrerpolicy, sizes, r#type] true,
/// The `<meta>` HTML element represents Metadata that cannot be represented by other HTML meta-related elements, like base, link, script, style or title.

View File

@@ -317,26 +317,6 @@ where
type State = ElementState<At::State, Ch::State>;
fn rebuild(self, state: &mut Self::State) {
// check whether the tag is the same, for custom elements
// because this is const `false` for all other element types,
// the compiler should be able to optimize it out
if E::TAG.is_empty() {
// see https://github.com/leptos-rs/leptos/issues/4412
let new_tag = self.tag.tag();
// this is not particularly efficient, but it saves us from
// having to keep track of the tag name for every element state
let old_tag = state.el.tag_name();
if new_tag != old_tag {
let mut new_state = self.build();
state.insert_before_this(&mut new_state);
state.unmount();
*state = new_state;
return;
}
}
// rebuild attributes and children for any element
let ElementState {
attrs, children, ..
} = state;

View File

@@ -113,7 +113,7 @@ where
event,
#[cfg(feature = "reactive_graph")]
owner: reactive_graph::owner::Owner::current().unwrap_or_default(),
cb: (!cfg!(feature = "ssr")).then(|| SendWrapper::new(cb)),
cb: Some(SendWrapper::new(cb)),
}
}
@@ -352,7 +352,13 @@ where
}
}
fn dry_resolve(&mut self) {}
fn dry_resolve(&mut self) {
// dry_resolve() only runs during SSR, and we should use it to
// synchronously remove and drop the SendWrapper value
// we don't need this value during SSR and leaving it here could drop it
// from a different thread
self.cb.take();
}
async fn resolve(self) -> Self::AsyncOutput {
self

View File

@@ -22,7 +22,7 @@ where
{
Property {
key,
value: (!cfg!(feature = "ssr")).then(|| SendWrapper::new(value)),
value: Some(SendWrapper::new(value)),
}
}
@@ -115,7 +115,13 @@ where
}
}
fn dry_resolve(&mut self) {}
fn dry_resolve(&mut self) {
// dry_resolve() only runs during SSR, and we should use it to
// synchronously remove and drop the SendWrapper value
// we don't need this value during SSR and leaving it here could drop it
// from a different thread
self.value.take();
}
async fn resolve(self) -> Self::AsyncOutput {
self

Some files were not shown because too many files have changed in this diff Show More