mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 17:11:55 -05:00
Compare commits
2 Commits
0.8.0-beta
...
3606
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1bc89576e | ||
|
|
5aa160dabf |
1
.github/workflows/ci-changed-examples.yml
vendored
1
.github/workflows/ci-changed-examples.yml
vendored
@@ -28,6 +28,5 @@ jobs:
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
erased_mode: ${{ matrix.erased_mode }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: stable
|
||||
|
||||
1
.github/workflows/ci-examples.yml
vendored
1
.github/workflows/ci-examples.yml
vendored
@@ -25,6 +25,5 @@ jobs:
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
erased_mode: ${{ matrix.erased_mode }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: stable
|
||||
|
||||
4
.github/workflows/ci-semver.yml
vendored
4
.github/workflows/ci-semver.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
test:
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
|
||||
name: Run semver check (nightly-2025-03-05)
|
||||
name: Run semver check (nightly-2024-08-01)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Glib
|
||||
@@ -30,4 +30,4 @@ jobs:
|
||||
- name: Semver Checks
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
with:
|
||||
rust-toolchain: nightly-2025-03-05
|
||||
rust-toolchain: nightly-2024-08-01
|
||||
|
||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -25,6 +25,5 @@ jobs:
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
erased_mode: ${{ matrix.erased_mode }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly-2025-03-05
|
||||
toolchain: nightly-2024-08-01
|
||||
|
||||
@@ -50,5 +50,5 @@ jobs:
|
||||
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Create matrix with one item to prevent an empty vector error
|
||||
echo "matrix={\"directory\":[\"NO_CHANGE\"], \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix={\"directory\":[\"NO_CHANGE\"]}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
2
.github/workflows/get-examples-matrix.yml
vendored
2
.github/workflows/get-examples-matrix.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
sed 's/\/$//' |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Example Directories: $examples"
|
||||
echo "matrix={\"directory\":$examples, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
|
||||
- name: Print Location Info
|
||||
run: |
|
||||
echo "Workspace: ${{ github.workspace }}"
|
||||
|
||||
2
.github/workflows/get-leptos-matrix.yml
vendored
2
.github/workflows/get-leptos-matrix.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
sed "s|$(pwd)/||" |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Leptos Directories: $crates"
|
||||
echo "matrix={\"directory\":$crates, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix={\"directory\":$crates}" >> "$GITHUB_OUTPUT"
|
||||
- name: Print Location Info
|
||||
run: |
|
||||
echo "Workspace: ${{ github.workspace }}"
|
||||
|
||||
6
.github/workflows/run-cargo-make-task.yml
vendored
6
.github/workflows/run-cargo-make-task.yml
vendored
@@ -5,9 +5,6 @@ on:
|
||||
directory:
|
||||
required: true
|
||||
type: string
|
||||
erased_mode:
|
||||
required: true
|
||||
type: boolean
|
||||
cargo_make_task:
|
||||
required: true
|
||||
type: string
|
||||
@@ -18,10 +15,9 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
RUSTFLAGS: ${{ inputs.erased_mode && '--cfg erase_components' || '' }}
|
||||
jobs:
|
||||
test:
|
||||
name: "Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }}) (erased_mode: ${{ inputs.erased_mode }})"
|
||||
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
|
||||
844
Cargo.lock
generated
844
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
40
Cargo.toml
40
Cargo.toml
@@ -40,38 +40,38 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.0-beta"
|
||||
version = "0.7.7"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
throw_error = { path = "./any_error/", version = "0.3.0" }
|
||||
throw_error = { path = "./any_error/", version = "0.2.0" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.2.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
|
||||
either_of = { path = "./either_of/", version = "0.1.5" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.3.0" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0" }
|
||||
itertools = "0.14.0"
|
||||
leptos = { path = "./leptos", version = "0.8.0-beta" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.0-beta" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.0-beta" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-beta" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-beta" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.0-beta" }
|
||||
leptos_router = { path = "./router", version = "0.8.0-beta" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.0-beta" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.0-beta" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.0-beta" }
|
||||
leptos = { path = "./leptos", version = "0.7.7" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.7" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.7" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.7" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.7" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.7" }
|
||||
leptos_router = { path = "./router", version = "0.7.7" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.7" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.7" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.7" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.0-beta" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.0-beta" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-beta" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.7" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.7" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.7" }
|
||||
serde_json = "1.0.0"
|
||||
server_fn = { path = "./server_fn", version = "0.8.0-beta" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-beta" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-beta" }
|
||||
tachys = { path = "./tachys", version = "0.2.0-beta" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.7" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.7" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.7" }
|
||||
tachys = { path = "./tachys", version = "0.1.7" }
|
||||
wasm-bindgen = { version = "0.2.100" }
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "throw_error"
|
||||
version = "0.3.0"
|
||||
version = "0.2.0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
error,
|
||||
fmt::{self, Display},
|
||||
future::Future,
|
||||
ops,
|
||||
mem, ops,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
@@ -17,6 +17,11 @@ use std::{
|
||||
|
||||
/* Wrapper Types */
|
||||
|
||||
/// This is a result type into which any error can be converted.
|
||||
///
|
||||
/// Results are stored as [`Error`].
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
|
||||
/// A generic wrapper for any error.
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(transparent)]
|
||||
@@ -104,7 +109,7 @@ pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
|
||||
/// Sets the current thread-local error hook, which will be invoked when [`throw`] is called.
|
||||
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) -> ResetErrorHookOnDrop {
|
||||
ResetErrorHookOnDrop(
|
||||
ERROR_HOOK.with_borrow_mut(|this| Option::replace(this, hook)),
|
||||
ERROR_HOOK.with_borrow_mut(|this| mem::replace(this, Some(hook))),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0"
|
||||
gloo-utils = "0.2.0"
|
||||
@@ -20,27 +20,18 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.39", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
], optional = true }
|
||||
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2.92"
|
||||
web-sys = { version = "0.3.69", features = [
|
||||
"AddEventListenerOptions",
|
||||
"Document",
|
||||
"Element",
|
||||
"Event",
|
||||
"EventListener",
|
||||
"EventTarget",
|
||||
"Performance",
|
||||
"Window",
|
||||
], optional = true }
|
||||
web-sys = { version = "0.3.69", features = [ "AddEventListenerOptions", "Document", "Element", "Event", "EventListener", "EventTarget", "Performance", "Window" ], optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "dep:js-sys", "dep:web-sys"]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"dep:js-sys",
|
||||
"dep:web-sys",
|
||||
]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:http-body-util",
|
||||
|
||||
@@ -13,7 +13,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -45,7 +45,7 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/special/{id}", get(custom_handler))
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos::tachys::html::style::style;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -15,7 +16,7 @@ pub enum CatError {
|
||||
|
||||
type CatCount = usize;
|
||||
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>, Error> {
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
||||
if count > 0 {
|
||||
gloo_timers::future::TimeoutFuture::new(1000).await;
|
||||
// make the request
|
||||
@@ -41,7 +42,11 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>, Error> {
|
||||
pub fn fetch_example() -> impl IntoView {
|
||||
let (cat_count, set_cat_count) = signal::<CatCount>(1);
|
||||
|
||||
let cats = LocalResource::new(move || fetch_cats(cat_count.get()));
|
||||
// we use new_unsync here because the reqwasm request type isn't Send
|
||||
// if we were doing SSR, then
|
||||
// 1) we'd want to use a Resource, so the data would be serialized to the client
|
||||
// 2) we'd need to make sure there was a thread-local spawner set up
|
||||
let cats = AsyncDerived::new_unsync(move || fetch_cats(cat_count.get()));
|
||||
|
||||
let fallback = move |errors: ArcRwSignal<Errors>| {
|
||||
let error_list = move || {
|
||||
@@ -61,6 +66,8 @@ pub fn fetch_example() -> impl IntoView {
|
||||
}
|
||||
};
|
||||
|
||||
let spreadable = style(("background-color", "AliceBlue"));
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<label>
|
||||
@@ -75,7 +82,7 @@ pub fn fetch_example() -> impl IntoView {
|
||||
/>
|
||||
|
||||
</label>
|
||||
<Transition fallback=|| view! { <div>"Loading..."</div> }>
|
||||
<Transition fallback=|| view! { <div>"Loading..."</div> } {..spreadable}>
|
||||
<ErrorBoundary fallback>
|
||||
<ul>
|
||||
{move || Suspend::new(async move {
|
||||
@@ -85,7 +92,7 @@ pub fn fetch_example() -> impl IntoView {
|
||||
.map(|s| {
|
||||
view! {
|
||||
<li>
|
||||
<img src=s.clone() />
|
||||
<img src=s.clone()/>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.8.1", optional = true, features = ["http2"] }
|
||||
axum = { version = "0.7.5", optional = true, features = ["http2"] }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = [
|
||||
"fs",
|
||||
|
||||
@@ -23,7 +23,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.8.1", default-features = false, optional = true }
|
||||
axum = { version = "0.7.5", default-features = false, optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
http = { version = "1.1", optional = true }
|
||||
web-sys = { version = "0.3.70", features = [
|
||||
|
||||
@@ -10,12 +10,15 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"tracing",
|
||||
"islands",
|
||||
] }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -10,20 +10,22 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"tracing",
|
||||
"islands",
|
||||
] }
|
||||
leptos_router = { path = "../../router" }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", features = [
|
||||
"islands-router",
|
||||
"dont-use-islands-router",
|
||||
], optional = true }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
wasm-bindgen = "0.2.100"
|
||||
serde_json = "1.0.133"
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
@@ -56,11 +58,11 @@ site-root = "target/site"
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "style.css"
|
||||
style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3009"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
140
examples/islands_router/public/routing.js
Normal file
140
examples/islands_router/public/routing.js
Normal file
@@ -0,0 +1,140 @@
|
||||
window.addEventListener("click", async (ev) => {
|
||||
// confirm that this is an <a> that meets our requirements
|
||||
if (
|
||||
ev.defaultPrevented ||
|
||||
ev.button !== 0 ||
|
||||
ev.metaKey ||
|
||||
ev.altKey ||
|
||||
ev.ctrlKey ||
|
||||
ev.shiftKey
|
||||
)
|
||||
return;
|
||||
|
||||
/** @type HTMLAnchorElement | undefined;*/
|
||||
const a = ev
|
||||
.composedPath()
|
||||
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
||||
|
||||
if (!a) return;
|
||||
|
||||
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
|
||||
const href = svg ? a.href.baseVal : a.href;
|
||||
const target = svg ? a.target.baseVal : a.target;
|
||||
if (target || (!href && !a.hasAttribute("state"))) return;
|
||||
|
||||
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
||||
if (a.hasAttribute("download") || (rel && rel.includes("external"))) return;
|
||||
|
||||
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
||||
if (
|
||||
url.origin !== window.location.origin // ||
|
||||
// TODO base
|
||||
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
|
||||
)
|
||||
return;
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
// fetch the new page
|
||||
const resp = await fetch(url);
|
||||
const htmlString = await resp.text();
|
||||
|
||||
// Use DOMParser to parse the HTML string
|
||||
const parser = new DOMParser();
|
||||
// TODO parse from the request stream instead?
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
|
||||
// The 'doc' variable now contains the parsed DOM
|
||||
const transition = async () => {
|
||||
const oldDocWalker = document.createTreeWalker(document);
|
||||
const newDocWalker = doc.createTreeWalker(doc);
|
||||
let oldNode = oldDocWalker.currentNode;
|
||||
let newNode = newDocWalker.currentNode;
|
||||
while(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
|
||||
oldNode = oldDocWalker.currentNode;
|
||||
newNode = newDocWalker.currentNode;
|
||||
// if the nodes are different, we need to replace the old with the new
|
||||
// because of the typed view tree, this should never actually happen
|
||||
if (oldNode.nodeType !== newNode.nodeType) {
|
||||
oldNode.replaceWith(newNode);
|
||||
}
|
||||
// if it's a text node, just update the text with the new text
|
||||
else if (oldNode.nodeType === Node.TEXT_NODE) {
|
||||
oldNode.textContent = newNode.textContent;
|
||||
}
|
||||
// if it's an element, replace if it's a different tag, or update attributes
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
|
||||
/** @type Element */
|
||||
const oldEl = oldNode;
|
||||
/** @type Element */
|
||||
const newEl = newNode;
|
||||
if (oldEl.tagName !== newEl.tagName) {
|
||||
oldEl.replaceWith(newEl);
|
||||
}
|
||||
else {
|
||||
for(const attr of newEl.attributes) {
|
||||
oldEl.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
|
||||
// if one of these marker is hit, then there are two options
|
||||
// 1) it's the same branch, and we just keep walking until the end
|
||||
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
|
||||
else if (oldNode.nodeType === Node.COMMENT_NODE) {
|
||||
const oldText = oldNode.textContent;
|
||||
const newText = newNode.textContent;
|
||||
if(oldText.startsWith("bo") && newText !== oldText) {
|
||||
oldDocWalker.nextNode();
|
||||
newDocWalker.nextNode();
|
||||
const oldRange = new Range();
|
||||
const newRange = new Range();
|
||||
let oldBranches = 1;
|
||||
let newBranches = 1;
|
||||
while(oldBranches > 0 && newBranches > 0) {
|
||||
if(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
|
||||
console.log(oldDocWalker.currentNode, newDocWalker.currentNode);
|
||||
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
oldBranches += 1;
|
||||
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
oldBranches -= 1;
|
||||
}
|
||||
}
|
||||
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
newBranches += 1;
|
||||
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
newBranches -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
oldRange.setStartAfter(oldNode);
|
||||
oldRange.setEndBefore(oldDocWalker.currentNode);
|
||||
newRange.setStartAfter(newNode);
|
||||
newRange.setEndBefore(newDocWalker.currentNode);
|
||||
const newContents = newRange.extractContents();
|
||||
oldRange.deleteContents();
|
||||
oldRange.insertNode(newContents);
|
||||
oldNode.replaceWith(newNode);
|
||||
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} }
|
||||
}
|
||||
};
|
||||
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
|
||||
if (document.startViewTransition) {
|
||||
await document.startViewTransition(transition);
|
||||
} else {
|
||||
await transition()
|
||||
}
|
||||
window.history.pushState(undefined, null, url);
|
||||
});
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
use leptos::{
|
||||
either::{Either, EitherOf3},
|
||||
prelude::*,
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::{
|
||||
components::{Route, Router, Routes},
|
||||
hooks::{use_params_map, use_query_map},
|
||||
path,
|
||||
components::{FlatRoutes, Route, Router},
|
||||
StaticSegment,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
@@ -17,7 +12,7 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=options.clone()/>
|
||||
<HydrationScripts options=options islands=true islands_router=true/>
|
||||
<HydrationScripts options=options islands=true/>
|
||||
<link rel="stylesheet" id="leptos" href="/pkg/islands.css"/>
|
||||
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
|
||||
</head>
|
||||
@@ -31,180 +26,34 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
view! {
|
||||
<script src="/routing.js"></script>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"My Contacts"</h1>
|
||||
<h1>"My Application"</h1>
|
||||
</header>
|
||||
<nav>
|
||||
<a href="/">"Home"</a>
|
||||
<a href="/about">"About"</a>
|
||||
<a href="/">"Page A"</a>
|
||||
<a href="/b">"Page B"</a>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes fallback=|| "Not found.">
|
||||
<Route path=path!("") view=Home/>
|
||||
<Route path=path!("user/:id") view=Details/>
|
||||
<Route path=path!("about") view=About/>
|
||||
</Routes>
|
||||
<p>
|
||||
<label>"Home Checkbox" <input type="checkbox"/></label>
|
||||
</p>
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=StaticSegment("") view=PageA/>
|
||||
<Route path=StaticSegment("b") view=PageB/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn search(query: String) -> Result<Vec<User>, ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let data: Vec<User> = serde_json::from_str(&users)?;
|
||||
let query = query.to_ascii_lowercase();
|
||||
Ok(data
|
||||
.into_iter()
|
||||
.filter(|user| {
|
||||
user.first_name.to_ascii_lowercase().contains(&query)
|
||||
|| user.last_name.to_ascii_lowercase().contains(&query)
|
||||
|| user.email.to_ascii_lowercase().contains(&query)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn delete_user(id: u32) -> Result<(), ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let mut data: Vec<User> = serde_json::from_str(&users)?;
|
||||
data.retain(|user| user.id != id);
|
||||
let new_json = serde_json::to_string(&data)?;
|
||||
tokio::fs::write("./mock_data.json", &new_json).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct User {
|
||||
id: u32,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
email: String,
|
||||
#[component]
|
||||
pub fn PageA() -> impl IntoView {
|
||||
view! { <label>"Page A" <input type="checkbox"/></label> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Home() -> impl IntoView {
|
||||
let q = use_query_map();
|
||||
let q = move || q.read().get("q");
|
||||
let data = Resource::new(q, |q| async move {
|
||||
if let Some(q) = q {
|
||||
search(q).await
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
});
|
||||
let delete_user_action = ServerAction::<DeleteUser>::new();
|
||||
|
||||
let view = move || {
|
||||
Suspend::new(async move {
|
||||
let users = data.await.unwrap();
|
||||
if q().is_none() {
|
||||
EitherOf3::A(view! {
|
||||
<p class="note">"Enter a search to begin viewing contacts."</p>
|
||||
})
|
||||
} else if users.is_empty() {
|
||||
EitherOf3::B(view! {
|
||||
<p class="note">"No users found matching that search."</p>
|
||||
})
|
||||
} else {
|
||||
EitherOf3::C(view! {
|
||||
<table>
|
||||
<tbody>
|
||||
<For
|
||||
each=move || users.clone()
|
||||
key=|user| user.id
|
||||
let:user
|
||||
>
|
||||
<tr>
|
||||
<td>{user.first_name}</td>
|
||||
<td>{user.last_name}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>
|
||||
<a href=format!("/user/{}", user.id)>"Details"</a>
|
||||
<input type="checkbox"/>
|
||||
<ActionForm action=delete_user_action>
|
||||
<input type="hidden" name="id" value=user.id/>
|
||||
<input type="submit" value="Delete"/>
|
||||
</ActionForm>
|
||||
</td>
|
||||
</tr>
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
view! {
|
||||
<section class="page">
|
||||
<form method="GET" class="search">
|
||||
<input type="search" name="q" value=q autofocus oninput="this.form.requestSubmit()"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
<Suspense fallback=|| view! { <p>"Loading..."</p> }>{view}</Suspense>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Details() -> impl IntoView {
|
||||
#[server]
|
||||
pub async fn get_user(id: u32) -> Result<Option<User>, ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let data: Vec<User> = serde_json::from_str(&users)?;
|
||||
Ok(data.iter().find(|user| user.id == id).cloned())
|
||||
}
|
||||
let params = use_params_map();
|
||||
let id = move || {
|
||||
params
|
||||
.read()
|
||||
.get("id")
|
||||
.and_then(|id| id.parse::<u32>().ok())
|
||||
};
|
||||
let user = Resource::new(id, |id| async move {
|
||||
match id {
|
||||
None => Ok(None),
|
||||
Some(id) => get_user(id).await,
|
||||
}
|
||||
});
|
||||
|
||||
move || {
|
||||
Suspend::new(async move {
|
||||
user.await.map(|user| match user {
|
||||
None => Either::Left(view! {
|
||||
<section class="page">
|
||||
<h2>"Not found."</h2>
|
||||
<p>"Sorry — we couldn’t find that user."</p>
|
||||
</section>
|
||||
}),
|
||||
Some(user) => Either::Right(view! {
|
||||
<section class="page">
|
||||
<h2>{user.first_name} " " { user.last_name}</h2>
|
||||
<p class="email">{user.email}</p>
|
||||
</section>
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn About() -> impl IntoView {
|
||||
view! {
|
||||
<section class="page">
|
||||
<h2>"About"</h2>
|
||||
<p>"This demo is intended to show off an experimental “islands router” feature, which mimics the smooth transitions and user experience of client-side routing while minimizing the amount of code that actually runs in the browser."</p>
|
||||
<p>"By default, all the content in this application is only rendered on the server. But you can add client-side interactivity via islands like this one:"</p>
|
||||
<Counter/>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
#[island]
|
||||
pub fn Counter() -> impl IntoView {
|
||||
let count = RwSignal::new(0);
|
||||
view! {
|
||||
<button class="counter" on:click=move |_| *count.write() += 1>{count}</button>
|
||||
}
|
||||
pub fn PageB() -> impl IntoView {
|
||||
view! { <label>"Page B" <input type="checkbox"/></label> }
|
||||
}
|
||||
|
||||
@@ -1,52 +1,3 @@
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background-color: #f6f6fa;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: ui-rounded, 'Hiragino Maru Gothic ProN', Quicksand, Comfortaa, Manjari, 'Arial Rounded MT', 'Arial Rounded MT Bold', Calibri, source-sans-pro, sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav a {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
form.search {
|
||||
display: flex;
|
||||
margin: 2rem auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
td {
|
||||
min-width: 10rem;
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
table {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
td:last-child > * {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.note, .note {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.counter {
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
margin: auto;
|
||||
.pending {
|
||||
color: purple;
|
||||
}
|
||||
|
||||
@@ -149,12 +149,12 @@ pub fn App() -> impl IntoView {
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
<Button id="run" text="Create 1,000 rows" on:click=run />
|
||||
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots />
|
||||
<Button id="add" text="Append 1,000 rows" on:click=add />
|
||||
<Button id="update" text="Update every 10th row" on:click=update />
|
||||
<Button id="clear" text="Clear" on:click=clear />
|
||||
<Button id="swaprows" text="Swap Rows" on:click=swap_rows />
|
||||
<Button id="run" text="Create 1,000 rows" on:click=run/>
|
||||
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots/>
|
||||
<Button id="add" text="Append 1,000 rows" on:click=add/>
|
||||
<Button id="update" text="Update every 10th row" on:click=update/>
|
||||
<Button id="clear" text="Clear" on:click=clear/>
|
||||
<Button id="swaprows" text="Swap Rows" on:click=swap_rows/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ use leptos_router::{
|
||||
},
|
||||
hooks::{use_navigate, use_params, use_query_map},
|
||||
params::Params,
|
||||
MatchNestedRoutes,
|
||||
};
|
||||
use leptos_router_macro::path;
|
||||
use std::time::Duration;
|
||||
@@ -24,7 +25,7 @@ pub fn RouterExample() -> impl IntoView {
|
||||
// contexts are passed down through the route tree
|
||||
provide_context(ExampleContext(0));
|
||||
|
||||
// this signal will be used to set whether we are allowed to access a protected route
|
||||
// this signal will be ued to set whether we are allowed to access a protected route
|
||||
let (logged_in, set_logged_in) = signal(true);
|
||||
let (is_routing, set_is_routing) = signal(false);
|
||||
|
||||
@@ -32,7 +33,7 @@ pub fn RouterExample() -> impl IntoView {
|
||||
<Router set_is_routing>
|
||||
// shows a progress bar while async data are loading
|
||||
<div class="routing-progress">
|
||||
<RoutingProgress is_routing max_time=Duration::from_millis(250) />
|
||||
<RoutingProgress is_routing max_time=Duration::from_millis(250)/>
|
||||
</div>
|
||||
<nav>
|
||||
// ordinary <a> elements can be used for client-side navigation
|
||||
@@ -52,15 +53,15 @@ pub fn RouterExample() -> impl IntoView {
|
||||
<Routes transition=true fallback=|| "This page could not be found.">
|
||||
// paths can be created using the path!() macro, or provided as types like
|
||||
// StaticSegment("about")
|
||||
<Route path=path!("about") view=About />
|
||||
<Route path=path!("about") view=About/>
|
||||
<ProtectedRoute
|
||||
path=path!("settings")
|
||||
condition=move || Some(logged_in.get())
|
||||
redirect_path=|| "/"
|
||||
view=Settings
|
||||
/>
|
||||
<Route path=path!("redirect-home") view=|| view! { <Redirect path="/" /> } />
|
||||
<ContactRoutes />
|
||||
<Route path=path!("redirect-home") view=|| view! { <Redirect path="/"/> }/>
|
||||
<ContactRoutes/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -70,11 +71,11 @@ pub fn RouterExample() -> impl IntoView {
|
||||
// You can define other routes in their own component.
|
||||
// Routes implement the MatchNestedRoutes
|
||||
#[component(transparent)]
|
||||
pub fn ContactRoutes() -> impl leptos_router::MatchNestedRoutes + Clone {
|
||||
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("") view=ContactList>
|
||||
<Route path=path!("/") view=|| "Select a contact." />
|
||||
<Route path=path!("/:id") view=Contact />
|
||||
<Route path=path!("/") view=|| "Select a contact."/>
|
||||
<Route path=path!("/:id") view=Contact/>
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
@@ -121,7 +122,7 @@ pub fn ContactList() -> impl IntoView {
|
||||
<Suspense fallback=move || view! { <p>"Loading contacts..."</p> }>
|
||||
<ul>{contacts}</ul>
|
||||
</Suspense>
|
||||
<Outlet />
|
||||
<Outlet/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -165,7 +166,7 @@ pub fn Contact() -> impl IntoView {
|
||||
Some(contact) => Either::Right(view! {
|
||||
<section class="card">
|
||||
<h1>{contact.first_name} " " {contact.last_name}</h1>
|
||||
<p>{contact.address_1} <br /> {contact.address_2}</p>
|
||||
<p>{contact.address_1} <br/> {contact.address_2}</p>
|
||||
</section>
|
||||
}),
|
||||
}
|
||||
@@ -223,10 +224,10 @@ pub fn Settings() -> impl IntoView {
|
||||
<Form action="">
|
||||
<fieldset>
|
||||
<legend>"Name"</legend>
|
||||
<input type="text" name="first_name" placeholder="First" />
|
||||
<input type="text" name="last_name" placeholder="Last" />
|
||||
<input type="text" name="first_name" placeholder="First"/>
|
||||
<input type="text" name="last_name" placeholder="Last"/>
|
||||
</fieldset>
|
||||
<input type="submit" />
|
||||
<input type="submit"/>
|
||||
<p>
|
||||
"This uses the " <code>"<Form/>"</code>
|
||||
" component, which enhances forms by using client-side navigation for "
|
||||
|
||||
@@ -21,21 +21,21 @@ server_fn = { path = "../../server_fn", features = [
|
||||
log = "0.4.22"
|
||||
simple_logger = "5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.5.2", optional = true }
|
||||
tower-http = { version = "0.6.2", features = [
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = [
|
||||
"fs",
|
||||
"tracing",
|
||||
"trace",
|
||||
], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
thiserror = "2.0.11"
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2.93"
|
||||
serde_toml = "0.0.1"
|
||||
toml = "0.8.19"
|
||||
web-sys = { version = "0.3.70", features = ["FileList", "File"] }
|
||||
strum = { version = "0.27.1", features = ["strum_macros", "derive"] }
|
||||
notify = { version = "8.0", optional = true }
|
||||
strum = { version = "0.26.3", features = ["strum_macros", "derive"] }
|
||||
notify = { version = "6.1", optional = true }
|
||||
pin-project-lite = "0.2.14"
|
||||
dashmap = { version = "6.0", optional = true }
|
||||
once_cell = { version = "1.19", optional = true }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use futures::{Sink, Stream, StreamExt};
|
||||
use futures::StreamExt;
|
||||
use http::Method;
|
||||
use leptos::{html::Input, prelude::*, task::spawn_local};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
@@ -9,10 +9,8 @@ use server_fn::{
|
||||
MultipartFormData, Postcard, Rkyv, SerdeLite, StreamingText,
|
||||
TextStream,
|
||||
},
|
||||
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
|
||||
request::{browser::BrowserRequest, ClientReq, Req},
|
||||
response::{browser::BrowserResponse, ClientRes, TryRes},
|
||||
ContentType,
|
||||
response::{browser::BrowserResponse, ClientRes, Res},
|
||||
};
|
||||
use std::future::Future;
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -654,72 +652,32 @@ pub fn FileWatcher() -> impl IntoView {
|
||||
/// implementations if you'd like. However, it's much lighter weight to use something like `strum`
|
||||
/// simply to generate those trait implementations.
|
||||
#[server]
|
||||
pub async fn ascii_uppercase(text: String) -> Result<String, MyErrors> {
|
||||
other_error()?;
|
||||
Ok(ascii_uppercase_inner(text)?)
|
||||
}
|
||||
|
||||
pub fn other_error() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ascii_uppercase_inner(text: String) -> Result<String, InvalidArgument> {
|
||||
pub async fn ascii_uppercase(
|
||||
text: String,
|
||||
) -> Result<String, ServerFnError<InvalidArgument>> {
|
||||
if text.len() < 5 {
|
||||
Err(InvalidArgument::TooShort)
|
||||
Err(InvalidArgument::TooShort.into())
|
||||
} else if text.len() > 15 {
|
||||
Err(InvalidArgument::TooLong)
|
||||
Err(InvalidArgument::TooLong.into())
|
||||
} else if text.is_ascii() {
|
||||
Ok(text.to_ascii_uppercase())
|
||||
} else {
|
||||
Err(InvalidArgument::NotAscii)
|
||||
Err(InvalidArgument::NotAscii.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn ascii_uppercase_classic(
|
||||
text: String,
|
||||
) -> Result<String, ServerFnError<InvalidArgument>> {
|
||||
Ok(ascii_uppercase_inner(text)?)
|
||||
}
|
||||
|
||||
// The EnumString and Display derive macros are provided by strum
|
||||
#[derive(Debug, Clone, Display, EnumString, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, EnumString, Display)]
|
||||
pub enum InvalidArgument {
|
||||
TooShort,
|
||||
TooLong,
|
||||
NotAscii,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Display, Serialize, Deserialize)]
|
||||
pub enum MyErrors {
|
||||
InvalidArgument(InvalidArgument),
|
||||
ServerFnError(ServerFnErrorErr),
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<InvalidArgument> for MyErrors {
|
||||
fn from(value: InvalidArgument) -> Self {
|
||||
MyErrors::InvalidArgument(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for MyErrors {
|
||||
fn from(value: String) -> Self {
|
||||
MyErrors::Other(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromServerFnError for MyErrors {
|
||||
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
|
||||
MyErrors::ServerFnError(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CustomErrorTypes() -> impl IntoView {
|
||||
let input_ref = NodeRef::<Input>::new();
|
||||
let (result, set_result) = signal(None);
|
||||
let (result_classic, set_result_classic) = signal(None);
|
||||
|
||||
view! {
|
||||
<h3>Using custom error types</h3>
|
||||
@@ -734,17 +692,14 @@ pub fn CustomErrorTypes() -> impl IntoView {
|
||||
<button on:click=move |_| {
|
||||
let value = input_ref.get().unwrap().value();
|
||||
spawn_local(async move {
|
||||
let data = ascii_uppercase(value.clone()).await;
|
||||
let data_classic = ascii_uppercase_classic(value).await;
|
||||
let data = ascii_uppercase(value).await;
|
||||
set_result.set(Some(data));
|
||||
set_result_classic.set(Some(data_classic));
|
||||
});
|
||||
}>
|
||||
|
||||
"Submit"
|
||||
</button>
|
||||
<p>{move || format!("{:?}", result.get())}</p>
|
||||
<p>{move || format!("{:?}", result_classic.get())}</p>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -762,11 +717,8 @@ pub struct Toml;
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TomlEncoded<T>(T);
|
||||
|
||||
impl ContentType for Toml {
|
||||
const CONTENT_TYPE: &'static str = "application/toml";
|
||||
}
|
||||
|
||||
impl Encoding for Toml {
|
||||
const CONTENT_TYPE: &'static str = "application/toml";
|
||||
const METHOD: Method = Method::POST;
|
||||
}
|
||||
|
||||
@@ -774,12 +726,14 @@ impl<T, Request, Err> IntoReq<Toml, Request, Err> for TomlEncoded<T>
|
||||
where
|
||||
Request: ClientReq<Err>,
|
||||
T: Serialize,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
fn into_req(self, path: &str, accepts: &str) -> Result<Request, Err> {
|
||||
let data = toml::to_string(&self.0).map_err(|e| {
|
||||
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
|
||||
})?;
|
||||
fn into_req(
|
||||
self,
|
||||
path: &str,
|
||||
accepts: &str,
|
||||
) -> Result<Request, ServerFnError<Err>> {
|
||||
let data = toml::to_string(&self.0)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data)
|
||||
}
|
||||
}
|
||||
@@ -788,26 +742,23 @@ impl<T, Request, Err> FromReq<Toml, Request, Err> for TomlEncoded<T>
|
||||
where
|
||||
Request: Req<Err> + Send,
|
||||
T: DeserializeOwned,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
async fn from_req(req: Request) -> Result<Self, Err> {
|
||||
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
|
||||
let string_data = req.try_into_string().await?;
|
||||
toml::from_str::<T>(&string_data)
|
||||
.map(TomlEncoded)
|
||||
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
|
||||
.map_err(|e| ServerFnError::Args(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Response, Err> IntoRes<Toml, Response, Err> for TomlEncoded<T>
|
||||
where
|
||||
Response: TryRes<Err>,
|
||||
Response: Res<Err>,
|
||||
T: Serialize + Send,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
async fn into_res(self) -> Result<Response, Err> {
|
||||
let data = toml::to_string(&self.0).map_err(|e| {
|
||||
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
|
||||
})?;
|
||||
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
|
||||
let data = toml::to_string(&self.0)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
Response::try_from_string(Toml::CONTENT_TYPE, data)
|
||||
}
|
||||
}
|
||||
@@ -816,13 +767,12 @@ impl<T, Response, Err> FromRes<Toml, Response, Err> for TomlEncoded<T>
|
||||
where
|
||||
Response: ClientRes<Err> + Send,
|
||||
T: DeserializeOwned,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
async fn from_res(res: Response) -> Result<Self, Err> {
|
||||
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
|
||||
let data = res.try_into_string().await?;
|
||||
toml::from_str(&data).map(TomlEncoded).map_err(|e| {
|
||||
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
|
||||
})
|
||||
toml::from_str(&data)
|
||||
.map(TomlEncoded)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -885,10 +835,7 @@ pub fn CustomClientExample() -> impl IntoView {
|
||||
pub struct CustomClient;
|
||||
|
||||
// Implement the `Client` trait for it.
|
||||
impl<E> Client<E> for CustomClient
|
||||
where
|
||||
E: FromServerFnError,
|
||||
{
|
||||
impl<CustErr> Client<CustErr> for CustomClient {
|
||||
// BrowserRequest and BrowserResponse are the defaults used by other server functions.
|
||||
// They are wrappers for the underlying Web Fetch API types.
|
||||
type Request = BrowserRequest;
|
||||
@@ -897,7 +844,8 @@ pub fn CustomClientExample() -> impl IntoView {
|
||||
// Our custom `send()` implementation does all the work.
|
||||
fn send(
|
||||
req: Self::Request,
|
||||
) -> impl Future<Output = Result<Self::Response, E>> + Send {
|
||||
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
|
||||
+ Send {
|
||||
// BrowserRequest derefs to the underlying Request type from gloo-net,
|
||||
// so we can get access to the headers here
|
||||
let headers = req.headers();
|
||||
@@ -906,24 +854,6 @@ pub fn CustomClientExample() -> impl IntoView {
|
||||
// delegate back out to BrowserClient to send the modified request
|
||||
BrowserClient::send(req)
|
||||
}
|
||||
|
||||
fn open_websocket(
|
||||
path: &str,
|
||||
) -> impl Future<
|
||||
Output = Result<
|
||||
(
|
||||
impl Stream<Item = Result<server_fn::Bytes, E>> + Send + 'static,
|
||||
impl Sink<Result<server_fn::Bytes, E>> + Send + 'static,
|
||||
),
|
||||
E,
|
||||
>,
|
||||
> + Send {
|
||||
BrowserClient::open_websocket(path)
|
||||
}
|
||||
|
||||
fn spawn(future: impl Future<Output = ()> + Send + 'static) {
|
||||
<BrowserClient as Client<E>>::spawn(future)
|
||||
}
|
||||
}
|
||||
|
||||
// Specify our custom client with `client = `
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly-2025-03-05"
|
||||
channel = "nightly-2024-01-29"
|
||||
|
||||
@@ -20,7 +20,7 @@ leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = [
|
||||
|
||||
@@ -18,7 +18,7 @@ leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = [
|
||||
@@ -45,7 +45,7 @@ ssr = [
|
||||
"dep:leptos_axum",
|
||||
"leptos_router/ssr",
|
||||
"dep:notify",
|
||||
"dep:http",
|
||||
"dep:http"
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
@check_instrumented_issue_3719
|
||||
Feature: Using instrumented counters to test regression from #3502.
|
||||
Check that the suspend/suspense and the underlying resources are
|
||||
called with the expected number of times. If this was already in
|
||||
place by #3502 (5c43c18) it should have caught this regression.
|
||||
For a better minimum demonstration see #3719.
|
||||
|
||||
Background:
|
||||
|
||||
Given I see the app
|
||||
And I select the mode Instrumented
|
||||
|
||||
Scenario: follow all paths via CSR avoids #3502
|
||||
Given I select the following links
|
||||
| Item Listing |
|
||||
| Item 1 |
|
||||
| Inspect path2 |
|
||||
| Inspect path2/field3 |
|
||||
And I click on Reset CSR Counters
|
||||
When I select the following links
|
||||
| Inspect path2/field1 |
|
||||
| Inspect path2/field2 |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 0 |
|
||||
| item_inspect | 2 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 2 |
|
||||
|
||||
# To show that starting directly from within a param will simply
|
||||
# cause the problem.
|
||||
Scenario: Quicker way to demonstrate regression caused by #3502
|
||||
Given I select the link Target 123
|
||||
# And I click on Reset CSR Counters
|
||||
When I select the following links
|
||||
| Inspect path2/field1 |
|
||||
| Inspect path2/field2 |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 0 |
|
||||
| item_inspect | 3 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 1 |
|
||||
| get_item | 1 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 4 |
|
||||
|
||||
Scenario: Follow paths ordinarily down to a target
|
||||
Given I select the following links
|
||||
| Item Listing |
|
||||
| Item 1 |
|
||||
And I click on Reset CSR Counters
|
||||
When I select the following links
|
||||
| Target 4## |
|
||||
| Target 3## |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 2 |
|
||||
| item_inspect | 0 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 2 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Same as above, but add a refresh to test hydration
|
||||
Given I select the following links
|
||||
| Item Listing |
|
||||
| Item 1 |
|
||||
And I refresh the page
|
||||
And I click on Reset CSR Counters
|
||||
When I select the following links
|
||||
| Target 4## |
|
||||
| Target 3## |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 2 |
|
||||
| item_inspect | 0 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 2 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
@@ -3,28 +3,12 @@ mod fixtures;
|
||||
use anyhow::Result;
|
||||
use cucumber::World;
|
||||
use fixtures::world::AppWorld;
|
||||
use std::{ffi::OsStr, fs::read_dir};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Normally the below is done, but it's now gotten to the point of
|
||||
// having a sufficient number of tests where the resource contention
|
||||
// of the concurrently running browsers will cause failures on CI.
|
||||
// AppWorld::cucumber()
|
||||
// .fail_on_skipped()
|
||||
// .run_and_exit("./features")
|
||||
// .await;
|
||||
|
||||
// Mitigate the issue by manually stepping through each feature,
|
||||
// rather than letting cucumber glob them and dispatch all at once.
|
||||
for entry in read_dir("./features")? {
|
||||
let path = entry?.path();
|
||||
if path.extension() == Some(OsStr::new("feature")) {
|
||||
AppWorld::cucumber()
|
||||
.fail_on_skipped()
|
||||
.run_and_exit(path)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
AppWorld::cucumber()
|
||||
.fail_on_skipped()
|
||||
.run_and_exit("./features")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use leptos_router::{
|
||||
hooks::use_params,
|
||||
nested_router::Outlet,
|
||||
params::Params,
|
||||
ParamSegment, SsrMode, StaticSegment, WildcardSegment,
|
||||
MatchNestedRoutes, ParamSegment, SsrMode, StaticSegment, WildcardSegment,
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -21,7 +21,6 @@ pub(super) mod counter {
|
||||
pub struct Counter(AtomicU32);
|
||||
|
||||
impl Counter {
|
||||
#[allow(dead_code)]
|
||||
pub const fn new() -> Self {
|
||||
Self(AtomicU32::new(0))
|
||||
}
|
||||
@@ -204,20 +203,20 @@ pub struct SuspenseCounters {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn InstrumentedRoutes() -> impl leptos_router::MatchNestedRoutes + Clone {
|
||||
pub fn InstrumentedRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
// TODO should make this mode configurable via feature flag?
|
||||
let ssr = SsrMode::Async;
|
||||
view! {
|
||||
<ParentRoute path=StaticSegment("instrumented") view=InstrumentedRoot ssr>
|
||||
<Route path=StaticSegment("/") view=InstrumentedTop />
|
||||
<Route path=StaticSegment("/") view=InstrumentedTop/>
|
||||
<ParentRoute path=StaticSegment("item") view=ItemRoot>
|
||||
<Route path=StaticSegment("/") view=ItemListing />
|
||||
<Route path=StaticSegment("/") view=ItemListing/>
|
||||
<ParentRoute path=ParamSegment("id") view=ItemTop>
|
||||
<Route path=StaticSegment("/") view=ItemOverview />
|
||||
<Route path=WildcardSegment("path") view=ItemInspect />
|
||||
<Route path=StaticSegment("/") view=ItemOverview/>
|
||||
<Route path=WildcardSegment("path") view=ItemInspect/>
|
||||
</ParentRoute>
|
||||
</ParentRoute>
|
||||
<Route path=StaticSegment("counters") view=ShowCounters />
|
||||
<Route path=StaticSegment("counters") view=ShowCounters/>
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
@@ -280,41 +279,32 @@ fn InstrumentedRoot() -> impl IntoView {
|
||||
<section id="instrumented">
|
||||
<nav>
|
||||
<a href="/">"Site Root"</a>
|
||||
<A href="./" exact=true>
|
||||
"Instrumented Root"
|
||||
</A>
|
||||
<A href="item/" strict_trailing_slash=true>
|
||||
"Item Listing"
|
||||
</A>
|
||||
<A href="counters" strict_trailing_slash=true>
|
||||
"Counters"
|
||||
</A>
|
||||
<A href="./" exact=true>"Instrumented Root"</A>
|
||||
<A href="item/" strict_trailing_slash=true>"Item Listing"</A>
|
||||
<A href="counters" strict_trailing_slash=true>"Counters"</A>
|
||||
</nav>
|
||||
<FieldNavPortlet />
|
||||
<Outlet />
|
||||
<Suspense>
|
||||
{move || Suspend::new(async move {
|
||||
<FieldNavPortlet/>
|
||||
<Outlet/>
|
||||
<Suspense>{
|
||||
move || Suspend::new(async move {
|
||||
let clear_suspense_counters = move |_| {
|
||||
counters.update(|c| *c = SuspenseCounters::default());
|
||||
};
|
||||
csr_ticket
|
||||
.get()
|
||||
.map(|ticket| {
|
||||
let ticket = ticket.0;
|
||||
view! {
|
||||
<ActionForm action=reset_counters>
|
||||
<input type="hidden" name="ticket" value=format!("{ticket}") />
|
||||
<input
|
||||
id="reset-csr-counters"
|
||||
type="submit"
|
||||
value="Reset CSR Counters"
|
||||
on:click=clear_suspense_counters
|
||||
/>
|
||||
</ActionForm>
|
||||
}
|
||||
})
|
||||
})}
|
||||
</Suspense>
|
||||
csr_ticket.get().map(|ticket| {
|
||||
let ticket = ticket.0;
|
||||
view! {
|
||||
<ActionForm action=reset_counters>
|
||||
<input type="hidden" name="ticket" value=format!("{ticket}") />
|
||||
<input
|
||||
id="reset-csr-counters"
|
||||
type="submit"
|
||||
value="Reset CSR Counters"
|
||||
on:click=clear_suspense_counters/>
|
||||
</ActionForm>
|
||||
}
|
||||
})
|
||||
})
|
||||
}</Suspense>
|
||||
<footer>
|
||||
<nav>
|
||||
<A href="item/3/">"Target 3##"</A>
|
||||
@@ -333,17 +323,11 @@ fn InstrumentedRoot() -> impl IntoView {
|
||||
fn InstrumentedTop() -> impl IntoView {
|
||||
view! {
|
||||
<h1>"Instrumented Tests"</h1>
|
||||
<p>
|
||||
"These tests validates the number of invocations of server functions and suspenses per access."
|
||||
</p>
|
||||
<p>"These tests validates the number of invocations of server functions and suspenses per access."</p>
|
||||
<ul>
|
||||
// not using `A` because currently some bugs with artix
|
||||
<li>
|
||||
<a href="item/">"Item Listing"</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="item/4/path1/">"Target 41#"</a>
|
||||
</li>
|
||||
<li><a href="item/">"Item Listing"</a></li>
|
||||
<li><a href="item/4/path1/">"Target 41#"</a></li>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
@@ -358,7 +342,7 @@ fn ItemRoot() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<h2>"<ItemRoot/>"</h2>
|
||||
<Outlet />
|
||||
<Outlet/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,9 +360,7 @@ fn ItemListing() -> impl IntoView {
|
||||
// adding an extra `/` in artix; manually construct `a` instead.
|
||||
// <li><A href=format!("./{item}/")>"Item "{item}</A></li>
|
||||
view! {
|
||||
<li>
|
||||
<a href=format!("/instrumented/item/{item}/")>"Item "{item}</a>
|
||||
</li>
|
||||
<li><a href=format!("/instrumented/item/{item}/")>"Item "{item}</a></li>
|
||||
}
|
||||
)
|
||||
.collect_view()
|
||||
@@ -391,7 +373,9 @@ fn ItemListing() -> impl IntoView {
|
||||
view! {
|
||||
<h3>"<ItemListing/>"</h3>
|
||||
<ul>
|
||||
<Suspense>{item_listing}</Suspense>
|
||||
<Suspense>
|
||||
{item_listing}
|
||||
</Suspense>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
@@ -418,7 +402,7 @@ fn ItemTop() -> impl IntoView {
|
||||
));
|
||||
view! {
|
||||
<h4>"<ItemTop/>"</h4>
|
||||
<Outlet />
|
||||
<Outlet/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,29 +412,24 @@ fn ItemOverview() -> impl IntoView {
|
||||
let resource = expect_context::<Resource<Option<GetItemResult>>>();
|
||||
let item_view = move || {
|
||||
Suspend::new(async move {
|
||||
let result = resource.await.map(|GetItemResult(item, names)| {
|
||||
view! {
|
||||
<p>{format!("Viewing {item:?}")}</p>
|
||||
<ul>
|
||||
{names
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
let id = item.id;
|
||||
// FIXME seems like relative link isn't working, it is currently
|
||||
// adding an extra `/` in artix; manually construct `a` instead.
|
||||
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
|
||||
view! {
|
||||
<li>
|
||||
<a href=format!(
|
||||
"/instrumented/item/{id}/{name}/",
|
||||
)>"Inspect "{name.clone()}</a>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view()}
|
||||
</ul>
|
||||
}
|
||||
});
|
||||
let result = resource.await.map(|GetItemResult(item, names)| view! {
|
||||
<p>{format!("Viewing {item:?}")}</p>
|
||||
<ul>{
|
||||
names.into_iter()
|
||||
.map(|name| {
|
||||
// FIXME seems like relative link isn't working, it is currently
|
||||
// adding an extra `/` in artix; manually construct `a` instead.
|
||||
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
|
||||
let id = item.id;
|
||||
view! {
|
||||
<li><a href=format!("/instrumented/item/{id}/{name}/")>
|
||||
"Inspect "{name.clone()}
|
||||
</a></li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}</ul>
|
||||
});
|
||||
suspense_counters.update_untracked(|c| c.item_overview += 1);
|
||||
result
|
||||
})
|
||||
@@ -458,7 +437,9 @@ fn ItemOverview() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<h5>"<ItemOverview/>"</h5>
|
||||
<Suspense>{item_view}</Suspense>
|
||||
<Suspense>
|
||||
{item_view}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,9 +473,8 @@ fn ItemInspect() -> impl IntoView {
|
||||
// result
|
||||
},
|
||||
);
|
||||
let ws = use_context::<WriteSignal<Option<FieldNavCtx>>>();
|
||||
on_cleanup(move || {
|
||||
if let Some(c) = ws {
|
||||
on_cleanup(|| {
|
||||
if let Some(c) = use_context::<WriteSignal<Option<FieldNavCtx>>>() {
|
||||
c.set(None);
|
||||
}
|
||||
});
|
||||
@@ -516,26 +496,23 @@ fn ItemInspect() -> impl IntoView {
|
||||
));
|
||||
view! {
|
||||
<p>{format!("Inspecting {item:?}")}</p>
|
||||
<ul>
|
||||
{fields
|
||||
.iter()
|
||||
<ul>{
|
||||
fields.iter()
|
||||
.map(|field| {
|
||||
// FIXME seems like relative link to root for a wildcard isn't
|
||||
// working as expected, so manually construct `a` instead.
|
||||
// let text = format!("Inspect {name}/{field}");
|
||||
// view! {
|
||||
// <li><A href=format!("{field}")>{text}</A></li>
|
||||
// <li><A href=format!("{field}")>{text}</A></li>
|
||||
// }
|
||||
view! {
|
||||
<li>
|
||||
<a href=format!(
|
||||
"/instrumented/item/{id}/{name}/{field}",
|
||||
)>{format!("Inspect {name}/{field}")}</a>
|
||||
</li>
|
||||
<li><a href=format!("/instrumented/item/{id}/{name}/{field}")>{
|
||||
format!("Inspect {name}/{field}")
|
||||
}</a></li>
|
||||
}
|
||||
})
|
||||
.collect_view()}
|
||||
</ul>
|
||||
.collect_view()
|
||||
}</ul>
|
||||
}
|
||||
});
|
||||
suspense_counters.update_untracked(|c| c.item_inspect += 1);
|
||||
@@ -550,7 +527,9 @@ fn ItemInspect() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<h5>"<ItemInspect/>"</h5>
|
||||
<Suspense>{inspect_view}</Suspense>
|
||||
<Suspense>
|
||||
{inspect_view}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,8 +590,7 @@ fn ShowCounters() -> impl IntoView {
|
||||
id="reset-counters"
|
||||
type="submit"
|
||||
value="Reset Counters"
|
||||
on:click=clear_suspense_counters
|
||||
/>
|
||||
on:click=clear_suspense_counters/>
|
||||
</ActionForm>
|
||||
}
|
||||
})
|
||||
@@ -623,23 +601,20 @@ fn ShowCounters() -> impl IntoView {
|
||||
<h2>"Counters"</h2>
|
||||
|
||||
<h3 id="suspend-calls">"Suspend Calls"</h3>
|
||||
{move || {
|
||||
suspense_counters
|
||||
.with(|c| {
|
||||
view! {
|
||||
<dl>
|
||||
<dt>"item_listing"</dt>
|
||||
<dd id="item_listing">{c.item_listing}</dd>
|
||||
<dt>"item_overview"</dt>
|
||||
<dd id="item_overview">{c.item_overview}</dd>
|
||||
<dt>"item_inspect"</dt>
|
||||
<dd id="item_inspect">{c.item_inspect}</dd>
|
||||
</dl>
|
||||
}
|
||||
})
|
||||
}}
|
||||
{move || suspense_counters.with(|c| view! {
|
||||
<dl>
|
||||
<dt>"item_listing"</dt>
|
||||
<dd id="item_listing">{c.item_listing}</dd>
|
||||
<dt>"item_overview"</dt>
|
||||
<dd id="item_overview">{c.item_overview}</dd>
|
||||
<dt>"item_inspect"</dt>
|
||||
<dd id="item_inspect">{c.item_inspect}</dd>
|
||||
</dl>
|
||||
})}
|
||||
|
||||
<Suspense>{counter_view}</Suspense>
|
||||
<Suspense>
|
||||
{counter_view}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,17 +642,17 @@ pub fn FieldNavPortlet() -> impl IntoView {
|
||||
view! {
|
||||
<div id="FieldNavPortlet">
|
||||
<span>"FieldNavPortlet:"</span>
|
||||
<nav>
|
||||
{ctx
|
||||
.0
|
||||
.map(|ctx| {
|
||||
ctx.into_iter()
|
||||
.map(|FieldNavItem { href, text }| {
|
||||
view! { <A href=href>{text}</A> }
|
||||
})
|
||||
.collect_view()
|
||||
})}
|
||||
</nav>
|
||||
<nav>{
|
||||
ctx.0.map(|ctx| {
|
||||
ctx.into_iter()
|
||||
.map(|FieldNavItem { href, text }| {
|
||||
view! {
|
||||
<A href=href>{text}</A>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
})
|
||||
}</nav>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
|
||||
22
examples/tailwind_axum/package-lock.json
generated
22
examples/tailwind_axum/package-lock.json
generated
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "leptos-tailwind",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "leptos-tailwind",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0.tgz",
|
||||
"integrity": "sha512-ULRPI3A+e39T7pSaf1xoi58AqqJxVCLg8F/uM5A3FadUbnyDTgltVnXJvdkTjwCOGA6NazqHVcwPJC5h2vRYVQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/*! tailwindcss v4.0.0 | MIT License | https://tailwindcss.com */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
.m-auto {
|
||||
margin: auto;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.transform {
|
||||
transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex-row-reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.border-b-4 {
|
||||
border-bottom-style: var(--tw-border-style);
|
||||
border-bottom-width: 4px;
|
||||
}
|
||||
.border-l-2 {
|
||||
border-left-style: var(--tw-border-style);
|
||||
border-left-width: 2px;
|
||||
}
|
||||
.bg-gradient-to-tl {
|
||||
--tw-gradient-position: to top left in oklab,;
|
||||
background-image: linear-gradient(var(--tw-gradient-stops));
|
||||
}
|
||||
@property --tw-rotate-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: rotateX(0);
|
||||
}
|
||||
@property --tw-rotate-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: rotateY(0);
|
||||
}
|
||||
@property --tw-rotate-z {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: rotateZ(0);
|
||||
}
|
||||
@property --tw-skew-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: skewX(0);
|
||||
}
|
||||
@property --tw-skew-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: skewY(0);
|
||||
}
|
||||
@property --tw-border-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
log = "0.4.22"
|
||||
simple_logger = "5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::todo::*;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::Path,
|
||||
@@ -8,9 +8,10 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use todo_app_sqlite_axum::*;
|
||||
|
||||
//Define a handler to test extractor with state
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn custom_handler(
|
||||
Path(id): Path<String>,
|
||||
req: Request<Body>,
|
||||
@@ -19,16 +20,14 @@ async fn custom_handler(
|
||||
move || {
|
||||
provide_context(id.clone());
|
||||
},
|
||||
todo::TodoApp,
|
||||
TodoApp,
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use crate::todo::{ssr::db, *};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use crate::todo::ssr::db;
|
||||
|
||||
simple_logger::init_with_level(log::Level::Error)
|
||||
.expect("couldn't initialize logging");
|
||||
@@ -46,7 +45,7 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/special/{id}", get(custom_handler))
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
@@ -62,12 +61,3 @@ async fn main() {
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
use leptos::mount::mount_to_body;
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(todo::TodoApp);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
leptos_integration_utils = { path = "../../integrations/utils", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.5.1", features = ["util"], optional = true }
|
||||
tower-http = { version = "0.6.1", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -34,7 +34,7 @@ async fn main() {
|
||||
// here, we're not actually doing server side rendering, so we set up a manual
|
||||
// handler for the server fns
|
||||
// this should include a get() handler if you have any GetUrl-based server fns
|
||||
.route("/api/{*fn_name}", post(leptos_axum::handle_server_fns))
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.fallback(file_or_index_handler)
|
||||
.with_state(leptos_options);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hydration_context"
|
||||
version = "0.3.0"
|
||||
version = "0.2.1"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -21,7 +21,6 @@ leptos_macro = { workspace = true, features = ["actix"] }
|
||||
leptos_meta = { workspace = true, features = ["nonce"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
server_fn = { workspace = true, features = ["actix"] }
|
||||
tachys = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
parking_lot = "0.12.3"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
@@ -34,7 +33,7 @@ once_cell = "1"
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
islands-router = ["tachys/islands"]
|
||||
dont-use-islands-router = []
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
|
||||
@@ -23,7 +23,6 @@ use hydration_context::SsrSharedContext;
|
||||
use leptos::{
|
||||
config::LeptosOptions,
|
||||
context::{provide_context, use_context},
|
||||
hydration::IslandsRouterNavigation,
|
||||
prelude::expect_context,
|
||||
reactive::{computed::ScopedFuture, owner::Owner},
|
||||
IntoView,
|
||||
@@ -275,13 +274,14 @@ pub fn redirect(path: &str) {
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
/// ```no_run
|
||||
/// ```
|
||||
/// use actix_web::*;
|
||||
///
|
||||
/// fn register_server_functions() {
|
||||
/// // call ServerFn::register() for each of the server functions you've defined
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// // make sure you actually register your server functions
|
||||
@@ -297,6 +297,7 @@ pub fn redirect(path: &str) {
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -368,6 +369,7 @@ pub fn handle_server_fns_with_context(
|
||||
// actually run the server fn
|
||||
let mut res = ActixResponse(
|
||||
service
|
||||
.0
|
||||
.run(ActixRequest::from((req, payload)))
|
||||
.await
|
||||
.take(),
|
||||
@@ -431,7 +433,7 @@ pub fn handle_server_fns_with_context(
|
||||
/// but requires some client-side JavaScript.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```no_run
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_router::Method;
|
||||
@@ -442,6 +444,7 @@ pub fn handle_server_fns_with_context(
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
|
||||
@@ -461,6 +464,7 @@ pub fn handle_server_fns_with_context(
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -488,7 +492,7 @@ where
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```no_run
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_router::Method;
|
||||
@@ -499,6 +503,7 @@ where
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
|
||||
@@ -521,6 +526,7 @@ where
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -546,7 +552,7 @@ where
|
||||
/// `async` resources have loaded.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```no_run
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_router::Method;
|
||||
@@ -557,6 +563,7 @@ where
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).unwrap();
|
||||
@@ -576,6 +583,7 @@ where
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -655,27 +663,12 @@ where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
_ = replace_blocks; // TODO
|
||||
handle_response(
|
||||
method,
|
||||
additional_context,
|
||||
app_fn,
|
||||
|app, chunks, supports_ooo| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
if supports_ooo {
|
||||
app.to_html_stream_out_of_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order_branching()
|
||||
}
|
||||
} else if supports_ooo {
|
||||
app.to_html_stream_out_of_order()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
};
|
||||
Box::pin(app.chain(chunks())) as PinnedStream<String>
|
||||
})
|
||||
},
|
||||
)
|
||||
handle_response(method, additional_context, app_fn, |app, chunks| {
|
||||
Box::pin(async move {
|
||||
Box::pin(app.to_html_stream_out_of_order().chain(chunks()))
|
||||
as PinnedStream<String>
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
@@ -701,21 +694,12 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
handle_response(
|
||||
method,
|
||||
additional_context,
|
||||
app_fn,
|
||||
|app, chunks, _supports_ooo| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
};
|
||||
Box::pin(app.chain(chunks())) as PinnedStream<String>
|
||||
})
|
||||
},
|
||||
)
|
||||
handle_response(method, additional_context, app_fn, |app, chunks| {
|
||||
Box::pin(async move {
|
||||
Box::pin(app.to_html_stream_in_order().chain(chunks()))
|
||||
as PinnedStream<String>
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
@@ -747,13 +731,12 @@ where
|
||||
fn async_stream_builder<IV>(
|
||||
app: IV,
|
||||
chunks: BoxedFnOnce<PinnedStream<String>>,
|
||||
_supports_ooo: bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -793,7 +776,6 @@ fn leptos_corrected_path(req: &HttpRequest) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn handle_response<IV>(
|
||||
method: Method,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
@@ -801,7 +783,6 @@ fn handle_response<IV>(
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
) -> Route
|
||||
where
|
||||
@@ -812,9 +793,6 @@ where
|
||||
let add_context = additional_context.clone();
|
||||
|
||||
async move {
|
||||
let is_island_router_navigation = cfg!(feature = "islands-router")
|
||||
&& req.headers().get("Islands-Router").is_some();
|
||||
|
||||
let res_options = ResponseOptions::default();
|
||||
let (meta_context, meta_output) = ServerMetaContext::new();
|
||||
|
||||
@@ -825,10 +803,6 @@ where
|
||||
move || {
|
||||
provide_contexts(req, &meta_context, &res_options);
|
||||
add_context();
|
||||
|
||||
if is_island_router_navigation {
|
||||
provide_context(IslandsRouterNavigation);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -838,7 +812,6 @@ where
|
||||
additional_context,
|
||||
res_options,
|
||||
stream_builder,
|
||||
!is_island_router_navigation,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1129,7 +1102,6 @@ impl StaticRouteGenerator {
|
||||
app_fn.clone(),
|
||||
additional_context,
|
||||
async_stream_builder,
|
||||
false,
|
||||
);
|
||||
|
||||
let sc = owner.shared_context().unwrap();
|
||||
|
||||
@@ -4,14 +4,14 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Axum integrations for the Leptos web framework."
|
||||
version = "0.8.0-beta"
|
||||
version = { workspace = true }
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
axum = { version = "0.8.1", default-features = false, features = [
|
||||
axum = { version = "0.7.9", default-features = false, features = [
|
||||
"matched-path",
|
||||
] }
|
||||
dashmap = "6"
|
||||
@@ -22,7 +22,6 @@ leptos_macro = { workspace = true, features = ["axum"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr", "nonce"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
tachys = { workspace = true }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12.3"
|
||||
tokio = { version = "1.43", default-features = false }
|
||||
@@ -31,19 +30,13 @@ tower-http = "0.6.2"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = "0.8.1"
|
||||
axum = "0.7.9"
|
||||
tokio = { version = "1.43", features = ["net", "rt-multi-thread"] }
|
||||
|
||||
[features]
|
||||
wasm = []
|
||||
default = [
|
||||
"tokio/fs",
|
||||
"tokio/sync",
|
||||
"tower-http/fs",
|
||||
"tower/util",
|
||||
"server_fn/axum",
|
||||
]
|
||||
islands-router = ["tachys/islands"]
|
||||
default = ["tokio/fs", "tokio/sync", "tower-http/fs", "tower/util"]
|
||||
dont-use-islands-router = []
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
//! Provides functions to easily integrate Leptos with Axum.
|
||||
//!
|
||||
@@ -279,11 +278,12 @@ pub fn generate_request_and_parts(
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
/// ```no_run
|
||||
/// ```
|
||||
/// use axum::{handler::Handler, routing::post, Router};
|
||||
/// use leptos::prelude::*;
|
||||
/// use std::net::SocketAddr;
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[cfg(feature = "default")]
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
@@ -299,9 +299,7 @@ pub fn generate_request_and_parts(
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// }
|
||||
///
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() { }
|
||||
/// # }
|
||||
/// ```
|
||||
/// Leptos provides a generic implementation of `handle_server_fns`. If access to more specific parts of the Request is desired,
|
||||
/// you can specify your own server fn handler based on this one and give it it's own route in the server macro.
|
||||
@@ -370,6 +368,8 @@ async fn handle_server_fns_inner(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
use server_fn::middleware::Service;
|
||||
|
||||
let method = req.method().clone();
|
||||
let path = req.uri().path().to_string();
|
||||
let (req, parts) = generate_request_and_parts(req);
|
||||
@@ -442,7 +442,7 @@ pub type PinnedHtmlStream =
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```no_run
|
||||
/// ```
|
||||
/// use axum::{handler::Handler, Router};
|
||||
/// use leptos::{config::get_configuration, prelude::*};
|
||||
/// use std::{env, net::SocketAddr};
|
||||
@@ -452,6 +452,7 @@ pub type PinnedHtmlStream =
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[cfg(feature = "default")]
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
@@ -470,9 +471,7 @@ pub type PinnedHtmlStream =
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// }
|
||||
///
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() { }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -485,7 +484,7 @@ pub type PinnedHtmlStream =
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -509,7 +508,7 @@ where
|
||||
)]
|
||||
pub fn render_route<S, IV>(
|
||||
paths: Vec<AxumRouteListing>,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
State<S>,
|
||||
Request<Body>,
|
||||
@@ -531,7 +530,7 @@ where
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```no_run
|
||||
/// ```
|
||||
/// use axum::{handler::Handler, Router};
|
||||
/// use leptos::{config::get_configuration, prelude::*};
|
||||
/// use std::{env, net::SocketAddr};
|
||||
@@ -541,6 +540,7 @@ where
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[cfg(feature = "default")]
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
@@ -559,9 +559,7 @@ where
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// }
|
||||
///
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() { }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -574,7 +572,7 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_in_order<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -627,14 +625,13 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
@@ -657,8 +654,8 @@ where
|
||||
)]
|
||||
pub fn render_route_with_context<S, IV>(
|
||||
paths: Vec<AxumRouteListing>,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
State<S>,
|
||||
Request<Body>,
|
||||
@@ -759,32 +756,25 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
replace_blocks: bool,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
_ = replace_blocks; // TODO
|
||||
handle_response(additional_context, app_fn, |app, chunks, supports_ooo| {
|
||||
handle_response(additional_context, app_fn, |app, chunks| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
if supports_ooo {
|
||||
app.to_html_stream_out_of_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order_branching()
|
||||
}
|
||||
} else if supports_ooo {
|
||||
app.to_html_stream_out_of_order()
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
app.to_html_stream_out_of_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
app.to_html_stream_out_of_order()
|
||||
};
|
||||
Box::pin(app.chain(chunks())) as PinnedStream<String>
|
||||
})
|
||||
@@ -833,8 +823,8 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -844,8 +834,8 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
handle_response(additional_context, app_fn, |app, chunks| {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -857,18 +847,13 @@ where
|
||||
}
|
||||
|
||||
fn handle_response<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static
|
||||
) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>> + Clone + Send + 'static
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
@@ -886,16 +871,12 @@ fn handle_response_inner<IV>(
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
) -> PinnedFuture<Response<Body>>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let is_island_router_navigation = cfg!(feature = "islands-router")
|
||||
&& req.headers().get("Islands-Router").is_some();
|
||||
|
||||
let add_context = additional_context.clone();
|
||||
let res_options = ResponseOptions::default();
|
||||
let (meta_context, meta_output) = ServerMetaContext::new();
|
||||
@@ -917,10 +898,6 @@ where
|
||||
res_options.clone(),
|
||||
);
|
||||
add_context();
|
||||
|
||||
if is_island_router_navigation {
|
||||
provide_context(IslandsRouterNavigation);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -930,7 +907,6 @@ where
|
||||
additional_context,
|
||||
res_options,
|
||||
stream_builder,
|
||||
!is_island_router_navigation,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -961,7 +937,7 @@ fn provide_contexts(
|
||||
/// `async` resources have loaded.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```no_run
|
||||
/// ```
|
||||
/// use axum::{handler::Handler, Router};
|
||||
/// use leptos::{config::get_configuration, prelude::*};
|
||||
/// use std::{env, net::SocketAddr};
|
||||
@@ -971,6 +947,7 @@ fn provide_contexts(
|
||||
/// view! { <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[cfg(feature = "default")]
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
@@ -990,9 +967,7 @@ fn provide_contexts(
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// }
|
||||
///
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() { }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -1005,7 +980,7 @@ fn provide_contexts(
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -1059,8 +1034,8 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async_stream_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -1070,9 +1045,9 @@ pub fn render_app_async_stream_with_context<IV>(
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
|
||||
handle_response(additional_context, app_fn, |app, chunks| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -1126,8 +1101,8 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -1143,13 +1118,12 @@ where
|
||||
fn async_stream_builder<IV>(
|
||||
app: IV,
|
||||
chunks: BoxedFnOnce<PinnedStream<String>>,
|
||||
_supports_ooo: bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -1422,7 +1396,6 @@ impl StaticRouteGenerator {
|
||||
app_fn.clone(),
|
||||
additional_context,
|
||||
async_stream_builder,
|
||||
false,
|
||||
);
|
||||
|
||||
let sc = owner.shared_context().unwrap();
|
||||
@@ -1668,7 +1641,7 @@ where
|
||||
self,
|
||||
options: &S,
|
||||
paths: Vec<AxumRouteListing>,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static;
|
||||
@@ -1683,8 +1656,8 @@ where
|
||||
self,
|
||||
options: &S,
|
||||
paths: Vec<AxumRouteListing>,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static;
|
||||
@@ -1717,15 +1690,12 @@ impl AxumPath for Vec<PathSegment> {
|
||||
match segment {
|
||||
PathSegment::Static(s) => path.push_str(s),
|
||||
PathSegment::Param(s) => {
|
||||
path.push('{');
|
||||
path.push(':');
|
||||
path.push_str(s);
|
||||
path.push('}');
|
||||
}
|
||||
PathSegment::Splat(s) => {
|
||||
path.push('{');
|
||||
path.push('*');
|
||||
path.push_str(s);
|
||||
path.push('}');
|
||||
}
|
||||
PathSegment::Unit => {}
|
||||
PathSegment::OptionalParam(_) => {
|
||||
@@ -1757,7 +1727,7 @@ where
|
||||
self,
|
||||
state: &S,
|
||||
paths: Vec<AxumRouteListing>,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
@@ -1773,8 +1743,8 @@ where
|
||||
self,
|
||||
state: &S,
|
||||
paths: Vec<AxumRouteListing>,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
@@ -1855,64 +1825,64 @@ where
|
||||
}
|
||||
} else {
|
||||
router.route(
|
||||
path,
|
||||
match listing.mode() {
|
||||
SsrMode::OutOfOrder => {
|
||||
let s = render_app_to_stream_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
path,
|
||||
match listing.mode() {
|
||||
SsrMode::OutOfOrder => {
|
||||
let s = render_app_to_stream_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
SsrMode::PartiallyBlocked => {
|
||||
let s = render_app_to_stream_with_context_and_replace_blocks(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
true
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
SsrMode::PartiallyBlocked => {
|
||||
let s = render_app_to_stream_with_context_and_replace_blocks(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
true
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
SsrMode::Async => {
|
||||
let s = render_app_async_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
SsrMode::Async => {
|
||||
let s = render_app_async_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
_ => unreachable!()
|
||||
},
|
||||
)
|
||||
}
|
||||
_ => unreachable!()
|
||||
},
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2013,7 +1983,7 @@ where
|
||||
#[cfg(feature = "default")]
|
||||
pub fn file_and_error_handler_with_context<S, IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
shell: impl Fn(LeptosOptions) -> IV + 'static + Clone + Send,
|
||||
shell: fn(LeptosOptions) -> IV,
|
||||
) -> impl Fn(
|
||||
Uri,
|
||||
State<S>,
|
||||
@@ -2030,7 +2000,6 @@ where
|
||||
move |uri: Uri, State(state): State<S>, req: Request<Body>| {
|
||||
Box::pin({
|
||||
let additional_context = additional_context.clone();
|
||||
let shell = shell.clone();
|
||||
async move {
|
||||
let options = LeptosOptions::from_ref(&state);
|
||||
let res =
|
||||
@@ -2047,7 +2016,7 @@ where
|
||||
},
|
||||
move || shell(options),
|
||||
req,
|
||||
|app, chunks, _supports_ooo| {
|
||||
|app, chunks| {
|
||||
Box::pin(async move {
|
||||
let app = app
|
||||
.to_html_stream_in_order()
|
||||
@@ -2074,7 +2043,7 @@ where
|
||||
/// simply reuse the source code of this function in your own application.
|
||||
#[cfg(feature = "default")]
|
||||
pub fn file_and_error_handler<S, IV>(
|
||||
shell: impl Fn(LeptosOptions) -> IV + 'static + Clone + Send,
|
||||
shell: fn(LeptosOptions) -> IV,
|
||||
) -> impl Fn(
|
||||
Uri,
|
||||
State<S>,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use futures::{stream::once, Stream, StreamExt};
|
||||
use hydration_context::{SharedContext, SsrSharedContext};
|
||||
use leptos::{
|
||||
@@ -33,20 +31,14 @@ pub trait ExtendResponse: Sized {
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
supports_ooo: bool,
|
||||
) -> impl Future<Output = Self> + Send
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
async move {
|
||||
let (owner, stream) = build_response(
|
||||
app_fn,
|
||||
additional_context,
|
||||
stream_builder,
|
||||
supports_ooo,
|
||||
);
|
||||
let (owner, stream) =
|
||||
build_response(app_fn, additional_context, stream_builder);
|
||||
|
||||
let sc = owner.shared_context().unwrap();
|
||||
|
||||
@@ -102,11 +94,7 @@ pub fn build_response<IV>(
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
// this argument indicates whether a request wants to support out-of-order streaming
|
||||
// responses
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
is_islands_router_navigation: bool,
|
||||
) -> (Owner, PinnedFuture<PinnedStream<String>>)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
@@ -150,7 +138,7 @@ where
|
||||
//
|
||||
// we also don't actually start hydrating until after the whole stream is complete,
|
||||
// so it's not useful to send those scripts down earlier.
|
||||
stream_builder(app, chunks, is_islands_router_navigation)
|
||||
stream_builder(app, chunks)
|
||||
});
|
||||
|
||||
stream.await
|
||||
|
||||
@@ -42,7 +42,11 @@ typed-builder = "0.20.0"
|
||||
typed-builder-macro = "0.20.0"
|
||||
serde = "1.0"
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
server_fn = { workspace = true, features = ["form-redirects", "browser"] }
|
||||
server_fn = { workspace = true, features = [
|
||||
"form-redirects",
|
||||
"browser",
|
||||
"url",
|
||||
] }
|
||||
web-sys = { version = "0.3.72", features = [
|
||||
"ShadowRoot",
|
||||
"ShadowRootInit",
|
||||
@@ -96,15 +100,6 @@ trace-component-props = [
|
||||
]
|
||||
delegation = ["tachys/delegation"]
|
||||
|
||||
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
|
||||
# https://github.com/rust-lang/cargo/issues/4423
|
||||
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
|
||||
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
|
||||
# This is automatic as long as the leptos crate is depended upon,
|
||||
# downstream usage should never manually enable this feature.
|
||||
[target.'cfg(erase_components)'.dependencies]
|
||||
leptos_macro = { workspace = true, features = ["__internal_erase_components"] }
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = [
|
||||
"nightly",
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn AttributeInterceptor<Chil, T>(
|
||||
) -> impl IntoView
|
||||
where
|
||||
Chil: Fn(AnyAttribute) -> T + Send + Sync + 'static,
|
||||
T: IntoView + 'static,
|
||||
T: IntoView,
|
||||
{
|
||||
AttributeInterceptorInner::new(children)
|
||||
}
|
||||
@@ -86,7 +86,7 @@ impl<T: IntoView, A: Attribute> Render for AttributeInterceptorInner<T, A> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoView + 'static, A> AddAnyAttr for AttributeInterceptorInner<T, A>
|
||||
impl<T: IntoView, A> AddAnyAttr for AttributeInterceptorInner<T, A>
|
||||
where
|
||||
A: Attribute,
|
||||
{
|
||||
@@ -114,11 +114,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoView + 'static, A: Attribute> RenderHtml
|
||||
for AttributeInterceptorInner<T, A>
|
||||
{
|
||||
impl<T: IntoView, A: Attribute> RenderHtml for AttributeInterceptorInner<T, A> {
|
||||
type AsyncOutput = T::AsyncOutput;
|
||||
type Owned = AttributeInterceptorInner<T, A::CloneableOwned>;
|
||||
|
||||
const MIN_LENGTH: usize = T::MIN_LENGTH;
|
||||
|
||||
@@ -138,15 +135,9 @@ impl<T: IntoView + 'static, A: Attribute> RenderHtml
|
||||
position: &mut leptos::tachys::view::Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
_extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
self.children.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
vec![],
|
||||
)
|
||||
self.children
|
||||
.to_html_with_buf(buf, position, escape, mark_branches)
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
@@ -156,12 +147,4 @@ impl<T: IntoView + 'static, A: Attribute> RenderHtml
|
||||
) -> Self::State {
|
||||
self.children.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
AttributeInterceptorInner {
|
||||
children_builder: self.children_builder,
|
||||
children: self.children,
|
||||
attributes: self.attributes.into_cloneable_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,20 +43,13 @@
|
||||
|
||||
use reactive_graph::{
|
||||
owner::{LocalStorage, StoredValue},
|
||||
traits::{Dispose, WithValue},
|
||||
traits::WithValue,
|
||||
};
|
||||
use std::{fmt, rc::Rc, sync::Arc};
|
||||
|
||||
/// A wrapper trait for calling callbacks.
|
||||
pub trait Callable<In: 'static, Out: 'static = ()> {
|
||||
/// calls the callback with the specified argument.
|
||||
///
|
||||
/// Returns None if the callback has been disposed
|
||||
fn try_run(&self, input: In) -> Option<Out>;
|
||||
/// calls the callback with the specified argument.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to run a callback that has been disposed
|
||||
fn run(&self, input: In) -> Out;
|
||||
}
|
||||
|
||||
@@ -79,12 +72,6 @@ impl<In, Out> Clone for UnsyncCallback<In, Out> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> Dispose for UnsyncCallback<In, Out> {
|
||||
fn dispose(self) {
|
||||
self.0.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> UnsyncCallback<In, Out> {
|
||||
/// Creates a new callback from the given function.
|
||||
pub fn new<F>(f: F) -> UnsyncCallback<In, Out>
|
||||
@@ -106,10 +93,6 @@ impl<In, Out> UnsyncCallback<In, Out> {
|
||||
}
|
||||
|
||||
impl<In: 'static, Out: 'static> Callable<In, Out> for UnsyncCallback<In, Out> {
|
||||
fn try_run(&self, input: In) -> Option<Out> {
|
||||
self.0.try_with_value(|fun| fun(input))
|
||||
}
|
||||
|
||||
fn run(&self, input: In) -> Out {
|
||||
self.0.with_value(|fun| fun(input))
|
||||
}
|
||||
@@ -185,12 +168,10 @@ impl<In, Out> fmt::Debug for Callback<In, Out> {
|
||||
}
|
||||
|
||||
impl<In, Out> Callable<In, Out> for Callback<In, Out> {
|
||||
fn try_run(&self, input: In) -> Option<Out> {
|
||||
self.0.try_with_value(|fun| fun(input))
|
||||
}
|
||||
|
||||
fn run(&self, input: In) -> Out {
|
||||
self.0.with_value(|f| f(input))
|
||||
self.0
|
||||
.try_with_value(|f| f(input))
|
||||
.expect("called a callback that has been disposed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,12 +181,6 @@ impl<In, Out> Clone for Callback<In, Out> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> Dispose for Callback<In, Out> {
|
||||
fn dispose(self) {
|
||||
self.0.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> Copy for Callback<In, Out> {}
|
||||
|
||||
macro_rules! impl_callable_from_fn {
|
||||
@@ -264,9 +239,7 @@ impl<In: 'static, Out: 'static> Callback<In, Out> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Callable;
|
||||
use crate::callback::{Callback, UnsyncCallback};
|
||||
use reactive_graph::traits::Dispose;
|
||||
|
||||
struct NoClone {}
|
||||
|
||||
@@ -297,26 +270,10 @@ mod tests {
|
||||
(|num, s| format!("{num} {s}")).into();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_callback_try_run() {
|
||||
let callback = Callback::new(move |arg| arg);
|
||||
assert_eq!(callback.try_run((0,)), Some((0,)));
|
||||
callback.dispose();
|
||||
assert_eq!(callback.try_run((0,)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsync_callback_try_run() {
|
||||
let callback = UnsyncCallback::new(move |arg| arg);
|
||||
assert_eq!(callback.try_run((0,)), Some((0,)));
|
||||
callback.dispose();
|
||||
assert_eq!(callback.try_run((0,)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_matches_same() {
|
||||
let callback1 = Callback::new(|x: i32| x * 2);
|
||||
let callback2 = callback1;
|
||||
let callback2 = callback1.clone();
|
||||
assert!(callback1.matches(&callback2));
|
||||
}
|
||||
|
||||
@@ -330,7 +287,7 @@ mod tests {
|
||||
#[test]
|
||||
fn unsync_callback_matches_same() {
|
||||
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
|
||||
let callback2 = callback1;
|
||||
let callback2 = callback1.clone();
|
||||
assert!(callback1.matches(&callback2));
|
||||
}
|
||||
|
||||
|
||||
@@ -91,9 +91,6 @@ pub trait ToChildren<F> {
|
||||
fn to_children(f: F) -> Self;
|
||||
}
|
||||
|
||||
/// Compiler optimisation, can be used with certain type to avoid unique closures in the view!{} macro.
|
||||
pub struct ChildrenOptContainer<T>(pub T);
|
||||
|
||||
impl<F, C> ToChildren<F> for Children
|
||||
where
|
||||
F: FnOnce() -> C + Send + 'static,
|
||||
@@ -105,16 +102,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for Children
|
||||
where
|
||||
T: IntoAny + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
Box::new(move || t.0.into_any())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, C> ToChildren<F> for ChildrenFn
|
||||
where
|
||||
F: Fn() -> C + Send + Sync + 'static,
|
||||
@@ -126,16 +113,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFn
|
||||
where
|
||||
T: IntoAny + Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
Arc::new(move || t.0.clone().into_any())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, C> ToChildren<F> for ChildrenFnMut
|
||||
where
|
||||
F: Fn() -> C + Send + 'static,
|
||||
@@ -147,16 +124,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFnMut
|
||||
where
|
||||
T: IntoAny + Clone + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
Box::new(move || t.0.clone().into_any())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, C> ToChildren<F> for BoxedChildrenFn
|
||||
where
|
||||
F: Fn() -> C + Send + 'static,
|
||||
@@ -168,16 +135,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for BoxedChildrenFn
|
||||
where
|
||||
T: IntoAny + Clone + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
Box::new(move || t.0.clone().into_any())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, C> ToChildren<F> for ChildrenFragment
|
||||
where
|
||||
F: FnOnce() -> C + Send + 'static,
|
||||
@@ -189,16 +146,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFragment
|
||||
where
|
||||
T: IntoAny + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
Box::new(move || Fragment::new(vec![t.0.into_any()]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, C> ToChildren<F> for ChildrenFragmentFn
|
||||
where
|
||||
F: Fn() -> C + Send + 'static,
|
||||
@@ -210,16 +157,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFragmentFn
|
||||
where
|
||||
T: IntoAny + Clone + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
Arc::new(move || Fragment::new(vec![t.0.clone().into_any()]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, C> ToChildren<F> for ChildrenFragmentMut
|
||||
where
|
||||
F: FnMut() -> C + Send + 'static,
|
||||
@@ -231,16 +168,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for ChildrenFragmentMut
|
||||
where
|
||||
T: IntoAny + Clone + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
Box::new(move || Fragment::new(vec![t.0.clone().into_any()]))
|
||||
}
|
||||
}
|
||||
|
||||
/// New-type wrapper for a function that returns a view with `From` and `Default` traits implemented
|
||||
/// to enable optional props in for example `<Show>` and `<Suspense>`.
|
||||
#[derive(Clone)]
|
||||
@@ -319,16 +246,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for TypedChildren<T>
|
||||
where
|
||||
T: IntoView + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
TypedChildren(Box::new(move || t.0.into_view()))
|
||||
}
|
||||
}
|
||||
|
||||
/// A typed equivalent to [`ChildrenFnMut`], which takes a generic but preserves type information to
|
||||
/// allow the compiler to optimize the view more effectively.
|
||||
pub struct TypedChildrenMut<T>(Box<dyn FnMut() -> View<T> + Send>);
|
||||
@@ -358,16 +275,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for TypedChildrenMut<T>
|
||||
where
|
||||
T: IntoView + Clone + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
TypedChildrenMut(Box::new(move || t.0.clone().into_view()))
|
||||
}
|
||||
}
|
||||
|
||||
/// A typed equivalent to [`ChildrenFn`], which takes a generic but preserves type information to
|
||||
/// allow the compiler to optimize the view more effectively.
|
||||
pub struct TypedChildrenFn<T>(Arc<dyn Fn() -> View<T> + Send + Sync>);
|
||||
@@ -403,13 +310,3 @@ where
|
||||
TypedChildrenFn(Arc::new(move || f().into_view()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToChildren<ChildrenOptContainer<T>> for TypedChildrenFn<T>
|
||||
where
|
||||
T: IntoView + Clone + Sync + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(t: ChildrenOptContainer<T>) -> Self {
|
||||
TypedChildrenFn(Arc::new(move || t.0.clone().into_view()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use reactive_graph::{
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
use tachys::{
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
reactive_graph::OwnedView,
|
||||
ssr::StreamBuilder,
|
||||
@@ -163,14 +163,6 @@ where
|
||||
self.children.insert_before_this(child)
|
||||
}
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<tachys::renderer::types::Element> {
|
||||
if let Some(fallback) = &self.fallback {
|
||||
fallback.elements()
|
||||
} else {
|
||||
self.children.elements()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Chil, FalFn, Fal> Render for ErrorBoundaryView<Chil, FalFn>
|
||||
@@ -276,7 +268,6 @@ where
|
||||
Fal: RenderHtml + Send + 'static,
|
||||
{
|
||||
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn>;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
|
||||
|
||||
@@ -310,7 +301,6 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
// first, attempt to serialize the children to HTML, then check for errors
|
||||
let _hook = throw_error::set_error_hook(self.hook);
|
||||
@@ -321,7 +311,6 @@ where
|
||||
&mut new_pos,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs.clone(),
|
||||
);
|
||||
|
||||
// any thrown errors would've been caught here
|
||||
@@ -334,7 +323,6 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -345,7 +333,6 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -358,7 +345,6 @@ where
|
||||
&mut new_pos,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs.clone(),
|
||||
);
|
||||
|
||||
// any thrown errors would've been caught here
|
||||
@@ -372,7 +358,6 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
buf.push_sync(&fallback);
|
||||
}
|
||||
@@ -438,10 +423,6 @@ where
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -6,10 +6,7 @@ use reactive_graph::{
|
||||
traits::Set,
|
||||
};
|
||||
use std::hash::Hash;
|
||||
use tachys::{
|
||||
reactive_graph::OwnedView,
|
||||
view::keyed::{keyed, SerializableKey},
|
||||
};
|
||||
use tachys::{reactive_graph::OwnedView, view::keyed::keyed};
|
||||
|
||||
/// Iterates over children and displays them, keyed by the `key` function given.
|
||||
///
|
||||
@@ -124,7 +121,7 @@ where
|
||||
EF: Fn(T) -> N + Send + Clone + 'static,
|
||||
N: IntoView + 'static,
|
||||
KF: Fn(&T) -> K + Send + Clone + 'static,
|
||||
K: Eq + Hash + SerializableKey + 'static,
|
||||
K: Eq + Hash + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
// this takes the owner of the For itself
|
||||
@@ -198,7 +195,7 @@ where
|
||||
EF: Fn(ReadSignal<usize>, T) -> N + Send + Clone + 'static,
|
||||
N: IntoView + 'static,
|
||||
KF: Fn(&T) -> K + Send + Clone + 'static,
|
||||
K: Eq + Hash + SerializableKey + 'static,
|
||||
K: Eq + Hash + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
// this takes the owner of the For itself
|
||||
@@ -221,7 +218,6 @@ where
|
||||
};
|
||||
move || keyed(each(), key.clone(), children.clone())
|
||||
}
|
||||
|
||||
/*
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -3,11 +3,7 @@ use leptos_dom::helpers::window;
|
||||
use leptos_server::{ServerAction, ServerMultiAction};
|
||||
use serde::de::DeserializeOwned;
|
||||
use server_fn::{
|
||||
client::Client,
|
||||
codec::PostUrl,
|
||||
error::{IntoAppError, ServerFnErrorErr},
|
||||
request::ClientReq,
|
||||
Http, ServerFn,
|
||||
client::Client, codec::PostUrl, request::ClientReq, ServerFn, ServerFnError,
|
||||
};
|
||||
use tachys::{
|
||||
either::Either,
|
||||
@@ -75,7 +71,7 @@ use web_sys::{
|
||||
/// ```
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
#[component]
|
||||
pub fn ActionForm<ServFn, OutputProtocol>(
|
||||
pub fn ActionForm<ServFn>(
|
||||
/// The action from which to build the form.
|
||||
action: ServerAction<ServFn>,
|
||||
/// A [`NodeRef`] in which the `<form>` element should be stored.
|
||||
@@ -86,7 +82,7 @@ pub fn ActionForm<ServFn, OutputProtocol>(
|
||||
) -> impl IntoView
|
||||
where
|
||||
ServFn: DeserializeOwned
|
||||
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
|
||||
+ ServerFn<InputEncoding = PostUrl>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
@@ -125,10 +121,9 @@ where
|
||||
"Error converting form field into server function \
|
||||
arguments: {err:?}"
|
||||
);
|
||||
value.set(Some(Err(ServerFnErrorErr::Serialization(
|
||||
value.set(Some(Err(ServerFnError::Serialization(
|
||||
err.to_string(),
|
||||
)
|
||||
.into_app_error())));
|
||||
))));
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
}
|
||||
@@ -151,7 +146,7 @@ where
|
||||
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
|
||||
/// progressively enhanced to use client-side routing.
|
||||
#[component]
|
||||
pub fn MultiActionForm<ServFn, OutputProtocol>(
|
||||
pub fn MultiActionForm<ServFn>(
|
||||
/// The action from which to build the form.
|
||||
action: ServerMultiAction<ServFn>,
|
||||
/// A [`NodeRef`] in which the `<form>` element should be stored.
|
||||
@@ -165,7 +160,7 @@ where
|
||||
+ Sync
|
||||
+ Clone
|
||||
+ DeserializeOwned
|
||||
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
|
||||
+ ServerFn<InputEncoding = PostUrl>
|
||||
+ 'static,
|
||||
ServFn::Output: Send + Sync + 'static,
|
||||
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
|
||||
@@ -192,10 +187,9 @@ where
|
||||
action.dispatch(new_input);
|
||||
}
|
||||
Err(err) => {
|
||||
action.dispatch_sync(Err(ServerFnErrorErr::Serialization(
|
||||
action.dispatch_sync(Err(ServerFnError::Serialization(
|
||||
err.to_string(),
|
||||
)
|
||||
.into_app_error()));
|
||||
)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,8 +52,6 @@
|
||||
mod.hydrate();
|
||||
hydrateIslands(document.body, mod);
|
||||
});
|
||||
|
||||
window.__hydrateIsland = (el, id) => hydrateIsland(el, id, mod);
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,378 +0,0 @@
|
||||
let NAVIGATION = 0;
|
||||
|
||||
window.addEventListener("click", async (ev) => {
|
||||
const req = clickToReq(ev);
|
||||
if(!req) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
await navigateToPage(req, true);
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", async (ev) => {
|
||||
const req = new Request(window.location);
|
||||
ev.preventDefault();
|
||||
await navigateToPage(req, true, true);
|
||||
});
|
||||
|
||||
window.addEventListener("submit", async (ev) => {
|
||||
const req = submitToReq(ev);
|
||||
if(!req) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
await navigateToPage(req, true);
|
||||
});
|
||||
|
||||
async function navigateToPage(
|
||||
/** @type Request */
|
||||
req,
|
||||
/** @type bool */
|
||||
useViewTransition,
|
||||
/** @type bool */
|
||||
replace
|
||||
) {
|
||||
NAVIGATION += 1;
|
||||
const currentNav = NAVIGATION;
|
||||
|
||||
// add a custom header to indicate that we're on a subsequent navigation
|
||||
req.headers.append("Islands-Router", "true");
|
||||
|
||||
// fetch the new page
|
||||
const resp = await fetch(req);
|
||||
const redirected = resp.redirected;
|
||||
const htmlString = await resp.text();
|
||||
|
||||
if(NAVIGATION === currentNav) {
|
||||
// The 'doc' variable now contains the parsed DOM
|
||||
const transition = async () => {
|
||||
try {
|
||||
diffPages(htmlString);
|
||||
for(const island of document.querySelectorAll("leptos-island")) {
|
||||
if(!island.$$hydrated) {
|
||||
__hydrateIsland(island, island.dataset.component);
|
||||
island.$$hydrated = true;
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
|
||||
if (useViewTransition && document.startViewTransition) {
|
||||
await document.startViewTransition(transition);
|
||||
} else {
|
||||
await transition()
|
||||
}
|
||||
|
||||
const url = redirected ? resp.url : req.url;
|
||||
|
||||
if(replace) {
|
||||
window.history.replaceState(undefined, null, url);
|
||||
} else {
|
||||
window.history.pushState(undefined, null, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clickToReq(ev) {
|
||||
// confirm that this is an <a> that meets our requirements
|
||||
if (
|
||||
ev.defaultPrevented ||
|
||||
ev.button !== 0 ||
|
||||
ev.metaKey ||
|
||||
ev.altKey ||
|
||||
ev.ctrlKey ||
|
||||
ev.shiftKey
|
||||
)
|
||||
return;
|
||||
|
||||
/** @type HTMLAnchorElement | undefined;*/
|
||||
const a = ev
|
||||
.composedPath()
|
||||
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
||||
|
||||
if (!a) return;
|
||||
|
||||
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
|
||||
const href = svg ? a.href.baseVal : a.href;
|
||||
const target = svg ? a.target.baseVal : a.target;
|
||||
if (target || (!href && !a.hasAttribute("state"))) return;
|
||||
|
||||
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
||||
if (a.hasAttribute("download") || (rel?.includes("external"))) return;
|
||||
|
||||
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
||||
if (
|
||||
url.origin !== window.location.origin // ||
|
||||
// TODO base
|
||||
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
|
||||
)
|
||||
return;
|
||||
|
||||
return new Request(url);
|
||||
}
|
||||
|
||||
function submitToReq(ev) {
|
||||
event.preventDefault();
|
||||
|
||||
const target = ev.target;
|
||||
/** @type HTMLFormElement */
|
||||
let form;
|
||||
if(target instanceof HTMLFormElement) {
|
||||
form = target;
|
||||
} else {
|
||||
if(!target.form) {
|
||||
return;
|
||||
}
|
||||
form = target.form;
|
||||
}
|
||||
|
||||
const method = form.method.toUpperCase();
|
||||
if(method !== "GET" && method !== "POST") {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(form.action);
|
||||
let path = url.pathname;
|
||||
const requestInit = {};
|
||||
const data = new FormData(form);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of data.entries()) {
|
||||
params.append(key, value);
|
||||
}
|
||||
|
||||
requestInit.headers = {
|
||||
Accept: "text/html"
|
||||
};
|
||||
if(method === "GET") {
|
||||
path += `?${params.toString()}`;
|
||||
}
|
||||
else {
|
||||
requestInit.method = "POST";
|
||||
requestInit.body = params;
|
||||
}
|
||||
|
||||
return new Request(
|
||||
path,
|
||||
requestInit
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function diffPages(htmlString) {
|
||||
// Use DOMParser to parse the HTML string
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
|
||||
diffRange(document, document, doc, doc);
|
||||
}
|
||||
|
||||
function diffRange(oldDocument, oldRoot, newDocument, newRoot, oldEnd, newEnd) {
|
||||
const oldDocWalker = oldDocument.createTreeWalker(oldRoot);
|
||||
const newDocWalker = newDocument.createTreeWalker(newRoot);
|
||||
let oldNode = oldDocWalker.currentNode;
|
||||
let newNode = newDocWalker.currentNode;
|
||||
|
||||
while (oldDocWalker.nextNode() && newDocWalker.nextNode()) {
|
||||
oldNode = oldDocWalker.currentNode;
|
||||
newNode = newDocWalker.currentNode;
|
||||
|
||||
if (oldNode == oldEnd || newNode == newEnd) {
|
||||
break;
|
||||
}
|
||||
|
||||
// if the nodes are different, we need to replace the old with the new
|
||||
// because of the typed view tree, this should never actually happen
|
||||
if (oldNode.nodeType !== newNode.nodeType) {
|
||||
oldNode.replaceWith(newNode);
|
||||
}
|
||||
// if it's a text node, just update the text with the new text
|
||||
else if (oldNode.nodeType === Node.TEXT_NODE) {
|
||||
oldNode.textContent = newNode.textContent;
|
||||
}
|
||||
// if it's an element, replace if it's a different tag, or update attributes
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
|
||||
diffElement(oldNode, newNode);
|
||||
}
|
||||
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
|
||||
// if one of these marker is hit, then there are two options
|
||||
// 1) it's the same branch, and we just keep walking until the end
|
||||
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
|
||||
else if (oldNode.nodeType === Node.COMMENT_NODE) {
|
||||
const oldText = oldNode.textContent;
|
||||
const newText = newNode.textContent;
|
||||
if(oldText.startsWith("bo-for")) {
|
||||
replaceFor(oldDocument, oldDocWalker, newDocument, newDocWalker, oldNode, newNode);
|
||||
}
|
||||
else if (oldText.startsWith("bo-item")) {
|
||||
// skip, this means we're diffing a new item within a For
|
||||
}
|
||||
else if(oldText.startsWith("bo") && newText !== oldText) {
|
||||
replaceBranch(oldDocWalker, newDocWalker, oldNode, newNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function replaceFor(oldDocument, oldDocWalker, newDocument, newDocWalker, oldNode, newNode) {
|
||||
oldDocWalker.nextNode();
|
||||
newDocWalker.nextNode();
|
||||
const oldRange = new Range();
|
||||
const newRange = new Range();
|
||||
let oldBranches = 1;
|
||||
let newBranches = 1;
|
||||
|
||||
const oldKeys = {};
|
||||
const newKeys = {};
|
||||
|
||||
while(oldBranches > 0) {
|
||||
const c = oldDocWalker.currentNode;
|
||||
if(c.nodeType === Node.COMMENT_NODE) {
|
||||
const t = c.textContent;
|
||||
if(t.startsWith("bo-for")) {
|
||||
oldBranches += 1;
|
||||
} else if(t.startsWith("bc-for")) {
|
||||
|
||||
oldBranches -= 1;
|
||||
} else if (t.startsWith("bo-item")) {
|
||||
const k = t.replace("bo-item-", "");
|
||||
oldKeys[k] = { open: c, close: null };
|
||||
} else if (t.startsWith("bc-item")) {
|
||||
const k = t.replace("bc-item-", "");
|
||||
oldKeys[k].close = c;
|
||||
}
|
||||
}
|
||||
oldDocWalker.nextNode();
|
||||
}
|
||||
while(newBranches > 0) {
|
||||
const c = newDocWalker.currentNode;
|
||||
if(c.nodeType === Node.COMMENT_NODE) {
|
||||
const t = c.textContent;
|
||||
if(t.startsWith("bo-for")) {
|
||||
newBranches += 1;
|
||||
} else if(t.startsWith("bc-for")) {
|
||||
|
||||
newBranches -= 1;
|
||||
} else if (t.startsWith("bo-item")) {
|
||||
const k = t.replace("bo-item-", "");
|
||||
newKeys[k] = { open: c, close: null };
|
||||
} else if (t.startsWith("bc-item")) {
|
||||
const k = t.replace("bc-item-", "");
|
||||
newKeys[k].close = c;
|
||||
}
|
||||
}
|
||||
newDocWalker.nextNode();
|
||||
}
|
||||
|
||||
for(const key in oldKeys) {
|
||||
if(newKeys[key]) {
|
||||
const oldOne = oldKeys[key];
|
||||
const newOne = newKeys[key];
|
||||
const oldRange = new Range();
|
||||
const newRange = new Range();
|
||||
|
||||
// then replace the item in the *new* list with the *old* DOM elements
|
||||
oldRange.setStartAfter(oldOne.open);
|
||||
oldRange.setEndBefore(oldOne.close);
|
||||
newRange.setStartAfter(newOne.open);
|
||||
newRange.setEndBefore(newOne.close);
|
||||
const oldContents = oldRange.extractContents();
|
||||
const newContents = newRange.extractContents();
|
||||
|
||||
// patch the *old* DOM elements with the new ones
|
||||
diffRange(oldDocument, oldContents, newDocument, newContents, oldOne.close, newOne.close);
|
||||
|
||||
// then insert the old DOM elements into the new tree
|
||||
// this means you'll end up with any new attributes or content from the server,
|
||||
// but with any old DOM state (because they are the old elements)
|
||||
newRange.insertNode(oldContents);
|
||||
newOne.open.replaceWith(oldOne.open);
|
||||
newOne.close.replaceWith(oldOne.close);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
oldRange.setStartAfter(oldNode);
|
||||
oldRange.setEndBefore(oldDocWalker.currentNode);
|
||||
newRange.setStartAfter(newNode);
|
||||
newRange.setEndAfter(newDocWalker.currentNode);
|
||||
const newContents = newRange.extractContents();
|
||||
oldRange.deleteContents();
|
||||
oldRange.insertNode(newContents);
|
||||
oldNode.replaceWith(newNode);
|
||||
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function replaceBranch(oldDocWalker, newDocWalker, oldNode, newNode) {
|
||||
oldDocWalker.nextNode();
|
||||
newDocWalker.nextNode();
|
||||
const oldRange = new Range();
|
||||
const newRange = new Range();
|
||||
let oldBranches = 1;
|
||||
let newBranches = 1;
|
||||
while(oldBranches > 0) {
|
||||
if(oldDocWalker.nextNode()) {
|
||||
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
oldBranches += 1;
|
||||
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
oldBranches -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while(newBranches > 0) {
|
||||
if(newDocWalker.nextNode()) {
|
||||
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
newBranches += 1;
|
||||
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
newBranches -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
oldRange.setStartAfter(oldNode);
|
||||
oldRange.setEndBefore(oldDocWalker.currentNode);
|
||||
newRange.setStartAfter(newNode);
|
||||
newRange.setEndAfter(newDocWalker.currentNode);
|
||||
const newContents = newRange.extractContents();
|
||||
oldRange.deleteContents();
|
||||
oldRange.insertNode(newContents);
|
||||
oldNode.replaceWith(newNode);
|
||||
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function diffElement(oldNode, newNode) {
|
||||
/** @type Element */
|
||||
const oldEl = oldNode;
|
||||
/** @type Element */
|
||||
const newEl = newNode;
|
||||
if (oldEl.tagName !== newEl.tagName) {
|
||||
oldEl.replaceWith(newEl);
|
||||
|
||||
}
|
||||
else {
|
||||
for(const attr of newEl.attributes) {
|
||||
oldEl.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(const island of document.querySelectorAll("leptos-island")) {
|
||||
island.$$hydrated = true;
|
||||
}
|
||||
@@ -50,10 +50,6 @@ pub fn HydrationScripts(
|
||||
/// Should be `true` to hydrate in `islands` mode.
|
||||
#[prop(optional)]
|
||||
islands: bool,
|
||||
/// Should be `true` to add the “islands router,” which enables limited client-side routing
|
||||
/// when running in islands mode.
|
||||
#[prop(optional)]
|
||||
islands_router: bool,
|
||||
/// A base url, not including a trailing slash
|
||||
#[prop(optional, into)]
|
||||
root: Option<String>,
|
||||
@@ -102,36 +98,18 @@ pub fn HydrationScripts(
|
||||
include_str!("./hydration_script.js")
|
||||
};
|
||||
|
||||
let islands_router = islands_router
|
||||
.then_some(include_str!("./islands_routing.js"))
|
||||
.unwrap_or_default();
|
||||
|
||||
let root = root.unwrap_or_default();
|
||||
use_context::<IslandsRouterNavigation>().is_none().then(|| {
|
||||
view! {
|
||||
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
|
||||
<link
|
||||
rel="preload"
|
||||
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
|
||||
r#as="fetch"
|
||||
r#type="application/wasm"
|
||||
crossorigin=nonce.clone().unwrap_or_default()
|
||||
/>
|
||||
<script type="module" nonce=nonce>
|
||||
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
|
||||
</script>
|
||||
}
|
||||
})
|
||||
view! {
|
||||
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
|
||||
<link
|
||||
rel="preload"
|
||||
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
|
||||
r#as="fetch"
|
||||
r#type="application/wasm"
|
||||
crossorigin=nonce.clone().unwrap_or_default()
|
||||
/>
|
||||
<script type="module" nonce=nonce>
|
||||
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?})")}
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
/// If this is provided via context, it means that you are using the islands router and
|
||||
/// this is a subsequent navigation, made from the client.
|
||||
///
|
||||
/// This should be provided automatically by a server integration if it detects that the
|
||||
/// header `Islands-Router` is present in the request.
|
||||
///
|
||||
/// This is used to determine how much of the hydration script to include in the page.
|
||||
/// If it is present, then the contents of the `<HydrationScripts>` component will not be
|
||||
/// included, as they only need to be sent to the client once.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IslandsRouterNavigation;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
use tachys::{
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
@@ -87,7 +87,6 @@ impl<T: Render> Render for View<T> {
|
||||
|
||||
impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
type AsyncOutput = T::AsyncOutput;
|
||||
type Owned = View<T::Owned>;
|
||||
|
||||
const MIN_LENGTH: usize = <T as RenderHtml>::MIN_LENGTH;
|
||||
|
||||
@@ -105,7 +104,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
let vm = self.view_marker.to_owned();
|
||||
@@ -114,13 +112,8 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
buf.push_str(&format!("<!--hot-reload|{vm}|open-->"));
|
||||
}
|
||||
|
||||
self.inner.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
self.inner
|
||||
.to_html_with_buf(buf, position, escape, mark_branches);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
@@ -134,7 +127,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -150,7 +142,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -166,14 +157,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
) -> Self::State {
|
||||
self.inner.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
View {
|
||||
inner: self.inner.into_owned(),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: self.view_marker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToTemplate> ToTemplate for View<T> {
|
||||
|
||||
@@ -172,10 +172,12 @@ pub mod prelude {
|
||||
actions::*, computed::*, effect::*, graph::untrack, owner::*,
|
||||
signal::*, wrappers::read::*,
|
||||
};
|
||||
pub use server_fn::{self, error::ServerFnError};
|
||||
pub use server_fn::{self, ServerFnError};
|
||||
pub use tachys::{
|
||||
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
|
||||
view::{fragment::Fragment, template::ViewTemplate},
|
||||
view::{
|
||||
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
|
||||
},
|
||||
};
|
||||
}
|
||||
pub use export_types::*;
|
||||
|
||||
@@ -19,7 +19,7 @@ use slotmap::{DefaultKey, SlotMap};
|
||||
use std::sync::Arc;
|
||||
use tachys::{
|
||||
either::Either,
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
reactive_graph::{OwnedView, OwnedViewState},
|
||||
ssr::StreamBuilder,
|
||||
@@ -247,7 +247,6 @@ where
|
||||
// i.e., if this is the child of another Suspense during SSR, don't wait for it: it will handle
|
||||
// itself
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
|
||||
|
||||
@@ -263,15 +262,9 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
self.fallback.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
self.fallback
|
||||
.to_html_with_buf(buf, position, escape, mark_branches);
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
@@ -280,7 +273,6 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -379,7 +371,6 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
Some(None) => {
|
||||
@@ -389,7 +380,6 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
@@ -403,14 +393,12 @@ where
|
||||
self.fallback,
|
||||
&mut fallback_position,
|
||||
mark_branches,
|
||||
extra_attrs.clone(),
|
||||
);
|
||||
buf.push_async_out_of_order_with_nonce(
|
||||
fut,
|
||||
position,
|
||||
mark_branches,
|
||||
nonce_or_not(),
|
||||
extra_attrs,
|
||||
);
|
||||
} else {
|
||||
buf.push_async({
|
||||
@@ -426,7 +414,6 @@ where
|
||||
&mut position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
builder.finish().take_chunks()
|
||||
}
|
||||
@@ -476,10 +463,6 @@ where
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper that prevents [`Suspense`] from waiting for any resource reads that happen inside
|
||||
@@ -532,7 +515,6 @@ where
|
||||
T: RenderHtml + 'static,
|
||||
{
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = T::MIN_LENGTH;
|
||||
|
||||
@@ -548,15 +530,8 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
(self.0)().to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
(self.0)().to_html_with_buf(buf, position, escape, mark_branches);
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
@@ -565,7 +540,6 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -574,7 +548,6 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -585,8 +558,4 @@ where
|
||||
) -> Self::State {
|
||||
(self.0)().hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +76,9 @@ impl Default for TextProp {
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for TextProp {
|
||||
type Output = Arc<dyn Fn() -> Oco<'static, str> + Send + Sync>;
|
||||
type Output = Oco<'static, str>;
|
||||
|
||||
fn into_attribute_value(self) -> Self::Output {
|
||||
self.0
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,8 @@ use typed_builder::TypedBuilder;
|
||||
|
||||
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
|
||||
/// occur with LeptosOptions
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[derive(Clone, Debug, serde::Deserialize, Default)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[non_exhaustive]
|
||||
pub struct ConfFile {
|
||||
pub leptos_options: LeptosOptions,
|
||||
}
|
||||
@@ -25,14 +24,9 @@ pub struct ConfFile {
|
||||
/// It shares keys with cargo-leptos, to allow for easy interoperability
|
||||
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[non_exhaustive]
|
||||
pub struct LeptosOptions {
|
||||
/// The name of the WASM and JS files generated by wasm-bindgen.
|
||||
///
|
||||
/// This should match the name that will be output when building your application.
|
||||
///
|
||||
/// You can easily set this using `env!("CARGO_CRATE_NAME")`.
|
||||
#[builder(setter(into))]
|
||||
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
|
||||
#[builder(setter(into), default=default_output_name())]
|
||||
pub output_name: Arc<str>,
|
||||
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
|
||||
/// tools.
|
||||
@@ -84,40 +78,6 @@ pub struct LeptosOptions {
|
||||
#[builder(default = default_hash_files())]
|
||||
#[serde(default = "default_hash_files")]
|
||||
pub hash_files: bool,
|
||||
/// The default prefix to use for server functions when generating API routes. Can be
|
||||
/// overridden for individual functions using `#[server(prefix = "...")]` as usual.
|
||||
///
|
||||
/// This is useful to override the default prefix (`/api`) for all server functions without
|
||||
/// needing to manually specify via `#[server(prefix = "...")]` on every server function.
|
||||
#[builder(default, setter(strip_option))]
|
||||
#[serde(default)]
|
||||
pub server_fn_prefix: Option<String>,
|
||||
/// Whether to disable appending the server functions' hashes to the end of their API names.
|
||||
///
|
||||
/// This is useful when an app's client side needs a stable server API. For example, shipping
|
||||
/// the CSR WASM binary in a Tauri app. Tauri app releases are dependent on each platform's
|
||||
/// distribution method (e.g., the Apple App Store or the Google Play Store), which typically
|
||||
/// are much slower than the frequency at which a website can be updated. In addition, it's
|
||||
/// common for users to not have the latest app version installed. In these cases, the CSR WASM
|
||||
/// app would need to be able to continue calling the backend server function API, so the API
|
||||
/// path needs to be consistent and not have a hash appended.
|
||||
///
|
||||
/// Note that the hash suffixes is intended as a way to ensure duplicate API routes are created.
|
||||
/// Without the hash, server functions will need to have unique names to avoid creating
|
||||
/// duplicate routes. Axum will throw an error if a duplicate route is added to the router, but
|
||||
/// Actix will not.
|
||||
#[builder(default)]
|
||||
#[serde(default)]
|
||||
pub disable_server_fn_hash: bool,
|
||||
/// Include the module path of the server function in the API route. This is an alternative
|
||||
/// strategy to prevent duplicate server function API routes (the default strategy is to add
|
||||
/// a hash to the end of the route). Each element of the module path will be separated by a `/`.
|
||||
/// For example, a server function with a fully qualified name of `parent::child::server_fn`
|
||||
/// would have an API route of `/api/parent/child/server_fn` (possibly with a
|
||||
/// different prefix and a hash suffix depending on the values of the other server fn configs).
|
||||
#[builder(default)]
|
||||
#[serde(default)]
|
||||
pub server_fn_mod_path: bool,
|
||||
}
|
||||
|
||||
impl LeptosOptions {
|
||||
@@ -160,14 +120,20 @@ impl LeptosOptions {
|
||||
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?
|
||||
.into(),
|
||||
hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?,
|
||||
server_fn_prefix: env_wo_default("SERVER_FN_PREFIX")?,
|
||||
disable_server_fn_hash: env_wo_default("DISABLE_SERVER_FN_HASH")?
|
||||
.is_some(),
|
||||
server_fn_mod_path: env_wo_default("SERVER_FN_MOD_PATH")?.is_some(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LeptosOptions {
|
||||
fn default() -> Self {
|
||||
LeptosOptions::builder().build()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_output_name() -> Arc<str> {
|
||||
env!("CARGO_CRATE_NAME").replace('-', "_").into()
|
||||
}
|
||||
|
||||
fn default_site_root() -> Arc<str> {
|
||||
".".into()
|
||||
}
|
||||
|
||||
@@ -51,13 +51,6 @@ trace-component-props = []
|
||||
actix = ["server_fn_macro/actix"]
|
||||
axum = ["server_fn_macro/axum"]
|
||||
generic = ["server_fn_macro/generic"]
|
||||
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
|
||||
# https://github.com/rust-lang/cargo/issues/4423
|
||||
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
|
||||
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
|
||||
# This is automatic as long as the leptos crate is depended upon,
|
||||
# downstream usage should never manually enable this feature.
|
||||
__internal_erase_components = []
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly", "tracing", "trace-component-props"]
|
||||
@@ -90,3 +83,9 @@ skip_feature_sets = [
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(leptos_debuginfo)',
|
||||
'cfg(erase_components)',
|
||||
] }
|
||||
|
||||
@@ -11,13 +11,13 @@ dependencies = [
|
||||
[tasks.test-leptos_macro-example]
|
||||
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly-2025-03-05", "test", "--doc"]
|
||||
args = ["+nightly-2024-08-01", "test", "--doc"]
|
||||
cwd = "example"
|
||||
install_crate = false
|
||||
|
||||
[tasks.doc-leptos_macro-example]
|
||||
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly-2025-03-05", "doc"]
|
||||
args = ["+nightly-2024-08-01", "doc"]
|
||||
cwd = "example"
|
||||
install_crate = false
|
||||
|
||||
@@ -32,8 +32,6 @@ pub struct Model {
|
||||
impl Parse for Model {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let mut item = ItemFn::parse(input)?;
|
||||
maybe_modify_return_type(&mut item.sig.output);
|
||||
|
||||
convert_impl_trait_to_generic(&mut item.sig);
|
||||
|
||||
let docs = Docs::new(&item.attrs);
|
||||
@@ -78,39 +76,6 @@ impl Parse for Model {
|
||||
}
|
||||
}
|
||||
|
||||
/// Exists to fix nested routes defined in a separate component in erased mode,
|
||||
/// by replacing the return type with AnyNestedRoute, which is what it'll be, but is required as the return type for compiler inference.
|
||||
fn maybe_modify_return_type(ret: &mut ReturnType) {
|
||||
#[cfg(feature = "__internal_erase_components")]
|
||||
{
|
||||
if let ReturnType::Type(_, ty) = ret {
|
||||
if let Type::ImplTrait(TypeImplTrait { bounds, .. }) = ty.as_ref() {
|
||||
// If one of the bounds is MatchNestedRoutes, we need to replace the return type with AnyNestedRoute:
|
||||
if bounds.iter().any(|bound| {
|
||||
if let syn::TypeParamBound::Trait(trait_bound) = bound {
|
||||
if trait_bound.path.segments.iter().any(
|
||||
|path_segment| {
|
||||
path_segment.ident == "MatchNestedRoutes"
|
||||
},
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}) {
|
||||
*ty = parse_quote!(
|
||||
::leptos_router::any_nested_route::AnyNestedRoute
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "__internal_erase_components"))]
|
||||
{
|
||||
let _ = ret;
|
||||
}
|
||||
}
|
||||
|
||||
// implemented manually because Vec::drain_filter is nightly only
|
||||
// follows std recommended parallel
|
||||
pub fn drain_filter<T>(
|
||||
@@ -331,9 +296,9 @@ impl ToTokens for Model {
|
||||
|
||||
let component = if *is_transparent {
|
||||
body_expr
|
||||
} else if cfg!(feature = "__internal_erase_components") {
|
||||
} else if cfg!(erase_components) {
|
||||
quote! {
|
||||
::leptos::prelude::IntoMaybeErased::into_maybe_erased(
|
||||
::leptos::prelude::IntoAny::into_any(
|
||||
::leptos::reactive::graph::untrack_with_diagnostics(
|
||||
move || {
|
||||
#tracing_guard_expr
|
||||
@@ -648,8 +613,7 @@ impl Parse for DummyModel {
|
||||
drain_filter(&mut attrs, |attr| !attr.path().is_ident("doc"));
|
||||
|
||||
let vis: Visibility = input.parse()?;
|
||||
let mut sig: Signature = input.parse()?;
|
||||
maybe_modify_return_type(&mut sig.output);
|
||||
let sig: Signature = input.parse()?;
|
||||
|
||||
// The body is left untouched, so it will not cause an error
|
||||
// even if the syntax is invalid.
|
||||
|
||||
@@ -281,11 +281,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
#[proc_macro]
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
pub fn template(tokens: TokenStream) -> TokenStream {
|
||||
if cfg!(feature = "__internal_erase_components") {
|
||||
view(tokens)
|
||||
} else {
|
||||
view_macro_impl(tokens, true)
|
||||
}
|
||||
view_macro_impl(tokens, true)
|
||||
}
|
||||
|
||||
fn view_macro_impl(tokens: TokenStream, template: bool) -> TokenStream {
|
||||
@@ -927,7 +923,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
args.into(),
|
||||
s.into(),
|
||||
Some(syn::parse_quote!(::leptos::server_fn)),
|
||||
option_env!("SERVER_FN_PREFIX").unwrap_or("/api"),
|
||||
"/api",
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
use super::{
|
||||
fragment_to_tokens, utils::is_nostrip_optional_and_update_key, TagType,
|
||||
};
|
||||
use crate::view::{
|
||||
attribute_absolute, text_to_tokens, utils::filter_prefixed_attrs,
|
||||
};
|
||||
use crate::view::{attribute_absolute, utils::filter_prefixed_attrs};
|
||||
use proc_macro2::{Ident, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use rstml::node::{
|
||||
CustomNode, KeyedAttributeValue, Node, NodeAttribute, NodeBlock,
|
||||
NodeElement, NodeName,
|
||||
CustomNode, KeyedAttributeValue, NodeAttribute, NodeBlock, NodeElement,
|
||||
NodeName,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use syn::{
|
||||
spanned::Spanned, Expr, ExprPath, ExprRange, Item, RangeLimits, Stmt,
|
||||
};
|
||||
use syn::{spanned::Spanned, Expr, ExprPath, ExprRange, RangeLimits, Stmt};
|
||||
|
||||
pub(crate) fn component_to_tokens(
|
||||
node: &mut NodeElement<impl CustomNode>,
|
||||
@@ -174,14 +170,8 @@ pub(crate) fn component_to_tokens(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let spreads = (!(spreads.is_empty())).then(|| {
|
||||
if cfg!(feature = "__internal_erase_components") {
|
||||
quote! {
|
||||
.add_any_attr(vec![#(#spreads.into_any_attr(),)*])
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.add_any_attr((#(#spreads,)*))
|
||||
}
|
||||
quote! {
|
||||
.add_any_attr((#(#spreads,)*).into_attr())
|
||||
}
|
||||
});
|
||||
|
||||
@@ -201,12 +191,6 @@ pub(crate) fn component_to_tokens(
|
||||
let mut slots = HashMap::new();
|
||||
let children = if node.children.is_empty() {
|
||||
quote! {}
|
||||
} else if let Some(children) = maybe_optimised_component_children(
|
||||
&node.children,
|
||||
&items_to_bind,
|
||||
&items_to_clone,
|
||||
) {
|
||||
children
|
||||
} else {
|
||||
let children = fragment_to_tokens(
|
||||
&mut node.children,
|
||||
@@ -235,7 +219,10 @@ pub(crate) fn component_to_tokens(
|
||||
let bindables =
|
||||
items_to_bind.iter().map(|ident| quote! { #ident, });
|
||||
|
||||
let clonables = items_to_clone_to_tokens(&items_to_clone);
|
||||
let clonables = items_to_clone.iter().map(|ident| {
|
||||
let ident_ref = quote_spanned!(ident.span()=> &#ident);
|
||||
quote! { let #ident = ::core::clone::Clone::clone(#ident_ref); }
|
||||
});
|
||||
|
||||
if bindables.len() > 0 {
|
||||
quote_spanned! {children.span()=>
|
||||
@@ -326,111 +313,3 @@ fn is_attr_let(key: &NodeName) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn items_to_clone_to_tokens<'a>(
|
||||
items_to_clone: &'a [Ident],
|
||||
) -> impl Iterator<Item = TokenStream> + 'a {
|
||||
items_to_clone.iter().map(|ident| {
|
||||
let ident_ref = quote_spanned!(ident.span()=> &#ident);
|
||||
quote! { let #ident = ::core::clone::Clone::clone(#ident_ref); }
|
||||
})
|
||||
}
|
||||
|
||||
/// By default all children are placed in an outer closure || #children.
|
||||
/// This is to work with all the variants of the leptos::children::ToChildren::to_children trait.
|
||||
/// Strings are optimised to be passed without the wrapping closure, providing significant compile time and binary size improvements.
|
||||
pub fn maybe_optimised_component_children(
|
||||
children: &[Node<impl CustomNode>],
|
||||
items_to_bind: &[TokenStream],
|
||||
items_to_clone: &[Ident],
|
||||
) -> Option<TokenStream> {
|
||||
// If there are bindables will have to be in a closure:
|
||||
if !items_to_bind.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Filter out comments:
|
||||
let mut children_iter = children
|
||||
.iter()
|
||||
.filter(|child| !matches!(child, Node::Comment(_)));
|
||||
|
||||
let children = if let Some(child) = children_iter.next() {
|
||||
// If more than one child after filtering out comments, don't think we can optimise:
|
||||
if children_iter.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
match child {
|
||||
Node::Text(text) => text_to_tokens(&text.value),
|
||||
Node::RawText(raw) => {
|
||||
let text = raw.to_string_best();
|
||||
let text = syn::LitStr::new(&text, raw.span());
|
||||
text_to_tokens(&text)
|
||||
}
|
||||
// Specifically allow std macros that produce strings:
|
||||
Node::Block(NodeBlock::ValidBlock(block)) => {
|
||||
fn is_supported(mac: &syn::Macro) -> bool {
|
||||
for string_macro in ["format", "include_str"] {
|
||||
if mac.path.is_ident(string_macro) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
if block.stmts.len() > 1 {
|
||||
return None;
|
||||
} else if let Some(stmt) = block.stmts.first() {
|
||||
match stmt {
|
||||
Stmt::Macro(mac) => {
|
||||
// eprintln!("Macro: {:?}", mac.mac.path);
|
||||
if is_supported(&mac.mac) {
|
||||
quote! { #block }
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Stmt::Item(Item::Macro(mac)) => {
|
||||
// eprintln!("Item Macro: {:?}", mac.mac.path);
|
||||
if is_supported(&mac.mac) {
|
||||
quote! { #block }
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Stmt::Expr(Expr::Macro(mac), _) => {
|
||||
// eprintln!("Expr Macro: {:?}", mac.mac.path);
|
||||
if is_supported(&mac.mac) {
|
||||
quote! { #block }
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
} else {
|
||||
return Some(quote! {});
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// // Debug check to see how many use this optimisation:
|
||||
// static COUNT: std::sync::atomic::AtomicUsize =
|
||||
// std::sync::atomic::AtomicUsize::new(0);
|
||||
// COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
// eprintln!(
|
||||
// "Optimised children: {}",
|
||||
// COUNT.load(std::sync::atomic::Ordering::Relaxed)
|
||||
// );
|
||||
|
||||
let clonables = items_to_clone_to_tokens(items_to_clone);
|
||||
Some(quote_spanned! {children.span()=>
|
||||
.children({
|
||||
#(#clonables)*
|
||||
|
||||
::leptos::children::ToChildren::to_children(::leptos::children::ChildrenOptContainer(#children))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -428,12 +428,6 @@ fn element_children_to_tokens(
|
||||
{ #child }
|
||||
)
|
||||
})
|
||||
} else if cfg!(feature = "__internal_erase_components") {
|
||||
Some(quote! {
|
||||
.child(
|
||||
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
|
||||
)
|
||||
})
|
||||
} else if children.len() > 16 {
|
||||
// implementations of various traits used in routing and rendering are implemented for
|
||||
// tuples of sizes 0, 1, 2, 3, ... N. N varies but is > 16. The traits are also implemented
|
||||
@@ -479,10 +473,6 @@ fn fragment_to_tokens(
|
||||
None
|
||||
} else if children.len() == 1 {
|
||||
children.into_iter().next()
|
||||
} else if cfg!(feature = "__internal_erase_components") {
|
||||
Some(quote! {
|
||||
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
|
||||
})
|
||||
} else if children.len() > 16 {
|
||||
// implementations of various traits used in routing and rendering are implemented for
|
||||
// tuples of sizes 0, 1, 2, 3, ... N. N varies but is > 16. The traits are also implemented
|
||||
@@ -767,18 +757,10 @@ pub(crate) fn element_to_tokens(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg!(feature = "__internal_erase_components") {
|
||||
Some(quote! {
|
||||
vec![#(#attributes.into_any_attr(),)*]
|
||||
#(.add_any_attr(#additions))*
|
||||
})
|
||||
} else {
|
||||
Some(quote! {
|
||||
(#(#attributes,)*)
|
||||
#(.add_any_attr(#additions))*
|
||||
})
|
||||
}
|
||||
Some(quote! {
|
||||
(#(#attributes,)*)
|
||||
#(.add_any_attr(#additions))*
|
||||
})
|
||||
} else {
|
||||
let tag = name.to_string();
|
||||
// collect close_tag name to emit semantic information for IDE.
|
||||
@@ -1175,14 +1157,8 @@ pub(crate) fn two_way_binding_to_tokens(
|
||||
let ident =
|
||||
format_ident!("{}", name.to_case(UpperCamel), span = node.key.span());
|
||||
|
||||
if name == "group" {
|
||||
quote! {
|
||||
.bind(leptos::tachys::reactive_graph::bind::#ident, #value)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.bind(::leptos::attr::#ident, #value)
|
||||
}
|
||||
quote! {
|
||||
.bind(::leptos::attr::#ident, #value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use super::{
|
||||
component_builder::maybe_optimised_component_children,
|
||||
convert_to_snake_case, ident_from_tag_name,
|
||||
};
|
||||
use super::{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};
|
||||
use quote::{quote, quote_spanned};
|
||||
@@ -73,10 +70,7 @@ pub(crate) fn slot_to_tokens(
|
||||
}
|
||||
});
|
||||
|
||||
let items_to_bind = filter_prefixed_attrs(attrs.iter(), "let:")
|
||||
.into_iter()
|
||||
.map(|ident| quote! { #ident })
|
||||
.collect::<Vec<_>>();
|
||||
let items_to_bind = filter_prefixed_attrs(attrs.iter(), "let:");
|
||||
|
||||
let items_to_clone = filter_prefixed_attrs(attrs.iter(), "clone:");
|
||||
|
||||
@@ -89,7 +83,7 @@ pub(crate) fn slot_to_tokens(
|
||||
let value = attr.value().map(|v| {
|
||||
quote! { #v }
|
||||
})?;
|
||||
Some(quote! { (#name, #value) })
|
||||
Some(quote! { (#name, ::leptos::IntoAttribute::into_attribute(#value)) })
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -102,12 +96,6 @@ pub(crate) fn slot_to_tokens(
|
||||
let mut slots = HashMap::new();
|
||||
let children = if node.children.is_empty() {
|
||||
quote! {}
|
||||
} else if let Some(children) = maybe_optimised_component_children(
|
||||
&node.children,
|
||||
&items_to_bind,
|
||||
&items_to_clone,
|
||||
) {
|
||||
children
|
||||
} else {
|
||||
let children = fragment_to_tokens(
|
||||
&mut node.children,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub mod tests {
|
||||
|
||||
use leptos::{
|
||||
server,
|
||||
server_fn::{codec, Http, ServerFn, ServerFnError},
|
||||
server_fn::{codec, ServerFn, ServerFnError},
|
||||
};
|
||||
use std::any::TypeId;
|
||||
|
||||
@@ -18,8 +19,8 @@ pub mod tests {
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,8 +32,8 @@ pub mod tests {
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::Cbor, codec::Cbor>>()
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::Cbor>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,8 +45,8 @@ pub mod tests {
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::Cbor, codec::Cbor>>()
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::Cbor>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,8 +58,8 @@ pub mod tests {
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/api/my_path");
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,8 +74,8 @@ pub mod tests {
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,8 +91,8 @@ pub mod tests {
|
||||
"/foo/bar/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,8 +108,8 @@ pub mod tests {
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::GetUrl, codec::Json>>()
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::GetUrl>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,8 +124,8 @@ pub mod tests {
|
||||
"/api/path/to/my/endpoint"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[cfg(not(feature = "__internal_erase_components"))]
|
||||
#[cfg(not(erase_components))]
|
||||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_server"
|
||||
# TODO revert to { workspace = true } before 0.8.0 release
|
||||
# this is a hack because I missing bumping the hydration_context version number before publishing
|
||||
version = "0.8.0-beta"
|
||||
version = { workspace = true }
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
|
||||
@@ -3,7 +3,7 @@ use reactive_graph::{
|
||||
owner::use_context,
|
||||
traits::DefinedAt,
|
||||
};
|
||||
use server_fn::{error::FromServerFnError, ServerFn};
|
||||
use server_fn::{error::ServerFnErrorSerde, ServerFn, ServerFnError};
|
||||
use std::{ops::Deref, panic::Location, sync::Arc};
|
||||
|
||||
/// An error that can be caused by a server action.
|
||||
@@ -42,7 +42,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
inner: ArcAction<S, Result<S::Output, S::Error>>,
|
||||
inner: ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
@@ -52,14 +52,13 @@ where
|
||||
S: ServerFn + Clone + Send + Sync + 'static,
|
||||
S::Output: Send + Sync + 'static,
|
||||
S::Error: Send + Sync + 'static,
|
||||
S::Error: FromServerFnError,
|
||||
{
|
||||
/// Creates a new [`ArcAction`] that will call the server function `S` when dispatched.
|
||||
#[track_caller]
|
||||
pub fn new() -> Self {
|
||||
let err = use_context::<ServerActionError>().and_then(|error| {
|
||||
(error.path() == S::PATH)
|
||||
.then(|| S::Error::de(error.err()))
|
||||
.then(|| ServerFnError::<S::Error>::de(error.err()))
|
||||
.map(Err)
|
||||
});
|
||||
Self {
|
||||
@@ -77,7 +76,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
type Target = ArcAction<S, Result<S::Output, S::Error>>;
|
||||
type Target = ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
@@ -132,7 +131,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
inner: Action<S, Result<S::Output, S::Error>>,
|
||||
inner: Action<S, Result<S::Output, ServerFnError<S::Error>>>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
@@ -147,7 +146,7 @@ where
|
||||
pub fn new() -> Self {
|
||||
let err = use_context::<ServerActionError>().and_then(|error| {
|
||||
(error.path() == S::PATH)
|
||||
.then(|| S::Error::de(error.err()))
|
||||
.then(|| ServerFnError::<S::Error>::de(error.err()))
|
||||
.map(Err)
|
||||
});
|
||||
Self {
|
||||
@@ -183,14 +182,15 @@ where
|
||||
S::Output: Send + Sync + 'static,
|
||||
S::Error: Send + Sync + 'static,
|
||||
{
|
||||
type Target = Action<S, Result<S::Output, S::Error>>;
|
||||
type Target = Action<S, Result<S::Output, ServerFnError<S::Error>>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> From<ServerAction<S>> for Action<S, Result<S::Output, S::Error>>
|
||||
impl<S> From<ServerAction<S>>
|
||||
for Action<S, Result<S::Output, ServerFnError<S::Error>>>
|
||||
where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
|
||||
@@ -79,7 +79,7 @@ mod view_implementations {
|
||||
use reactive_graph::traits::Read;
|
||||
use std::future::Future;
|
||||
use tachys::{
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
reactive_graph::{RenderEffectState, Suspend, SuspendState},
|
||||
ssr::StreamBuilder,
|
||||
@@ -135,7 +135,6 @@ mod view_implementations {
|
||||
Ser: Send + 'static,
|
||||
{
|
||||
type AsyncOutput = Option<T>;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
@@ -153,14 +152,12 @@ mod view_implementations {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
(move || Suspend::new(async move { self.await })).to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -170,7 +167,6 @@ mod view_implementations {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -180,7 +176,6 @@ mod view_implementations {
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -192,9 +187,5 @@ mod view_implementations {
|
||||
(move || Suspend::new(async move { self.await }))
|
||||
.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,13 @@ use reactive_graph::{
|
||||
ToAnySource, ToAnySubscriber,
|
||||
},
|
||||
owner::use_context,
|
||||
send_wrapper_ext::MaybeSendWrapperOption,
|
||||
signal::{
|
||||
guards::{AsyncPlain, Mapped, ReadGuard},
|
||||
guards::{AsyncPlain, ReadGuard},
|
||||
ArcRwSignal, RwSignal,
|
||||
},
|
||||
traits::{
|
||||
DefinedAt, IsDisposed, ReadUntracked, Track, Update, With, Write,
|
||||
},
|
||||
traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Update, Write},
|
||||
};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::{
|
||||
future::{pending, Future, IntoFuture},
|
||||
panic::Location,
|
||||
@@ -24,7 +22,7 @@ use std::{
|
||||
|
||||
/// A reference-counted resource that only loads its data locally on the client.
|
||||
pub struct ArcLocalResource<T> {
|
||||
data: ArcAsyncDerived<T>,
|
||||
data: ArcAsyncDerived<SendWrapper<T>>,
|
||||
refetch: ArcRwSignal<usize>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
@@ -71,12 +69,14 @@ impl<T> ArcLocalResource<T> {
|
||||
}
|
||||
}
|
||||
};
|
||||
let fetcher = SendWrapper::new(fetcher);
|
||||
let refetch = ArcRwSignal::new(0);
|
||||
let data = {
|
||||
let refetch = refetch.clone();
|
||||
ArcAsyncDerived::new_unsync(move || {
|
||||
ArcAsyncDerived::new(move || {
|
||||
refetch.track();
|
||||
fetcher()
|
||||
let fut = fetcher();
|
||||
SendWrapper::new(async move { SendWrapper::new(fut.await) })
|
||||
})
|
||||
};
|
||||
Self {
|
||||
@@ -91,34 +91,6 @@ impl<T> ArcLocalResource<T> {
|
||||
pub fn refetch(&self) {
|
||||
*self.refetch.write() += 1;
|
||||
}
|
||||
|
||||
/// Synchronously, reactively reads the current value of the resource and applies the function
|
||||
/// `f` to its value if it is `Some(_)`.
|
||||
#[track_caller]
|
||||
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.data.try_with(|n| n.as_ref().map(f))?
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> ArcLocalResource<Result<T, E>>
|
||||
where
|
||||
T: 'static,
|
||||
E: Clone + 'static,
|
||||
{
|
||||
/// Applies the given function when a resource that returns `Result<T, E>`
|
||||
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
|
||||
/// calls over the `Option<Result<_, _>>` returned by the resource.
|
||||
///
|
||||
/// This is useful when used with features like server functions, in conjunction
|
||||
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
|
||||
/// left to handle the `None` and `Err(_)` states.
|
||||
#[track_caller]
|
||||
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
|
||||
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoFuture for ArcLocalResource<T>
|
||||
@@ -126,9 +98,14 @@ where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
type Output = T;
|
||||
type IntoFuture = AsyncDerivedFuture<T>;
|
||||
type IntoFuture = futures::future::Map<
|
||||
AsyncDerivedFuture<SendWrapper<T>>,
|
||||
fn(SendWrapper<T>) -> T,
|
||||
>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
use futures::FutureExt;
|
||||
|
||||
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||
notifier.notify();
|
||||
} else if cfg!(feature = "ssr") {
|
||||
@@ -138,7 +115,7 @@ where
|
||||
always pending on the server."
|
||||
);
|
||||
}
|
||||
self.data.into_future()
|
||||
self.data.into_future().map(|value| (*value).clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,14 +136,18 @@ impl<T> ReadUntracked for ArcLocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value = ReadGuard<
|
||||
Option<T>,
|
||||
Mapped<AsyncPlain<MaybeSendWrapperOption<T>>, Option<T>>,
|
||||
>;
|
||||
type Value =
|
||||
ReadGuard<Option<SendWrapper<T>>, AsyncPlain<Option<SendWrapper<T>>>>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||
notifier.notify();
|
||||
} else if cfg!(feature = "ssr") {
|
||||
panic!(
|
||||
"Reading from a LocalResource outside Suspense in `ssr` mode \
|
||||
will cause the response to hang, because LocalResources are \
|
||||
always pending on the server."
|
||||
);
|
||||
}
|
||||
self.data.try_read_untracked()
|
||||
}
|
||||
@@ -235,7 +216,7 @@ impl<T> Subscriber for ArcLocalResource<T> {
|
||||
|
||||
/// A resource that only loads its data locally on the client.
|
||||
pub struct LocalResource<T> {
|
||||
data: AsyncDerived<T>,
|
||||
data: AsyncDerived<SendWrapper<T>>,
|
||||
refetch: RwSignal<usize>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
@@ -285,9 +266,11 @@ impl<T> LocalResource<T> {
|
||||
data: if cfg!(feature = "ssr") {
|
||||
AsyncDerived::new_mock(fetcher)
|
||||
} else {
|
||||
AsyncDerived::new_unsync_threadsafe_storage(move || {
|
||||
let fetcher = SendWrapper::new(fetcher);
|
||||
AsyncDerived::new(move || {
|
||||
refetch.track();
|
||||
fetcher()
|
||||
let fut = fetcher();
|
||||
SendWrapper::new(async move { SendWrapper::new(fut.await) })
|
||||
})
|
||||
},
|
||||
refetch,
|
||||
@@ -307,9 +290,14 @@ where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
type Output = T;
|
||||
type IntoFuture = AsyncDerivedFuture<T>;
|
||||
type IntoFuture = futures::future::Map<
|
||||
AsyncDerivedFuture<SendWrapper<T>>,
|
||||
fn(SendWrapper<T>) -> T,
|
||||
>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
use futures::FutureExt;
|
||||
|
||||
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||
notifier.notify();
|
||||
} else if cfg!(feature = "ssr") {
|
||||
@@ -319,7 +307,7 @@ where
|
||||
always pending on the server."
|
||||
);
|
||||
}
|
||||
self.data.into_future()
|
||||
self.data.into_future().map(|value| (*value).clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,14 +328,18 @@ impl<T> ReadUntracked for LocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value = ReadGuard<
|
||||
Option<T>,
|
||||
Mapped<AsyncPlain<MaybeSendWrapperOption<T>>, Option<T>>,
|
||||
>;
|
||||
type Value =
|
||||
ReadGuard<Option<SendWrapper<T>>, AsyncPlain<Option<SendWrapper<T>>>>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||
notifier.notify();
|
||||
} else if cfg!(feature = "ssr") {
|
||||
panic!(
|
||||
"Reading from a LocalResource outside Suspense in `ssr` mode \
|
||||
will cause the response to hang, because LocalResources are \
|
||||
always pending on the server."
|
||||
);
|
||||
}
|
||||
self.data.try_read_untracked()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use reactive_graph::{
|
||||
actions::{ArcMultiAction, MultiAction},
|
||||
traits::DefinedAt,
|
||||
};
|
||||
use server_fn::ServerFn;
|
||||
use server_fn::{ServerFn, ServerFnError};
|
||||
use std::{ops::Deref, panic::Location};
|
||||
|
||||
/// An [`ArcMultiAction`] that can be used to call a server function.
|
||||
@@ -11,7 +11,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
inner: ArcMultiAction<S, Result<S::Output, S::Error>>,
|
||||
inner: ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
@@ -40,7 +40,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
type Target = ArcMultiAction<S, Result<S::Output, S::Error>>;
|
||||
type Target = ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
@@ -95,13 +95,13 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
inner: MultiAction<S, Result<S::Output, S::Error>>,
|
||||
inner: MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl<S> From<ServerMultiAction<S>>
|
||||
for MultiAction<S, Result<S::Output, S::Error>>
|
||||
for MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>
|
||||
where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
@@ -152,7 +152,7 @@ where
|
||||
S::Output: 'static,
|
||||
S::Error: 'static,
|
||||
{
|
||||
type Target = MultiAction<S, Result<S::Output, S::Error>>;
|
||||
type Target = MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
|
||||
@@ -168,41 +168,6 @@ where
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
/// Synchronously, reactively reads the current value of the resource and applies the function
|
||||
/// `f` to its value if it is `Some(_)`.
|
||||
#[track_caller]
|
||||
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
self.try_with(|n| n.as_ref().map(f))?
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E, Ser> ArcOnceResource<Result<T, E>, Ser>
|
||||
where
|
||||
Ser: Encoder<Result<T, E>> + Decoder<Result<T, E>>,
|
||||
<Ser as Encoder<Result<T, E>>>::Error: Debug,
|
||||
<Ser as Decoder<Result<T, E>>>::Error: Debug,
|
||||
<<Ser as Decoder<Result<T, E>>>::Encoded as FromEncodedStr>::DecodingError:
|
||||
Debug,
|
||||
<Ser as Encoder<Result<T, E>>>::Encoded: IntoEncodedString,
|
||||
<Ser as Decoder<Result<T, E>>>::Encoded: FromEncodedStr,
|
||||
T: Send + Sync + 'static,
|
||||
E: Send + Sync + Clone + 'static,
|
||||
{
|
||||
/// Applies the given function when a resource that returns `Result<T, E>`
|
||||
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
|
||||
/// calls over the `Option<Result<_, _>>` returned by the resource.
|
||||
///
|
||||
/// This is useful when used with features like server functions, in conjunction
|
||||
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
|
||||
/// left to handle the `None` and `Err(_)` states.
|
||||
#[track_caller]
|
||||
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
|
||||
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ArcOnceResource<T, Ser> {
|
||||
@@ -569,37 +534,6 @@ where
|
||||
defined_at,
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronously, reactively reads the current value of the resource and applies the function
|
||||
/// `f` to its value if it is `Some(_)`.
|
||||
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
self.try_with(|n| n.as_ref().map(|n| Some(f(n))))?.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E, Ser> OnceResource<Result<T, E>, Ser>
|
||||
where
|
||||
Ser: Encoder<Result<T, E>> + Decoder<Result<T, E>>,
|
||||
<Ser as Encoder<Result<T, E>>>::Error: Debug,
|
||||
<Ser as Decoder<Result<T, E>>>::Error: Debug,
|
||||
<<Ser as Decoder<Result<T, E>>>::Encoded as FromEncodedStr>::DecodingError:
|
||||
Debug,
|
||||
<Ser as Encoder<Result<T, E>>>::Encoded: IntoEncodedString,
|
||||
<Ser as Decoder<Result<T, E>>>::Encoded: FromEncodedStr,
|
||||
T: Send + Sync + 'static,
|
||||
E: Send + Sync + Clone + 'static,
|
||||
{
|
||||
/// Applies the given function when a resource that returns `Result<T, E>`
|
||||
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
|
||||
/// calls over the `Option<Result<_, _>>` returned by the resource.
|
||||
///
|
||||
/// This is useful when used with features like server functions, in conjunction
|
||||
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
|
||||
/// left to handle the `None` and `Err(_)` states.
|
||||
#[track_caller]
|
||||
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
|
||||
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> OnceResource<T, Ser>
|
||||
|
||||
@@ -215,11 +215,16 @@ where
|
||||
None
|
||||
}
|
||||
Ok(encoded) => {
|
||||
let decoded = Ser::decode(encoded.borrow());
|
||||
#[cfg(feature = "tracing")]
|
||||
let decoded = decoded
|
||||
.inspect_err(|e| tracing::error!("{e:?}"));
|
||||
decoded.ok()
|
||||
match Ser::decode(encoded.borrow()) {
|
||||
#[allow(unused_variables)]
|
||||
// used in tracing
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!("{e:?}");
|
||||
None
|
||||
}
|
||||
Ok(value) => Some(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.8.0-beta"
|
||||
version = "0.7.7"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::ServerMetaContext;
|
||||
use leptos::{
|
||||
attr::{any_attribute::AnyAttribute, NextAttribute},
|
||||
attr::NextAttribute,
|
||||
component, html,
|
||||
reactive::owner::use_context,
|
||||
tachys::{
|
||||
@@ -103,7 +103,6 @@ where
|
||||
At: Attribute,
|
||||
{
|
||||
type AsyncOutput = BodyView<At::AsyncOutput>;
|
||||
type Owned = BodyView<At::CloneableOwned>;
|
||||
|
||||
const MIN_LENGTH: usize = At::MIN_LENGTH;
|
||||
|
||||
@@ -123,14 +122,10 @@ where
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
if let Some(meta) = use_context::<ServerMetaContext>() {
|
||||
let mut buf = String::new();
|
||||
_ = html::attributes_to_html(
|
||||
(self.attributes, extra_attrs),
|
||||
&mut buf,
|
||||
);
|
||||
_ = html::attributes_to_html(self.attributes, &mut buf);
|
||||
if !buf.is_empty() {
|
||||
_ = meta.body.send(buf);
|
||||
}
|
||||
@@ -147,12 +142,6 @@ where
|
||||
|
||||
BodyViewState { attributes }
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
BodyView {
|
||||
attributes: self.attributes.into_cloneable_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<At> Mountable for BodyViewState<At>
|
||||
@@ -171,11 +160,4 @@ where
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
|
||||
vec![document()
|
||||
.body()
|
||||
.expect("there to be a <body> element")
|
||||
.into()]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::ServerMetaContext;
|
||||
use leptos::{
|
||||
attr::{any_attribute::AnyAttribute, NextAttribute},
|
||||
attr::NextAttribute,
|
||||
component, html,
|
||||
reactive::owner::use_context,
|
||||
tachys::{
|
||||
@@ -103,7 +103,6 @@ where
|
||||
At: Attribute,
|
||||
{
|
||||
type AsyncOutput = HtmlView<At::AsyncOutput>;
|
||||
type Owned = HtmlView<At::CloneableOwned>;
|
||||
|
||||
const MIN_LENGTH: usize = At::MIN_LENGTH;
|
||||
|
||||
@@ -123,14 +122,10 @@ where
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
if let Some(meta) = use_context::<ServerMetaContext>() {
|
||||
let mut buf = String::new();
|
||||
_ = html::attributes_to_html(
|
||||
(self.attributes, extra_attrs),
|
||||
&mut buf,
|
||||
);
|
||||
_ = html::attributes_to_html(self.attributes, &mut buf);
|
||||
if !buf.is_empty() {
|
||||
_ = meta.html.send(buf);
|
||||
}
|
||||
@@ -150,12 +145,6 @@ where
|
||||
|
||||
HtmlViewState { attributes }
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
HtmlView {
|
||||
attributes: self.attributes.into_cloneable_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<At> Mountable for HtmlViewState<At>
|
||||
@@ -176,10 +165,4 @@ where
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
|
||||
vec![document()
|
||||
.document_element()
|
||||
.expect("there to be a <html> element")]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
use futures::{Stream, StreamExt};
|
||||
use leptos::{
|
||||
attr::{any_attribute::AnyAttribute, NextAttribute},
|
||||
attr::NextAttribute,
|
||||
component,
|
||||
logging::debug_warn,
|
||||
oco::Oco,
|
||||
@@ -323,13 +323,37 @@ pub(crate) fn register<E, At, Ch>(
|
||||
where
|
||||
HtmlElement<E, At, Ch>: RenderHtml,
|
||||
{
|
||||
#[allow(unused_mut)] // used for `ssr`
|
||||
let mut el = Some(el);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(cx) = use_context::<ServerMetaContext>() {
|
||||
let mut buf = String::new();
|
||||
el.take().unwrap().to_html_with_buf(
|
||||
&mut buf,
|
||||
&mut Position::NextChild,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
|
||||
} else {
|
||||
let msg = "tried to use a leptos_meta component without \
|
||||
`ServerMetaContext` provided";
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("{}", msg);
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!("{}", msg);
|
||||
}
|
||||
|
||||
RegisteredMetaTag { el }
|
||||
}
|
||||
|
||||
struct RegisteredMetaTag<E, At, Ch> {
|
||||
// this is `None` if we've already taken it out to render to HTML on the server
|
||||
// we don't render it in place in RenderHtml, so it's fine
|
||||
el: HtmlElement<E, At, Ch>,
|
||||
el: Option<HtmlElement<E, At, Ch>>,
|
||||
}
|
||||
|
||||
struct RegisteredMetaTagState<E, At, Ch>
|
||||
@@ -367,12 +391,12 @@ where
|
||||
type State = RegisteredMetaTagState<E, At, Ch>;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
let state = self.el.build();
|
||||
let state = self.el.unwrap().build();
|
||||
RegisteredMetaTagState { state }
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
self.el.rebuild(&mut state.state);
|
||||
self.el.unwrap().rebuild(&mut state.state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +417,7 @@ where
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
RegisteredMetaTag {
|
||||
el: self.el.add_any_attr(attr),
|
||||
el: self.el.map(|inner| inner.add_any_attr(attr)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -405,7 +429,6 @@ where
|
||||
Ch: RenderHtml + Send,
|
||||
{
|
||||
type AsyncOutput = Self;
|
||||
type Owned = RegisteredMetaTag<E, At::CloneableOwned, Ch::Owned>;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
@@ -423,31 +446,9 @@ where
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
_extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
// meta tags are rendered into the buffer stored into the context
|
||||
// the value has already been taken out, when we're on the server
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(cx) = use_context::<ServerMetaContext>() {
|
||||
let mut buf = String::new();
|
||||
self.el.to_html_with_buf(
|
||||
&mut buf,
|
||||
&mut Position::NextChild,
|
||||
false,
|
||||
false,
|
||||
vec![],
|
||||
);
|
||||
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
|
||||
} else {
|
||||
let msg = "tried to use a leptos_meta component without \
|
||||
`ServerMetaContext` provided";
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("{}", msg);
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
@@ -461,18 +462,12 @@ where
|
||||
MetaContext provided",
|
||||
)
|
||||
.cursor;
|
||||
let state = self.el.hydrate::<FROM_SERVER>(
|
||||
let state = self.el.unwrap().hydrate::<FROM_SERVER>(
|
||||
&cursor,
|
||||
&PositionState::new(Position::NextChild),
|
||||
);
|
||||
RegisteredMetaTagState { state }
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
RegisteredMetaTag {
|
||||
el: self.el.into_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, At, Ch> Mountable for RegisteredMetaTagState<E, At, Ch>
|
||||
@@ -505,10 +500,6 @@ where
|
||||
// we intended!
|
||||
false
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
|
||||
self.state.elements()
|
||||
}
|
||||
}
|
||||
|
||||
/// During server rendering, inserts the meta tags that have been generated by the other components
|
||||
@@ -550,7 +541,6 @@ impl AddAnyAttr for MetaTagsView {
|
||||
|
||||
impl RenderHtml for MetaTagsView {
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
@@ -566,7 +556,6 @@ impl RenderHtml for MetaTagsView {
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
_extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
buf.push_str("<!--HEAD-->");
|
||||
}
|
||||
@@ -577,10 +566,6 @@ impl RenderHtml for MetaTagsView {
|
||||
_position: &PositionState,
|
||||
) -> Self::State {
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait OrDefaultNonce {
|
||||
|
||||
@@ -36,10 +36,6 @@ pub fn Stylesheet(
|
||||
}
|
||||
|
||||
/// Injects an [`HTMLLinkElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document head that loads a `cargo-leptos`-hashed stylesheet.
|
||||
///
|
||||
/// This should only be used in the application’s server-side `shell` function, as
|
||||
/// [`LeptosOptions`] is not available in the browser. Unlike other `leptos_meta` components, it
|
||||
/// will render the `<link>` it creates exactly where it is called.
|
||||
#[component]
|
||||
pub fn HashedStylesheet(
|
||||
/// Leptos options
|
||||
@@ -78,9 +74,11 @@ pub fn HashedStylesheet(
|
||||
css_file_name.push_str(".css");
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let root = root.unwrap_or_default();
|
||||
|
||||
link()
|
||||
.id(id)
|
||||
.rel("stylesheet")
|
||||
.href(format!("{root}/{pkg_path}/{css_file_name}"))
|
||||
// TODO additional attributes
|
||||
register(
|
||||
link()
|
||||
.id(id)
|
||||
.rel("stylesheet")
|
||||
.href(format!("{root}/{pkg_path}/{css_file_name}")),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{use_head, MetaContext, ServerMetaContext};
|
||||
use leptos::{
|
||||
attr::{any_attribute::AnyAttribute, Attribute},
|
||||
attr::Attribute,
|
||||
component,
|
||||
oco::Oco,
|
||||
reactive::{
|
||||
@@ -234,7 +234,6 @@ impl AddAnyAttr for TitleView {
|
||||
|
||||
impl RenderHtml for TitleView {
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
@@ -250,7 +249,6 @@ impl RenderHtml for TitleView {
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
_extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
// meta tags are rendered into the buffer stored into the context
|
||||
// the value has already been taken out, when we're on the server
|
||||
@@ -284,10 +282,6 @@ impl RenderHtml for TitleView {
|
||||
});
|
||||
TitleViewState { effect }
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Mountable for TitleViewState {
|
||||
@@ -305,8 +299,4 @@ impl Mountable for TitleViewState {
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ pub trait OrPoisoned {
|
||||
fn or_poisoned(self) -> Self::Inner;
|
||||
}
|
||||
|
||||
impl<'a, T: ?Sized> OrPoisoned
|
||||
impl<'a, T> OrPoisoned
|
||||
for Result<RwLockReadGuard<'a, T>, PoisonError<RwLockReadGuard<'a, T>>>
|
||||
{
|
||||
type Inner = RwLockReadGuard<'a, T>;
|
||||
@@ -45,7 +45,7 @@ impl<'a, T: ?Sized> OrPoisoned
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: ?Sized> OrPoisoned
|
||||
impl<'a, T> OrPoisoned
|
||||
for Result<RwLockWriteGuard<'a, T>, PoisonError<RwLockWriteGuard<'a, T>>>
|
||||
{
|
||||
type Inner = RwLockWriteGuard<'a, T>;
|
||||
@@ -55,7 +55,7 @@ impl<'a, T: ?Sized> OrPoisoned
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: ?Sized> OrPoisoned for LockResult<MutexGuard<'a, T>> {
|
||||
impl<'a, T> OrPoisoned for LockResult<MutexGuard<'a, T>> {
|
||||
type Inner = MutexGuard<'a, T>;
|
||||
|
||||
fn or_poisoned(self) -> Self::Inner {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.2.0-beta"
|
||||
version = "0.1.7"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -961,10 +961,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O> Action<I, O>
|
||||
impl<I, O, S> Action<I, O, S>
|
||||
where
|
||||
I: Send + Sync + 'static,
|
||||
O: Send + Sync + 'static,
|
||||
S: Storage<ArcAction<I, O>>,
|
||||
{
|
||||
/// Creates a new action, which does not require the action itself to be `Send`, but will run
|
||||
/// it on the same thread it was created on.
|
||||
@@ -1005,56 +1006,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O> Action<I, O, LocalStorage>
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
{
|
||||
/// Creates a new action, which neither requires the action itself nor the
|
||||
/// value it returns to be `Send`. If this action is accessed from outside the
|
||||
/// thread on which it was created, it panics.
|
||||
///
|
||||
/// This combines the features of [`Action::new_local`] and [`Action::new_unsync`].
|
||||
#[track_caller]
|
||||
pub fn new_unsync_local<F, Fu>(action_fn: F) -> Self
|
||||
where
|
||||
F: Fn(&I) -> Fu + 'static,
|
||||
Fu: Future<Output = O> + 'static,
|
||||
{
|
||||
Self {
|
||||
inner: ArenaItem::new_with_storage(ArcAction::new_unsync(
|
||||
action_fn,
|
||||
)),
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new action, which neither requires the action itself nor the
|
||||
/// value it returns to be `Send`, and provides it with an initial value.
|
||||
/// If this action is accessed from outside the thread on which it was created, it panics.
|
||||
///
|
||||
/// This combines the features of [`Action::new_local_with_value`] and
|
||||
/// [`Action::new_unsync_with_value`].
|
||||
#[track_caller]
|
||||
pub fn new_unsync_local_with_value<F, Fu>(
|
||||
value: Option<O>,
|
||||
action_fn: F,
|
||||
) -> Self
|
||||
where
|
||||
F: Fn(&I) -> Fu + 'static,
|
||||
Fu: Future<Output = O> + 'static,
|
||||
{
|
||||
Self {
|
||||
inner: ArenaItem::new_with_storage(
|
||||
ArcAction::new_unsync_with_value(value, action_fn),
|
||||
),
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O, S> DefinedAt for Action<I, O, S> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
|
||||
@@ -13,9 +13,8 @@ use crate::{
|
||||
SubscriberSet, ToAnySource, ToAnySubscriber, WithObserver,
|
||||
},
|
||||
owner::{use_context, Owner},
|
||||
send_wrapper_ext::MaybeSendWrapperOption,
|
||||
signal::{
|
||||
guards::{AsyncPlain, Mapped, MappedMut, ReadGuard, WriteGuard},
|
||||
guards::{AsyncPlain, ReadGuard, WriteGuard},
|
||||
ArcTrigger,
|
||||
},
|
||||
traits::{
|
||||
@@ -29,10 +28,11 @@ use async_lock::RwLock as AsyncRwLock;
|
||||
use core::fmt::Debug;
|
||||
use futures::{channel::oneshot, FutureExt, StreamExt};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::{
|
||||
future::Future,
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
ops::DerefMut,
|
||||
panic::Location,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -110,7 +110,7 @@ pub struct ArcAsyncDerived<T> {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
pub(crate) defined_at: &'static Location<'static>,
|
||||
// the current state of this signal
|
||||
pub(crate) value: Arc<AsyncRwLock<MaybeSendWrapperOption<T>>>,
|
||||
pub(crate) value: Arc<AsyncRwLock<Option<T>>>,
|
||||
// holds wakers generated when you .await this
|
||||
pub(crate) wakers: Arc<RwLock<Vec<Waker>>>,
|
||||
pub(crate) inner: Arc<RwLock<ArcAsyncDerivedInner>>,
|
||||
@@ -280,7 +280,7 @@ macro_rules! spawn_derived {
|
||||
let mut guard = this.inner.write().or_poisoned();
|
||||
|
||||
guard.state = AsyncDerivedState::Clean;
|
||||
*value.blocking_write() = orig_value;
|
||||
*value.blocking_write() = Some(orig_value);
|
||||
this.loading.store(false, Ordering::Relaxed);
|
||||
(true, None)
|
||||
}
|
||||
@@ -405,14 +405,14 @@ macro_rules! spawn_derived {
|
||||
|
||||
impl<T: 'static> ArcAsyncDerived<T> {
|
||||
async fn set_inner_value(
|
||||
new_value: MaybeSendWrapperOption<T>,
|
||||
value: Arc<AsyncRwLock<MaybeSendWrapperOption<T>>>,
|
||||
new_value: T,
|
||||
value: Arc<AsyncRwLock<Option<T>>>,
|
||||
wakers: Arc<RwLock<Vec<Waker>>>,
|
||||
inner: Arc<RwLock<ArcAsyncDerivedInner>>,
|
||||
loading: Arc<AtomicBool>,
|
||||
ready_tx: Option<oneshot::Sender<()>>,
|
||||
) {
|
||||
*value.write().await.deref_mut() = new_value;
|
||||
*value.write().await = Some(new_value);
|
||||
Self::notify_subs(&wakers, &inner, &loading, ready_tx);
|
||||
}
|
||||
|
||||
@@ -479,11 +479,6 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
T: Send + Sync + 'static,
|
||||
Fut: Future<Output = T> + Send + 'static,
|
||||
{
|
||||
let fun = move || {
|
||||
let fut = fun();
|
||||
async move { MaybeSendWrapperOption::new(Some(fut.await)) }
|
||||
};
|
||||
let initial_value = MaybeSendWrapperOption::new(initial_value);
|
||||
let (this, _) = spawn_derived!(
|
||||
Executor::spawn,
|
||||
initial_value,
|
||||
@@ -513,11 +508,6 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
Fut: Future<Output = T> + Send + 'static,
|
||||
S: Track,
|
||||
{
|
||||
let fun = move || {
|
||||
let fut = fun();
|
||||
async move { MaybeSendWrapperOption::new(Some(fut.await)) }
|
||||
};
|
||||
let initial_value = MaybeSendWrapperOption::new(initial_value);
|
||||
let (this, _) = spawn_derived!(
|
||||
Executor::spawn,
|
||||
initial_value,
|
||||
@@ -555,11 +545,6 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
T: 'static,
|
||||
Fut: Future<Output = T> + 'static,
|
||||
{
|
||||
let fun = move || {
|
||||
let fut = fun();
|
||||
async move { MaybeSendWrapperOption::new_local(Some(fut.await)) }
|
||||
};
|
||||
let initial_value = MaybeSendWrapperOption::new_local(initial_value);
|
||||
let (this, _) = spawn_derived!(
|
||||
Executor::spawn_local,
|
||||
initial_value,
|
||||
@@ -582,7 +567,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> ArcAsyncDerived<T> {
|
||||
impl<T: 'static> ArcAsyncDerived<SendWrapper<T>> {
|
||||
#[doc(hidden)]
|
||||
#[track_caller]
|
||||
pub fn new_mock<Fut>(fun: impl Fn() -> Fut + 'static) -> Self
|
||||
@@ -590,10 +575,13 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
T: 'static,
|
||||
Fut: Future<Output = T> + 'static,
|
||||
{
|
||||
let initial = MaybeSendWrapperOption::new_local(None::<T>);
|
||||
let initial = None::<SendWrapper<T>>;
|
||||
let fun = move || {
|
||||
let fut = fun();
|
||||
async move { MaybeSendWrapperOption::new_local(Some(fut.await)) }
|
||||
async move {
|
||||
let value = fut.await;
|
||||
SendWrapper::new(value)
|
||||
}
|
||||
};
|
||||
let (this, _) = spawn_derived!(
|
||||
Executor::spawn_local,
|
||||
@@ -609,10 +597,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
}
|
||||
|
||||
impl<T: 'static> ReadUntracked for ArcAsyncDerived<T> {
|
||||
type Value = ReadGuard<
|
||||
Option<T>,
|
||||
Mapped<AsyncPlain<MaybeSendWrapperOption<T>>, Option<T>>,
|
||||
>;
|
||||
type Value = ReadGuard<Option<T>, AsyncPlain<Option<T>>>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
if let Some(suspense_context) = use_context::<SuspenseContext>() {
|
||||
@@ -628,9 +613,7 @@ impl<T: 'static> ReadUntracked for ArcAsyncDerived<T> {
|
||||
.suspenses
|
||||
.push(suspense_context);
|
||||
}
|
||||
AsyncPlain::try_new(&self.value).map(|plain| {
|
||||
ReadGuard::new(Mapped::new_with_guard(plain, |v| v.deref()))
|
||||
})
|
||||
AsyncPlain::try_new(&self.value).map(ReadGuard::new)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,21 +627,13 @@ impl<T: 'static> Write for ArcAsyncDerived<T> {
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
Some(MappedMut::new(
|
||||
WriteGuard::new(self.clone(), self.value.blocking_write()),
|
||||
|v| v.deref(),
|
||||
|v| v.deref_mut(),
|
||||
))
|
||||
Some(WriteGuard::new(self.clone(), self.value.blocking_write()))
|
||||
}
|
||||
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
Some(MappedMut::new(
|
||||
self.value.blocking_write(),
|
||||
|v| v.deref(),
|
||||
|v| v.deref_mut(),
|
||||
))
|
||||
Some(self.value.blocking_write())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ use crate::{
|
||||
ToAnySource, ToAnySubscriber,
|
||||
},
|
||||
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
|
||||
send_wrapper_ext::MaybeSendWrapperOption,
|
||||
signal::guards::{AsyncPlain, Mapped, MappedMut, ReadGuard, WriteGuard},
|
||||
signal::guards::{AsyncPlain, ReadGuard, WriteGuard},
|
||||
traits::{
|
||||
DefinedAt, Dispose, IsDisposed, Notify, ReadUntracked,
|
||||
UntrackableGuard, Write,
|
||||
@@ -14,11 +13,8 @@ use crate::{
|
||||
unwrap_signal,
|
||||
};
|
||||
use core::fmt::Debug;
|
||||
use std::{
|
||||
future::Future,
|
||||
ops::{Deref, DerefMut},
|
||||
panic::Location,
|
||||
};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::{future::Future, ops::DerefMut, panic::Location};
|
||||
|
||||
/// A reactive value that is derived by running an asynchronous computation in response to changes
|
||||
/// in its sources.
|
||||
@@ -98,10 +94,9 @@ impl<T, S> Dispose for AsyncDerived<T, S> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<ArcAsyncDerived<T>> for AsyncDerived<T, S>
|
||||
impl<T> From<ArcAsyncDerived<T>> for AsyncDerived<T>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<ArcAsyncDerived<T>>,
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
fn from(value: ArcAsyncDerived<T>) -> Self {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
@@ -114,13 +109,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<AsyncDerived<T, S>> for ArcAsyncDerived<T>
|
||||
impl<T> From<AsyncDerived<T>> for ArcAsyncDerived<T>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<ArcAsyncDerived<T>>,
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: AsyncDerived<T, S>) -> Self {
|
||||
fn from(value: AsyncDerived<T>) -> Self {
|
||||
value
|
||||
.inner
|
||||
.try_get_value()
|
||||
@@ -185,7 +179,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsyncDerived<T> {
|
||||
impl<T> AsyncDerived<SendWrapper<T>> {
|
||||
#[doc(hidden)]
|
||||
pub fn new_mock<Fut>(fun: impl Fn() -> Fut + 'static) -> Self
|
||||
where
|
||||
@@ -198,24 +192,6 @@ impl<T> AsyncDerived<T> {
|
||||
inner: ArenaItem::new_with_storage(ArcAsyncDerived::new_mock(fun)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as [`AsyncDerived::new_unsync`] except it produces AsyncDerived<T> instead of AsyncDerived<T, LocalStorage>.
|
||||
/// The internal value will still be wrapped in a [`send_wrapper::SendWrapper`].
|
||||
pub fn new_unsync_threadsafe_storage<Fut>(
|
||||
fun: impl Fn() -> Fut + 'static,
|
||||
) -> Self
|
||||
where
|
||||
T: 'static,
|
||||
Fut: Future<Output = T> + 'static,
|
||||
{
|
||||
Self {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
inner: ArenaItem::new_with_storage(ArcAsyncDerived::new_unsync(
|
||||
fun,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsyncDerived<T, LocalStorage>
|
||||
@@ -318,10 +294,7 @@ where
|
||||
T: 'static,
|
||||
S: Storage<ArcAsyncDerived<T>>,
|
||||
{
|
||||
type Value = ReadGuard<
|
||||
Option<T>,
|
||||
Mapped<AsyncPlain<MaybeSendWrapperOption<T>>, Option<T>>,
|
||||
>;
|
||||
type Value = ReadGuard<Option<T>, AsyncPlain<Option<T>>>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
self.inner
|
||||
@@ -351,21 +324,13 @@ where
|
||||
let guard = self
|
||||
.inner
|
||||
.try_with_value(|n| n.value.blocking_write_arc())?;
|
||||
Some(MappedMut::new(
|
||||
WriteGuard::new(*self, guard),
|
||||
|v| v.deref(),
|
||||
|v| v.deref_mut(),
|
||||
))
|
||||
Some(WriteGuard::new(*self, guard))
|
||||
}
|
||||
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
self.inner
|
||||
.try_with_value(|n| n.value.blocking_write_arc())
|
||||
.map(|inner| {
|
||||
MappedMut::new(inner, |v| v.deref(), |v| v.deref_mut())
|
||||
})
|
||||
self.inner.try_with_value(|n| n.value.blocking_write_arc())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ use crate::{
|
||||
diagnostics::SpecialNonReactiveZone,
|
||||
graph::{AnySource, ToAnySource},
|
||||
owner::{use_context, Storage},
|
||||
send_wrapper_ext::MaybeSendWrapperOption,
|
||||
signal::guards::{AsyncPlain, Mapped, ReadGuard},
|
||||
traits::{DefinedAt, Track},
|
||||
unwrap_signal,
|
||||
@@ -25,8 +24,7 @@ use std::{
|
||||
///
|
||||
/// Implements [`Deref`](std::ops::Deref) to access the inner value. This should not be held longer
|
||||
/// than it is needed, as it prevents updates to the inner value.
|
||||
pub type AsyncDerivedGuard<T> =
|
||||
ReadGuard<T, Mapped<AsyncPlain<MaybeSendWrapperOption<T>>, T>>;
|
||||
pub type AsyncDerivedGuard<T> = ReadGuard<T, Mapped<AsyncPlain<Option<T>>, T>>;
|
||||
|
||||
/// A [`Future`] that is ready when an [`ArcAsyncDerived`] is finished loading or reloading,
|
||||
/// but does not contain its value.
|
||||
@@ -108,7 +106,7 @@ where
|
||||
/// and contains its value. `.await`ing this clones the value `T`.
|
||||
pub struct AsyncDerivedFuture<T> {
|
||||
source: AnySource,
|
||||
value: Arc<async_lock::RwLock<MaybeSendWrapperOption<T>>>,
|
||||
value: Arc<async_lock::RwLock<Option<T>>>,
|
||||
loading: Arc<AtomicBool>,
|
||||
wakers: Arc<RwLock<Vec<Waker>>>,
|
||||
inner: Arc<RwLock<ArcAsyncDerivedInner>>,
|
||||
@@ -185,7 +183,7 @@ where
|
||||
/// and yields an [`AsyncDerivedGuard`] that dereferences to its value.
|
||||
pub struct AsyncDerivedRefFuture<T> {
|
||||
source: AnySource,
|
||||
value: Arc<async_lock::RwLock<MaybeSendWrapperOption<T>>>,
|
||||
value: Arc<async_lock::RwLock<Option<T>>>,
|
||||
loading: Arc<AtomicBool>,
|
||||
wakers: Arc<RwLock<Vec<Waker>>>,
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
sync::{Arc, RwLock, RwLockWriteGuard},
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
pub struct MemoInner<T, S>
|
||||
@@ -72,21 +72,17 @@ where
|
||||
}
|
||||
|
||||
fn mark_check(&self) {
|
||||
/// codegen optimisation:
|
||||
fn inner(reactivity: &RwLock<MemoInnerReactivity>) {
|
||||
{
|
||||
let mut lock = reactivity.write().or_poisoned();
|
||||
if lock.state != ReactiveNodeState::Dirty {
|
||||
lock.state = ReactiveNodeState::Check;
|
||||
}
|
||||
}
|
||||
for sub in
|
||||
(&reactivity.read().or_poisoned().subscribers).into_iter()
|
||||
{
|
||||
sub.mark_check();
|
||||
{
|
||||
let mut lock = self.reactivity.write().or_poisoned();
|
||||
if lock.state != ReactiveNodeState::Dirty {
|
||||
lock.state = ReactiveNodeState::Check;
|
||||
}
|
||||
}
|
||||
inner(&self.reactivity);
|
||||
for sub in
|
||||
(&self.reactivity.read().or_poisoned().subscribers).into_iter()
|
||||
{
|
||||
sub.mark_check();
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_subscribers_check(&self) {
|
||||
@@ -97,87 +93,64 @@ where
|
||||
}
|
||||
|
||||
fn update_if_necessary(&self) -> bool {
|
||||
/// codegen optimisation:
|
||||
fn needs_update(reactivity: &RwLock<MemoInnerReactivity>) -> bool {
|
||||
let (state, sources) = {
|
||||
let inner = reactivity.read().or_poisoned();
|
||||
(inner.state, inner.sources.clone())
|
||||
};
|
||||
match state {
|
||||
ReactiveNodeState::Clean => false,
|
||||
ReactiveNodeState::Dirty => true,
|
||||
ReactiveNodeState::Check => {
|
||||
(&sources).into_iter().any(|source| {
|
||||
source.update_if_necessary()
|
||||
|| reactivity.read().or_poisoned().state
|
||||
== ReactiveNodeState::Dirty
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
let (state, sources) = {
|
||||
let inner = self.reactivity.read().or_poisoned();
|
||||
(inner.state, inner.sources.clone())
|
||||
};
|
||||
|
||||
if needs_update(&self.reactivity) {
|
||||
let needs_update = match state {
|
||||
ReactiveNodeState::Clean => false,
|
||||
ReactiveNodeState::Dirty => true,
|
||||
ReactiveNodeState::Check => (&sources).into_iter().any(|source| {
|
||||
source.update_if_necessary()
|
||||
|| self.reactivity.read().or_poisoned().state
|
||||
== ReactiveNodeState::Dirty
|
||||
}),
|
||||
};
|
||||
|
||||
if needs_update {
|
||||
let fun = self.fun.clone();
|
||||
let owner = self.owner.clone();
|
||||
// No deadlock risk, because we only hold the value lock.
|
||||
let value = self.value.write().or_poisoned().take();
|
||||
|
||||
/// codegen optimisation:
|
||||
fn inner_1(
|
||||
reactivity: &RwLock<MemoInnerReactivity>,
|
||||
) -> AnySubscriber {
|
||||
let any_subscriber =
|
||||
reactivity.read().or_poisoned().any_subscriber.clone();
|
||||
any_subscriber.clear_sources(&any_subscriber);
|
||||
let any_subscriber =
|
||||
{ self.reactivity.read().or_poisoned().any_subscriber.clone() };
|
||||
any_subscriber.clear_sources(&any_subscriber);
|
||||
let (new_value, changed) = owner.with_cleanup(|| {
|
||||
any_subscriber
|
||||
}
|
||||
let any_subscriber = inner_1(&self.reactivity);
|
||||
|
||||
let (new_value, changed) = self.owner.with_cleanup(|| {
|
||||
any_subscriber.with_observer(|| {
|
||||
(self.fun)(value.map(StorageAccess::into_taken))
|
||||
})
|
||||
.with_observer(|| fun(value.map(StorageAccess::into_taken)))
|
||||
});
|
||||
|
||||
// Two locks are aquired, so order matters.
|
||||
let reactivity_lock = self.reactivity.write().or_poisoned();
|
||||
let mut reactivity_lock = self.reactivity.write().or_poisoned();
|
||||
{
|
||||
// Safety: Can block endlessly if the user is has a ReadGuard on the value
|
||||
let mut value_lock = self.value.write().or_poisoned();
|
||||
*value_lock = Some(S::wrap(new_value));
|
||||
}
|
||||
reactivity_lock.state = ReactiveNodeState::Clean;
|
||||
|
||||
/// codegen optimisation:
|
||||
fn inner_2(
|
||||
changed: bool,
|
||||
mut reactivity_lock: RwLockWriteGuard<'_, MemoInnerReactivity>,
|
||||
) {
|
||||
reactivity_lock.state = ReactiveNodeState::Clean;
|
||||
|
||||
if changed {
|
||||
let subs = reactivity_lock.subscribers.clone();
|
||||
drop(reactivity_lock);
|
||||
for sub in subs {
|
||||
// don't trigger reruns of effects/memos
|
||||
// basically: if one of the observers has triggered this memo to
|
||||
// run, it doesn't need to be re-triggered because of this change
|
||||
if !Observer::is(&sub) {
|
||||
sub.mark_dirty();
|
||||
}
|
||||
if changed {
|
||||
let subs = reactivity_lock.subscribers.clone();
|
||||
drop(reactivity_lock);
|
||||
for sub in subs {
|
||||
// don't trigger reruns of effects/memos
|
||||
// basically: if one of the observers has triggered this memo to
|
||||
// run, it doesn't need to be re-triggered because of this change
|
||||
if !Observer::is(&sub) {
|
||||
sub.mark_dirty();
|
||||
}
|
||||
} else {
|
||||
drop(reactivity_lock);
|
||||
}
|
||||
} else {
|
||||
drop(reactivity_lock);
|
||||
}
|
||||
inner_2(changed, reactivity_lock);
|
||||
|
||||
changed
|
||||
} else {
|
||||
/// codegen optimisation:
|
||||
fn inner(reactivity: &RwLock<MemoInnerReactivity>) -> bool {
|
||||
let mut lock = reactivity.write().or_poisoned();
|
||||
lock.state = ReactiveNodeState::Clean;
|
||||
false
|
||||
}
|
||||
inner(&self.reactivity)
|
||||
let mut lock = self.reactivity.write().or_poisoned();
|
||||
lock.state = ReactiveNodeState::Clean;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
#[allow(clippy::module_inception)]
|
||||
mod effect;
|
||||
mod effect_function;
|
||||
mod immediate;
|
||||
mod inner;
|
||||
mod render_effect;
|
||||
|
||||
pub use effect::*;
|
||||
pub use effect_function::*;
|
||||
pub use immediate::*;
|
||||
pub use render_effect::*;
|
||||
|
||||
/// Creates a new render effect, which immediately runs `fun`.
|
||||
|
||||
@@ -374,16 +374,47 @@ impl Effect<SyncStorage> {
|
||||
/// This spawns a task that can be run on any thread. For an effect that will be spawned on
|
||||
/// the current thread, use [`new`](Effect::new).
|
||||
pub fn new_sync<T, M>(
|
||||
fun: impl EffectFunction<T, M> + Send + Sync + 'static,
|
||||
mut fun: impl EffectFunction<T, M> + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
if !cfg!(feature = "effects") {
|
||||
return Self { inner: None };
|
||||
}
|
||||
let inner = cfg!(feature = "effects").then(|| {
|
||||
let (mut rx, owner, inner) = effect_base();
|
||||
let mut first_run = true;
|
||||
let value = Arc::new(RwLock::new(None::<T>));
|
||||
|
||||
Self::new_isomorphic(fun)
|
||||
crate::spawn({
|
||||
let value = Arc::clone(&value);
|
||||
let subscriber = inner.to_any_subscriber();
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if !owner.paused()
|
||||
&& (subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
}) || first_run)
|
||||
{
|
||||
first_run = false;
|
||||
subscriber.clear_sources(&subscriber);
|
||||
|
||||
let old_value =
|
||||
mem::take(&mut *value.write().or_poisoned());
|
||||
let new_value = owner.with_cleanup(|| {
|
||||
subscriber.with_observer(|| {
|
||||
run_in_effect_scope(|| fun.run(old_value))
|
||||
})
|
||||
});
|
||||
*value.write().or_poisoned() = Some(new_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ArenaItem::new_with_storage(Some(inner))
|
||||
});
|
||||
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
/// Creates a new effect, which runs once on the next “tick”, and then runs again when reactive values
|
||||
|
||||
@@ -1,379 +0,0 @@
|
||||
use crate::{
|
||||
graph::{AnySubscriber, ReactiveNode, ToAnySubscriber},
|
||||
owner::on_cleanup,
|
||||
traits::{DefinedAt, Dispose},
|
||||
};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
panic::Location,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
};
|
||||
|
||||
/// Effects run a certain chunk of code whenever the signals they depend on change.
|
||||
///
|
||||
/// The effect runs on creation and again as soon as any tracked signal changes.
|
||||
///
|
||||
/// NOTE: you probably want use [`Effect`](super::Effect) instead.
|
||||
/// This is for the few cases where it's important to execute effects immediately and in order.
|
||||
///
|
||||
/// [ImmediateEffect]s stop running when dropped.
|
||||
///
|
||||
/// NOTE: since effects are executed immediately, they might recurse.
|
||||
/// Under recursion or parallelism only the last run to start is tracked.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use reactive_graph::computed::*;
|
||||
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # use reactive_graph::effect::ImmediateEffect;
|
||||
/// # use reactive_graph::owner::ArenaItem;
|
||||
/// # let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// let a = RwSignal::new(0);
|
||||
/// let b = RwSignal::new(0);
|
||||
///
|
||||
/// // ✅ use effects to interact between reactive state and the outside world
|
||||
/// let _drop_guard = ImmediateEffect::new(move || {
|
||||
/// // on the next “tick” prints "Value: 0" and subscribes to `a`
|
||||
/// println!("Value: {}", a.get());
|
||||
/// });
|
||||
///
|
||||
/// // The effect runs immediately and subscribes to `a`, in the process it prints "Value: 0"
|
||||
/// # assert_eq!(a.get(), 0);
|
||||
/// a.set(1);
|
||||
/// # assert_eq!(a.get(), 1);
|
||||
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
|
||||
/// ```
|
||||
/// ## Notes
|
||||
///
|
||||
/// 1. **Scheduling**: Effects run immediately, as soon as any tracked signal changes.
|
||||
/// 2. By default, effects do not run unless the `effects` feature is enabled. If you are using
|
||||
/// this with a web framework, this generally means that effects **do not run on the server**.
|
||||
/// and you can call browser-specific APIs within the effect function without causing issues.
|
||||
/// If you need an effect to run on the server, use [`ImmediateEffect::new_isomorphic`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImmediateEffect {
|
||||
inner: StoredEffect,
|
||||
}
|
||||
|
||||
type StoredEffect = Option<Arc<RwLock<inner::EffectInner>>>;
|
||||
|
||||
impl Dispose for ImmediateEffect {
|
||||
fn dispose(self) {}
|
||||
}
|
||||
|
||||
impl ImmediateEffect {
|
||||
/// Creates a new effect which runs immediately, then again as soon as any tracked signal changes.
|
||||
///
|
||||
/// NOTE: this requires a `Fn` function because it might recurse.
|
||||
/// Use [Self::new_mut] to pass a `FnMut` function, it'll panic on recursion.
|
||||
#[track_caller]
|
||||
#[must_use]
|
||||
pub fn new(fun: impl Fn() + Send + Sync + 'static) -> Self {
|
||||
if !cfg!(feature = "effects") {
|
||||
return Self { inner: None };
|
||||
}
|
||||
|
||||
let inner = inner::EffectInner::new(fun);
|
||||
|
||||
inner.update_if_necessary();
|
||||
|
||||
Self { inner: Some(inner) }
|
||||
}
|
||||
/// Creates a new effect which runs immediately, then again as soon as any tracked signal changes.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics on recursion or if triggered in parallel. Also see [Self::new]
|
||||
#[track_caller]
|
||||
#[must_use]
|
||||
pub fn new_mut(fun: impl FnMut() + Send + Sync + 'static) -> Self {
|
||||
const MSG: &str = "The effect recursed or its function panicked.";
|
||||
let fun = Mutex::new(fun);
|
||||
Self::new(move || fun.try_lock().expect(MSG)())
|
||||
}
|
||||
/// Creates a new effect which runs immediately, then again as soon as any tracked signal changes.
|
||||
///
|
||||
/// NOTE: this requires a `Fn` function because it might recurse.
|
||||
/// 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) {
|
||||
let effect = Self::new(fun);
|
||||
|
||||
on_cleanup(move || effect.dispose());
|
||||
}
|
||||
|
||||
/// Creates a new effect which runs immediately, then again as soon as any tracked signal changes.
|
||||
///
|
||||
/// This will run whether the `effects` feature is enabled or not.
|
||||
#[track_caller]
|
||||
#[must_use]
|
||||
pub fn new_isomorphic(fun: impl Fn() + Send + Sync + 'static) -> Self {
|
||||
let inner = inner::EffectInner::new(fun);
|
||||
|
||||
inner.update_if_necessary();
|
||||
|
||||
Self { inner: Some(inner) }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToAnySubscriber for ImmediateEffect {
|
||||
fn to_any_subscriber(&self) -> AnySubscriber {
|
||||
const MSG: &str = "tried to set effect that has been stopped";
|
||||
self.inner.as_ref().expect(MSG).to_any_subscriber()
|
||||
}
|
||||
}
|
||||
|
||||
impl DefinedAt for ImmediateEffect {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
self.inner.as_ref()?.read().or_poisoned().defined_at()
|
||||
}
|
||||
}
|
||||
|
||||
mod inner {
|
||||
use crate::{
|
||||
graph::{
|
||||
AnySource, AnySubscriber, ReactiveNode, ReactiveNodeState,
|
||||
SourceSet, Subscriber, ToAnySubscriber, WithObserver,
|
||||
},
|
||||
log_warning,
|
||||
owner::Owner,
|
||||
traits::DefinedAt,
|
||||
};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
panic::Location,
|
||||
sync::{Arc, RwLock, Weak},
|
||||
thread::{self, ThreadId},
|
||||
};
|
||||
|
||||
/// Handles subscription logic for effects.
|
||||
///
|
||||
/// To handle parallelism and recursion we assign ordered (1..) ids to each run.
|
||||
/// We only keep the sources tracked by the run with the highest id (the last one).
|
||||
///
|
||||
/// We do this by:
|
||||
/// - Clearing the sources before every run, so the last one clears anything before it.
|
||||
/// - We stop tracking sources after the last run has completed.
|
||||
/// (A parent run will start before and end after a recursive child run.)
|
||||
/// - To handle parallelism with the last run, we only allow sources to be added by its thread.
|
||||
pub(super) struct EffectInner {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
owner: Owner,
|
||||
state: ReactiveNodeState,
|
||||
/// The number of effect runs in this 'batch'.
|
||||
/// Cleared when no runs are *ongoing* anymore.
|
||||
/// Used to assign ordered ids to each run, and to know when we can clear these values.
|
||||
run_count_start: usize,
|
||||
/// The number of effect runs that have completed in the current 'batch'.
|
||||
/// Cleared when no runs are *ongoing* anymore.
|
||||
/// Used to know when we can clear these values.
|
||||
run_done_count: usize,
|
||||
/// Given ordered ids (1..), the run with the highest id that has completed in this 'batch'.
|
||||
/// Cleared when no runs are *ongoing* anymore.
|
||||
/// Used to know whether the current run is the latest one.
|
||||
run_done_max: usize,
|
||||
/// The [ThreadId] of the run with the highest id.
|
||||
/// Used to prevent over-subscribing during parallel execution with the last run.
|
||||
///
|
||||
/// ```text
|
||||
/// Thread 1:
|
||||
/// -------------------------
|
||||
/// --- --- =======
|
||||
///
|
||||
/// Thread 2:
|
||||
/// -------------------------
|
||||
/// -----------
|
||||
/// ```
|
||||
///
|
||||
/// In the parallel example above, we can see why we need this.
|
||||
/// The last run is marked using `=`, but another run in the other thread might
|
||||
/// also be gathering sources. So we only allow the run from the correct [ThreadId] to push sources.
|
||||
last_run_thread_id: ThreadId,
|
||||
fun: Arc<dyn Fn() + Send + Sync>,
|
||||
sources: SourceSet,
|
||||
any_subscriber: AnySubscriber,
|
||||
}
|
||||
|
||||
impl EffectInner {
|
||||
#[track_caller]
|
||||
pub fn new(
|
||||
fun: impl Fn() + Send + Sync + 'static,
|
||||
) -> Arc<RwLock<EffectInner>> {
|
||||
let owner = Owner::new();
|
||||
|
||||
Arc::new_cyclic(|weak| {
|
||||
let any_subscriber = AnySubscriber(
|
||||
weak.as_ptr() as usize,
|
||||
Weak::clone(weak) as Weak<dyn Subscriber + Send + Sync>,
|
||||
);
|
||||
|
||||
RwLock::new(EffectInner {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
owner,
|
||||
state: ReactiveNodeState::Dirty,
|
||||
run_count_start: 0,
|
||||
run_done_count: 0,
|
||||
run_done_max: 0,
|
||||
last_run_thread_id: thread::current().id(),
|
||||
fun: Arc::new(fun),
|
||||
sources: SourceSet::new(),
|
||||
any_subscriber,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToAnySubscriber for Arc<RwLock<EffectInner>> {
|
||||
fn to_any_subscriber(&self) -> AnySubscriber {
|
||||
AnySubscriber(
|
||||
Arc::as_ptr(self) as usize,
|
||||
Arc::downgrade(self) as Weak<dyn Subscriber + Send + Sync>,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReactiveNode for RwLock<EffectInner> {
|
||||
fn mark_subscribers_check(&self) {}
|
||||
|
||||
fn update_if_necessary(&self) -> bool {
|
||||
let state = {
|
||||
let guard = self.read().or_poisoned();
|
||||
|
||||
if guard.owner.paused() {
|
||||
return false;
|
||||
}
|
||||
|
||||
guard.state
|
||||
};
|
||||
|
||||
let needs_update = match state {
|
||||
ReactiveNodeState::Clean => false,
|
||||
ReactiveNodeState::Check => {
|
||||
let sources = self.read().or_poisoned().sources.clone();
|
||||
sources
|
||||
.into_iter()
|
||||
.any(|source| source.update_if_necessary())
|
||||
}
|
||||
ReactiveNodeState::Dirty => true,
|
||||
};
|
||||
|
||||
if needs_update {
|
||||
let mut guard = self.write().or_poisoned();
|
||||
|
||||
let owner = guard.owner.clone();
|
||||
let any_subscriber = guard.any_subscriber.clone();
|
||||
let fun = guard.fun.clone();
|
||||
|
||||
// New run has started.
|
||||
guard.run_count_start += 1;
|
||||
// We get a value for this run, the highest value will be what we keep the sources from.
|
||||
let recursion_count = guard.run_count_start;
|
||||
// We clear the sources before running the effect.
|
||||
// Note that this is tied to the ordering of the initial write lock acquisition
|
||||
// to ensure the last run is also the last to clear them.
|
||||
guard.sources.clear_sources(&any_subscriber);
|
||||
// Only this thread will be able to subscribe.
|
||||
guard.last_run_thread_id = thread::current().id();
|
||||
|
||||
if recursion_count > 2 {
|
||||
warn_excessive_recursion(&guard);
|
||||
}
|
||||
|
||||
drop(guard);
|
||||
|
||||
// We execute the effect.
|
||||
// Note that *this could happen in parallel across threads*.
|
||||
owner.with_cleanup(|| any_subscriber.with_observer(|| fun()));
|
||||
|
||||
let mut guard = self.write().or_poisoned();
|
||||
|
||||
// This run has completed.
|
||||
guard.run_done_count += 1;
|
||||
|
||||
// We update the done count.
|
||||
// Sources will only be added if recursion_done_max < recursion_count_start.
|
||||
// (Meaning the last run is not done yet.)
|
||||
guard.run_done_max =
|
||||
Ord::max(recursion_count, guard.run_done_max);
|
||||
|
||||
// The same amount of runs has started and completed,
|
||||
// so we can clear everything up for next time.
|
||||
if guard.run_count_start == guard.run_done_count {
|
||||
guard.run_count_start = 0;
|
||||
guard.run_done_count = 0;
|
||||
guard.run_done_max = 0;
|
||||
// Can be left unchanged, it'll be set again next time.
|
||||
// guard.last_run_thread_id = thread::current().id();
|
||||
}
|
||||
|
||||
guard.state = ReactiveNodeState::Clean;
|
||||
}
|
||||
|
||||
needs_update
|
||||
}
|
||||
|
||||
fn mark_check(&self) {
|
||||
self.write().or_poisoned().state = ReactiveNodeState::Check;
|
||||
self.update_if_necessary();
|
||||
}
|
||||
|
||||
fn mark_dirty(&self) {
|
||||
self.write().or_poisoned().state = ReactiveNodeState::Dirty;
|
||||
self.update_if_necessary();
|
||||
}
|
||||
}
|
||||
|
||||
impl Subscriber for RwLock<EffectInner> {
|
||||
fn add_source(&self, source: AnySource) {
|
||||
let mut guard = self.write().or_poisoned();
|
||||
if guard.run_done_max < guard.run_count_start
|
||||
&& guard.last_run_thread_id == thread::current().id()
|
||||
{
|
||||
guard.sources.insert(source);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_sources(&self, subscriber: &AnySubscriber) {
|
||||
self.write().or_poisoned().sources.clear_sources(subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
impl DefinedAt for EffectInner {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EffectInner {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("EffectInner")
|
||||
.field("owner", &self.owner)
|
||||
.field("state", &self.state)
|
||||
.field("sources", &self.sources)
|
||||
.field("any_subscriber", &self.any_subscriber)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn warn_excessive_recursion(effect: &EffectInner) {
|
||||
const MSG: &str = "ImmediateEffect recursed more than once.";
|
||||
match effect.defined_at() {
|
||||
Some(defined_at) => {
|
||||
log_warning(format_args!("{MSG} Defined at: {}", defined_at));
|
||||
}
|
||||
None => {
|
||||
log_warning(format_args!("{MSG}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,19 +30,21 @@ impl ReactiveNode for RwLock<EffectInner> {
|
||||
|
||||
fn update_if_necessary(&self) -> bool {
|
||||
let mut guard = self.write().or_poisoned();
|
||||
let (is_dirty, sources) =
|
||||
(guard.dirty, (!guard.dirty).then(|| guard.sources.clone()));
|
||||
|
||||
if guard.dirty {
|
||||
if is_dirty {
|
||||
guard.dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
let sources = guard.sources.clone();
|
||||
|
||||
drop(guard);
|
||||
|
||||
sources
|
||||
.into_iter()
|
||||
.any(|source| source.update_if_necessary())
|
||||
for source in sources.into_iter().flatten() {
|
||||
if source.update_if_necessary() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn mark_check(&self) {
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::{
|
||||
},
|
||||
owner::Owner,
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
use futures::StreamExt;
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
@@ -53,7 +54,7 @@ where
|
||||
{
|
||||
/// Creates a new render effect, which immediately runs `fun`.
|
||||
pub fn new(fun: impl FnMut(Option<T>) -> T + 'static) -> Self {
|
||||
Self::new_with_value_erased(Box::new(fun), None)
|
||||
Self::new_with_value(fun, None)
|
||||
}
|
||||
|
||||
/// Creates a new render effect with an initial value.
|
||||
@@ -61,70 +62,59 @@ where
|
||||
fun: impl FnMut(Option<T>) -> T + 'static,
|
||||
initial_value: Option<T>,
|
||||
) -> Self {
|
||||
Self::new_with_value_erased(Box::new(fun), initial_value)
|
||||
}
|
||||
|
||||
fn new_with_value_erased(
|
||||
mut fun: Box<dyn FnMut(Option<T>) -> T + 'static>,
|
||||
initial_value: Option<T>,
|
||||
) -> Self {
|
||||
// codegen optimisation:
|
||||
fn prep() -> (Owner, Arc<RwLock<EffectInner>>, crate::channel::Receiver)
|
||||
{
|
||||
let (observer, rx) = channel();
|
||||
fn erased<T>(
|
||||
mut fun: Box<dyn FnMut(Option<T>) -> T + 'static>,
|
||||
initial_value: Option<T>,
|
||||
) -> RenderEffect<T> {
|
||||
let (observer, mut rx) = channel();
|
||||
let value = Arc::new(RwLock::new(None::<T>));
|
||||
let owner = Owner::new();
|
||||
let inner = Arc::new(RwLock::new(EffectInner {
|
||||
dirty: false,
|
||||
observer,
|
||||
sources: SourceSet::new(),
|
||||
}));
|
||||
(owner, inner, rx)
|
||||
}
|
||||
|
||||
let (owner, inner, mut rx) = prep();
|
||||
let initial_value = cfg!(feature = "effects").then(|| {
|
||||
owner.with(|| {
|
||||
inner
|
||||
.to_any_subscriber()
|
||||
.with_observer(|| fun(initial_value))
|
||||
})
|
||||
});
|
||||
*value.write().or_poisoned() = initial_value;
|
||||
|
||||
let value = Arc::new(RwLock::new(None::<T>));
|
||||
if cfg!(feature = "effects") {
|
||||
Executor::spawn_local({
|
||||
let value = Arc::clone(&value);
|
||||
let subscriber = inner.to_any_subscriber();
|
||||
|
||||
#[cfg(not(feature = "effects"))]
|
||||
{
|
||||
let _ = initial_value;
|
||||
let _ = owner;
|
||||
let _ = &mut rx;
|
||||
let _ = &mut fun;
|
||||
}
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if !owner.paused()
|
||||
&& subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
})
|
||||
{
|
||||
subscriber.clear_sources(&subscriber);
|
||||
|
||||
#[cfg(feature = "effects")]
|
||||
{
|
||||
let subscriber = inner.to_any_subscriber();
|
||||
*value.write().or_poisoned() = Some(
|
||||
owner.with(|| subscriber.with_observer(|| fun(initial_value))),
|
||||
);
|
||||
|
||||
any_spawner::Executor::spawn_local({
|
||||
let value = Arc::clone(&value);
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if !owner.paused()
|
||||
&& subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
})
|
||||
{
|
||||
subscriber.clear_sources(&subscriber);
|
||||
|
||||
let old_value =
|
||||
mem::take(&mut *value.write().or_poisoned());
|
||||
let new_value = owner.with_cleanup(|| {
|
||||
subscriber.with_observer(|| fun(old_value))
|
||||
});
|
||||
*value.write().or_poisoned() = Some(new_value);
|
||||
let old_value = mem::take(
|
||||
&mut *value.write().or_poisoned(),
|
||||
);
|
||||
let new_value = owner.with_cleanup(|| {
|
||||
subscriber.with_observer(|| fun(old_value))
|
||||
});
|
||||
*value.write().or_poisoned() = Some(new_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
RenderEffect { value, inner }
|
||||
}
|
||||
|
||||
RenderEffect { value, inner }
|
||||
erased(Box::new(fun), initial_value)
|
||||
}
|
||||
|
||||
/// Mutably accesses the current value.
|
||||
|
||||
@@ -48,10 +48,10 @@
|
||||
//!
|
||||
//! ## Design Principles and Assumptions
|
||||
//! - **Effects are expensive.** The library is built on the assumption that the side effects
|
||||
//! (making a network request, rendering something to the DOM, writing to disk) are orders of
|
||||
//! magnitude more expensive than propagating signal updates. As a result, the algorithm is
|
||||
//! designed to avoid re-running side effects unnecessarily, and is willing to sacrifice a small
|
||||
//! amount of raw update speed to that goal.
|
||||
//! (making a network request, rendering something to the DOM, writing to disk) are orders of
|
||||
//! magnitude more expensive than propagating signal updates. As a result, the algorithm is
|
||||
//! designed to avoid re-running side effects unnecessarily, and is willing to sacrifice a small
|
||||
//! amount of raw update speed to that goal.
|
||||
//! - **Automatic dependency tracking.** Dependencies are not specified as a compile-time list, but
|
||||
//! tracked at runtime. This in turn enables **dynamic dependency tracking**: subscribers
|
||||
//! unsubscribe from their sources between runs, which means that a subscriber that contains a
|
||||
@@ -81,7 +81,6 @@ pub mod diagnostics;
|
||||
pub mod effect;
|
||||
pub mod graph;
|
||||
pub mod owner;
|
||||
pub mod send_wrapper_ext;
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde;
|
||||
pub mod signal;
|
||||
|
||||
@@ -60,38 +60,6 @@ pub struct Owner {
|
||||
pub(crate) shared_context: Option<Arc<dyn SharedContext + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl Owner {
|
||||
fn downgrade(&self) -> WeakOwner {
|
||||
WeakOwner {
|
||||
inner: Arc::downgrade(&self.inner),
|
||||
#[cfg(feature = "hydration")]
|
||||
shared_context: self.shared_context.as_ref().map(Arc::downgrade),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WeakOwner {
|
||||
inner: Weak<RwLock<OwnerInner>>,
|
||||
#[cfg(feature = "hydration")]
|
||||
shared_context: Option<Weak<dyn SharedContext + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl WeakOwner {
|
||||
fn upgrade(&self) -> Option<Owner> {
|
||||
self.inner.upgrade().map(|inner| {
|
||||
#[cfg(feature = "hydration")]
|
||||
let shared_context =
|
||||
self.shared_context.as_ref().and_then(|sc| sc.upgrade());
|
||||
Owner {
|
||||
inner,
|
||||
#[cfg(feature = "hydration")]
|
||||
shared_context,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Owner {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.inner, &other.inner)
|
||||
@@ -99,7 +67,7 @@ impl PartialEq for Owner {
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static OWNER: RefCell<Option<WeakOwner>> = Default::default();
|
||||
static OWNER: RefCell<Option<Owner>> = Default::default();
|
||||
}
|
||||
|
||||
impl Owner {
|
||||
@@ -139,16 +107,12 @@ impl Owner {
|
||||
/// Creates a new `Owner` and registers it as a child of the current `Owner`, if there is one.
|
||||
pub fn new() -> Self {
|
||||
#[cfg(not(feature = "hydration"))]
|
||||
let parent = OWNER.with(|o| {
|
||||
o.borrow()
|
||||
.as_ref()
|
||||
.and_then(|o| o.upgrade())
|
||||
.map(|o| Arc::downgrade(&o.inner))
|
||||
});
|
||||
let parent = OWNER
|
||||
.with(|o| o.borrow().as_ref().map(|o| Arc::downgrade(&o.inner)));
|
||||
#[cfg(feature = "hydration")]
|
||||
let (parent, shared_context) = OWNER
|
||||
.with(|o| {
|
||||
o.borrow().as_ref().and_then(|o| o.upgrade()).map(|o| {
|
||||
o.borrow().as_ref().map(|o| {
|
||||
(Some(Arc::downgrade(&o.inner)), o.shared_context.clone())
|
||||
})
|
||||
})
|
||||
@@ -236,34 +200,24 @@ impl Owner {
|
||||
|
||||
/// Sets this as the current `Owner`.
|
||||
pub fn set(&self) {
|
||||
OWNER.with_borrow_mut(|owner| *owner = Some(self.downgrade()));
|
||||
OWNER.with_borrow_mut(|owner| *owner = Some(self.clone()));
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
Arena::set(&self.inner.read().or_poisoned().arena);
|
||||
}
|
||||
|
||||
/// Runs the given function with this as the current `Owner`.
|
||||
pub fn with<T>(&self, fun: impl FnOnce() -> T) -> T {
|
||||
// codegen optimisation:
|
||||
fn inner_1(self_: &Owner) -> Option<WeakOwner> {
|
||||
let prev = {
|
||||
OWNER.with(|o| (*o.borrow_mut()).replace(self_.downgrade()))
|
||||
};
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
Arena::set(&self_.inner.read().or_poisoned().arena);
|
||||
prev
|
||||
}
|
||||
let prev = inner_1(self);
|
||||
|
||||
let val = fun();
|
||||
|
||||
// monomorphisation optimisation:
|
||||
fn inner_2(prev: Option<WeakOwner>) {
|
||||
let prev = {
|
||||
OWNER.with(|o| {
|
||||
*o.borrow_mut() = prev;
|
||||
});
|
||||
}
|
||||
inner_2(prev);
|
||||
|
||||
mem::replace(&mut *o.borrow_mut(), Some(self.clone()))
|
||||
})
|
||||
};
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
Arena::set(&self.inner.read().or_poisoned().arena);
|
||||
let val = fun();
|
||||
OWNER.with(|o| {
|
||||
*o.borrow_mut() = prev;
|
||||
});
|
||||
val
|
||||
}
|
||||
|
||||
@@ -303,7 +257,7 @@ impl Owner {
|
||||
|
||||
/// Returns the current `Owner`, if any.
|
||||
pub fn current() -> Option<Owner> {
|
||||
OWNER.with(|o| o.borrow().as_ref().and_then(|n| n.upgrade()))
|
||||
OWNER.with(|o| o.borrow().clone())
|
||||
}
|
||||
|
||||
/// Returns the [`SharedContext`] associated with this owner, if any.
|
||||
@@ -317,7 +271,7 @@ impl Owner {
|
||||
/// Removes this from its state as the thread-local owner and drops it.
|
||||
pub fn unset(self) {
|
||||
OWNER.with_borrow_mut(|owner| {
|
||||
if owner.as_ref().and_then(|n| n.upgrade()) == Some(self) {
|
||||
if owner.as_ref() == Some(&self) {
|
||||
mem::take(owner);
|
||||
}
|
||||
})
|
||||
@@ -330,7 +284,6 @@ impl Owner {
|
||||
OWNER.with(|o| {
|
||||
o.borrow()
|
||||
.as_ref()
|
||||
.and_then(|o| o.upgrade())
|
||||
.and_then(|current| current.shared_context.clone())
|
||||
})
|
||||
}
|
||||
@@ -344,7 +297,6 @@ impl Owner {
|
||||
|
||||
let sc = OWNER.with_borrow(|o| {
|
||||
o.as_ref()
|
||||
.and_then(|o| o.upgrade())
|
||||
.and_then(|current| current.shared_context.clone())
|
||||
});
|
||||
match sc {
|
||||
@@ -370,7 +322,6 @@ impl Owner {
|
||||
|
||||
let sc = OWNER.with_borrow(|o| {
|
||||
o.as_ref()
|
||||
.and_then(|o| o.upgrade())
|
||||
.and_then(|current| current.shared_context.clone())
|
||||
});
|
||||
match sc {
|
||||
|
||||
@@ -48,30 +48,20 @@ impl Arena {
|
||||
fun(&MAP.get_or_init(Default::default).read().or_poisoned())
|
||||
}
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
{
|
||||
Arena::try_with(fun).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"at {}, the `sandboxed-arenas` feature is active, but no \
|
||||
Arena is active",
|
||||
std::panic::Location::caller()
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn try_with<U>(fun: impl FnOnce(&ArenaMap) -> U) -> Option<U> {
|
||||
#[cfg(not(feature = "sandboxed-arenas"))]
|
||||
{
|
||||
Some(fun(&MAP.get_or_init(Default::default).read().or_poisoned()))
|
||||
}
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
{
|
||||
MAP.with_borrow(|arena| {
|
||||
arena
|
||||
fun(&arena
|
||||
.as_ref()
|
||||
.and_then(Weak::upgrade)
|
||||
.map(|n| fun(&n.read().or_poisoned()))
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"at {}, the `sandboxed-arenas` feature is active, \
|
||||
but no Arena is active",
|
||||
std::panic::Location::caller()
|
||||
)
|
||||
})
|
||||
.read()
|
||||
.or_poisoned())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -84,32 +74,20 @@ impl Arena {
|
||||
}
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
{
|
||||
Arena::try_with_mut(fun).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"at {}, the `sandboxed-arenas` feature is active, but no \
|
||||
Arena is active",
|
||||
std::panic::Location::caller()
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn try_with_mut<U>(fun: impl FnOnce(&mut ArenaMap) -> U) -> Option<U> {
|
||||
#[cfg(not(feature = "sandboxed-arenas"))]
|
||||
{
|
||||
Some(fun(&mut MAP
|
||||
.get_or_init(Default::default)
|
||||
.write()
|
||||
.or_poisoned()))
|
||||
}
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
{
|
||||
let caller = std::panic::Location::caller();
|
||||
MAP.with_borrow(|arena| {
|
||||
arena
|
||||
fun(&mut arena
|
||||
.as_ref()
|
||||
.and_then(Weak::upgrade)
|
||||
.map(|n| fun(&mut n.write().or_poisoned()))
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"at {}, the `sandboxed-arenas` feature is active, \
|
||||
but no Arena is active",
|
||||
caller
|
||||
)
|
||||
})
|
||||
.write()
|
||||
.or_poisoned())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -148,7 +126,6 @@ pub mod sandboxed {
|
||||
/// called.
|
||||
///
|
||||
/// [item]:[crate::owner::ArenaItem]
|
||||
#[track_caller]
|
||||
pub fn new(inner: T) -> Self {
|
||||
let arena = MAP.with_borrow(|n| n.as_ref().and_then(Weak::upgrade));
|
||||
Self { arena, inner }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user