Compare commits

..

41 Commits

Author SHA1 Message Date
Greg Johnston
a1bc89576e clippy 2025-02-17 07:39:54 -05:00
Greg Johnston
5aa160dabf fix: allow decoding already-decoded URI components (closes #3606) 2025-02-16 17:08:45 -05:00
martin frances
37cf25fba5 serde_json is common to (#3610)
integrations/actix
leptos/server
oco
server_fn

This is a defensive PR - Putting the crate definition into the root
workspcace makes it less likely to get difficult to trace version
slip bugs.

This does not help where sede_json is optional so care manual review
is required.
2025-02-15 10:24:07 -08:00
Greg Johnston
f975b8d33b fix: hydration of () (#3615) 2025-02-15 13:23:09 -05:00
Greg Johnston
4804dac32d chore: update either_of minimum version in workspace (#3612) 2025-02-15 08:19:05 -05:00
martin frances
a9f27d6128 chore: update syntax in README example (#3611) 2025-02-15 08:18:48 -05:00
Greg Johnston
04cb036a7d fix: reorder pause check in new_isomorphic (#3613) 2025-02-15 08:18:23 -05:00
jasper
1d3784ed7b feat: impl Into<Signal> for store subfields (#3579) 2025-02-14 17:14:49 -05:00
Greg Johnston
8cc1a34c00 feat: allow pausing and resuming effects (#3599) 2025-02-14 14:48:27 -05:00
mahdi739
68f4d46c5f feat: add invert to OptionStoreExt (#3534) 2025-02-14 13:52:38 -05:00
martin frances
590728e47e projects/bevy3d_ui: Bevy - Bugfix, clippy and crate bump (#3603)
The bugfix is related to access to a signal

"
bevy3d_ui-b20a0a6a298e7144.js:2637 At src/routes/demo1.rs:24:23, you access a
reactive_graph::signal::read::ReadSignal<bevy3d_ui::demos::bevydemo1::scene::Scene>
(defined at src/routes/demo1.rs:19:39) outside a reactive tracking context.
This might mean your app is not responding to changes in signal values in the way you expect.
"

The solution is to use .get_untracked() inside an "effect" block.

Lots of small clippy fixes.

Also here are the crates that have been bumped

-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"
2025-02-13 15:37:23 -08:00
martin frances
e84b527743 Minor: Bump tokio to 1.43. (#3600) 2025-02-12 23:14:00 -08:00
martin frances
96b125d54f Remove getrandom (#3589)
* Resolved this warning see while running cargo outdated

warning: Feature js of package getrandom has been obsolete in version 0.3.1

Now using the feature "wasm_js"

* the crate "getrandom" needs special configuration.

* getrandom_backend - a more generic config.

* Removed the crate getrandom
2025-02-12 23:13:27 -08:00
martin frances
16d66362f8 Minor: "wasm-bindgen" - Moved the crate definition up to the root workspace (#3588)
* Minor: "wasm-bindgen" - Moved the crate definition up to the root workspace

This synchronizes the version number amongst all sub-projects.
[Where the definition is "optional" manual adjustment is still required]

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-02-12 12:25:22 -08:00
martin frances
e27801a2c2 Minor: Bumped version of convert_case to 0.7 (#3590) 2025-02-12 12:17:36 -08:00
martin frances
b81f71997b Minor: Bump itertools to "0.14.0" (#3593)
The crate is used by "leptos_macro","reactive_stores" and "tachys"

So the definition of itertools can be centralized up into the root workspace
2025-02-12 12:16:47 -08:00
martin frances
2a11325749 Minor: leptos_config - Bump the "config" crate to version 0.15.8 (#3594) 2025-02-12 12:15:57 -08:00
martin frances
5604f3e979 projects/bevy3d_ui: Bevy migration (#3597)
-bevy = "0.14.1"
+bevy = "0.15.2"

<https://bevyengine.org/learn/migration-guides/0-14-to-0-15/>
2025-02-12 12:14:33 -08:00
martin frances
3a9a0891a3 projects/bevy3d_ui Migrate to leptos 0.7.7 (#3596)
* projects/bevy3d_ui Migrate to leptos 0.7.7

* Minor: projects/bevy3d_ui - Better Meta.
2025-02-12 12:13:32 -08:00
Greg Johnston
a39add50c0 fix: occasional use-after-disposed panic in Suspense (#3595) 2025-02-12 14:59:44 -05:00
Greg Johnston
2a2675dd36 fix: remove extra placeholder in Vec of text nodes (closes #3583) (#3592) 2025-02-12 10:45:56 -05:00
Greg Johnston
7be6a9da86 v0.7.7 2025-02-11 20:29:31 -05:00
Greg Johnston
c51e07b5a4 v0.7.6 (#3586) 2025-02-11 20:23:57 -05:00
jasper
b17a4c92c7 fix: allow non static lifetimes in component macro (#3571) 2025-02-10 20:51:15 -05:00
Alexis Fontaine
03f9c6cb6d fix: add <fieldset> attributes (#3581) 2025-02-10 15:19:00 -08:00
Greg Johnston
9e8b8886da feat: add :capture flag for events to handle them during capture phase (closes #3457) (#3575) 2025-02-10 08:26:00 -05:00
Danik Vitek
6a6b3dee15 fix(reactive_stores_macro): store attribute signature error message (#3567) 2025-02-09 14:09:43 -05:00
Alexis Fontaine
5d71913523 Handle erase_components on Either<A, B> (#3572) 2025-02-09 10:50:20 -08:00
mahdi739
706617ab0a fix: panic at fewer places in stores (#3551) 2025-02-07 14:45:12 -05:00
mahdi739
cd64bb9d67 fix: return empty set of keys instead of panicking when the KeyedSubfield is disposed (#3550) 2025-02-07 14:43:33 -05:00
Greg Johnston
f881c1877d fix: do not hold lock on arena when dispatching Action (#3561) 2025-02-07 14:26:58 -05:00
Greg Johnston
8783f6a478 fix: don't use InertElement for style: etc. (closes #3554) (#3558) 2025-02-06 21:26:15 -05:00
Greg Johnston
2add714c65 chore(ci): cargo install --locked for cargo-leptos installation (#3559) 2025-02-06 21:22:37 -05:00
Alexis Fontaine
88b1b2d882 feat: implement Attribute for Either<A, B> (#3556) 2025-02-06 21:21:53 -05:00
zakstucke
9d3a743d33 Fix ci (#3557) 2025-02-06 16:17:31 -08:00
Greg Johnston
c6de7c714e fix: emit syntax errors in components rather than swallowing them (closes #3535) (#3538) 2025-02-01 15:29:40 -05:00
Greg Johnston
6154199850 fix: attribute type erasure nightly (closes #3536) (#3537) 2025-02-01 11:32:32 -05:00
Ivan Radiček
32be3a023a feat: implement PatchField for Option<_> (#3528) 2025-02-01 09:40:43 -05:00
Greg Johnston
d9043e4f34 feat: impl From<ArcField<T>> for Field<T> (#3533)
* feat: impl `From<ArcField<T>>` for `Field<T>`

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-02-01 09:37:38 -05:00
Serhii Stepanchuk
e3010c7f1f feat: add file_and_error_handler_with_context (#3526)
* add file_and_error_handler_with_context
like in leptos_routes_with_context func

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-31 12:33:55 -05:00
Greg Johnston
1dcc5838f7 v0.7.5 2025-01-30 21:42:39 -05:00
150 changed files with 2806 additions and 3946 deletions

View File

@@ -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
View File

@@ -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",
]

View File

@@ -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

View File

@@ -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);

View File

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

View File

@@ -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)]

View File

@@ -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"]

View File

@@ -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"

View File

@@ -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 = []

View File

@@ -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",

View File

@@ -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"

View File

@@ -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()
);
}

View File

@@ -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 }

View File

@@ -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())

View File

@@ -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 }

View File

@@ -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",

View File

@@ -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 = [

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -21,7 +21,7 @@ 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 }
axum = { version = "0.7.5", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = [
"fs",

View File

@@ -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();

View File

@@ -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 = [

View File

@@ -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]

View File

@@ -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" }

View File

@@ -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 }

View File

@@ -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);
}

View File

@@ -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 }

View File

@@ -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);

View File

@@ -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"

View File

@@ -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"

View File

@@ -369,6 +369,7 @@ pub fn handle_server_fns_with_context(
// actually run the server fn
let mut res = ActixResponse(
service
.0
.run(ActixRequest::from((req, payload)))
.await
.take(),

View File

@@ -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 = []

View File

@@ -368,6 +368,8 @@ async fn handle_server_fns_inner(
additional_context: impl Fn() + 'static + Clone + Send,
req: Request<Body>,
) -> impl IntoResponse {
use server_fn::middleware::Service;
let method = req.method().clone();
let path = req.uri().path().to_string();
let (req, parts) = generate_request_and_parts(req);
@@ -482,7 +484,7 @@ pub type PinnedHtmlStream =
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream<IV>(
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -506,7 +508,7 @@ where
)]
pub fn render_route<S, IV>(
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
State<S>,
Request<Body>,
@@ -570,7 +572,7 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order<IV>(
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -623,14 +625,13 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ Sync
+ 'static
where
IV: IntoView + 'static,
@@ -653,8 +654,8 @@ where
)]
pub fn render_route_with_context<S, IV>(
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
State<S>,
Request<Body>,
@@ -755,15 +756,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 +823,8 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -847,17 +847,13 @@ where
}
fn handle_response<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
) -> 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,
{
@@ -984,7 +980,7 @@ fn provide_contexts(
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async<IV>(
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -1038,8 +1034,8 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -1105,8 +1101,8 @@ where
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
@@ -1645,7 +1641,7 @@ where
self,
options: &S,
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
@@ -1660,8 +1656,8 @@ where
self,
options: &S,
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
@@ -1694,15 +1690,12 @@ impl AxumPath for Vec<PathSegment> {
match segment {
PathSegment::Static(s) => path.push_str(s),
PathSegment::Param(s) => {
path.push('{');
path.push(':');
path.push_str(s);
path.push('}');
}
PathSegment::Splat(s) => {
path.push('{');
path.push('*');
path.push_str(s);
path.push('}');
}
PathSegment::Unit => {}
PathSegment::OptionalParam(_) => {
@@ -1734,7 +1727,7 @@ where
self,
state: &S,
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
@@ -1750,8 +1743,8 @@ where
self,
state: &S,
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
@@ -1988,7 +1981,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 +1998,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,

View File

@@ -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",

View File

@@ -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)
}
}

View File

@@ -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,22 +270,6 @@ 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);

View File

@@ -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)]

View File

@@ -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()));
)));
}
}
};

View File

@@ -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)
}
}

View File

@@ -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::{

View File

@@ -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
});

View File

@@ -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)
}
}

View File

@@ -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)'] }

View File

@@ -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()
}

View File

@@ -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)'] }

View File

@@ -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)',
] }

View File

@@ -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

View File

@@ -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,
) {

View File

@@ -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
}
@@ -1174,8 +1179,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 +1197,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 +1220,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 +1448,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 +1655,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 +1681,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 {

View File

@@ -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())
}
}

View File

@@ -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)'] }

View File

@@ -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,

View File

@@ -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)
}
}
}

