mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 11:21:55 -05:00
Compare commits
80 Commits
result-ali
...
3704v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea3d772eb7 | ||
|
|
2a19515c41 | ||
|
|
d0bf843821 | ||
|
|
7250bc312e | ||
|
|
0e8242f94c | ||
|
|
439b41f0e8 | ||
|
|
a4e47d4086 | ||
|
|
3164721fdb | ||
|
|
2242ad1847 | ||
|
|
131b18bddb | ||
|
|
5e9d6e2dfd | ||
|
|
d76e5bb4ea | ||
|
|
f752e32ae3 | ||
|
|
a9197102a6 | ||
|
|
58f1bf95e1 | ||
|
|
38a3aae28e | ||
|
|
5eaaff045f | ||
|
|
e9384e3286 | ||
|
|
083f9c663f | ||
|
|
63c9549120 | ||
|
|
6232f6482a | ||
|
|
825e89f25c | ||
|
|
5da4c438d9 | ||
|
|
80ed74c075 | ||
|
|
79e9340a9b | ||
|
|
41d01cedb2 | ||
|
|
374a020d84 | ||
|
|
83fcf8663c | ||
|
|
1363b941bc | ||
|
|
f7a1a2cab2 | ||
|
|
92b82688a6 | ||
|
|
42988b1bc1 | ||
|
|
c75397ea75 | ||
|
|
848fd724dd | ||
|
|
5bc254d49f | ||
|
|
885f4a1654 | ||
|
|
ddd243d07a | ||
|
|
362c300eac | ||
|
|
284a724e5f | ||
|
|
6ecc681cdd | ||
|
|
7c34b4a4a5 | ||
|
|
37cf25fba5 | ||
|
|
f975b8d33b | ||
|
|
4804dac32d | ||
|
|
a9f27d6128 | ||
|
|
04cb036a7d | ||
|
|
1d3784ed7b | ||
|
|
8cc1a34c00 | ||
|
|
68f4d46c5f | ||
|
|
590728e47e | ||
|
|
e84b527743 | ||
|
|
96b125d54f | ||
|
|
16d66362f8 | ||
|
|
e27801a2c2 | ||
|
|
b81f71997b | ||
|
|
2a11325749 | ||
|
|
5604f3e979 | ||
|
|
3a9a0891a3 | ||
|
|
a39add50c0 | ||
|
|
2a2675dd36 | ||
|
|
7be6a9da86 | ||
|
|
c51e07b5a4 | ||
|
|
b17a4c92c7 | ||
|
|
03f9c6cb6d | ||
|
|
9e8b8886da | ||
|
|
6a6b3dee15 | ||
|
|
5d71913523 | ||
|
|
706617ab0a | ||
|
|
cd64bb9d67 | ||
|
|
f881c1877d | ||
|
|
8783f6a478 | ||
|
|
2add714c65 | ||
|
|
88b1b2d882 | ||
|
|
9d3a743d33 | ||
|
|
c6de7c714e | ||
|
|
6154199850 | ||
|
|
32be3a023a | ||
|
|
d9043e4f34 | ||
|
|
e3010c7f1f | ||
|
|
1dcc5838f7 |
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-2024-08-01)
|
||||
name: Run semver check (nightly-2025-03-05)
|
||||
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-2024-08-01
|
||||
rust-toolchain: nightly-2025-03-05
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -26,4 +26,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly-2024-08-01
|
||||
toolchain: nightly-2025-03-05
|
||||
|
||||
2
.github/workflows/run-cargo-make-task.yml
vendored
2
.github/workflows/run-cargo-make-task.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
- name: Install wasm-bindgen
|
||||
run: cargo binstall wasm-bindgen-cli --no-confirm
|
||||
- name: Install cargo-leptos
|
||||
run: cargo binstall cargo-leptos --no-confirm
|
||||
run: cargo binstall cargo-leptos --locked --no-confirm
|
||||
- name: Install Trunk
|
||||
uses: jetli/trunk-action@v0.5.0
|
||||
with:
|
||||
|
||||
139
Cargo.lock
generated
139
Cargo.lock
generated
@@ -389,13 +389,13 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.1"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
||||
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http 1.2.0",
|
||||
"http-body",
|
||||
@@ -424,10 +424,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.0"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
|
||||
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http 1.2.0",
|
||||
@@ -507,9 +508,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "bytecheck"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50c8f430744b23b54ad15161fcbc22d82a29b73eacbe425fea23ec822600bc6f"
|
||||
checksum = "50690fb3370fb9fe3550372746084c46f2ac8c9685c583d2be10eefd89d3d1a3"
|
||||
dependencies = [
|
||||
"bytecheck_derive",
|
||||
"ptr_meta",
|
||||
@@ -519,9 +520,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bytecheck_derive"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "523363cbe1df49b68215efdf500b103ac3b0fb4836aed6d15689a076eadb8fff"
|
||||
checksum = "efb7846e0cb180355c2dec69e721edafa36919850f1a9f52ffba4ebc0393cb71"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -557,9 +558,9 @@ checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.12"
|
||||
version = "1.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2"
|
||||
checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
@@ -655,15 +656,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "0.14.1"
|
||||
version = "0.15.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
|
||||
checksum = "8cf9dc8d4ef88e27a8cb23e85cb116403dedd57f7971964dc4b18ccead548901"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"nom",
|
||||
"pathdiff",
|
||||
"serde",
|
||||
"toml",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -678,12 +679,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-str"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9"
|
||||
|
||||
[[package]]
|
||||
name = "const_format"
|
||||
version = "0.2.34"
|
||||
@@ -723,6 +718,15 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.16.2"
|
||||
@@ -884,7 +888,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "either_of"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
dependencies = [
|
||||
"paste",
|
||||
"pin-project-lite",
|
||||
@@ -1254,9 +1258,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "guardian"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "493913a18c0d7bebb75127a26a432162c59edbe06f6cf712001e3e769345e8b5"
|
||||
checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f"
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
@@ -1690,9 +1694,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
@@ -1730,14 +1734,13 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
||||
|
||||
[[package]]
|
||||
name = "leptos"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"any_spawner",
|
||||
"base64",
|
||||
"cfg-if",
|
||||
"either_of",
|
||||
"futures",
|
||||
"getrandom 0.2.15",
|
||||
"hydration_context",
|
||||
"leptos-spin-macro",
|
||||
"leptos_config",
|
||||
@@ -1781,7 +1784,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_actix"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"actix-files",
|
||||
"actix-http",
|
||||
@@ -1806,7 +1809,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_axum"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"any_spawner",
|
||||
"axum",
|
||||
@@ -1829,7 +1832,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_config"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"config",
|
||||
"regex",
|
||||
@@ -1843,7 +1846,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_dom"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"leptos",
|
||||
@@ -1860,7 +1863,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_hot_reload"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
@@ -1876,7 +1879,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_integration_utils"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"hydration_context",
|
||||
@@ -1889,11 +1892,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_macro"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"attribute-derive",
|
||||
"cfg-if",
|
||||
"convert_case 0.6.0",
|
||||
"convert_case 0.7.1",
|
||||
"html-escape",
|
||||
"insta",
|
||||
"itertools",
|
||||
@@ -1908,7 +1911,7 @@ dependencies = [
|
||||
"rstml",
|
||||
"serde",
|
||||
"server_fn",
|
||||
"server_fn_macro 0.8.0-alpha",
|
||||
"server_fn_macro 0.7.7",
|
||||
"syn 2.0.98",
|
||||
"tracing",
|
||||
"trybuild",
|
||||
@@ -1918,7 +1921,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_meta"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"indexmap",
|
||||
@@ -1933,7 +1936,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_router"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"any_spawner",
|
||||
"either_of",
|
||||
@@ -1957,7 +1960,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_router_macro"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"leptos_macro",
|
||||
"leptos_router",
|
||||
@@ -1969,7 +1972,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_server"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"any_spawner",
|
||||
"base64",
|
||||
@@ -2076,9 +2079,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
@@ -2113,12 +2116,6 @@ dependencies = [
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniserde"
|
||||
version = "0.1.41"
|
||||
@@ -2132,9 +2129,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
|
||||
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
@@ -2209,16 +2206,6 @@ dependencies = [
|
||||
name = "next_tuple"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
@@ -2671,7 +2658,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reactive_graph"
|
||||
version = "0.1.4"
|
||||
version = "0.1.7"
|
||||
dependencies = [
|
||||
"any_spawner",
|
||||
"async-lock",
|
||||
@@ -2693,7 +2680,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reactive_stores"
|
||||
version = "0.1.3"
|
||||
version = "0.1.7"
|
||||
dependencies = [
|
||||
"any_spawner",
|
||||
"guardian",
|
||||
@@ -2710,9 +2697,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reactive_stores_macro"
|
||||
version = "0.1.0"
|
||||
version = "0.1.7"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"convert_case 0.7.1",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2945,9 +2932,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.22"
|
||||
version = "0.23.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7"
|
||||
checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
@@ -3168,14 +3155,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "server_fn"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"axum",
|
||||
"base64",
|
||||
"bytes",
|
||||
"ciborium",
|
||||
"const-str",
|
||||
"const_format",
|
||||
"dashmap",
|
||||
"futures",
|
||||
@@ -3226,7 +3211,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "server_fn_macro"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"const_format",
|
||||
"convert_case 0.6.0",
|
||||
@@ -3238,9 +3223,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "server_fn_macro_default"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
dependencies = [
|
||||
"server_fn_macro 0.8.0-alpha",
|
||||
"server_fn_macro 0.7.7",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
@@ -3434,7 +3419,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tachys"
|
||||
version = "0.1.4"
|
||||
version = "0.1.7"
|
||||
dependencies = [
|
||||
"any_spawner",
|
||||
"async-trait",
|
||||
@@ -3726,9 +3711,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.23"
|
||||
version = "0.22.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
|
||||
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@@ -4231,9 +4216,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f"
|
||||
checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
41
Cargo.toml
41
Cargo.toml
@@ -40,7 +40,7 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
@@ -48,28 +48,31 @@ rust-version = "1.76"
|
||||
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.0" }
|
||||
either_of = { path = "./either_of/", version = "0.1.5" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0" }
|
||||
leptos = { path = "./leptos", version = "0.8.0-alpha" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.0-alpha" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.0-alpha" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-alpha" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-alpha" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.0-alpha" }
|
||||
leptos_router = { path = "./router", version = "0.8.0-alpha" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.0-alpha" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.0-alpha" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.0-alpha" }
|
||||
itertools = "0.14.0"
|
||||
leptos = { path = "./leptos", version = "0.7.7" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.7" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.7" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.7" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.7" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.7" }
|
||||
leptos_router = { path = "./router", version = "0.7.7" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.7" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.7" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.7" }
|
||||
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.1.4" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.3" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.0-alpha" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-alpha" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-alpha" }
|
||||
tachys = { path = "./tachys", version = "0.1.4" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.7" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.7" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.7" }
|
||||
serde_json = "1.0.0"
|
||||
server_fn = { path = "./server_fn", version = "0.7.7" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.7" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.7" }
|
||||
tachys = { path = "./tachys", version = "0.1.7" }
|
||||
wasm-bindgen = { version = "0.2.100" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -21,7 +21,7 @@ use leptos::*;
|
||||
#[component]
|
||||
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
// create a reactive signal with the initial value
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
let (value, set_value) = signal(initial_value);
|
||||
|
||||
// create event handlers for our buttons
|
||||
// note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
|
||||
@@ -46,7 +46,7 @@ pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
pub fn SimpleCounterWithBuilder(initial_value: i32) -> impl IntoView {
|
||||
use leptos::html::*;
|
||||
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
let (value, set_value) = signal(initial_value);
|
||||
let clear = move |_| set_value(0);
|
||||
let decrement = move |_| set_value.update(|value| *value -= 1);
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
@@ -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,
|
||||
mem, ops,
|
||||
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| mem::replace(this, Some(hook))),
|
||||
ERROR_HOOK.with_borrow_mut(|this| Option::replace(this, hook)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ tokio = { version = "1.41", optional = true, default-features = false, features
|
||||
"rt",
|
||||
] }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.47", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.50", optional = true }
|
||||
|
||||
[features]
|
||||
async-executor = ["dep:async-executor"]
|
||||
|
||||
@@ -23,7 +23,7 @@ tokio-test = "0.4.0"
|
||||
miniserde = "0.1.0"
|
||||
gloo = "0.8.0"
|
||||
uuid = { version = "1.0", features = ["serde", "v4", "wasm-bindgen"] }
|
||||
wasm-bindgen = "0.2.0"
|
||||
wasm-bindgen = "0.2.100"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4.0"
|
||||
strum = "0.24.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "either_of"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -15,4 +15,4 @@ paste = "1.0.15"
|
||||
|
||||
[features]
|
||||
default = ["no_std"]
|
||||
no_std = []
|
||||
no_std = []
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[tasks.install-cargo-leptos]
|
||||
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
|
||||
args = ["--locked"]
|
||||
|
||||
[tasks.cargo-leptos-e2e]
|
||||
command = "cargo"
|
||||
|
||||
@@ -19,7 +19,7 @@ async fn clear() {
|
||||
// note that we start at the initial value of 10
|
||||
let _dispose = mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|| view! { <SimpleCounter initial_value=10 step=1 /> },
|
||||
|| view! { <SimpleCounter initial_value=10 step=1/> },
|
||||
);
|
||||
|
||||
// now we extract the buttons by iterating over the DOM
|
||||
@@ -59,9 +59,9 @@ async fn clear() {
|
||||
// .into_view() here is just a convenient way of specifying "use the regular DOM renderer"
|
||||
.into_view()
|
||||
// views are lazy -- they describe a DOM tree but don't create it yet
|
||||
// calling .build(None) will actually build the DOM elements
|
||||
.build(None)
|
||||
// .build(None) returned an ElementState, which is a smart pointer for
|
||||
// calling .build() will actually build the DOM elements
|
||||
.build()
|
||||
// .build() returned an ElementState, which is a smart pointer for
|
||||
// a DOM element. So we can still just call .outer_html(), which access the outerHTML on
|
||||
// the actual DOM element
|
||||
.outer_html()
|
||||
@@ -87,7 +87,7 @@ async fn inc() {
|
||||
|
||||
let _dispose = mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|| view! { <SimpleCounter initial_value=0 step=1 /> },
|
||||
|| view! { <SimpleCounter initial_value=0 step=1/> },
|
||||
);
|
||||
|
||||
// You can do testing with vanilla DOM operations
|
||||
@@ -150,7 +150,7 @@ async fn inc() {
|
||||
}
|
||||
}
|
||||
.into_view()
|
||||
.build(None)
|
||||
.build()
|
||||
.outer_html()
|
||||
);
|
||||
|
||||
@@ -173,7 +173,7 @@ async fn inc() {
|
||||
}
|
||||
}
|
||||
.into_view()
|
||||
.build(None)
|
||||
.build()
|
||||
.outer_html()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,7 +10,10 @@ 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 = [
|
||||
@@ -18,7 +21,7 @@ leptos_axum = { path = "../../integrations/axum", features = [
|
||||
], 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 }
|
||||
|
||||
@@ -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.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = [
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.5.2", optional = true }
|
||||
tower-http = { version = "0.6.2", features = [
|
||||
"fs",
|
||||
"tracing",
|
||||
"trace",
|
||||
], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.11"
|
||||
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.26.3", features = ["strum_macros", "derive"] }
|
||||
notify = { version = "6.1", optional = true }
|
||||
strum = { version = "0.27.1", features = ["strum_macros", "derive"] }
|
||||
notify = { version = "8.0", optional = true }
|
||||
pin-project-lite = "0.2.14"
|
||||
dashmap = { version = "6.0", optional = true }
|
||||
once_cell = { version = "1.19", optional = true }
|
||||
|
||||
@@ -9,9 +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},
|
||||
response::{browser::BrowserResponse, ClientRes, Res},
|
||||
};
|
||||
use std::future::Future;
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -653,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>
|
||||
@@ -733,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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,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)
|
||||
}
|
||||
}
|
||||
@@ -784,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)
|
||||
}
|
||||
}
|
||||
@@ -812,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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -881,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;
|
||||
@@ -893,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();
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly-2024-01-29"
|
||||
channel = "nightly-2025-03-05"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ throw_error = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
futures = "0.3.31"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
wasm-bindgen = { version = "0.2.97", optional = true }
|
||||
wasm-bindgen = { version = "0.2.100", optional = true }
|
||||
js-sys = { version = "0.3.74", optional = true }
|
||||
once_cell = "1.20"
|
||||
pin-project-lite = "0.2.15"
|
||||
|
||||
@@ -21,10 +21,10 @@ leptos_macro = { workspace = true, features = ["actix"] }
|
||||
leptos_meta = { workspace = true, features = ["nonce"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
server_fn = { workspace = true, features = ["actix"] }
|
||||
serde_json = "1.0"
|
||||
serde_json = { workspace = true }
|
||||
parking_lot = "0.12.3"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
tokio = { version = "1.41", features = ["rt", "fs"] }
|
||||
tokio = { version = "1.43", features = ["rt", "fs"] }
|
||||
send_wrapper = "0.6.0"
|
||||
dashmap = "6"
|
||||
once_cell = "1"
|
||||
|
||||
@@ -274,14 +274,13 @@ 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,7 +296,6 @@ pub fn redirect(path: &str) {
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -369,6 +367,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(),
|
||||
@@ -432,7 +431,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;
|
||||
@@ -443,7 +442,6 @@ 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();
|
||||
@@ -463,7 +461,6 @@ pub fn handle_server_fns_with_context(
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -491,7 +488,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;
|
||||
@@ -502,7 +499,6 @@ 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();
|
||||
@@ -525,7 +521,6 @@ where
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -551,7 +546,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;
|
||||
@@ -562,7 +557,6 @@ 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();
|
||||
@@ -582,7 +576,6 @@ where
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
|
||||
@@ -11,7 +11,7 @@ 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"
|
||||
@@ -24,14 +24,14 @@ leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12.3"
|
||||
tokio = { version = "1.41", default-features = false }
|
||||
tokio = { version = "1.43", default-features = false }
|
||||
tower = { version = "0.5.1", features = ["util"] }
|
||||
tower-http = "0.6.2"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = "0.8.1"
|
||||
tokio = { version = "1.41", features = ["net", "rt-multi-thread"] }
|
||||
axum = "0.7.9"
|
||||
tokio = { version = "1.43", features = ["net", "rt-multi-thread"] }
|
||||
|
||||
[features]
|
||||
wasm = []
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
//! Provides functions to easily integrate Leptos with Axum.
|
||||
//!
|
||||
@@ -278,12 +279,11 @@ 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,7 +299,9 @@ 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.
|
||||
@@ -368,6 +370,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);
|
||||
@@ -440,7 +444,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};
|
||||
@@ -450,7 +454,6 @@ 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() {
|
||||
@@ -469,7 +472,9 @@ pub type PinnedHtmlStream =
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// }
|
||||
/// # }
|
||||
///
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() { }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -482,7 +487,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>>
|
||||
@@ -506,7 +511,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>,
|
||||
@@ -528,7 +533,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};
|
||||
@@ -538,7 +543,6 @@ 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() {
|
||||
@@ -557,7 +561,9 @@ where
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// }
|
||||
/// # }
|
||||
///
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() { }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -570,7 +576,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>>
|
||||
@@ -623,14 +629,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,
|
||||
@@ -653,8 +658,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>,
|
||||
@@ -755,15 +760,14 @@ 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,
|
||||
@@ -823,8 +827,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>>
|
||||
@@ -847,17 +851,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>>,
|
||||
) -> 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,
|
||||
{
|
||||
@@ -941,7 +941,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};
|
||||
@@ -951,7 +951,6 @@ 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() {
|
||||
@@ -971,7 +970,9 @@ fn provide_contexts(
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// }
|
||||
/// # }
|
||||
///
|
||||
/// # #[cfg(not(feature = "default"))]
|
||||
/// # fn main() { }
|
||||
/// ```
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
@@ -984,7 +985,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>>
|
||||
@@ -1038,8 +1039,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>>
|
||||
@@ -1105,8 +1106,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>>
|
||||
@@ -1645,7 +1646,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;
|
||||
@@ -1660,8 +1661,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;
|
||||
@@ -1694,15 +1695,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(_) => {
|
||||
@@ -1734,7 +1732,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,
|
||||
@@ -1750,8 +1748,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,
|
||||
@@ -1988,7 +1986,8 @@ where
|
||||
/// This is provided as a convenience, but is a fairly simple function. If you need to adapt it,
|
||||
/// simply reuse the source code of this function in your own application.
|
||||
#[cfg(feature = "default")]
|
||||
pub fn file_and_error_handler<S, IV>(
|
||||
pub fn file_and_error_handler_with_context<S, IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
shell: fn(LeptosOptions) -> IV,
|
||||
) -> impl Fn(
|
||||
Uri,
|
||||
@@ -2004,40 +2003,68 @@ where
|
||||
LeptosOptions: FromRef<S>,
|
||||
{
|
||||
move |uri: Uri, State(state): State<S>, req: Request<Body>| {
|
||||
Box::pin(async move {
|
||||
let options = LeptosOptions::from_ref(&state);
|
||||
let res = get_static_file(uri, &options.site_root, req.headers());
|
||||
let res = res.await.unwrap();
|
||||
Box::pin({
|
||||
let additional_context = additional_context.clone();
|
||||
async move {
|
||||
let options = LeptosOptions::from_ref(&state);
|
||||
let res =
|
||||
get_static_file(uri, &options.site_root, req.headers());
|
||||
let res = res.await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let mut res = handle_response_inner(
|
||||
move || {
|
||||
provide_context(state.clone());
|
||||
},
|
||||
move || shell(options),
|
||||
req,
|
||||
|app, chunks| {
|
||||
Box::pin(async move {
|
||||
let app = app
|
||||
.to_html_stream_in_order()
|
||||
.collect::<String>()
|
||||
.await;
|
||||
let chunks = chunks();
|
||||
Box::pin(once(async move { app }).chain(chunks))
|
||||
as PinnedStream<String>
|
||||
})
|
||||
},
|
||||
)
|
||||
.await;
|
||||
*res.status_mut() = StatusCode::NOT_FOUND;
|
||||
res
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let mut res = handle_response_inner(
|
||||
move || {
|
||||
additional_context();
|
||||
provide_context(state.clone());
|
||||
},
|
||||
move || shell(options),
|
||||
req,
|
||||
|app, chunks| {
|
||||
Box::pin(async move {
|
||||
let app = app
|
||||
.to_html_stream_in_order()
|
||||
.collect::<String>()
|
||||
.await;
|
||||
let chunks = chunks();
|
||||
Box::pin(once(async move { app }).chain(chunks))
|
||||
as PinnedStream<String>
|
||||
})
|
||||
},
|
||||
)
|
||||
.await;
|
||||
*res.status_mut() = StatusCode::NOT_FOUND;
|
||||
res
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A reasonable handler for serving static files (like JS/WASM/CSS) and 404 errors.
|
||||
///
|
||||
/// This is provided as a convenience, but is a fairly simple function. If you need to adapt it,
|
||||
/// simply reuse the source code of this function in your own application.
|
||||
#[cfg(feature = "default")]
|
||||
pub fn file_and_error_handler<S, IV>(
|
||||
shell: fn(LeptosOptions) -> IV,
|
||||
) -> impl Fn(
|
||||
Uri,
|
||||
State<S>,
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
S: Send + Sync + Clone + 'static,
|
||||
LeptosOptions: FromRef<S>,
|
||||
{
|
||||
file_and_error_handler_with_context(move || (), shell)
|
||||
}
|
||||
|
||||
#[cfg(feature = "default")]
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
|
||||
@@ -52,12 +52,11 @@ web-sys = { version = "0.3.72", features = [
|
||||
"ShadowRootInit",
|
||||
"ShadowRootMode",
|
||||
] }
|
||||
wasm-bindgen = "0.2.97"
|
||||
wasm-bindgen = { workspace = true }
|
||||
serde_qs = "0.13.0"
|
||||
slotmap = "1.0"
|
||||
futures = "0.3.31"
|
||||
send_wrapper = "0.6.0"
|
||||
getrandom = { version = "0.2", features = ["js"], optional = true }
|
||||
|
||||
[features]
|
||||
hydration = [
|
||||
@@ -66,13 +65,12 @@ hydration = [
|
||||
"hydration_context/browser",
|
||||
"leptos_dom/hydration",
|
||||
]
|
||||
csr = ["leptos_macro/csr", "reactive_graph/effects", "dep:getrandom"]
|
||||
csr = ["leptos_macro/csr", "reactive_graph/effects"]
|
||||
hydrate = [
|
||||
"leptos_macro/hydrate",
|
||||
"hydration",
|
||||
"tachys/hydrate",
|
||||
"reactive_graph/effects",
|
||||
"dep:getrandom",
|
||||
]
|
||||
default-tls = ["server_fn/default-tls"]
|
||||
rustls = ["server_fn/rustls"]
|
||||
@@ -84,10 +82,7 @@ ssr = [
|
||||
"tachys/ssr",
|
||||
]
|
||||
nightly = ["leptos_macro/nightly", "reactive_graph/nightly", "tachys/nightly"]
|
||||
rkyv = [
|
||||
"server_fn/rkyv",
|
||||
"leptos_server/rkyv"
|
||||
]
|
||||
rkyv = ["server_fn/rkyv", "leptos_server/rkyv"]
|
||||
tracing = [
|
||||
"dep:tracing",
|
||||
"reactive_graph/tracing",
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::attr::{
|
||||
Attribute, NextAttribute,
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use tachys::view::any_view::ExtraAttrsMut;
|
||||
|
||||
/// Function stored to build/rebuild the wrapped children when attributes are added.
|
||||
type ChildBuilder<T> = dyn Fn(AnyAttribute) -> T + Send + Sync + 'static;
|
||||
@@ -44,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)
|
||||
}
|
||||
@@ -78,20 +77,16 @@ impl<T: IntoView> AttributeInterceptorInner<T, ()> {
|
||||
impl<T: IntoView, A: Attribute> Render for AttributeInterceptorInner<T, A> {
|
||||
type State = <T as Render>::State;
|
||||
|
||||
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
|
||||
self.children.build(extra_attrs)
|
||||
fn build(self) -> Self::State {
|
||||
self.children.build()
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
self,
|
||||
state: &mut Self::State,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
self.children.rebuild(state, extra_attrs);
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
self.children.rebuild(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoView + 'static, A> AddAnyAttr for AttributeInterceptorInner<T, A>
|
||||
impl<T: IntoView, A> AddAnyAttr for AttributeInterceptorInner<T, A>
|
||||
where
|
||||
A: Attribute,
|
||||
{
|
||||
@@ -119,23 +114,19 @@ 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;
|
||||
|
||||
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
|
||||
self.children.dry_resolve(extra_attrs)
|
||||
fn dry_resolve(&mut self) {
|
||||
self.children.dry_resolve()
|
||||
}
|
||||
|
||||
fn resolve(
|
||||
self,
|
||||
extra_attrs: ExtraAttrsMut<'_>,
|
||||
) -> impl std::future::Future<Output = Self::AsyncOutput> + Send {
|
||||
self.children.resolve(extra_attrs)
|
||||
self.children.resolve()
|
||||
}
|
||||
|
||||
fn to_html_with_buf(
|
||||
@@ -144,32 +135,16 @@ impl<T: IntoView + 'static, A: Attribute> RenderHtml
|
||||
position: &mut leptos::tachys::view::Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
self.children.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
)
|
||||
self.children
|
||||
.to_html_with_buf(buf, position, escape, mark_branches)
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &leptos::tachys::hydration::Cursor,
|
||||
position: &leptos::tachys::view::PositionState,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) -> Self::State {
|
||||
self.children
|
||||
.hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
AttributeInterceptorInner {
|
||||
children_builder: self.children_builder,
|
||||
children: self.children,
|
||||
attributes: self.attributes.into_cloneable_owned(),
|
||||
}
|
||||
self.children.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.clone();
|
||||
let callback2 = callback1;
|
||||
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.clone();
|
||||
let callback2 = callback1;
|
||||
assert!(callback1.matches(&callback2));
|
||||
}
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@ 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,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
|
||||
PositionState, Render, RenderHtml,
|
||||
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
|
||||
RenderHtml,
|
||||
},
|
||||
};
|
||||
use throw_error::{Error, ErrorHook, ErrorId};
|
||||
@@ -173,10 +173,10 @@ where
|
||||
{
|
||||
type State = RenderEffect<ErrorBoundaryViewState<Chil::State, Fal::State>>;
|
||||
|
||||
fn build(mut self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
|
||||
fn build(mut self) -> Self::State {
|
||||
let hook = Arc::clone(&self.hook);
|
||||
let _hook = throw_error::set_error_hook(Arc::clone(&hook));
|
||||
let mut children = Some(self.children.build(extra_attrs.clone()));
|
||||
let mut children = Some(self.children.build());
|
||||
RenderEffect::new(
|
||||
move |prev: Option<
|
||||
ErrorBoundaryViewState<Chil::State, Fal::State>,
|
||||
@@ -193,8 +193,7 @@ where
|
||||
// yes errors, and was showing children
|
||||
(false, None) => {
|
||||
state.fallback = Some(
|
||||
(self.fallback)(self.errors.clone())
|
||||
.build(extra_attrs.clone()),
|
||||
(self.fallback)(self.errors.clone()).build(),
|
||||
);
|
||||
state
|
||||
.children
|
||||
@@ -208,10 +207,8 @@ where
|
||||
}
|
||||
state
|
||||
} else {
|
||||
let fallback = (!self.errors_empty.get()).then(|| {
|
||||
(self.fallback)(self.errors.clone())
|
||||
.build(extra_attrs.clone())
|
||||
});
|
||||
let fallback = (!self.errors_empty.get())
|
||||
.then(|| (self.fallback)(self.errors.clone()).build());
|
||||
ErrorBoundaryViewState {
|
||||
children: children.take().unwrap(),
|
||||
fallback,
|
||||
@@ -221,12 +218,8 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
self,
|
||||
state: &mut Self::State,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
let new = self.build(extra_attrs);
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
let new = self.build();
|
||||
let mut old = std::mem::replace(state, new);
|
||||
old.insert_before_this(state);
|
||||
old.unmount();
|
||||
@@ -275,18 +268,14 @@ where
|
||||
Fal: RenderHtml + Send + 'static,
|
||||
{
|
||||
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn>;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
|
||||
|
||||
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
|
||||
self.children.dry_resolve(extra_attrs);
|
||||
fn dry_resolve(&mut self) {
|
||||
self.children.dry_resolve();
|
||||
}
|
||||
|
||||
async fn resolve(
|
||||
self,
|
||||
extra_attrs: ExtraAttrsMut<'_>,
|
||||
) -> Self::AsyncOutput {
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
let ErrorBoundaryView {
|
||||
hook,
|
||||
boundary_id,
|
||||
@@ -300,7 +289,7 @@ where
|
||||
hook,
|
||||
boundary_id,
|
||||
errors_empty,
|
||||
children: children.resolve(extra_attrs).await,
|
||||
children: children.resolve().await,
|
||||
fallback,
|
||||
errors,
|
||||
}
|
||||
@@ -312,7 +301,6 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
// first, attempt to serialize the children to HTML, then check for errors
|
||||
let _hook = throw_error::set_error_hook(self.hook);
|
||||
@@ -323,7 +311,6 @@ where
|
||||
&mut new_pos,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs.clone(),
|
||||
);
|
||||
|
||||
// any thrown errors would've been caught here
|
||||
@@ -336,7 +323,6 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -347,7 +333,6 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -360,7 +345,6 @@ where
|
||||
&mut new_pos,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs.clone(),
|
||||
);
|
||||
|
||||
// any thrown errors would've been caught here
|
||||
@@ -374,7 +358,6 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
buf.push_sync(&fallback);
|
||||
}
|
||||
@@ -384,7 +367,6 @@ where
|
||||
mut self,
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) -> Self::State {
|
||||
let mut children = Some(self.children);
|
||||
let hook = Arc::clone(&self.hook);
|
||||
@@ -406,8 +388,7 @@ where
|
||||
// yes errors, and was showing children
|
||||
(false, None) => {
|
||||
state.fallback = Some(
|
||||
(self.fallback)(self.errors.clone())
|
||||
.build(extra_attrs.clone()),
|
||||
(self.fallback)(self.errors.clone()).build(),
|
||||
);
|
||||
state
|
||||
.children
|
||||
@@ -424,23 +405,15 @@ where
|
||||
let children = children.take().unwrap();
|
||||
let (children, fallback) = if self.errors_empty.get() {
|
||||
(
|
||||
children.hydrate::<FROM_SERVER>(
|
||||
&cursor,
|
||||
&position,
|
||||
extra_attrs.clone(),
|
||||
),
|
||||
children.hydrate::<FROM_SERVER>(&cursor, &position),
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
children.build(extra_attrs.clone()),
|
||||
children.build(),
|
||||
Some(
|
||||
(self.fallback)(self.errors.clone())
|
||||
.hydrate::<FROM_SERVER>(
|
||||
&cursor,
|
||||
&position,
|
||||
extra_attrs.clone(),
|
||||
),
|
||||
.hydrate::<FROM_SERVER>(&cursor, &position),
|
||||
),
|
||||
)
|
||||
};
|
||||
@@ -450,10 +423,6 @@ where
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -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,
|
||||
ServerFn,
|
||||
client::Client, codec::PostUrl, request::ClientReq, ServerFn, ServerFnError,
|
||||
};
|
||||
use tachys::{
|
||||
either::Either,
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::borrow::Cow;
|
||||
use tachys::{
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Position, PositionState,
|
||||
Render, RenderHtml, ToTemplate,
|
||||
add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml,
|
||||
ToTemplate,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -76,34 +76,26 @@ where
|
||||
impl<T: Render> Render for View<T> {
|
||||
type State = T::State;
|
||||
|
||||
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
|
||||
self.inner.build(extra_attrs)
|
||||
fn build(self) -> Self::State {
|
||||
self.inner.build()
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
self,
|
||||
state: &mut Self::State,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
self.inner.rebuild(state, extra_attrs)
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
self.inner.rebuild(state)
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
async fn resolve(
|
||||
self,
|
||||
extra_attrs: ExtraAttrsMut<'_>,
|
||||
) -> Self::AsyncOutput {
|
||||
self.inner.resolve(extra_attrs).await
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self.inner.resolve().await
|
||||
}
|
||||
|
||||
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
|
||||
self.inner.dry_resolve(extra_attrs);
|
||||
fn dry_resolve(&mut self) {
|
||||
self.inner.dry_resolve();
|
||||
}
|
||||
|
||||
fn to_html_with_buf(
|
||||
@@ -112,7 +104,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
let vm = self.view_marker.to_owned();
|
||||
@@ -121,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() {
|
||||
@@ -141,7 +127,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -157,7 +142,6 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -170,18 +154,8 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
self,
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) -> Self::State {
|
||||
self.inner
|
||||
.hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
View {
|
||||
inner: self.inner.into_owned(),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: self.view_marker,
|
||||
}
|
||||
self.inner.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ 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::{
|
||||
|
||||
@@ -71,7 +71,6 @@ where
|
||||
view.hydrate::<true>(
|
||||
&Cursor::new(parent.unchecked_into()),
|
||||
&PositionState::default(),
|
||||
None,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -125,7 +124,7 @@ where
|
||||
let owner = Owner::new();
|
||||
let mountable = owner.with(move || {
|
||||
let view = f().into_view();
|
||||
let mut mountable = view.build(None);
|
||||
let mut mountable = view.build();
|
||||
mountable.mount(&parent, None);
|
||||
mountable
|
||||
});
|
||||
@@ -153,7 +152,7 @@ where
|
||||
let owner = Owner::new();
|
||||
let mountable = owner.with(move || {
|
||||
let view = f();
|
||||
let mut mountable = view.build(None);
|
||||
let mut mountable = view.build();
|
||||
mountable.mount(parent, None);
|
||||
mountable
|
||||
});
|
||||
|
||||
@@ -19,13 +19,12 @@ 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,
|
||||
view::{
|
||||
add_attr::AddAnyAttr,
|
||||
any_view::ExtraAttrsMut,
|
||||
either::{EitherKeepAlive, EitherKeepAliveState},
|
||||
Mountable, Position, PositionState, Render, RenderHtml,
|
||||
},
|
||||
@@ -163,7 +162,7 @@ where
|
||||
OwnedViewState<EitherKeepAliveState<Chil::State, Fal::State>>,
|
||||
>;
|
||||
|
||||
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
|
||||
fn build(self) -> Self::State {
|
||||
let mut children = Some(self.children);
|
||||
let mut fallback = Some(self.fallback);
|
||||
let none_pending = self.none_pending;
|
||||
@@ -188,20 +187,16 @@ where
|
||||
);
|
||||
|
||||
if let Some(mut state) = prev {
|
||||
this.rebuild(&mut state, extra_attrs.clone());
|
||||
this.rebuild(&mut state);
|
||||
state
|
||||
} else {
|
||||
this.build(extra_attrs.clone())
|
||||
this.build()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
self,
|
||||
state: &mut Self::State,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
let new = self.build(extra_attrs);
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
let new = self.build();
|
||||
let mut old = std::mem::replace(state, new);
|
||||
old.insert_before_this(state);
|
||||
old.unmount();
|
||||
@@ -252,16 +247,12 @@ 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;
|
||||
|
||||
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {}
|
||||
fn dry_resolve(&mut self) {}
|
||||
|
||||
async fn resolve(
|
||||
self,
|
||||
_extra_attrs: ExtraAttrsMut<'_>,
|
||||
) -> Self::AsyncOutput {
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
}
|
||||
|
||||
@@ -271,15 +262,9 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Option<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>(
|
||||
@@ -288,7 +273,6 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
mut extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -313,25 +297,25 @@ where
|
||||
provide_context(LocalResourceNotifier::from(local_tx));
|
||||
|
||||
// walk over the tree of children once to make sure that all resource loads are registered
|
||||
self.children
|
||||
.dry_resolve(ExtraAttrsMut::from_owned(&mut extra_attrs));
|
||||
self.children.dry_resolve();
|
||||
|
||||
// check the set of tasks to see if it is empty, now or later
|
||||
let eff = reactive_graph::effect::Effect::new_isomorphic({
|
||||
move |_| {
|
||||
tasks.track();
|
||||
if tasks.read().is_empty() {
|
||||
if let Some(tx) = tasks_tx.take() {
|
||||
// If the receiver has dropped, it means the ScopedFuture has already
|
||||
// dropped, so it doesn't matter if we manage to send this.
|
||||
_ = tx.send(());
|
||||
if let Some(tasks) = tasks.try_read() {
|
||||
if tasks.is_empty() {
|
||||
if let Some(tx) = tasks_tx.take() {
|
||||
// If the receiver has dropped, it means the ScopedFuture has already
|
||||
// dropped, so it doesn't matter if we manage to send this.
|
||||
_ = tx.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new({
|
||||
let mut extra_attrs = extra_attrs.clone();
|
||||
let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new(
|
||||
async move {
|
||||
// race the local resource notifier against the set of tasks
|
||||
//
|
||||
@@ -358,7 +342,7 @@ where
|
||||
// but in situations like a <For each=|| some_resource.snapshot()/> we actually
|
||||
// want to be able to 1) synchronously read a resource's value, but still 2) wait
|
||||
// for it to load before we render everything
|
||||
let mut children = Box::pin(self.children.resolve(ExtraAttrsMut::from_owned(&mut extra_attrs)).fuse());
|
||||
let mut children = Box::pin(self.children.resolve().fuse());
|
||||
|
||||
// we continue racing the children against the "do we have any local
|
||||
// resources?" Future
|
||||
@@ -377,8 +361,8 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})));
|
||||
},
|
||||
)));
|
||||
match fut.as_mut().now_or_never() {
|
||||
Some(Some(resolved)) => {
|
||||
Either::<Fal, _>::Right(resolved)
|
||||
@@ -387,7 +371,6 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
Some(None) => {
|
||||
@@ -397,7 +380,6 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
@@ -411,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({
|
||||
@@ -434,7 +414,6 @@ where
|
||||
&mut position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
builder.finish().take_chunks()
|
||||
}
|
||||
@@ -449,7 +428,6 @@ where
|
||||
self,
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) -> Self::State {
|
||||
let cursor = cursor.to_owned();
|
||||
let position = position.to_owned();
|
||||
@@ -478,21 +456,13 @@ where
|
||||
);
|
||||
|
||||
if let Some(mut state) = prev {
|
||||
this.rebuild(&mut state, extra_attrs.clone());
|
||||
this.rebuild(&mut state);
|
||||
state
|
||||
} else {
|
||||
this.hydrate::<FROM_SERVER>(
|
||||
&cursor,
|
||||
&position,
|
||||
extra_attrs.clone(),
|
||||
)
|
||||
this.hydrate::<FROM_SERVER>(&cursor, &position)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper that prevents [`Suspense`] from waiting for any resource reads that happen inside
|
||||
@@ -512,16 +482,12 @@ where
|
||||
{
|
||||
type State = T::State;
|
||||
|
||||
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
|
||||
(self.0)().build(extra_attrs)
|
||||
fn build(self) -> Self::State {
|
||||
(self.0)().build()
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
self,
|
||||
state: &mut Self::State,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
(self.0)().rebuild(state, extra_attrs);
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
(self.0)().rebuild(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,16 +515,12 @@ where
|
||||
T: RenderHtml + 'static,
|
||||
{
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = T::MIN_LENGTH;
|
||||
|
||||
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {}
|
||||
fn dry_resolve(&mut self) {}
|
||||
|
||||
async fn resolve(
|
||||
self,
|
||||
_extra_attrs: ExtraAttrsMut<'_>,
|
||||
) -> Self::AsyncOutput {
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
}
|
||||
|
||||
@@ -568,15 +530,8 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Option<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>(
|
||||
@@ -585,7 +540,6 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -594,7 +548,6 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -602,12 +555,7 @@ where
|
||||
self,
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) -> Self::State {
|
||||
(self.0)().hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
(self.0)().hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
config = { version = "0.14.1", default-features = false, features = [
|
||||
config = { version = "0.15.8", default-features = false, features = [
|
||||
"toml",
|
||||
"convert-case",
|
||||
] }
|
||||
@@ -20,7 +20,7 @@ thiserror = "2.0"
|
||||
typed-builder = "0.20.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.41", features = ["rt", "macros"] }
|
||||
tokio = { version = "1.43", features = ["rt", "macros"] }
|
||||
tempfile = "3.14"
|
||||
temp-env = { version = "0.3.6", features = ["async_closure"] }
|
||||
|
||||
@@ -28,4 +28,4 @@ temp-env = { version = "0.3.6", features = ["async_closure"] }
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ or_poisoned = { workspace = true }
|
||||
js-sys = "0.3.74"
|
||||
send_wrapper = "0.6.0"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen = "0.2.97"
|
||||
wasm-bindgen = { workspace = true }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
serde = { version = "1.0", optional = true }
|
||||
|
||||
@@ -39,4 +39,4 @@ rustdoc-args = ["--generate-link-to-definition"]
|
||||
denylist = ["tracing"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_macro"
|
||||
version = "0.8.0-alpha"
|
||||
version = { workspace = true }
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -16,7 +16,7 @@ proc-macro = true
|
||||
attribute-derive = { version = "0.10.3", features = ["syn-full"] }
|
||||
cfg-if = "1.0"
|
||||
html-escape = "0.2.13"
|
||||
itertools = "0.13.0"
|
||||
itertools = { workspace = true }
|
||||
prettyplease = "0.2.25"
|
||||
proc-macro-error2 = { version = "2.0", default-features = false }
|
||||
proc-macro2 = "1.0"
|
||||
@@ -25,7 +25,7 @@ syn = { version = "2.0", features = ["full"] }
|
||||
rstml = "0.12.0"
|
||||
leptos_hot_reload = { workspace = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
convert_case = "0.6.0"
|
||||
convert_case = "0.7"
|
||||
uuid = { version = "1.11", features = ["v4"] }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
|
||||
@@ -34,7 +34,7 @@ log = "0.4.22"
|
||||
typed-builder = "0.20.0"
|
||||
trybuild = "1.0"
|
||||
leptos = { path = "../leptos" }
|
||||
leptos_router = { path = "../router", features= ["ssr"] }
|
||||
leptos_router = { path = "../router", features = ["ssr"] }
|
||||
server_fn = { path = "../server_fn", features = ["cbor"] }
|
||||
insta = "1.41"
|
||||
serde = "1.0"
|
||||
@@ -55,30 +55,30 @@ generic = ["server_fn_macro/generic"]
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly", "tracing", "trace-component-props"]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"hydrate",
|
||||
"csr",
|
||||
],
|
||||
[
|
||||
"hydrate",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"actix",
|
||||
"axum",
|
||||
],
|
||||
[
|
||||
"actix",
|
||||
"generic",
|
||||
],
|
||||
[
|
||||
"generic",
|
||||
"axum",
|
||||
],
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"hydrate",
|
||||
"csr",
|
||||
],
|
||||
[
|
||||
"hydrate",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"actix",
|
||||
"axum",
|
||||
],
|
||||
[
|
||||
"actix",
|
||||
"generic",
|
||||
],
|
||||
[
|
||||
"generic",
|
||||
"axum",
|
||||
],
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
@@ -86,6 +86,6 @@ rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(leptos_debuginfo)',
|
||||
'cfg(erase_components)',
|
||||
'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-2024-08-01", "test", "--doc"]
|
||||
args = ["+nightly-2025-03-05", "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-2024-08-01", "doc"]
|
||||
args = ["+nightly-2025-03-05", "doc"]
|
||||
cwd = "example"
|
||||
install_crate = false
|
||||
|
||||
@@ -144,8 +144,6 @@ impl ToTokens for Model {
|
||||
let (impl_generics, generics, where_clause) =
|
||||
body.sig.generics.split_for_impl();
|
||||
|
||||
let lifetimes = body.sig.generics.lifetimes();
|
||||
|
||||
let props_name = format_ident!("{name}Props");
|
||||
let props_builder_name = format_ident!("{name}PropsBuilder");
|
||||
let props_serialized_name = format_ident!("{name}PropsSerialized");
|
||||
@@ -570,7 +568,7 @@ impl ToTokens for Model {
|
||||
#tracing_instrument_attr
|
||||
#vis fn #name #impl_generics (
|
||||
#props_arg
|
||||
) #ret #(+ #lifetimes)*
|
||||
) #ret
|
||||
#where_clause
|
||||
{
|
||||
#body
|
||||
|
||||
@@ -677,17 +677,21 @@ fn component_macro(
|
||||
#[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)]
|
||||
#unexpanded
|
||||
}
|
||||
} else if let Ok(mut dummy) = dummy {
|
||||
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
|
||||
quote! {
|
||||
#[doc(hidden)]
|
||||
#[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)]
|
||||
#dummy
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
}
|
||||
.into()
|
||||
match dummy {
|
||||
Ok(mut dummy) => {
|
||||
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
|
||||
quote! {
|
||||
#[doc(hidden)]
|
||||
#[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)]
|
||||
#dummy
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
proc_macro_error2::abort!(e.span(), e);
|
||||
}
|
||||
}
|
||||
}.into()
|
||||
}
|
||||
|
||||
/// Annotates a struct so that it can be used with your Component as a `slot`.
|
||||
@@ -919,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,
|
||||
) {
|
||||
|
||||
@@ -154,7 +154,12 @@ fn is_inert_element(orig_node: &Node<impl CustomNode>) -> bool {
|
||||
Some(value) => {
|
||||
matches!(&value.value, KVAttributeValue::Expr(expr) if {
|
||||
if let Expr::Lit(lit) = expr {
|
||||
matches!(&lit.lit, Lit::Str(_))
|
||||
let key = attr.key.to_string();
|
||||
if key.starts_with("style:") || key.starts_with("prop:") || key.starts_with("on:") || key.starts_with("use:") || key.starts_with("bind") {
|
||||
false
|
||||
} else {
|
||||
matches!(&lit.lit, Lit::Str(_))
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -1152,8 +1157,14 @@ pub(crate) fn two_way_binding_to_tokens(
|
||||
let ident =
|
||||
format_ident!("{}", name.to_case(UpperCamel), span = node.key.span());
|
||||
|
||||
quote! {
|
||||
.bind(::leptos::attr::#ident, #value)
|
||||
if name == "group" {
|
||||
quote! {
|
||||
.bind(leptos::tachys::reactive_graph::bind::#ident, #value)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.bind(::leptos::attr::#ident, #value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1174,8 +1185,7 @@ pub(crate) fn event_type_and_handler(
|
||||
) -> (TokenStream, TokenStream, TokenStream) {
|
||||
let handler = attribute_value(node, false);
|
||||
|
||||
let (event_type, is_custom, is_force_undelegated, is_targeted) =
|
||||
parse_event_name(name);
|
||||
let (event_type, is_custom, options) = parse_event_name(name);
|
||||
|
||||
let event_name_ident = match &node.key {
|
||||
NodeName::Punctuated(parts) => {
|
||||
@@ -1193,11 +1203,17 @@ pub(crate) fn event_type_and_handler(
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let capture_ident = match &node.key {
|
||||
NodeName::Punctuated(parts) => {
|
||||
parts.iter().find(|part| part.to_string() == "capture")
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let on = match &node.key {
|
||||
NodeName::Punctuated(parts) => &parts[0],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let on = if is_targeted {
|
||||
let on = if options.targeted {
|
||||
Ident::new("on_target", on.span()).to_token_stream()
|
||||
} else {
|
||||
on.to_token_stream()
|
||||
@@ -1210,15 +1226,29 @@ pub(crate) fn event_type_and_handler(
|
||||
event_type
|
||||
};
|
||||
|
||||
let event_type = if is_force_undelegated {
|
||||
let event_type = quote! {
|
||||
::leptos::tachys::html::event::#event_type
|
||||
};
|
||||
let event_type = if options.captured {
|
||||
let capture = if let Some(capture) = capture_ident {
|
||||
quote! { #capture }
|
||||
} else {
|
||||
quote! { capture }
|
||||
};
|
||||
quote! { ::leptos::tachys::html::event::#capture(#event_type) }
|
||||
} else {
|
||||
event_type
|
||||
};
|
||||
|
||||
let event_type = if options.undelegated {
|
||||
let undelegated = if let Some(undelegated) = undelegated_ident {
|
||||
quote! { #undelegated }
|
||||
} else {
|
||||
quote! { undelegated }
|
||||
};
|
||||
quote! { ::leptos::tachys::html::event::#undelegated(::leptos::tachys::html::event::#event_type) }
|
||||
quote! { ::leptos::tachys::html::event::#undelegated(#event_type) }
|
||||
} else {
|
||||
quote! { ::leptos::tachys::html::event::#event_type }
|
||||
event_type
|
||||
};
|
||||
|
||||
(on, event_type, handler)
|
||||
@@ -1424,13 +1454,22 @@ fn is_ambiguous_element(tag: &str) -> bool {
|
||||
tag == "a" || tag == "script" || tag == "title"
|
||||
}
|
||||
|
||||
fn parse_event(event_name: &str) -> (String, bool, bool) {
|
||||
let is_undelegated = event_name.contains(":undelegated");
|
||||
let is_targeted = event_name.contains(":target");
|
||||
fn parse_event(event_name: &str) -> (String, EventNameOptions) {
|
||||
let undelegated = event_name.contains(":undelegated");
|
||||
let targeted = event_name.contains(":target");
|
||||
let captured = event_name.contains(":capture");
|
||||
let event_name = event_name
|
||||
.replace(":undelegated", "")
|
||||
.replace(":target", "");
|
||||
(event_name, is_undelegated, is_targeted)
|
||||
.replace(":target", "")
|
||||
.replace(":capture", "");
|
||||
(
|
||||
event_name,
|
||||
EventNameOptions {
|
||||
undelegated,
|
||||
targeted,
|
||||
captured,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Escapes Rust keywords that are also HTML attribute names
|
||||
@@ -1622,8 +1661,17 @@ const TYPED_EVENTS: [&str; 126] = [
|
||||
|
||||
const CUSTOM_EVENT: &str = "Custom";
|
||||
|
||||
pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool, bool) {
|
||||
let (name, is_force_undelegated, is_targeted) = parse_event(name);
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct EventNameOptions {
|
||||
undelegated: bool,
|
||||
targeted: bool,
|
||||
captured: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn parse_event_name(
|
||||
name: &str,
|
||||
) -> (TokenStream, bool, EventNameOptions) {
|
||||
let (name, options) = parse_event(name);
|
||||
|
||||
let (event_type, is_custom) = TYPED_EVENTS
|
||||
.binary_search(&name.as_str())
|
||||
@@ -1639,7 +1687,7 @@ pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool, bool) {
|
||||
} else {
|
||||
event_type
|
||||
};
|
||||
(event_type, is_custom, is_force_undelegated, is_targeted)
|
||||
(event_type, is_custom, options)
|
||||
}
|
||||
|
||||
fn convert_to_snake_case(name: String) -> String {
|
||||
|
||||
@@ -104,3 +104,18 @@ fn component_nostrip() {
|
||||
/>
|
||||
};
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn WithLifetime<'a>(data: &'a str) -> impl IntoView {
|
||||
_ = data;
|
||||
"static lifetime"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_static_lifetime() {
|
||||
#[allow(unused)]
|
||||
fn can_return_impl_intoview_from_body() -> impl IntoView {
|
||||
let val = String::from("non_static_lifetime");
|
||||
WithLifetime(WithLifetimeProps::builder().data(&val).build())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ send_wrapper = "0.6"
|
||||
# serialization formats
|
||||
serde = { version = "1.0" }
|
||||
js-sys = { version = "0.3.74", optional = true }
|
||||
wasm-bindgen = { version = "0.2.97", optional = true }
|
||||
serde_json = { version = "1.0" }
|
||||
wasm-bindgen = { version = "0.2.100", optional = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[features]
|
||||
ssr = []
|
||||
@@ -46,4 +46,4 @@ denylist = ["tracing"]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
|
||||
|
||||
@@ -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,13 +79,12 @@ 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,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Position,
|
||||
PositionState, Render, RenderHtml,
|
||||
add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -96,17 +95,12 @@ mod view_implementations {
|
||||
{
|
||||
type State = RenderEffectState<SuspendState<T>>;
|
||||
|
||||
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
|
||||
(move || Suspend::new(async move { self.await })).build(extra_attrs)
|
||||
fn build(self) -> Self::State {
|
||||
(move || Suspend::new(async move { self.await })).build()
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
self,
|
||||
state: &mut Self::State,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
(move || Suspend::new(async move { self.await }))
|
||||
.rebuild(state, extra_attrs)
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
(move || Suspend::new(async move { self.await })).rebuild(state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,20 +135,15 @@ mod view_implementations {
|
||||
Ser: Send + 'static,
|
||||
{
|
||||
type AsyncOutput = Option<T>;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {
|
||||
fn dry_resolve(&mut self) {
|
||||
self.read();
|
||||
}
|
||||
|
||||
fn resolve(
|
||||
self,
|
||||
extra_attrs: ExtraAttrsMut<'_>,
|
||||
) -> impl Future<Output = Self::AsyncOutput> + Send {
|
||||
(move || Suspend::new(async move { self.await }))
|
||||
.resolve(extra_attrs)
|
||||
fn resolve(self) -> impl Future<Output = Self::AsyncOutput> + Send {
|
||||
(move || Suspend::new(async move { self.await })).resolve()
|
||||
}
|
||||
|
||||
fn to_html_with_buf(
|
||||
@@ -163,14 +152,12 @@ mod view_implementations {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
(move || Suspend::new(async move { self.await })).to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,7 +167,6 @@ mod view_implementations {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -190,7 +176,6 @@ mod view_implementations {
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -198,14 +183,9 @@ mod view_implementations {
|
||||
self,
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) -> Self::State {
|
||||
(move || Suspend::new(async move { self.await }))
|
||||
.hydrate::<FROM_SERVER>(cursor, position, extra_attrs)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ use reactive_graph::{
|
||||
guards::{AsyncPlain, ReadGuard},
|
||||
ArcRwSignal, RwSignal,
|
||||
},
|
||||
traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Update, Write},
|
||||
traits::{
|
||||
DefinedAt, IsDisposed, ReadUntracked, Track, Update, With, Write,
|
||||
},
|
||||
};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::{
|
||||
@@ -91,6 +93,34 @@ 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(&SendWrapper<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>
|
||||
@@ -142,12 +172,6 @@ where
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||
notifier.notify();
|
||||
} else if cfg!(feature = "ssr") {
|
||||
panic!(
|
||||
"Reading from a LocalResource outside Suspense in `ssr` mode \
|
||||
will cause the response to hang, because LocalResources are \
|
||||
always pending on the server."
|
||||
);
|
||||
}
|
||||
self.data.try_read_untracked()
|
||||
}
|
||||
@@ -334,12 +358,6 @@ where
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||
notifier.notify();
|
||||
} else if cfg!(feature = "ssr") {
|
||||
panic!(
|
||||
"Reading from a LocalResource outside Suspense in `ssr` mode \
|
||||
will cause the response to hang, because LocalResources are \
|
||||
always pending on the server."
|
||||
);
|
||||
}
|
||||
self.data.try_read_untracked()
|
||||
}
|
||||
|
||||
@@ -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,6 +168,41 @@ 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> {
|
||||
@@ -534,6 +569,37 @@ 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,16 +215,11 @@ where
|
||||
None
|
||||
}
|
||||
Ok(encoded) => {
|
||||
match Ser::decode(encoded.borrow()) {
|
||||
#[allow(unused_variables)]
|
||||
// used in tracing
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!("{e:?}");
|
||||
None
|
||||
}
|
||||
Ok(value) => Some(value),
|
||||
}
|
||||
let decoded = Ser::decode(encoded.borrow());
|
||||
#[cfg(feature = "tracing")]
|
||||
let decoded = decoded
|
||||
.inspect_err(|e| tracing::error!("{e:?}"));
|
||||
decoded.ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -15,7 +15,7 @@ or_poisoned = { workspace = true }
|
||||
indexmap = "2.6"
|
||||
send_wrapper = "0.6.0"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen = "0.2.97"
|
||||
wasm-bindgen = { workspace = true }
|
||||
futures = "0.3.31"
|
||||
|
||||
[dependencies.web-sys]
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use crate::ServerMetaContext;
|
||||
use leptos::{
|
||||
attr::{
|
||||
any_attribute::{AnyAttribute, AnyAttributeState},
|
||||
NextAttribute,
|
||||
},
|
||||
attr::NextAttribute,
|
||||
component, html,
|
||||
reactive::owner::use_context,
|
||||
tachys::{
|
||||
@@ -11,8 +8,8 @@ use leptos::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
|
||||
PositionState, Render, RenderHtml,
|
||||
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
|
||||
RenderHtml,
|
||||
},
|
||||
},
|
||||
IntoView,
|
||||
@@ -61,7 +58,6 @@ where
|
||||
At: Attribute,
|
||||
{
|
||||
attributes: At::State,
|
||||
extra_attrs: Option<Vec<AnyAttributeState>>,
|
||||
}
|
||||
|
||||
impl<At> Render for BodyView<At>
|
||||
@@ -70,27 +66,15 @@ where
|
||||
{
|
||||
type State = BodyViewState<At>;
|
||||
|
||||
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
|
||||
fn build(self) -> Self::State {
|
||||
let el = document().body().expect("there to be a <body> element");
|
||||
let attributes = self.attributes.build(&el);
|
||||
let extra_attrs = extra_attrs.map(|attrs| attrs.build(&el));
|
||||
BodyViewState {
|
||||
attributes,
|
||||
extra_attrs,
|
||||
}
|
||||
|
||||
BodyViewState { attributes }
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
self,
|
||||
state: &mut Self::State,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
self.attributes.rebuild(&mut state.attributes);
|
||||
if let (Some(extra_attrs), Some(extra_attr_states)) =
|
||||
(extra_attrs, &mut state.extra_attrs)
|
||||
{
|
||||
extra_attrs.rebuild(extra_attr_states);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,24 +103,17 @@ where
|
||||
At: Attribute,
|
||||
{
|
||||
type AsyncOutput = BodyView<At::AsyncOutput>;
|
||||
type Owned = BodyView<At::CloneableOwned>;
|
||||
|
||||
const MIN_LENGTH: usize = At::MIN_LENGTH;
|
||||
|
||||
fn dry_resolve(&mut self, mut extra_attrs: ExtraAttrsMut<'_>) {
|
||||
fn dry_resolve(&mut self) {
|
||||
self.attributes.dry_resolve();
|
||||
extra_attrs.iter_mut().for_each(Attribute::dry_resolve);
|
||||
}
|
||||
|
||||
async fn resolve(
|
||||
self,
|
||||
extra_attrs: ExtraAttrsMut<'_>,
|
||||
) -> Self::AsyncOutput {
|
||||
let (attributes, _) = futures::join!(
|
||||
self.attributes.resolve(),
|
||||
ExtraAttrsMut::resolve(extra_attrs)
|
||||
);
|
||||
BodyView { attributes }
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
BodyView {
|
||||
attributes: self.attributes.resolve().await,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_html_with_buf(
|
||||
@@ -145,15 +122,10 @@ where
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
extra_attrs: Option<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);
|
||||
}
|
||||
@@ -164,23 +136,11 @@ where
|
||||
self,
|
||||
_cursor: &Cursor,
|
||||
_position: &PositionState,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) -> Self::State {
|
||||
let el = document().body().expect("there to be a <body> element");
|
||||
let attributes = self.attributes.hydrate::<FROM_SERVER>(&el);
|
||||
let extra_attrs =
|
||||
extra_attrs.map(|attrs| attrs.hydrate::<FROM_SERVER>(&el));
|
||||
|
||||
BodyViewState {
|
||||
attributes,
|
||||
extra_attrs,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
BodyView {
|
||||
attributes: self.attributes.into_cloneable_owned(),
|
||||
}
|
||||
BodyViewState { attributes }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use crate::ServerMetaContext;
|
||||
use leptos::{
|
||||
attr::{
|
||||
any_attribute::{AnyAttribute, AnyAttributeState},
|
||||
NextAttribute,
|
||||
},
|
||||
attr::NextAttribute,
|
||||
component, html,
|
||||
reactive::owner::use_context,
|
||||
tachys::{
|
||||
@@ -11,8 +8,8 @@ use leptos::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
|
||||
PositionState, Render, RenderHtml,
|
||||
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
|
||||
RenderHtml,
|
||||
},
|
||||
},
|
||||
IntoView,
|
||||
@@ -58,7 +55,6 @@ where
|
||||
At: Attribute,
|
||||
{
|
||||
attributes: At::State,
|
||||
extra_attrs: Option<Vec<AnyAttributeState>>,
|
||||
}
|
||||
|
||||
impl<At> Render for HtmlView<At>
|
||||
@@ -67,33 +63,18 @@ where
|
||||
{
|
||||
type State = HtmlViewState<At>;
|
||||
|
||||
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
|
||||
fn build(self) -> Self::State {
|
||||
let el = document()
|
||||
.document_element()
|
||||
.expect("there to be a <html> element");
|
||||
|
||||
let attributes = self.attributes.build(&el);
|
||||
let extra_attrs = extra_attrs.map(|attrs| {
|
||||
attrs.into_iter().map(|attr| attr.build(&el)).collect()
|
||||
});
|
||||
|
||||
HtmlViewState {
|
||||
attributes,
|
||||
extra_attrs,
|
||||
}
|
||||
HtmlViewState { attributes }
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
self,
|
||||
state: &mut Self::State,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
self.attributes.rebuild(&mut state.attributes);
|
||||
if let (Some(extra_attrs), Some(extra_attr_states)) =
|
||||
(extra_attrs, &mut state.extra_attrs)
|
||||
{
|
||||
extra_attrs.rebuild(extra_attr_states);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,24 +103,17 @@ where
|
||||
At: Attribute,
|
||||
{
|
||||
type AsyncOutput = HtmlView<At::AsyncOutput>;
|
||||
type Owned = HtmlView<At::CloneableOwned>;
|
||||
|
||||
const MIN_LENGTH: usize = At::MIN_LENGTH;
|
||||
|
||||
fn dry_resolve(&mut self, mut extra_attrs: ExtraAttrsMut<'_>) {
|
||||
fn dry_resolve(&mut self) {
|
||||
self.attributes.dry_resolve();
|
||||
extra_attrs.iter_mut().for_each(Attribute::dry_resolve);
|
||||
}
|
||||
|
||||
async fn resolve(
|
||||
self,
|
||||
extra_attrs: ExtraAttrsMut<'_>,
|
||||
) -> Self::AsyncOutput {
|
||||
let (attributes, _) = futures::join!(
|
||||
self.attributes.resolve(),
|
||||
ExtraAttrsMut::resolve(extra_attrs)
|
||||
);
|
||||
HtmlView { attributes }
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
HtmlView {
|
||||
attributes: self.attributes.resolve().await,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_html_with_buf(
|
||||
@@ -148,15 +122,10 @@ where
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
extra_attrs: Option<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);
|
||||
}
|
||||
@@ -167,30 +136,14 @@ where
|
||||
self,
|
||||
_cursor: &Cursor,
|
||||
_position: &PositionState,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) -> Self::State {
|
||||
let el = document()
|
||||
.document_element()
|
||||
.expect("there to be a <html> element");
|
||||
|
||||
let attributes = self.attributes.hydrate::<FROM_SERVER>(&el);
|
||||
let extra_attrs = extra_attrs.map(|attrs| {
|
||||
attrs
|
||||
.into_iter()
|
||||
.map(|attr| attr.hydrate::<FROM_SERVER>(&el))
|
||||
.collect()
|
||||
});
|
||||
|
||||
HtmlViewState {
|
||||
attributes,
|
||||
extra_attrs,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
HtmlView {
|
||||
attributes: self.attributes.into_cloneable_owned(),
|
||||
}
|
||||
HtmlViewState { attributes }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
111
meta/src/lib.rs
111
meta/src/lib.rs
@@ -44,7 +44,7 @@
|
||||
|
||||
use futures::{Stream, StreamExt};
|
||||
use leptos::{
|
||||
attr::{any_attribute::AnyAttribute, NextAttribute},
|
||||
attr::NextAttribute,
|
||||
component,
|
||||
logging::debug_warn,
|
||||
oco::Oco,
|
||||
@@ -57,8 +57,8 @@ use leptos::{
|
||||
},
|
||||
hydration::Cursor,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
|
||||
PositionState, Render, RenderHtml,
|
||||
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
|
||||
RenderHtml,
|
||||
},
|
||||
},
|
||||
IntoView,
|
||||
@@ -323,38 +323,13 @@ 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,
|
||||
None,
|
||||
);
|
||||
_ = 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: Option<HtmlElement<E, At, Ch>>,
|
||||
el: HtmlElement<E, At, Ch>,
|
||||
}
|
||||
|
||||
struct RegisteredMetaTagState<E, At, Ch>
|
||||
@@ -391,17 +366,13 @@ where
|
||||
{
|
||||
type State = RegisteredMetaTagState<E, At, Ch>;
|
||||
|
||||
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
|
||||
let state = self.el.unwrap().build(extra_attrs);
|
||||
fn build(self) -> Self::State {
|
||||
let state = self.el.build();
|
||||
RegisteredMetaTagState { state }
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
self,
|
||||
state: &mut Self::State,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
self.el.unwrap().rebuild(&mut state.state, extra_attrs);
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
self.el.rebuild(&mut state.state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,7 +393,7 @@ where
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
RegisteredMetaTag {
|
||||
el: self.el.map(|inner| inner.add_any_attr(attr)),
|
||||
el: self.el.add_any_attr(attr),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,18 +405,14 @@ where
|
||||
Ch: RenderHtml + Send,
|
||||
{
|
||||
type AsyncOutput = Self;
|
||||
type Owned = RegisteredMetaTag<E, At::CloneableOwned, Ch::Owned>;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
fn dry_resolve(&mut self, extra_attrs: ExtraAttrsMut<'_>) {
|
||||
self.el.dry_resolve(extra_attrs)
|
||||
fn dry_resolve(&mut self) {
|
||||
self.el.dry_resolve()
|
||||
}
|
||||
|
||||
async fn resolve(
|
||||
self,
|
||||
_extra_attrs: ExtraAttrsMut<'_>,
|
||||
) -> Self::AsyncOutput {
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self // TODO?
|
||||
}
|
||||
|
||||
@@ -455,17 +422,35 @@ where
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
_extra_attrs: Option<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,
|
||||
);
|
||||
_ = 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>(
|
||||
self,
|
||||
_cursor: &Cursor,
|
||||
_position: &PositionState,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) -> Self::State {
|
||||
let cursor = use_context::<MetaContext>()
|
||||
.expect(
|
||||
@@ -473,19 +458,12 @@ where
|
||||
MetaContext provided",
|
||||
)
|
||||
.cursor;
|
||||
let state = self.el.unwrap().hydrate::<FROM_SERVER>(
|
||||
let state = self.el.hydrate::<FROM_SERVER>(
|
||||
&cursor,
|
||||
&PositionState::new(Position::NextChild),
|
||||
extra_attrs,
|
||||
);
|
||||
RegisteredMetaTagState { state }
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
RegisteredMetaTag {
|
||||
el: self.el.map(|inner| inner.into_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, At, Ch> Mountable for RegisteredMetaTagState<E, At, Ch>
|
||||
@@ -538,14 +516,9 @@ struct MetaTagsView;
|
||||
impl Render for MetaTagsView {
|
||||
type State = ();
|
||||
|
||||
fn build(self, _extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {}
|
||||
fn build(self) -> Self::State {}
|
||||
|
||||
fn rebuild(
|
||||
self,
|
||||
_state: &mut Self::State,
|
||||
_extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
}
|
||||
fn rebuild(self, _state: &mut Self::State) {}
|
||||
}
|
||||
|
||||
impl AddAnyAttr for MetaTagsView {
|
||||
@@ -564,16 +537,12 @@ impl AddAnyAttr for MetaTagsView {
|
||||
|
||||
impl RenderHtml for MetaTagsView {
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {}
|
||||
fn dry_resolve(&mut self) {}
|
||||
|
||||
async fn resolve(
|
||||
self,
|
||||
_extra_attrs: ExtraAttrsMut<'_>,
|
||||
) -> Self::AsyncOutput {
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
}
|
||||
|
||||
@@ -583,7 +552,6 @@ impl RenderHtml for MetaTagsView {
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
_extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
buf.push_str("<!--HEAD-->");
|
||||
}
|
||||
@@ -592,13 +560,8 @@ impl RenderHtml for MetaTagsView {
|
||||
self,
|
||||
_cursor: &Cursor,
|
||||
_position: &PositionState,
|
||||
_extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) -> Self::State {
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait OrDefaultNonce {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{use_head, MetaContext, ServerMetaContext};
|
||||
use leptos::{
|
||||
attr::{any_attribute::AnyAttribute, Attribute},
|
||||
attr::Attribute,
|
||||
component,
|
||||
oco::Oco,
|
||||
reactive::{
|
||||
@@ -11,8 +11,8 @@ use leptos::{
|
||||
dom::document,
|
||||
hydration::Cursor,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, any_view::ExtraAttrsMut, Mountable, Position,
|
||||
PositionState, Render, RenderHtml,
|
||||
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
|
||||
RenderHtml,
|
||||
},
|
||||
},
|
||||
text_prop::TextProp,
|
||||
@@ -189,7 +189,7 @@ struct TitleViewState {
|
||||
impl Render for TitleView {
|
||||
type State = TitleViewState;
|
||||
|
||||
fn build(mut self, _extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
|
||||
fn build(mut self) -> Self::State {
|
||||
let el = self.el();
|
||||
let meta = self.meta;
|
||||
if let Some(formatter) = self.formatter.take() {
|
||||
@@ -213,12 +213,8 @@ impl Render for TitleView {
|
||||
TitleViewState { effect }
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
self,
|
||||
state: &mut Self::State,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
*state = self.build(extra_attrs);
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
*state = self.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,16 +234,12 @@ impl AddAnyAttr for TitleView {
|
||||
|
||||
impl RenderHtml for TitleView {
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {}
|
||||
fn dry_resolve(&mut self) {}
|
||||
|
||||
async fn resolve(
|
||||
self,
|
||||
_extra_attrs: ExtraAttrsMut<'_>,
|
||||
) -> Self::AsyncOutput {
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
}
|
||||
|
||||
@@ -257,7 +249,6 @@ impl RenderHtml for TitleView {
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
_extra_attrs: Option<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
|
||||
@@ -267,7 +258,6 @@ impl RenderHtml for TitleView {
|
||||
mut self,
|
||||
_cursor: &Cursor,
|
||||
_position: &PositionState,
|
||||
_extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) -> Self::State {
|
||||
let el = self.el();
|
||||
let meta = self.meta;
|
||||
@@ -292,10 +282,6 @@ impl RenderHtml for TitleView {
|
||||
});
|
||||
TitleViewState { effect }
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Mountable for TitleViewState {
|
||||
|
||||
@@ -13,4 +13,4 @@ serde = "1.0"
|
||||
thiserror = "2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -35,7 +35,7 @@ pub trait OrPoisoned {
|
||||
fn or_poisoned(self) -> Self::Inner;
|
||||
}
|
||||
|
||||
impl<'a, T> OrPoisoned
|
||||
impl<'a, T: ?Sized> OrPoisoned
|
||||
for Result<RwLockReadGuard<'a, T>, PoisonError<RwLockReadGuard<'a, T>>>
|
||||
{
|
||||
type Inner = RwLockReadGuard<'a, T>;
|
||||
@@ -45,7 +45,7 @@ impl<'a, T> OrPoisoned
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> OrPoisoned
|
||||
impl<'a, T: ?Sized> OrPoisoned
|
||||
for Result<RwLockWriteGuard<'a, T>, PoisonError<RwLockWriteGuard<'a, T>>>
|
||||
{
|
||||
type Inner = RwLockWriteGuard<'a, T>;
|
||||
@@ -55,7 +55,7 @@ impl<'a, T> OrPoisoned
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> OrPoisoned for LockResult<MutexGuard<'a, T>> {
|
||||
impl<'a, T: ?Sized> OrPoisoned for LockResult<MutexGuard<'a, T>> {
|
||||
type Inner = MutexGuard<'a, T>;
|
||||
|
||||
fn or_poisoned(self) -> Self::Inner {
|
||||
|
||||
@@ -8,19 +8,19 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { version = "0.6.13", features = ["csr"] }
|
||||
leptos_meta = { version = "0.6.13", features = ["csr"] }
|
||||
leptos_router = { version = "0.6.13", features = ["csr"] }
|
||||
leptos = { version = "0.7.7", features = ["csr"] }
|
||||
leptos_meta = { version = "0.7.7" }
|
||||
leptos_router = { version = "0.7.7" }
|
||||
console_log = "1.0"
|
||||
log = "0.4.22"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
bevy = "0.14.1"
|
||||
bevy = "0.15.2"
|
||||
crossbeam-channel = "0.5.13"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen = "0.2.92"
|
||||
wasm-bindgen-test = "0.3.42"
|
||||
web-sys = "0.3.69"
|
||||
wasm-bindgen = "0.2.100"
|
||||
wasm-bindgen-test = "0.3.50"
|
||||
web-sys = "0.3.77"
|
||||
|
||||
[workspace]
|
||||
# The empty workspace here is to keep rust-analyzer satisfied
|
||||
|
||||
@@ -17,7 +17,7 @@ impl DuplexEventsPlugin {
|
||||
let (bevy_sender, client_receiver) = crossbeam_channel::bounded(50);
|
||||
// For sending message from the client to bevy
|
||||
let (client_sender, bevy_receiver) = crossbeam_channel::bounded(50);
|
||||
let instance = DuplexEventsPlugin {
|
||||
DuplexEventsPlugin {
|
||||
client_processor: EventProcessor {
|
||||
sender: client_sender,
|
||||
receiver: client_receiver,
|
||||
@@ -26,8 +26,7 @@ impl DuplexEventsPlugin {
|
||||
sender: bevy_sender,
|
||||
receiver: bevy_receiver,
|
||||
},
|
||||
};
|
||||
instance
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the client event processor
|
||||
|
||||
@@ -23,14 +23,13 @@ impl Scene {
|
||||
/// Create a new instance
|
||||
pub fn new(canvas_id: String) -> Scene {
|
||||
let plugin = DuplexEventsPlugin::new();
|
||||
let instance = Scene {
|
||||
Scene {
|
||||
is_setup: false,
|
||||
canvas_id: canvas_id,
|
||||
canvas_id,
|
||||
evt_plugin: plugin.clone(),
|
||||
shared_state: SharedState::new(),
|
||||
processor: plugin.get_processor(),
|
||||
};
|
||||
instance
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the shared state
|
||||
@@ -47,7 +46,7 @@ impl Scene {
|
||||
|
||||
/// Setup and attach the bevy instance to the html canvas element
|
||||
pub fn setup(&mut self) {
|
||||
if self.is_setup == true {
|
||||
if self.is_setup {
|
||||
return;
|
||||
};
|
||||
App::new()
|
||||
@@ -76,40 +75,37 @@ fn setup_scene(
|
||||
) {
|
||||
let name = resource.0.lock().unwrap().name.clone();
|
||||
// circular base
|
||||
commands.spawn(PbrBundle {
|
||||
mesh: meshes.add(Circle::new(4.0)),
|
||||
material: materials.add(Color::WHITE),
|
||||
transform: Transform::from_rotation(Quat::from_rotation_x(
|
||||
commands.spawn((
|
||||
Mesh3d(meshes.add(Circle::new(4.0))),
|
||||
MeshMaterial3d(materials.add(Color::WHITE)),
|
||||
Transform::from_rotation(Quat::from_rotation_x(
|
||||
-std::f32::consts::FRAC_PI_2,
|
||||
)),
|
||||
..default()
|
||||
});
|
||||
));
|
||||
|
||||
// cube
|
||||
commands.spawn((
|
||||
PbrBundle {
|
||||
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
|
||||
material: materials.add(Color::rgb_u8(124, 144, 255)),
|
||||
transform: Transform::from_xyz(0.0, 0.5, 0.0),
|
||||
..default()
|
||||
},
|
||||
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
|
||||
MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
|
||||
Transform::from_xyz(0.0, 0.5, 0.0),
|
||||
Cube,
|
||||
));
|
||||
|
||||
// light
|
||||
commands.spawn(PointLightBundle {
|
||||
point_light: PointLight {
|
||||
commands.spawn((
|
||||
PointLight {
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(4.0, 8.0, 4.0),
|
||||
..default()
|
||||
});
|
||||
Transform::from_xyz(4.0, 8.0, 4.0),
|
||||
));
|
||||
|
||||
// camera
|
||||
commands.spawn(Camera3dBundle {
|
||||
transform: Transform::from_xyz(-2.5, 4.5, 9.0)
|
||||
.looking_at(Vec3::ZERO, Vec3::Y),
|
||||
..default()
|
||||
});
|
||||
commands.spawn(TextBundle::from_section(name, TextStyle::default()));
|
||||
commands.spawn((
|
||||
Camera3d::default(),
|
||||
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||
));
|
||||
commands.spawn((Text::new(name), TextFont::default()));
|
||||
}
|
||||
|
||||
/// Move the Cube on event
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod demos;
|
||||
mod routes;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use routes::RootPage;
|
||||
|
||||
pub fn main() {
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::demos::bevydemo1::eventqueue::events::{
|
||||
ClientInEvents, CounterEvtData,
|
||||
};
|
||||
use crate::demos::bevydemo1::scene::Scene;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// 3d view component
|
||||
#[component]
|
||||
@@ -10,18 +10,18 @@ pub fn Demo1() -> impl IntoView {
|
||||
// Setup a Counter
|
||||
let initial_value: i32 = 0;
|
||||
let step: i32 = 1;
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
let (value, set_value) = signal(initial_value);
|
||||
|
||||
// Setup a bevy 3d scene
|
||||
let scene = Scene::new("#bevy".to_string());
|
||||
let sender = scene.get_processor().sender;
|
||||
let (sender_sig, _set_sender_sig) = create_signal(sender);
|
||||
let (scene_sig, _set_scene_sig) = create_signal(scene);
|
||||
let (sender_sig, _set_sender_sig) = signal(sender);
|
||||
let (scene_sig, _set_scene_sig) = signal(scene);
|
||||
|
||||
// We need to add the 3D view onto the canvas post render.
|
||||
create_effect(move |_| {
|
||||
Effect::new(move |_| {
|
||||
request_animation_frame(move || {
|
||||
scene_sig.get().setup();
|
||||
scene_sig.get_untracked().setup();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
pub mod demo1;
|
||||
use demo1::Demo1;
|
||||
use leptos::*;
|
||||
use leptos_meta::{provide_meta_context, Meta, Stylesheet, Title};
|
||||
use leptos_router::*;
|
||||
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::Meta;
|
||||
use leptos_meta::Title;
|
||||
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet};
|
||||
use leptos_router::components::*;
|
||||
use leptos_router::StaticSegment;
|
||||
#[component]
|
||||
pub fn RootPage() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
@@ -13,11 +15,12 @@ pub fn RootPage() -> impl IntoView {
|
||||
<Meta name="description" content="Leptonic CSR template"/>
|
||||
<Meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<Meta name="theme-color" content="#e66956"/>
|
||||
<Stylesheet href="https://fonts.googleapis.com/css?family=Roboto&display=swap"/>
|
||||
<Title text="Leptos Bevy3D Example"/>
|
||||
<Stylesheet href="https://fonts.googleapis.com/css?family=Roboto&display=swap"/>
|
||||
<MetaTags/>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="" view=|| view! { <Demo1/> }/>
|
||||
<Routes fallback=move || "Not found.">
|
||||
<Route path=StaticSegment("") view=Demo1 />
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.1.4"
|
||||
version = "0.1.7"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -28,7 +28,7 @@ send_wrapper = { version = "0.6.0", features = ["futures"] }
|
||||
web-sys = { version = "0.3.72", features = ["console"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.41", features = ["rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-test = { version = "0.4.4" }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
|
||||
|
||||
@@ -939,7 +939,8 @@ where
|
||||
#[track_caller]
|
||||
pub fn dispatch(&self, input: I) -> ActionAbortHandle {
|
||||
self.inner
|
||||
.try_with_value(|inner| inner.dispatch(input))
|
||||
.try_get_value()
|
||||
.map(|inner| inner.dispatch(input))
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
@@ -954,7 +955,8 @@ where
|
||||
#[track_caller]
|
||||
pub fn dispatch_local(&self, input: I) -> ActionAbortHandle {
|
||||
self.inner
|
||||
.try_with_value(|inner| inner.dispatch_local(input))
|
||||
.try_get_value()
|
||||
.map(|inner| inner.dispatch_local(input))
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ macro_rules! spawn_derived {
|
||||
}
|
||||
|
||||
while rx.next().await.is_some() {
|
||||
let update_if_necessary = if $should_track {
|
||||
let update_if_necessary = !owner.paused() && if $should_track {
|
||||
any_subscriber
|
||||
.with_observer(|| any_subscriber.update_if_necessary())
|
||||
} else {
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
#[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`.
|
||||
|
||||
@@ -170,9 +170,10 @@ impl Effect<LocalStorage> {
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if subscriber
|
||||
.with_observer(|| subscriber.update_if_necessary())
|
||||
|| first_run
|
||||
if !owner.paused()
|
||||
&& (subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
}) || first_run)
|
||||
{
|
||||
first_run = false;
|
||||
subscriber.clear_sources(&subscriber);
|
||||
@@ -321,9 +322,10 @@ impl Effect<LocalStorage> {
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if subscriber
|
||||
.with_observer(|| subscriber.update_if_necessary())
|
||||
|| first_run
|
||||
if !owner.paused()
|
||||
&& (subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
}) || first_run)
|
||||
{
|
||||
subscriber.clear_sources(&subscriber);
|
||||
|
||||
@@ -372,46 +374,16 @@ 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>(
|
||||
mut fun: impl EffectFunction<T, M> + Send + Sync + 'static,
|
||||
fun: impl EffectFunction<T, M> + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
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>));
|
||||
if !cfg!(feature = "effects") {
|
||||
return Self { inner: None };
|
||||
}
|
||||
|
||||
crate::spawn({
|
||||
let value = Arc::clone(&value);
|
||||
let subscriber = inner.to_any_subscriber();
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if 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 }
|
||||
Self::new_isomorphic(fun)
|
||||
}
|
||||
|
||||
/// Creates a new effect, which runs once on the next “tick”, and then runs again when reactive values
|
||||
@@ -434,9 +406,10 @@ impl Effect<SyncStorage> {
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if subscriber
|
||||
.with_observer(|| subscriber.update_if_necessary())
|
||||
|| first_run
|
||||
if !owner.paused()
|
||||
&& (subscriber
|
||||
.with_observer(|| subscriber.update_if_necessary())
|
||||
|| first_run)
|
||||
{
|
||||
first_run = false;
|
||||
subscriber.clear_sources(&subscriber);
|
||||
@@ -487,9 +460,10 @@ impl Effect<SyncStorage> {
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if subscriber
|
||||
.with_observer(|| subscriber.update_if_necessary())
|
||||
|| first_run
|
||||
if !owner.paused()
|
||||
&& (subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
}) || first_run)
|
||||
{
|
||||
subscriber.clear_sources(&subscriber);
|
||||
|
||||
|
||||
379
reactive_graph/src/effect/immediate.rs
Normal file
379
reactive_graph/src/effect/immediate.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
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,21 +30,19 @@ 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 is_dirty {
|
||||
if guard.dirty {
|
||||
guard.dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
let sources = guard.sources.clone();
|
||||
|
||||
drop(guard);
|
||||
for source in sources.into_iter().flatten() {
|
||||
if source.update_if_necessary() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
|
||||
sources
|
||||
.into_iter()
|
||||
.any(|source| source.update_if_necessary())
|
||||
}
|
||||
|
||||
fn mark_check(&self) {
|
||||
|
||||
@@ -91,9 +91,11 @@ where
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
}) {
|
||||
if !owner.paused()
|
||||
&& subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
})
|
||||
{
|
||||
subscriber.clear_sources(&subscriber);
|
||||
|
||||
let old_value = mem::take(
|
||||
@@ -159,8 +161,10 @@ where
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if subscriber
|
||||
.with_observer(|| subscriber.update_if_necessary())
|
||||
if !owner.paused()
|
||||
&& subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
})
|
||||
{
|
||||
subscriber.clear_sources(&subscriber);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -130,6 +130,7 @@ impl Owner {
|
||||
.and_then(|parent| parent.upgrade())
|
||||
.map(|parent| parent.read().or_poisoned().arena.clone())
|
||||
.unwrap_or_default(),
|
||||
paused: false,
|
||||
})),
|
||||
#[cfg(feature = "hydration")]
|
||||
shared_context,
|
||||
@@ -163,6 +164,7 @@ impl Owner {
|
||||
children: Default::default(),
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
arena: Default::default(),
|
||||
paused: false,
|
||||
})),
|
||||
#[cfg(feature = "hydration")]
|
||||
shared_context,
|
||||
@@ -174,8 +176,10 @@ impl Owner {
|
||||
/// Creates a new `Owner` that is the child of the current `Owner`, if any.
|
||||
pub fn child(&self) -> Self {
|
||||
let parent = Some(Arc::downgrade(&self.inner));
|
||||
let mut inner = self.inner.write().or_poisoned();
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
let arena = self.inner.read().or_poisoned().arena.clone();
|
||||
let arena = inner.arena.clone();
|
||||
let paused = inner.paused;
|
||||
let child = Self {
|
||||
inner: Arc::new(RwLock::new(OwnerInner {
|
||||
parent,
|
||||
@@ -185,15 +189,12 @@ impl Owner {
|
||||
children: Default::default(),
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
arena,
|
||||
paused,
|
||||
})),
|
||||
#[cfg(feature = "hydration")]
|
||||
shared_context: self.shared_context.clone(),
|
||||
};
|
||||
self.inner
|
||||
.write()
|
||||
.or_poisoned()
|
||||
.children
|
||||
.push(Arc::downgrade(&child.inner));
|
||||
inner.children.push(Arc::downgrade(&child.inner));
|
||||
child
|
||||
}
|
||||
|
||||
@@ -207,9 +208,7 @@ impl Owner {
|
||||
/// Runs the given function with this as the current `Owner`.
|
||||
pub fn with<T>(&self, fun: impl FnOnce() -> T) -> T {
|
||||
let prev = {
|
||||
OWNER.with(|o| {
|
||||
mem::replace(&mut *o.borrow_mut(), Some(self.clone()))
|
||||
})
|
||||
OWNER.with(|o| Option::replace(&mut *o.borrow_mut(), self.clone()))
|
||||
};
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
Arena::set(&self.inner.read().or_poisoned().arena);
|
||||
@@ -337,6 +336,47 @@ impl Owner {
|
||||
|
||||
inner(Box::new(fun))
|
||||
}
|
||||
|
||||
/// Pauses the execution of side effects for this owner, and any of its descendants.
|
||||
///
|
||||
/// If this owner is the owner for an [`Effect`](crate::effect::Effect) or [`RenderEffect`](crate::effect::RenderEffect), this effect will not run until [`Owner::resume`] is called. All children of this effects are also paused.
|
||||
///
|
||||
/// Any notifications will be ignored; effects that are notified will paused will not run when
|
||||
/// resumed, until they are notified again by a source after being resumed.
|
||||
pub fn pause(&self) {
|
||||
let mut stack = Vec::with_capacity(16);
|
||||
stack.push(Arc::downgrade(&self.inner));
|
||||
while let Some(curr) = stack.pop() {
|
||||
if let Some(curr) = curr.upgrade() {
|
||||
let mut curr = curr.write().or_poisoned();
|
||||
curr.paused = true;
|
||||
stack.extend(curr.children.iter().map(Weak::clone));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this owner has been paused by [`Owner::pause`].
|
||||
pub fn paused(&self) -> bool {
|
||||
self.inner.read().or_poisoned().paused
|
||||
}
|
||||
|
||||
/// Resumes side effects that have been paused by [`Owner::pause`].
|
||||
///
|
||||
/// All children will also be resumed.
|
||||
///
|
||||
/// This will *not* cause side effects that were notified while paused to run, until they are
|
||||
/// notified again by a source after being resumed.
|
||||
pub fn resume(&self) {
|
||||
let mut stack = Vec::with_capacity(16);
|
||||
stack.push(Arc::downgrade(&self.inner));
|
||||
while let Some(curr) = stack.pop() {
|
||||
if let Some(curr) = curr.upgrade() {
|
||||
let mut curr = curr.write().or_poisoned();
|
||||
curr.paused = false;
|
||||
stack.extend(curr.children.iter().map(Weak::clone));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@@ -363,6 +403,7 @@ pub(crate) struct OwnerInner {
|
||||
pub children: Vec<Weak<RwLock<OwnerInner>>>,
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
arena: Arc<RwLock<ArenaMap>>,
|
||||
paused: bool,
|
||||
}
|
||||
|
||||
impl Debug for OwnerInner {
|
||||
|
||||
@@ -55,7 +55,7 @@ impl<T: AsSubscriberSet + DefinedAt> ReactiveNode for T {
|
||||
|
||||
fn mark_subscribers_check(&self) {
|
||||
if let Some(inner) = self.as_subscriber_set() {
|
||||
let subs = inner.borrow().write().unwrap().take();
|
||||
let subs = inner.borrow().read().unwrap().clone();
|
||||
for sub in subs {
|
||||
sub.mark_dirty();
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ impl AsyncTransition {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let global_transition = global_transition();
|
||||
let inner = TransitionInner { tx };
|
||||
let prev = std::mem::replace(
|
||||
let prev = Option::replace(
|
||||
&mut *global_transition.write().or_poisoned(),
|
||||
Some(inner.clone()),
|
||||
inner.clone(),
|
||||
);
|
||||
let value = action().await;
|
||||
_ = std::mem::replace(
|
||||
|
||||
@@ -196,3 +196,62 @@ async fn recursive_effect_runs_recursively() {
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "effects")]
|
||||
#[tokio::test]
|
||||
async fn paused_effect_pauses() {
|
||||
use imports::*;
|
||||
use reactive_graph::owner::StoredValue;
|
||||
|
||||
_ = Executor::init_tokio();
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
task::LocalSet::new()
|
||||
.run_until(async {
|
||||
let a = RwSignal::new(-1);
|
||||
|
||||
// simulate an arbitrary side effect
|
||||
let runs = StoredValue::new(0);
|
||||
|
||||
let owner = StoredValue::new(None);
|
||||
|
||||
Effect::new({
|
||||
move || {
|
||||
*owner.write_value() = Owner::current();
|
||||
|
||||
let _ = a.get();
|
||||
*runs.write_value() += 1;
|
||||
}
|
||||
});
|
||||
|
||||
Executor::tick().await;
|
||||
assert_eq!(runs.get_value(), 1);
|
||||
|
||||
println!("setting to 1");
|
||||
a.set(1);
|
||||
|
||||
Executor::tick().await;
|
||||
assert_eq!(runs.get_value(), 2);
|
||||
|
||||
println!("pausing");
|
||||
owner.get_value().unwrap().pause();
|
||||
|
||||
println!("setting to 2");
|
||||
a.set(2);
|
||||
|
||||
Executor::tick().await;
|
||||
assert_eq!(runs.get_value(), 2);
|
||||
|
||||
println!("resuming");
|
||||
owner.get_value().unwrap().resume();
|
||||
|
||||
println!("setting to 3");
|
||||
a.set(3);
|
||||
|
||||
Executor::tick().await;
|
||||
println!("checking value");
|
||||
assert_eq!(runs.get_value(), 3);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
227
reactive_graph/tests/effect_immediate.rs
Normal file
227
reactive_graph/tests/effect_immediate.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
#[cfg(feature = "effects")]
|
||||
pub mod imports {
|
||||
pub use any_spawner::Executor;
|
||||
pub use reactive_graph::{
|
||||
effect::ImmediateEffect, owner::Owner, prelude::*, signal::RwSignal,
|
||||
};
|
||||
pub use std::sync::{Arc, RwLock};
|
||||
pub use tokio::task;
|
||||
}
|
||||
|
||||
#[cfg(feature = "effects")]
|
||||
#[test]
|
||||
fn effect_runs() {
|
||||
use imports::*;
|
||||
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let a = RwSignal::new(-1);
|
||||
|
||||
// simulate an arbitrary side effect
|
||||
let b = Arc::new(RwLock::new(String::new()));
|
||||
|
||||
let _guard = ImmediateEffect::new({
|
||||
let b = b.clone();
|
||||
move || {
|
||||
let formatted = format!("Value is {}", a.get());
|
||||
*b.write().unwrap() = formatted;
|
||||
}
|
||||
});
|
||||
assert_eq!(b.read().unwrap().as_str(), "Value is -1");
|
||||
|
||||
println!("setting to 1");
|
||||
a.set(1);
|
||||
assert_eq!(b.read().unwrap().as_str(), "Value is 1");
|
||||
}
|
||||
|
||||
#[cfg(feature = "effects")]
|
||||
#[test]
|
||||
fn dynamic_dependencies() {
|
||||
use imports::*;
|
||||
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let first = RwSignal::new("Greg");
|
||||
let last = RwSignal::new("Johnston");
|
||||
let use_last = RwSignal::new(true);
|
||||
|
||||
let combined_count = Arc::new(RwLock::new(0));
|
||||
|
||||
let _guard = ImmediateEffect::new({
|
||||
let combined_count = Arc::clone(&combined_count);
|
||||
move || {
|
||||
*combined_count.write().unwrap() += 1;
|
||||
if use_last.get() {
|
||||
println!("{} {}", first.get(), last.get());
|
||||
} else {
|
||||
println!("{}", first.get());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(*combined_count.read().unwrap(), 1);
|
||||
|
||||
println!("\nsetting `first` to Bob");
|
||||
first.set("Bob");
|
||||
assert_eq!(*combined_count.read().unwrap(), 2);
|
||||
|
||||
println!("\nsetting `last` to Bob");
|
||||
last.set("Thompson");
|
||||
assert_eq!(*combined_count.read().unwrap(), 3);
|
||||
|
||||
println!("\nsetting `use_last` to false");
|
||||
use_last.set(false);
|
||||
assert_eq!(*combined_count.read().unwrap(), 4);
|
||||
|
||||
println!("\nsetting `last` to Jones");
|
||||
last.set("Jones");
|
||||
assert_eq!(*combined_count.read().unwrap(), 4);
|
||||
|
||||
println!("\nsetting `last` to Jones");
|
||||
last.set("Smith");
|
||||
assert_eq!(*combined_count.read().unwrap(), 4);
|
||||
|
||||
println!("\nsetting `last` to Stevens");
|
||||
last.set("Stevens");
|
||||
assert_eq!(*combined_count.read().unwrap(), 4);
|
||||
|
||||
println!("\nsetting `use_last` to true");
|
||||
use_last.set(true);
|
||||
assert_eq!(*combined_count.read().unwrap(), 5);
|
||||
}
|
||||
|
||||
#[cfg(feature = "effects")]
|
||||
#[test]
|
||||
fn recursive_effect_runs_recursively() {
|
||||
use imports::*;
|
||||
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let s = RwSignal::new(0);
|
||||
|
||||
let logged_values = Arc::new(RwLock::new(Vec::new()));
|
||||
|
||||
let _guard = ImmediateEffect::new({
|
||||
let logged_values = Arc::clone(&logged_values);
|
||||
move || {
|
||||
let a = s.get();
|
||||
println!("a = {a}");
|
||||
logged_values.write().unwrap().push(a);
|
||||
if a == 0 {
|
||||
return;
|
||||
}
|
||||
s.set(0);
|
||||
}
|
||||
});
|
||||
|
||||
s.set(1);
|
||||
s.set(2);
|
||||
s.set(3);
|
||||
|
||||
assert_eq!(0, s.get_untracked());
|
||||
assert_eq!(&*logged_values.read().unwrap(), &[0, 1, 0, 2, 0, 3, 0]);
|
||||
}
|
||||
|
||||
#[cfg(feature = "effects")]
|
||||
#[test]
|
||||
fn paused_effect_pauses() {
|
||||
use imports::*;
|
||||
use reactive_graph::owner::StoredValue;
|
||||
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let a = RwSignal::new(-1);
|
||||
|
||||
// simulate an arbitrary side effect
|
||||
let runs = StoredValue::new(0);
|
||||
|
||||
let owner = StoredValue::new(None);
|
||||
|
||||
let _guard = ImmediateEffect::new({
|
||||
move || {
|
||||
*owner.write_value() = Owner::current();
|
||||
|
||||
let _ = a.get();
|
||||
*runs.write_value() += 1;
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(runs.get_value(), 1);
|
||||
|
||||
println!("setting to 1");
|
||||
a.set(1);
|
||||
|
||||
assert_eq!(runs.get_value(), 2);
|
||||
|
||||
println!("pausing");
|
||||
owner.get_value().unwrap().pause();
|
||||
|
||||
println!("setting to 2");
|
||||
a.set(2);
|
||||
|
||||
assert_eq!(runs.get_value(), 2);
|
||||
|
||||
println!("resuming");
|
||||
owner.get_value().unwrap().resume();
|
||||
|
||||
println!("setting to 3");
|
||||
a.set(3);
|
||||
|
||||
println!("checking value");
|
||||
assert_eq!(runs.get_value(), 3);
|
||||
}
|
||||
|
||||
#[cfg(feature = "effects")]
|
||||
#[test]
|
||||
#[ignore = "Parallel signal access can panic."]
|
||||
fn threaded_chaos_effect() {
|
||||
use imports::*;
|
||||
use reactive_graph::owner::StoredValue;
|
||||
|
||||
const SIGNAL_COUNT: usize = 5;
|
||||
const THREAD_COUNT: usize = 10;
|
||||
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let signals = vec![RwSignal::new(0); SIGNAL_COUNT];
|
||||
|
||||
let runs = StoredValue::new(0);
|
||||
|
||||
let _guard = ImmediateEffect::new({
|
||||
let signals = signals.clone();
|
||||
move || {
|
||||
*runs.write_value() += 1;
|
||||
|
||||
let mut values = vec![];
|
||||
for s in &signals {
|
||||
let v = s.get();
|
||||
values.push(v);
|
||||
if v != 0 {
|
||||
s.set(v - 1);
|
||||
}
|
||||
}
|
||||
println!("{values:?}");
|
||||
}
|
||||
});
|
||||
|
||||
std::thread::scope(|s| {
|
||||
for _ in 0..THREAD_COUNT {
|
||||
let signals = signals.clone();
|
||||
s.spawn(move || {
|
||||
for s in &signals {
|
||||
s.set(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(runs.get_value(), 1 + THREAD_COUNT * SIGNAL_COUNT);
|
||||
|
||||
let values: Vec<_> = signals.iter().map(|s| s.get_untracked()).collect();
|
||||
println!("FINAL: {values:?}");
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_stores"
|
||||
version = "0.1.3"
|
||||
version = "0.1.7"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -11,7 +11,7 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
guardian = "1.2"
|
||||
itertools = "0.13.0"
|
||||
itertools = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
paste = "1.0"
|
||||
reactive_graph = { workspace = true }
|
||||
@@ -19,7 +19,7 @@ rustc-hash = "2.0"
|
||||
reactive_stores_macro = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.41", features = ["rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-test = { version = "0.4.4" }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
reactive_graph = { workspace = true, features = ["effects"] }
|
||||
|
||||
@@ -38,6 +38,20 @@ where
|
||||
track_field: Arc<dyn Fn() + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<T> Debug for ArcField<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut f = f.debug_struct("ArcField");
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
let f = f.field("defined_at", &self.defined_at);
|
||||
f.field("path", &self.path)
|
||||
.field("trigger", &self.trigger)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StoreFieldReader<T>(Box<dyn Deref<Target = T>>);
|
||||
|
||||
impl<T> StoreFieldReader<T> {
|
||||
|
||||
@@ -10,7 +10,6 @@ use reactive_graph::{
|
||||
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
|
||||
Write,
|
||||
},
|
||||
unwrap_signal,
|
||||
};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
@@ -32,6 +31,19 @@ where
|
||||
inner: ArenaItem<ArcField<T>, S>,
|
||||
}
|
||||
|
||||
impl<T, S> Debug for Field<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut f = f.debug_struct("Field");
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
let f = f.field("defined_at", &self.defined_at);
|
||||
f.field("inner", &self.inner).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> StoreField for Field<T, S>
|
||||
where
|
||||
S: Storage<ArcField<T>>,
|
||||
@@ -44,14 +56,14 @@ where
|
||||
self.inner
|
||||
.try_get_value()
|
||||
.map(|inner| inner.get_trigger(path))
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.inner
|
||||
.try_get_value()
|
||||
.map(|inner| inner.path().into_iter().collect::<Vec<_>>())
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
@@ -82,6 +94,21 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<ArcField<T>> for Field<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<ArcField<T>>,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: ArcField<T>) -> Self {
|
||||
Field {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
inner: ArenaItem::new_with_storage(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<ArcStore<T>> for Field<T, S>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
|
||||
@@ -148,11 +148,8 @@ where
|
||||
{
|
||||
fn latest_keys(&self) -> Vec<K> {
|
||||
self.reader()
|
||||
.expect("trying to update keys")
|
||||
.deref()
|
||||
.into_iter()
|
||||
.map(|n| (self.key_fn)(n))
|
||||
.collect()
|
||||
.map(|r| r.deref().into_iter().map(|n| (self.key_fn)(n)).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,8 +480,7 @@ where
|
||||
|| self.inner.latest_keys(),
|
||||
)
|
||||
.flatten()
|
||||
.map(|(_, idx)| idx)
|
||||
.expect("reading from a keyed field that has not yet been created");
|
||||
.map(|(_, idx)| idx)?;
|
||||
|
||||
Some(WriteGuard::new(
|
||||
trigger.children,
|
||||
@@ -654,13 +650,15 @@ where
|
||||
self.track_field();
|
||||
|
||||
// get the current length of the field by accessing slice
|
||||
let reader = self
|
||||
.reader()
|
||||
.expect("creating iterator from unavailable store field");
|
||||
let reader = self.reader();
|
||||
|
||||
let keys = reader
|
||||
.into_iter()
|
||||
.map(|item| (self.key_fn)(item))
|
||||
.collect::<VecDeque<_>>();
|
||||
.map(|r| {
|
||||
r.into_iter()
|
||||
.map(|item| (self.key_fn)(item))
|
||||
.collect::<VecDeque<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// return the iterator
|
||||
StoreFieldKeyedIter { inner: self, keys }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{StoreField, Subfield};
|
||||
use reactive_graph::traits::{Read, ReadUntracked};
|
||||
use reactive_graph::traits::{FlattenOptionRefOption, Read, ReadUntracked};
|
||||
use std::ops::Deref;
|
||||
|
||||
/// Extends optional store fields, with the ability to unwrap or map over them.
|
||||
@@ -13,6 +13,13 @@ where
|
||||
/// Provides access to the inner value, as a subfield, unwrapping the outer value.
|
||||
fn unwrap(self) -> Subfield<Self, Option<Self::Output>, Self::Output>;
|
||||
|
||||
/// Inverts a subfield of an `Option` to an `Option` of a subfield.
|
||||
fn invert(
|
||||
self,
|
||||
) -> Option<Subfield<Self, Option<Self::Output>, Self::Output>> {
|
||||
self.map(|f| f)
|
||||
}
|
||||
|
||||
/// Reactively maps over the field.
|
||||
///
|
||||
/// This returns `None` if the subfield is currently `None`,
|
||||
@@ -56,7 +63,7 @@ where
|
||||
self,
|
||||
map_fn: impl FnOnce(Subfield<S, Option<T>, T>) -> U,
|
||||
) -> Option<U> {
|
||||
if self.read().is_some() {
|
||||
if self.try_read().as_deref().flatten().is_some() {
|
||||
Some(map_fn(self.unwrap()))
|
||||
} else {
|
||||
None
|
||||
@@ -67,7 +74,7 @@ where
|
||||
self,
|
||||
map_fn: impl FnOnce(Subfield<S, Option<T>, T>) -> U,
|
||||
) -> Option<U> {
|
||||
if self.read_untracked().is_some() {
|
||||
if self.try_read_untracked().as_deref().flatten().is_some() {
|
||||
Some(map_fn(self.unwrap()))
|
||||
} else {
|
||||
None
|
||||
@@ -77,11 +84,12 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{self as reactive_stores, Store};
|
||||
use crate::{self as reactive_stores, Patch as _, Store};
|
||||
use reactive_graph::{
|
||||
effect::Effect,
|
||||
traits::{Get, Read, ReadUntracked, Set, Write},
|
||||
};
|
||||
use reactive_stores_macro::Patch;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
@@ -237,4 +245,115 @@ mod tests {
|
||||
assert_eq!(parent_count.load(Ordering::Relaxed), 3);
|
||||
assert_eq!(inner_count.load(Ordering::Relaxed), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn patch() {
|
||||
use crate::OptionStoreExt;
|
||||
|
||||
#[derive(Debug, Clone, Store, Patch)]
|
||||
struct Outer {
|
||||
inner: Option<Inner>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Store, Patch)]
|
||||
struct Inner {
|
||||
first: String,
|
||||
second: String,
|
||||
}
|
||||
|
||||
let store = Store::new(Outer {
|
||||
inner: Some(Inner {
|
||||
first: "A".to_owned(),
|
||||
second: "B".to_owned(),
|
||||
}),
|
||||
});
|
||||
|
||||
_ = any_spawner::Executor::init_tokio();
|
||||
|
||||
let parent_count = Arc::new(AtomicUsize::new(0));
|
||||
let inner_first_count = Arc::new(AtomicUsize::new(0));
|
||||
let inner_second_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
Effect::new_sync({
|
||||
let parent_count = Arc::clone(&parent_count);
|
||||
move |prev: Option<()>| {
|
||||
if prev.is_none() {
|
||||
println!("parent: first run");
|
||||
} else {
|
||||
println!("parent: next run");
|
||||
}
|
||||
|
||||
println!(" value = {:?}", store.inner().get());
|
||||
parent_count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
Effect::new_sync({
|
||||
let inner_first_count = Arc::clone(&inner_first_count);
|
||||
move |prev: Option<()>| {
|
||||
if prev.is_none() {
|
||||
println!("inner_first: first run");
|
||||
} else {
|
||||
println!("inner_first: next run");
|
||||
}
|
||||
|
||||
println!(
|
||||
" value = {:?}",
|
||||
store.inner().map(|inner| inner.first().get())
|
||||
);
|
||||
inner_first_count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
Effect::new_sync({
|
||||
let inner_second_count = Arc::clone(&inner_second_count);
|
||||
move |prev: Option<()>| {
|
||||
if prev.is_none() {
|
||||
println!("inner_second: first run");
|
||||
} else {
|
||||
println!("inner_second: next run");
|
||||
}
|
||||
|
||||
println!(
|
||||
" value = {:?}",
|
||||
store.inner().map(|inner| inner.second().get())
|
||||
);
|
||||
inner_second_count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
|
||||
tick().await;
|
||||
assert_eq!(parent_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(inner_first_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(inner_second_count.load(Ordering::Relaxed), 1);
|
||||
|
||||
store.patch(Outer {
|
||||
inner: Some(Inner {
|
||||
first: "A".to_string(),
|
||||
second: "C".to_string(),
|
||||
}),
|
||||
});
|
||||
|
||||
tick().await;
|
||||
assert_eq!(parent_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(inner_first_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(inner_second_count.load(Ordering::Relaxed), 2);
|
||||
|
||||
store.patch(Outer { inner: None });
|
||||
|
||||
tick().await;
|
||||
assert_eq!(parent_count.load(Ordering::Relaxed), 2);
|
||||
assert_eq!(inner_first_count.load(Ordering::Relaxed), 2);
|
||||
assert_eq!(inner_second_count.load(Ordering::Relaxed), 3);
|
||||
|
||||
store.patch(Outer {
|
||||
inner: Some(Inner {
|
||||
first: "A".to_string(),
|
||||
second: "B".to_string(),
|
||||
}),
|
||||
});
|
||||
|
||||
tick().await;
|
||||
assert_eq!(parent_count.load(Ordering::Relaxed), 3);
|
||||
assert_eq!(inner_first_count.load(Ordering::Relaxed), 3);
|
||||
assert_eq!(inner_second_count.load(Ordering::Relaxed), 4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,35 @@ patch_primitives! {
|
||||
NonZeroUsize
|
||||
}
|
||||
|
||||
impl<T> PatchField for Option<T>
|
||||
where
|
||||
T: PatchField,
|
||||
{
|
||||
fn patch_field(
|
||||
&mut self,
|
||||
new: Self,
|
||||
path: &StorePath,
|
||||
notify: &mut dyn FnMut(&StorePath),
|
||||
) {
|
||||
match (self, new) {
|
||||
(None, None) => {}
|
||||
(old @ Some(_), None) => {
|
||||
old.take();
|
||||
notify(path);
|
||||
}
|
||||
(old @ None, new @ Some(_)) => {
|
||||
*old = new;
|
||||
notify(path);
|
||||
}
|
||||
(Some(old), Some(new)) => {
|
||||
let mut new_path = path.to_owned();
|
||||
new_path.push(0);
|
||||
old.patch_field(new, &new_path, notify);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PatchField for Vec<T>
|
||||
where
|
||||
T: PatchField,
|
||||
|
||||
@@ -9,8 +9,7 @@ use reactive_graph::{
|
||||
guards::{Plain, UntrackedWriteGuard, WriteGuard},
|
||||
ArcTrigger,
|
||||
},
|
||||
traits::{DefinedAt, Track, UntrackableGuard},
|
||||
unwrap_signal,
|
||||
traits::{Track, UntrackableGuard},
|
||||
};
|
||||
use std::{iter, ops::Deref, sync::Arc};
|
||||
|
||||
@@ -105,7 +104,7 @@ where
|
||||
self.inner
|
||||
.try_get_value()
|
||||
.map(|n| n.get_trigger(path))
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@@ -113,7 +112,7 @@ where
|
||||
self.inner
|
||||
.try_get_value()
|
||||
.map(|n| n.path().into_iter().collect::<Vec<_>>())
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -9,9 +9,10 @@ use reactive_graph::{
|
||||
ArcTrigger,
|
||||
},
|
||||
traits::{
|
||||
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
|
||||
Write,
|
||||
DefinedAt, Get as _, IsDisposed, Notify, ReadUntracked, Track,
|
||||
UntrackableGuard, Write,
|
||||
},
|
||||
wrappers::read::Signal,
|
||||
};
|
||||
use std::{iter, marker::PhantomData, ops::DerefMut, panic::Location};
|
||||
|
||||
@@ -99,6 +100,9 @@ where
|
||||
|
||||
let mut full_path = self.path().into_iter().collect::<StorePath>();
|
||||
full_path.pop();
|
||||
|
||||
// build a list of triggers, starting with the full path to this node and ending with the root
|
||||
// this will mean that the root is the final item, and this path is first
|
||||
let mut triggers = Vec::with_capacity(full_path.len());
|
||||
triggers.push(trigger.this.clone());
|
||||
loop {
|
||||
@@ -109,6 +113,17 @@ where
|
||||
}
|
||||
full_path.pop();
|
||||
}
|
||||
|
||||
// when the WriteGuard is dropped, each trigger will be notified, in order
|
||||
// reversing the list will cause the triggers to be notified starting from the root,
|
||||
// then to each child down to this one
|
||||
//
|
||||
// notifying from the root down is important for things like OptionStoreExt::map()/unwrap(),
|
||||
// where it's really important that any effects that subscribe to .is_some() run before effects
|
||||
// that subscribe to the inner value, so that the inner effect can be canceled if the outer switches to `None`
|
||||
// (see https://github.com/leptos-rs/leptos/issues/3704)
|
||||
triggers.reverse();
|
||||
|
||||
let guard = WriteGuard::new(triggers, parent);
|
||||
|
||||
Some(MappedMut::new(guard, self.read, self.write))
|
||||
@@ -223,3 +238,14 @@ where
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, Prev, T> From<Subfield<Inner, Prev, T>> for Signal<T>
|
||||
where
|
||||
Inner: StoreField<Value = Prev> + Track + Send + Sync + 'static,
|
||||
Prev: 'static,
|
||||
T: Send + Sync + Clone + 'static,
|
||||
{
|
||||
fn from(subfield: Subfield<Inner, Prev, T>) -> Self {
|
||||
Signal::derive(move || subfield.get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_stores_macro"
|
||||
version = "0.1.0"
|
||||
version = "0.1.7"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -13,7 +13,7 @@ edition.workspace = true
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
convert_case = "0.6"
|
||||
convert_case = "0.7"
|
||||
proc-macro-error2 = "2.0"
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
|
||||
@@ -79,7 +79,7 @@ impl Parse for Model {
|
||||
|
||||
#[derive(Clone)]
|
||||
enum SubfieldMode {
|
||||
Keyed(ExprClosure, Type),
|
||||
Keyed(ExprClosure, Box<Type>),
|
||||
Skip,
|
||||
}
|
||||
|
||||
@@ -87,15 +87,15 @@ impl Parse for SubfieldMode {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let mode: Ident = input.parse()?;
|
||||
if mode == "key" {
|
||||
let _col: Token!(:) = input.parse()?;
|
||||
let _col: Token![:] = input.parse()?;
|
||||
let ty: Type = input.parse()?;
|
||||
let _eq: Token!(=) = input.parse()?;
|
||||
let ident: ExprClosure = input.parse()?;
|
||||
Ok(SubfieldMode::Keyed(ident, ty))
|
||||
let _eq: Token![=] = input.parse()?;
|
||||
let closure: ExprClosure = input.parse()?;
|
||||
Ok(SubfieldMode::Keyed(closure, Box::new(ty)))
|
||||
} else if mode == "skip" {
|
||||
Ok(SubfieldMode::Skip)
|
||||
} else {
|
||||
Err(input.error("expected `key = <ident>: <Type>`"))
|
||||
Err(input.error("expected `key: <Type> = <closure>`"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.8.0-alpha"
|
||||
version = "0.7.7"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -20,7 +20,7 @@ tachys = { workspace = true, features = ["reactive_graph"] }
|
||||
futures = "0.3.31"
|
||||
url = "2.5"
|
||||
js-sys = { version = "0.3.74" }
|
||||
wasm-bindgen = { version = "0.2.97" }
|
||||
wasm-bindgen = { workspace = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
once_cell = "1.20"
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
@@ -10,7 +10,6 @@ use crate::{
|
||||
use any_spawner::Executor;
|
||||
use either_of::Either;
|
||||
use futures::FutureExt;
|
||||
use leptos::attr::any_attribute::AnyAttribute;
|
||||
use reactive_graph::{
|
||||
computed::{ArcMemo, ScopedFuture},
|
||||
owner::{provide_context, Owner},
|
||||
@@ -26,7 +25,7 @@ use tachys::{
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
add_attr::AddAnyAttr,
|
||||
any_view::{AnyView, AnyViewState, ExtraAttrsMut, IntoAny},
|
||||
any_view::{AnyView, AnyViewState, IntoAny},
|
||||
Mountable, Position, PositionState, Render, RenderHtml,
|
||||
},
|
||||
};
|
||||
@@ -80,7 +79,7 @@ where
|
||||
{
|
||||
type State = Rc<RefCell<FlatRoutesViewState>>;
|
||||
|
||||
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
|
||||
fn build(self) -> Self::State {
|
||||
let FlatRoutesView {
|
||||
current_url,
|
||||
routes,
|
||||
@@ -118,7 +117,7 @@ where
|
||||
|
||||
match new_match {
|
||||
None => Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: fallback().into_any().build(extra_attrs),
|
||||
view: fallback().into_any().build(),
|
||||
id,
|
||||
owner,
|
||||
params,
|
||||
@@ -151,7 +150,7 @@ where
|
||||
|
||||
match view.as_mut().now_or_never() {
|
||||
Some(view) => Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: view.into_any().build(extra_attrs),
|
||||
view: view.into_any().build(),
|
||||
id,
|
||||
owner,
|
||||
params,
|
||||
@@ -162,7 +161,7 @@ where
|
||||
None => {
|
||||
let state =
|
||||
Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: ().into_any().build(extra_attrs.clone()),
|
||||
view: ().into_any().build(),
|
||||
id,
|
||||
owner,
|
||||
params,
|
||||
@@ -175,10 +174,8 @@ where
|
||||
let state = Rc::clone(&state);
|
||||
async move {
|
||||
let view = view.await;
|
||||
view.into_any().rebuild(
|
||||
&mut state.borrow_mut().view,
|
||||
extra_attrs,
|
||||
);
|
||||
view.into_any()
|
||||
.rebuild(&mut state.borrow_mut().view);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -189,11 +186,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
self,
|
||||
state: &mut Self::State,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
let FlatRoutesView {
|
||||
current_url,
|
||||
location,
|
||||
@@ -271,9 +264,7 @@ where
|
||||
provide_context(url);
|
||||
provide_context(params_memo);
|
||||
provide_context(Matched(ArcMemo::from(new_matched)));
|
||||
fallback()
|
||||
.into_any()
|
||||
.rebuild(&mut state.borrow_mut().view, extra_attrs)
|
||||
fallback().into_any().rebuild(&mut state.borrow_mut().view)
|
||||
});
|
||||
if let Some(location) = location {
|
||||
location.ready_to_complete();
|
||||
@@ -323,10 +314,8 @@ where
|
||||
== spawned_path
|
||||
{
|
||||
let rebuild = move || {
|
||||
view.into_any().rebuild(
|
||||
&mut state.borrow_mut().view,
|
||||
extra_attrs,
|
||||
);
|
||||
view.into_any()
|
||||
.rebuild(&mut state.borrow_mut().view);
|
||||
};
|
||||
if transition {
|
||||
start_view_transition(0, is_back, rebuild);
|
||||
@@ -354,7 +343,7 @@ impl<Loc, Defs, FalFn, Fal> AddAnyAttr for FlatRoutesView<Loc, Defs, FalFn>
|
||||
where
|
||||
Loc: LocationProvider + Send,
|
||||
Defs: MatchNestedRoutes + Send + 'static,
|
||||
FalFn: FnOnce() -> Fal + Send + 'static,
|
||||
FalFn: FnOnce() -> Fal + Send,
|
||||
Fal: RenderHtml + 'static,
|
||||
{
|
||||
type Output<SomeNewAttr: leptos::attr::Attribute> =
|
||||
@@ -427,20 +416,16 @@ impl<Loc, Defs, FalFn, Fal> RenderHtml for FlatRoutesView<Loc, Defs, FalFn>
|
||||
where
|
||||
Loc: LocationProvider + Send,
|
||||
Defs: MatchNestedRoutes + Send + 'static,
|
||||
FalFn: FnOnce() -> Fal + Send + 'static,
|
||||
FalFn: FnOnce() -> Fal + Send,
|
||||
Fal: RenderHtml + 'static,
|
||||
{
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = <Either<Fal, AnyView> as RenderHtml>::MIN_LENGTH;
|
||||
|
||||
fn dry_resolve(&mut self, _extra_attrs: ExtraAttrsMut<'_>) {}
|
||||
fn dry_resolve(&mut self) {}
|
||||
|
||||
async fn resolve(
|
||||
self,
|
||||
_extra_attrs: ExtraAttrsMut<'_>,
|
||||
) -> Self::AsyncOutput {
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
}
|
||||
|
||||
@@ -450,7 +435,6 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) {
|
||||
// if this is being run on the server for the first time, generating all possible routes
|
||||
if RouteList::is_generating() {
|
||||
@@ -497,13 +481,7 @@ where
|
||||
RouteList::register(RouteList::from(routes));
|
||||
} else {
|
||||
let view = self.choose_ssr();
|
||||
view.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
view.to_html_with_buf(buf, position, escape, mark_branches);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,7 +491,6 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -523,7 +500,6 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -531,7 +507,6 @@ where
|
||||
self,
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
extra_attrs: Option<Vec<AnyAttribute>>,
|
||||
) -> Self::State {
|
||||
// this can be mostly the same as the build() implementation, but with hydrate()
|
||||
//
|
||||
@@ -576,11 +551,9 @@ where
|
||||
|
||||
match new_match {
|
||||
None => Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: fallback().into_any().hydrate::<FROM_SERVER>(
|
||||
cursor,
|
||||
position,
|
||||
extra_attrs,
|
||||
),
|
||||
view: fallback()
|
||||
.into_any()
|
||||
.hydrate::<FROM_SERVER>(cursor, position),
|
||||
id,
|
||||
owner,
|
||||
params,
|
||||
@@ -613,11 +586,9 @@ where
|
||||
|
||||
match view.as_mut().now_or_never() {
|
||||
Some(view) => Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: view.into_any().hydrate::<FROM_SERVER>(
|
||||
cursor,
|
||||
position,
|
||||
extra_attrs,
|
||||
),
|
||||
view: view
|
||||
.into_any()
|
||||
.hydrate::<FROM_SERVER>(cursor, position),
|
||||
id,
|
||||
owner,
|
||||
params,
|
||||
@@ -633,8 +604,4 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user