View File

@@ -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

View File

@@ -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]

View File

@@ -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 }
}
}

View File

@@ -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 }
}
}

View File

@@ -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,
@@ -334,7 +334,6 @@ where
&mut Position::NextChild,
false,
false,
None,
);
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
} else {
@@ -391,17 +390,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.unwrap().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.unwrap().rebuild(&mut state.state);
}
}
@@ -434,18 +429,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,7 +446,6 @@ 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
@@ -465,7 +455,6 @@ where
self,
_cursor: &Cursor,
_position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let cursor = use_context::<MetaContext>()
.expect(
@@ -476,16 +465,9 @@ where
let state = self.el.unwrap().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 +520,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 +541,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 +556,6 @@ impl RenderHtml for MetaTagsView {
_position: &mut Position,
_escape: bool,
_mark_branches: bool,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
buf.push_str("<!--HEAD-->");
}
@@ -592,13 +564,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 {

View File

@@ -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 {

View File

@@ -13,4 +13,4 @@ serde = "1.0"
thiserror = "2.0"
[dev-dependencies]
serde_json = "1.0"
serde_json = { workspace = true }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,6 @@
mod demos;
mod routes;
use leptos::*;
use leptos::prelude::*;
use routes::RootPage;
pub fn main() {

View File

@@ -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();
});
});

View File

@@ -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>
}

View File

@@ -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"] }

View File

@@ -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))
}
}

View File

@@ -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 {

View File

@@ -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);
@@ -388,9 +390,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);
@@ -434,9 +437,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 +491,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);

View File

@@ -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);

View File

@@ -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
}
@@ -337,6 +338,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 +405,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 {

View File

@@ -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();
}

View File

@@ -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
}

View File

@@ -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"] }

View File

@@ -10,7 +10,6 @@ use reactive_graph::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Write,
},
unwrap_signal,
};
use std::{
fmt::Debug,
@@ -44,14 +43,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 +81,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,

View File

@@ -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 }

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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]

View File

@@ -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};
@@ -223,3 +224,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())
}
}

View File

@@ -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"

View File

@@ -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, ty))
} else if mode == "skip" {
Ok(SubfieldMode::Skip)
} else {
Err(input.error("expected `key = <ident>: <Type>`"))
Err(input.error("expected `key: <Type> = <closure>`"))
}
}
}

View File

@@ -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"

View File

@@ -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
}
}

View File

@@ -123,14 +123,20 @@ impl Url {
#[cfg(not(feature = "ssr"))]
{
js_sys::decode_uri_component(s).unwrap().into()
match js_sys::decode_uri_component(s) {
Ok(v) => v.into(),
Err(_) => s.into(),
}
}
}
pub fn unescape_minimal(s: &str) -> String {
#[cfg(not(feature = "ssr"))]
{
js_sys::decode_uri(s).unwrap().into()
match js_sys::decode_uri(s) {
Ok(v) => v.into(),
Err(_) => s.into(),
}
}
#[cfg(feature = "ssr")]

View File

@@ -1,5 +1,4 @@
use super::{PartialPathMatch, PathSegment};
use std::sync::Arc;
mod param_segments;
mod static_segment;
mod tuples;
@@ -12,37 +11,9 @@ pub use static_segment::*;
/// This is a "horizontal" matching: i.e., it treats a tuple of route segments
/// as subsequent segments of the URL and tries to match them all.
pub trait PossibleRouteMatch {
fn optional(&self) -> bool;
const OPTIONAL: bool = false;
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>>;
fn generate_path(&self, path: &mut Vec<PathSegment>);
}
impl PossibleRouteMatch for Box<dyn PossibleRouteMatch + Send + Sync> {
fn optional(&self) -> bool {
(**self).optional()
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
(**self).test(path)
}
fn generate_path(&self, path: &mut Vec<PathSegment>) {
(**self).generate_path(path);
}
}
impl PossibleRouteMatch for Arc<dyn PossibleRouteMatch + Send + Sync> {
fn optional(&self) -> bool {
(**self).optional()
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
(**self).test(path)
}
fn generate_path(&self, path: &mut Vec<PathSegment>) {
(**self).generate_path(path);
}
}

View File

@@ -35,10 +35,6 @@ use std::borrow::Cow;
pub struct ParamSegment(pub &'static str);
impl PossibleRouteMatch for ParamSegment {
fn optional(&self) -> bool {
false
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let mut matched_len = 0;
let mut param_offset = 0;
@@ -125,10 +121,6 @@ impl PossibleRouteMatch for ParamSegment {
pub struct WildcardSegment(pub &'static str);
impl PossibleRouteMatch for WildcardSegment {
fn optional(&self) -> bool {
false
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let mut matched_len = 0;
let mut param_offset = 0;
@@ -166,9 +158,7 @@ impl PossibleRouteMatch for WildcardSegment {
pub struct OptionalParamSegment(pub &'static str);
impl PossibleRouteMatch for OptionalParamSegment {
fn optional(&self) -> bool {
true
}
const OPTIONAL: bool = true;
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let mut matched_len = 0;

View File

@@ -2,10 +2,6 @@ use super::{PartialPathMatch, PathSegment, PossibleRouteMatch};
use std::fmt::Debug;
impl PossibleRouteMatch for () {
fn optional(&self) -> bool {
false
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
Some(PartialPathMatch::new(path, vec![], ""))
}
@@ -58,10 +54,6 @@ impl AsPath for &'static str {
pub struct StaticSegment<T: AsPath>(pub T);
impl<T: AsPath> PossibleRouteMatch for StaticSegment<T> {
fn optional(&self) -> bool {
false
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let mut matched_len = 0;
let mut test = path.chars().peekable();

View File

@@ -8,21 +8,15 @@ macro_rules! tuples {
$first: PossibleRouteMatch,
$($ty: PossibleRouteMatch),*,
{
fn optional(&self) -> bool {
#[allow(non_snake_case)]
let ($first, $($ty,)*) = &self;
[$first.optional(), $($ty.optional()),*].into_iter().any(|n| n)
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
#[allow(non_snake_case)]
let ($first, $($ty,)*) = &self;
// on the first run, include all optionals
let mut include_optionals = {
[$first.optional(), $($ty.optional()),*].into_iter().filter(|n| *n).count()
[$first::OPTIONAL, $($ty::OPTIONAL),*].into_iter().filter(|n| *n).count()
};
#[allow(non_snake_case)]
let ($first, $($ty,)*) = &self;
loop {
let mut nth_field = 0;
let mut matched_len = 0;
@@ -31,7 +25,7 @@ macro_rules! tuples {
let mut p = Vec::new();
let mut m = String::new();
if !$first.optional() || nth_field < include_optionals {
if !$first::OPTIONAL || nth_field < include_optionals {
match $first.test(r) {
None => {
return None;
@@ -46,16 +40,16 @@ macro_rules! tuples {
matched_len += m.len();
$(
if $ty.optional() {
if $ty::OPTIONAL {
nth_field += 1;
}
if !$ty.optional() || nth_field < include_optionals {
if !$ty::OPTIONAL || nth_field < include_optionals {
let PartialPathMatch {
remaining,
matched,
params
} = match $ty.test(r) {
None => if $ty.optional() {
None => if $ty::OPTIONAL {
return None;
} else {
if include_optionals == 0 {
@@ -96,10 +90,6 @@ where
Self: core::fmt::Debug,
A: PossibleRouteMatch,
{
fn optional(&self) -> bool {
self.0.optional()
}
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
let remaining = path;
let PartialPathMatch {

View File

@@ -151,7 +151,7 @@ impl<Segments, Children, Data, View> MatchNestedRoutes
for NestedRoute<Segments, Children, Data, View>
where
Self: 'static,
Segments: PossibleRouteMatch,
Segments: PossibleRouteMatch + std::fmt::Debug,
Children: MatchNestedRoutes,
Children::Match: MatchParams,
Children: 'static,

View File

@@ -10,7 +10,7 @@ use crate::{
use any_spawner::Executor;
use either_of::{Either, EitherOf3};
use futures::{channel::oneshot, future::join_all, FutureExt};
use leptos::{attr::any_attribute::AnyAttribute, component, oco::Oco};
use leptos::{component, oco::Oco};
use or_poisoned::OrPoisoned;
use reactive_graph::{
computed::{ArcMemo, ScopedFuture},
@@ -36,7 +36,7 @@ use tachys::{
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr,
any_view::{AnyView, ExtraAttrsMut, IntoAny},
any_view::{AnyView, IntoAny},
either::EitherOf3State,
Mountable, Position, PositionState, Render, RenderHtml,
},
@@ -76,7 +76,7 @@ where
// TODO support fallback while loading
type State = NestedRouteViewState<Fal>;
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
fn build(self) -> Self::State {
let NestedRoutesView {
routes,
outer_owner,
@@ -95,7 +95,7 @@ where
let new_match = routes.match_route(url.path());
// start with an empty view because we'll be loading routes async
let view = EitherOf3::A(()).build(extra_attrs.clone());
let view = EitherOf3::A(()).build();
let view = Rc::new(RefCell::new(view));
let matched_view = match new_match {
None => EitherOf3::B(fallback()),
@@ -120,7 +120,7 @@ where
for trigger in triggers {
trigger.notify();
}
matched_view.rebuild(&mut *view.borrow_mut(), extra_attrs);
matched_view.rebuild(&mut *view.borrow_mut());
})
});
@@ -132,11 +132,7 @@ where
}
}
fn rebuild(
self,
state: &mut Self::State,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
fn rebuild(self, state: &mut Self::State) {
let url_snapshot = self.current_url.get_untracked();
// if the path is the same, we do not need to re-route
@@ -158,7 +154,7 @@ where
match new_match {
None => {
EitherOf3::<(), Fal, AnyView>::B((self.fallback)())
.rebuild(&mut state.view.borrow_mut(), extra_attrs);
.rebuild(&mut state.view.borrow_mut());
state.outlets.clear();
if let Some(loc) = self.location {
loc.ready_to_complete();
@@ -217,10 +213,7 @@ where
if matches!(state.view.borrow().state, EitherOf3::B(_)) {
self.outer_owner.with(|| {
EitherOf3::<(), Fal, AnyView>::C(Outlet().into_any())
.rebuild(
&mut *state.view.borrow_mut(),
extra_attrs,
);
.rebuild(&mut *state.view.borrow_mut());
})
}
}
@@ -235,8 +228,8 @@ where
impl<Loc, Defs, Fal, FalFn> AddAnyAttr for NestedRoutesView<Loc, Defs, FalFn>
where
Loc: LocationProvider + Send,
Defs: MatchNestedRoutes + Send + 'static,
FalFn: FnOnce() -> Fal + Send + 'static,
Defs: MatchNestedRoutes + Send,
FalFn: FnOnce() -> Fal + Send,
Fal: RenderHtml + 'static,
{
type Output<SomeNewAttr: leptos::attr::Attribute> =
@@ -256,21 +249,17 @@ where
impl<Loc, Defs, FalFn, Fal> RenderHtml for NestedRoutesView<Loc, Defs, FalFn>
where
Loc: LocationProvider + Send,
Defs: MatchNestedRoutes + Send + 'static,
FalFn: FnOnce() -> Fal + Send + 'static,
Defs: MatchNestedRoutes + Send,
FalFn: FnOnce() -> Fal + Send,
Fal: RenderHtml + 'static,
{
type AsyncOutput = Self;
type Owned = Self;
const MIN_LENGTH: usize = 0; // TODO
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
}
@@ -280,7 +269,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() {
@@ -360,13 +348,7 @@ where
outer_owner.with(|| Either::Right(Outlet().into_any()))
}
};
view.to_html_with_buf(
buf,
position,
escape,
mark_branches,
extra_attrs,
);
view.to_html_with_buf(buf, position, escape, mark_branches);
}
}
@@ -376,7 +358,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -419,7 +400,6 @@ where
position,
escape,
mark_branches,
extra_attrs,
);
}
@@ -427,7 +407,6 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let NestedRoutesView {
routes,
@@ -467,7 +446,7 @@ where
outer_owner.with(|| EitherOf3::C(Outlet().into_any()))
}
}
.hydrate::<FROM_SERVER>(cursor, position, extra_attrs),
.hydrate::<FROM_SERVER>(cursor, position),
));
NestedRouteViewState {
@@ -477,10 +456,6 @@ where
view,
}
}
fn into_owned(self) -> Self::Owned {
self
}
}
type OutletViewFn = Box<dyn Fn() -> Suspend<AnyView> + Send>;

View File

@@ -89,7 +89,7 @@ where
type State =
ReactiveRouterInnerState<Rndr, Loc, Defs, FallbackFn, Fallback>;
fn build(self, _extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
fn build(self) -> Self::State {
let (prev_id, inner) = self.inner.fallback_or_view();
let owner = self.owner.with(Owner::new);
ReactiveRouterInnerState {
@@ -100,11 +100,7 @@ where
}
}
fn rebuild(
self,
state: &mut Self::State,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
fn rebuild(self, state: &mut Self::State) {
let (new_id, view) = self.inner.fallback_or_view();
if new_id != state.prev_id {
state.owner = self.owner.with(Owner::new)
@@ -134,7 +130,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() {
@@ -161,7 +156,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
_extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -175,7 +169,6 @@ where
self,
cursor: &Cursor,
position: &PositionState,
_extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let (prev_id, inner) = self.inner.fallback_or_view();
let owner = self.owner.with(Owner::new);
@@ -287,7 +280,7 @@ where
{
type State = ReactiveRouteState<View::State>;
fn build(self, extra_attrs: Option<Vec<AnyAttribute>>) -> Self::State {
fn build(self) -> Self::State {
let MatchedRoute {
search_params,
params,
@@ -298,19 +291,14 @@ where
params: ArcRwSignal::new(params),
matched: ArcRwSignal::new(matched),
};
let view_state =
untrack(|| (self.view_fn)(&matched).build(extra_attrs.clone()));
let view_state = untrack(|| (self.view_fn)(&matched).build());
ReactiveRouteState {
matched,
view_state,
}
}
fn rebuild(
mut self,
state: &mut Self::State,
_extra_attrs: Option<Vec<AnyAttribute>>,
) {
fn rebuild(mut self, state: &mut Self::State) {
let ReactiveRouteState { matched, .. } = state;
matched
.search_params
@@ -336,7 +324,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) {
let MatchedRoute {
search_params,
@@ -349,12 +336,7 @@ where
matched: ArcRwSignal::new(matched),
};
untrack(|| {
(self.view_fn)(&matched).to_html_with_buf(
buf,
position,
escape,
extra_attrs.clone(),
)
(self.view_fn)(&matched).to_html_with_buf(buf, position, escape)
});
}
@@ -364,7 +346,6 @@ where
position: &mut Position,
escape: bool,
mark_branches: bool,
extra_attrs: Option<Vec<AnyAttribute>>,
) where
Self: Sized,
{
@@ -379,12 +360,8 @@ where
matched: ArcRwSignal::new(matched),
};
untrack(|| {
(self.view_fn)(&matched).to_html_async_with_buf::<OUT_OF_ORDER>(
buf,
position,
escape,
extra_attrs.clone(),
)
(self.view_fn)(&matched)
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape)
});
}
@@ -392,7 +369,6 @@ where
self,
cursor: &Cursor,
position: &PositionState,
extra_attrs: Option<Vec<AnyAttribute>>,
) -> Self::State {
let MatchedRoute {
search_params,
@@ -405,11 +381,7 @@ where
matched: ArcRwSignal::new(matched),
};
let view_state = untrack(|| {
(self.view_fn)(&matched).hydrate::<FROM_SERVER>(
cursor,
position,
extra_attrs.clone(),
)
(self.view_fn)(&matched).hydrate::<FROM_SERVER>(cursor, position)
});
ReactiveRouteState {
matched,

View File

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

View File

@@ -14,7 +14,6 @@ throw_error = { workspace = true }
server_fn_macro_default = { workspace = true }
# used for hashing paths in #[server] macro
const_format = "0.2.33"
const-str = "0.5.7"
xxhash-rust = { version = "0.8.12", features = ["const_xxh64"] }
# used across multiple features
serde = { version = "1.0", features = ["derive"] }
@@ -31,7 +30,7 @@ once_cell = "1.20"
actix-web = { version = "4.9", optional = true }
# axum
axum = { version = "0.8.1", optional = true, default-features = false, features = [
axum = { version = "0.7.9", optional = true, default-features = false, features = [
"multipart",
] }
tower = { version = "0.5.1", optional = true }
@@ -43,7 +42,7 @@ multer = { version = "3.1", optional = true }
## output encodings
# serde
serde_json = "1.0"
serde_json = { workspace = true }
serde-lite = { version = "0.5.0", features = ["derive"], optional = true }
futures = "0.3.31"
http = { version = "1.1" }
@@ -54,13 +53,12 @@ bytes = "1.9"
http-body-util = { version = "0.1.2", optional = true }
rkyv = { version = "0.8.9", optional = true }
rmp-serde = { version = "1.3.0", optional = true }
base64 = { version = "0.22.1" }
# client
gloo-net = { version = "0.6.0", optional = true }
js-sys = { version = "0.3.74", optional = true }
wasm-bindgen = { version = "0.2.97", optional = true }
wasm-bindgen-futures = { version = "0.4.47", optional = true }
wasm-bindgen = { version = "0.2.100", optional = true }
wasm-bindgen-futures = { version = "0.4.50", optional = true }
wasm-streams = { version = "0.4.2", optional = true }
web-sys = { version = "0.3.72", optional = true, features = [
"console",
@@ -233,4 +231,4 @@ skip_feature_sets = [
]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -74,7 +74,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
args.into(),
s.into(),
Some(syn::parse_quote!(server_fns)),
option_env!("SERVER_FN_PREFIX").unwrap_or("/api"),
"/api",
None,
None,
) {

View File

@@ -1,4 +1,4 @@
use crate::{request::ClientReq, response::ClientRes};
use crate::{error::ServerFnError, request::ClientReq, response::ClientRes};
use std::{future::Future, sync::OnceLock};
static ROOT_URL: OnceLock<&'static str> = OnceLock::new();
@@ -21,16 +21,16 @@ pub fn get_server_url() -> &'static str {
/// This trait is implemented for things like a browser `fetch` request or for
/// the `reqwest` trait. It should almost never be necessary to implement it
/// yourself, unless youre trying to use an alternative HTTP crate on the client side.
pub trait Client<E> {
pub trait Client<CustErr> {
/// The type of a request sent by this client.
type Request: ClientReq<E> + Send;
type Request: ClientReq<CustErr> + Send;
/// The type of a response received by this client.
type Response: ClientRes<E> + Send;
type Response: ClientRes<CustErr> + Send;
/// Sends the request and receives a response.
fn send(
req: Self::Request,
) -> impl Future<Output = Result<Self::Response, E>> + Send;
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>> + Send;
}
#[cfg(feature = "browser")]
@@ -38,23 +38,24 @@ pub trait Client<E> {
pub mod browser {
use super::Client;
use crate::{
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
error::ServerFnError,
request::browser::{BrowserRequest, RequestInner},
response::browser::BrowserResponse,
};
use send_wrapper::SendWrapper;
use std::future::Future;
/// Implements [`Client`] for a `fetch` request in the browser.
/// Implements [`Client`] for a `fetch` request in the browser.
pub struct BrowserClient;
impl<E: FromServerFnError> Client<E> for BrowserClient {
impl<CustErr> Client<CustErr> for BrowserClient {
type Request = BrowserRequest;
type Response = BrowserResponse;
fn send(
req: Self::Request,
) -> impl Future<Output = Result<Self::Response, E>> + Send {
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
+ Send {
SendWrapper::new(async move {
let req = req.0.take();
let RequestInner {
@@ -65,10 +66,7 @@ pub mod browser {
.send()
.await
.map(|res| BrowserResponse(SendWrapper::new(res)))
.map_err(|e| {
ServerFnErrorErr::Request(e.to_string())
.into_app_error()
});
.map_err(|e| ServerFnError::Request(e.to_string()));
// at this point, the future has successfully resolved without being dropped, so we
// can prevent the `AbortController` from firing
@@ -85,10 +83,7 @@ pub mod browser {
/// Implements [`Client`] for a request made by [`reqwest`].
pub mod reqwest {
use super::Client;
use crate::{
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
request::reqwest::CLIENT,
};
use crate::{error::ServerFnError, request::reqwest::CLIENT};
use futures::TryFutureExt;
use reqwest::{Request, Response};
use std::future::Future;
@@ -96,16 +91,17 @@ pub mod reqwest {
/// Implements [`Client`] for a request made by [`reqwest`].
pub struct ReqwestClient;
impl<E: FromServerFnError> Client<E> for ReqwestClient {
impl<CustErr> Client<CustErr> for ReqwestClient {
type Request = Request;
type Response = Response;
fn send(
req: Self::Request,
) -> impl Future<Output = Result<Self::Response, E>> + Send {
CLIENT.execute(req).map_err(|e| {
ServerFnErrorErr::Request(e.to_string()).into_app_error()
})
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
+ Send {
CLIENT
.execute(req)
.map_err(|e| ServerFnError::Request(e.to_string()))
}
}
}

View File

@@ -1,8 +1,8 @@
use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
use crate::{
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
error::ServerFnError,
request::{ClientReq, Req},
response::{ClientRes, TryRes},
response::{ClientRes, Res},
};
use bytes::Bytes;
use http::Method;
@@ -16,17 +16,19 @@ impl Encoding for Cbor {
const METHOD: Method = Method::POST;
}
impl<E, T, Request> IntoReq<Cbor, Request, E> for T
impl<CustErr, T, Request> IntoReq<Cbor, Request, CustErr> for T
where
Request: ClientReq<E>,
Request: ClientReq<CustErr>,
T: Serialize + Send,
E: FromServerFnError,
{
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
let mut buffer: Vec<u8> = Vec::new();
ciborium::ser::into_writer(&self, &mut buffer).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
ciborium::ser::into_writer(&self, &mut buffer)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Request::try_new_post_bytes(
path,
accepts,
@@ -36,44 +38,40 @@ where
}
}
impl<E, T, Request> FromReq<Cbor, Request, E> for T
impl<CustErr, T, Request> FromReq<Cbor, Request, CustErr> for T
where
Request: Req<E> + Send + 'static,
Request: Req<CustErr> + Send + 'static,
T: DeserializeOwned,
E: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, E> {
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
let body_bytes = req.try_into_bytes().await?;
ciborium::de::from_reader(body_bytes.as_ref())
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
.map_err(|e| ServerFnError::Args(e.to_string()))
}
}
impl<E, T, Response> IntoRes<Cbor, Response, E> for T
impl<CustErr, T, Response> IntoRes<Cbor, Response, CustErr> for T
where
Response: TryRes<E>,
Response: Res<CustErr>,
T: Serialize + Send,
E: FromServerFnError,
{
async fn into_res(self) -> Result<Response, E> {
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
let mut buffer: Vec<u8> = Vec::new();
ciborium::ser::into_writer(&self, &mut buffer).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
ciborium::ser::into_writer(&self, &mut buffer)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Response::try_from_bytes(Cbor::CONTENT_TYPE, Bytes::from(buffer))
}
}
impl<E, T, Response> FromRes<Cbor, Response, E> for T
impl<CustErr, T, Response> FromRes<Cbor, Response, CustErr> for T
where
Response: ClientRes<E> + Send,
Response: ClientRes<CustErr> + Send,
T: DeserializeOwned + Send,
E: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, E> {
async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> {
let data = res.try_into_bytes().await?;
ciborium::de::from_reader(data.as_ref())
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
.map_err(|e| ServerFnError::Args(e.to_string()))
}
}
@@ -116,20 +114,20 @@ where
<ResponseBody as HttpBody>::Data: Send ,
<RequestBody as HttpBody>::Data: Send ,
{
async fn from_req(req: http::Request<RequestBody>) -> Result<Self, ServerFnError<E>> {
async fn from_req(req: http::Request<RequestBody>) -> Result<Self, ServerFnError<CustErr>> {
let (_parts, body) = req.into_parts();
let body_bytes = body
.collect()
.await
.map(|c| c.to_bytes())
.map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into())?;
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
let data = ciborium::de::from_reader(body_bytes.as_ref())
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into())?;
.map_err(|e| ServerFnError::Args(e.to_string()))?;
Ok(data)
}
async fn into_req(self) -> Result<http::Request<Body>, ServerFnError<E>> {
async fn into_req(self) -> Result<http::Request<Body>, ServerFnError<CustErr>> {
let mut buffer: Vec<u8> = Vec::new();
ciborium::ser::into_writer(&self, &mut buffer)?;
let req = http::Request::builder()
@@ -141,17 +139,17 @@ where
.body(Body::from(buffer))?;
Ok(req)
}
async fn from_res(res: http::Response<ResponseBody>) -> Result<Self, ServerFnError<E>> {
async fn from_res(res: http::Response<ResponseBody>) -> Result<Self, ServerFnError<CustErr>> {
let (_parts, body) = res.into_parts();
let body_bytes = body
.collect()
.await
.map(|c| c.to_bytes())
.map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into())?;
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
ciborium::de::from_reader(body_bytes.as_ref())
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into())
.map_err(|e| ServerFnError::Args(e.to_string()))
}
async fn into_res(self) -> http::Response<Body> {

View File

@@ -1,8 +1,8 @@
use super::{Encoding, FromReq, FromRes, Streaming};
use crate::{
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
error::{NoCustomError, ServerFnError},
request::{ClientReq, Req},
response::{ClientRes, TryRes},
response::{ClientRes, Res},
IntoReq, IntoRes,
};
use bytes::Bytes;
@@ -18,58 +18,55 @@ impl Encoding for Json {
const METHOD: Method = Method::POST;
}
impl<E, T, Request> IntoReq<Json, Request, E> for T
impl<CustErr, T, Request> IntoReq<Json, Request, CustErr> for T
where
Request: ClientReq<E>,
Request: ClientReq<CustErr>,
T: Serialize + Send,
E: FromServerFnError,
{
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
let data = serde_json::to_string(&self).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
let data = serde_json::to_string(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Request::try_new_post(path, accepts, Json::CONTENT_TYPE, data)
}
}
impl<E, T, Request> FromReq<Json, Request, E> for T
impl<CustErr, T, Request> FromReq<Json, Request, CustErr> for T
where
Request: Req<E> + Send + 'static,
Request: Req<CustErr> + Send + 'static,
T: DeserializeOwned,
E: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, E> {
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
let string_data = req.try_into_string().await?;
serde_json::from_str::<Self>(&string_data)
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
.map_err(|e| ServerFnError::Args(e.to_string()))
}
}
impl<E, T, Response> IntoRes<Json, Response, E> for T
impl<CustErr, T, Response> IntoRes<Json, Response, CustErr> for T
where
Response: TryRes<E>,
Response: Res<CustErr>,
T: Serialize + Send,
E: FromServerFnError,
{
async fn into_res(self) -> Result<Response, E> {
let data = serde_json::to_string(&self).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
})?;
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
let data = serde_json::to_string(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Response::try_from_string(Json::CONTENT_TYPE, data)
}
}
impl<E, T, Response> FromRes<Json, Response, E> for T
impl<CustErr, T, Response> FromRes<Json, Response, CustErr> for T
where
Response: ClientRes<E> + Send,
Response: ClientRes<CustErr> + Send,
T: DeserializeOwned + Send,
E: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, E> {
async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> {
let data = res.try_into_string().await?;
serde_json::from_str(&data).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
})
serde_json::from_str(&data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
}
@@ -105,31 +102,35 @@ impl Encoding for StreamingJson {
/// end before the output will begin.
///
/// Streaming requests are only allowed over HTTP2 or HTTP3.
pub struct JsonStream<T, E>(Pin<Box<dyn Stream<Item = Result<T, E>> + Send>>);
pub struct JsonStream<T, CustErr = NoCustomError>(
Pin<Box<dyn Stream<Item = Result<T, ServerFnError<CustErr>>> + Send>>,
);
impl<T, E> std::fmt::Debug for JsonStream<T, E> {
impl<T, CustErr> std::fmt::Debug for JsonStream<T, CustErr> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("JsonStream").finish()
}
}
impl<T, E> JsonStream<T, E> {
impl<T> JsonStream<T> {
/// Creates a new `ByteStream` from the given stream.
pub fn new(
value: impl Stream<Item = Result<T, E>> + Send + 'static,
value: impl Stream<Item = Result<T, ServerFnError>> + Send + 'static,
) -> Self {
Self(Box::pin(value.map(|value| value)))
}
}
impl<T, E> JsonStream<T, E> {
impl<T, CustErr> JsonStream<T, CustErr> {
/// Consumes the wrapper, returning a stream of text.
pub fn into_inner(self) -> impl Stream<Item = Result<T, E>> + Send {
pub fn into_inner(
self,
) -> impl Stream<Item = Result<T, ServerFnError<CustErr>>> + Send {
self.0
}
}
impl<S, T: 'static, E: 'static> From<S> for JsonStream<T, E>
impl<S, T: 'static, CustErr: 'static> From<S> for JsonStream<T, CustErr>
where
S: Stream<Item = T> + Send + 'static,
{
@@ -138,15 +139,18 @@ where
}
}
impl<E, S, T, Request> IntoReq<StreamingJson, Request, E> for S
impl<CustErr, S, T, Request> IntoReq<StreamingJson, Request, CustErr> for S
where
Request: ClientReq<E>,
Request: ClientReq<CustErr>,
S: Stream<Item = T> + Send + 'static,
T: Serialize + 'static,
E: FromServerFnError + Serialize,
{
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
let data: JsonStream<T, E> = self.into();
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
let data: JsonStream<T> = self.into();
Request::try_new_streaming(
path,
accepts,
@@ -160,61 +164,56 @@ where
}
}
impl<E, T, S, Request> FromReq<StreamingJson, Request, E> for S
impl<CustErr, T, S, Request> FromReq<StreamingJson, Request, CustErr> for S
where
Request: Req<E> + Send + 'static,
Request: Req<CustErr> + Send + 'static,
// The additional `Stream<Item = T>` bound is never used, but it is required to avoid an error where `T` is unconstrained
S: Stream<Item = T> + From<JsonStream<T, E>> + Send + 'static,
S: Stream<Item = T> + From<JsonStream<T>> + Send + 'static,
T: DeserializeOwned + 'static,
E: FromServerFnError,
{
async fn from_req(req: Request) -> Result<Self, E> {
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
let data = req.try_into_stream()?;
let s = JsonStream::new(data.map(|chunk| {
chunk.and_then(|bytes| {
serde_json::from_slice(bytes.as_ref()).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string())
.into_app_error()
})
serde_json::from_slice(bytes.as_ref())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
})
}));
Ok(s.into())
}
}
impl<E, T, Response> IntoRes<StreamingJson, Response, E> for JsonStream<T, E>
impl<CustErr, T, Response> IntoRes<StreamingJson, Response, CustErr>
for JsonStream<T, CustErr>
where
Response: TryRes<E>,
Response: Res<CustErr>,
CustErr: 'static,
T: Serialize + 'static,
E: FromServerFnError,
{
async fn into_res(self) -> Result<Response, E> {
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
Response::try_from_stream(
Streaming::CONTENT_TYPE,
self.into_inner().map(|value| {
serde_json::to_vec(&value?).map(Bytes::from).map_err(|e| {
ServerFnErrorErr::Serialization(e.to_string())
.into_app_error()
})
serde_json::to_vec(&value?)
.map(Bytes::from)
.map_err(|e| ServerFnError::Serialization(e.to_string()))
}),
)
}
}
impl<E, T, Response> FromRes<StreamingJson, Response, E> for JsonStream<T, E>
impl<CustErr, T, Response> FromRes<StreamingJson, Response, CustErr>
for JsonStream<T>
where
Response: ClientRes<E> + Send,
Response: ClientRes<CustErr> + Send,
T: DeserializeOwned,
E: FromServerFnError,
{
async fn from_res(res: Response) -> Result<Self, E> {
async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> {
let stream = res.try_into_stream()?;
Ok(JsonStream::new(stream.map(|chunk| {
chunk.and_then(|bytes| {
serde_json::from_slice(bytes.as_ref()).map_err(|e| {
ServerFnErrorErr::Deserialization(e.to_string())
.into_app_error()
})
serde_json::from_slice(bytes.as_ref())
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
})
})))
}

View File

@@ -55,6 +55,7 @@ mod postcard;
pub use postcard::*;
mod stream;
use crate::error::ServerFnError;
use futures::Future;
use http::Method;
pub use stream::*;
@@ -70,27 +71,31 @@ pub use stream::*;
/// For example, heres the implementation for [`Json`].
///
/// ```rust,ignore
/// impl<E, T, Request> IntoReq<Json, Request, E> for T
/// impl<CustErr, T, Request> IntoReq<Json, Request, CustErr> for T
/// where
/// Request: ClientReq<E>,
/// Request: ClientReq<CustErr>,
/// T: Serialize + Send,
/// {
/// fn into_req(
/// self,
/// path: &str,
/// accepts: &str,
/// ) -> Result<Request, E> {
/// ) -> Result<Request, ServerFnError<CustErr>> {
/// // try to serialize the data
/// let data = serde_json::to_string(&self)
/// .map_err(|e| ServerFnErrorErr::Serialization(e.to_string()).into_app_error())?;
/// .map_err(|e| ServerFnError::Serialization(e.to_string()))?;
/// // and use it as the body of a POST request
/// Request::try_new_post(path, accepts, Json::CONTENT_TYPE, data)
/// }
/// }
/// ```
pub trait IntoReq<Encoding, Request, E> {
pub trait IntoReq<Encoding, Request, CustErr> {
/// Attempts to serialize the arguments into an HTTP request.
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E>;
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>>;
}
/// Deserializes an HTTP request into the data type, on the server.
@@ -104,31 +109,32 @@ pub trait IntoReq<Encoding, Request, E> {
/// For example, heres the implementation for [`Json`].
///
/// ```rust,ignore
/// impl<E, T, Request> FromReq<Json, Request, E> for T
/// impl<CustErr, T, Request> FromReq<Json, Request, CustErr> for T
/// where
/// // require the Request implement `Req`
/// Request: Req<E> + Send + 'static,
/// Request: Req<CustErr> + Send + 'static,
/// // require that the type can be deserialized with `serde`
/// T: DeserializeOwned,
/// E: FromServerFnError,
/// {
/// async fn from_req(
/// req: Request,
/// ) -> Result<Self, E> {
/// ) -> Result<Self, ServerFnError<CustErr>> {
/// // try to convert the body of the request into a `String`
/// let string_data = req.try_into_string().await?;
/// // deserialize the data
/// serde_json::from_str(&string_data)
/// .map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
/// serde_json::from_str::<Self>(&string_data)
/// .map_err(|e| ServerFnError::Args(e.to_string()))
/// }
/// }
/// ```
pub trait FromReq<Encoding, Request, E>
pub trait FromReq<Encoding, Request, CustErr>
where
Self: Sized,
{
/// Attempts to deserialize the arguments from a request.
fn from_req(req: Request) -> impl Future<Output = Result<Self, E>> + Send;
fn from_req(
req: Request,
) -> impl Future<Output = Result<Self, ServerFnError<CustErr>>> + Send;
}
/// Serializes the data type into an HTTP response.
@@ -142,24 +148,25 @@ where
/// For example, heres the implementation for [`Json`].
///
/// ```rust,ignore
/// impl<E, T, Response> IntoRes<Json, Response, E> for T
/// impl<CustErr, T, Response> IntoRes<Json, Response, CustErr> for T
/// where
/// Response: Res<E>,
/// Response: Res<CustErr>,
/// T: Serialize + Send,
/// E: FromServerFnError,
/// {
/// async fn into_res(self) -> Result<Response, E> {
/// async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
/// // try to serialize the data
/// let data = serde_json::to_string(&self)
/// .map_err(|e| ServerFnErrorErr::Serialization(e.to_string()).into())?;
/// .map_err(|e| ServerFnError::Serialization(e.to_string()))?;
/// // and use it as the body of a response
/// Response::try_from_string(Json::CONTENT_TYPE, data)
/// }
/// }
/// ```
pub trait IntoRes<Encoding, Response, E> {
pub trait IntoRes<Encoding, Response, CustErr> {
/// Attempts to serialize the output into an HTTP response.
fn into_res(self) -> impl Future<Output = Result<Response, E>> + Send;
fn into_res(
self,
) -> impl Future<Output = Result<Response, ServerFnError<CustErr>>> + Send;
}
/// Deserializes the data type from an HTTP response.
@@ -174,29 +181,30 @@ pub trait IntoRes<Encoding, Response, E> {
/// For example, heres the implementation for [`Json`].
///
/// ```rust,ignore
/// impl<E, T, Response> FromRes<Json, Response, E> for T
/// impl<CustErr, T, Response> FromRes<Json, Response, CustErr> for T
/// where
/// Response: ClientRes<E> + Send,
/// Response: ClientRes<CustErr> + Send,
/// T: DeserializeOwned + Send,
/// E: FromServerFnError,
/// {
/// async fn from_res(
/// res: Response,
/// ) -> Result<Self, E> {
/// ) -> Result<Self, ServerFnError<CustErr>> {
/// // extracts the request body
/// let data = res.try_into_string().await?;
/// // and tries to deserialize it as JSON
/// serde_json::from_str(&data)
/// .map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into_app_error())
/// .map_err(|e| ServerFnError::Deserialization(e.to_string()))
/// }
/// }
/// ```
pub trait FromRes<Encoding, Response, E>
pub trait FromRes<Encoding, Response, CustErr>
where
Self: Sized,
{
/// Attempts to deserialize the outputs from a response.
fn from_res(res: Response) -> impl Future<Output = Result<Self, E>> + Send;
fn from_res(
res: Response,
) -> impl Future<Output = Result<Self, ServerFnError<CustErr>>> + Send;
}
/// Defines a particular encoding format, which can be used for serializing or deserializing data.

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