Compare commits

...

127 Commits

Author SHA1 Message Date
Greg Johnston
ceed5e40e4 feat: allow dereferencing LocalResource to an AsyncDerived (see #4063) 2025-06-13 14:28:45 -04:00
Greg Johnston
f4e0be2d59 fix: do not track anything inside the async block of a Resource (closes #4060) (#4061) 2025-06-10 21:11:29 -04:00
Josiah Parry
05f50f7d27 chore: fix checkboxes in issue template (#4031) 2025-06-08 20:55:16 -04:00
Greg Johnston
a22d6f58be fix: allow nested untracked reads without false positives (closes #4032) (#4049) 2025-06-08 20:54:37 -04:00
Greg Johnston
ff21c9cae2 fix: suppress false-positive warning when adding children to a <For/> that is not currently mounted (closes #3385) (#4050)
* fix: suppress false-positive warning when adding children to a `<For/>` that is not currently mounted (closes #3385)

* remove track_caller
2025-06-08 20:54:25 -04:00
Greg Johnston
733a353820 fix: allow multiple #[middleware] macros (closes #4029) (#4048) 2025-06-06 20:50:52 -04:00
Greg Johnston
829b07b598 Merge pull request #4033 from leptos-rs/update_session_auth
Update `session_auth_axum` example
2025-06-02 19:44:21 -04:00
Greg Johnston
0df6cd74ee feat: simplify session_auth_axumby removing custom handlers 2025-05-30 18:32:22 -04:00
Greg Johnston
1da833a0aa fix: update session_auth_axum to Axum 0.8 2025-05-30 17:30:09 -04:00
Saikat Das
f37d124d6a Fix typo (#4025) 2025-05-29 12:39:32 -07:00
benwis
5d0e683b0f Update tachys to v0.2.3 2025-05-29 12:37:21 -07:00
lcnr
f34e3a5bc9 remove unnecessary where-clauses (#4023)
they may cause tachys to break with -Znext-solver
2025-05-29 12:35:50 -07:00
martin frances
d7dd6a1109 chore: bump rkyv to 0.8.10. (#4018) 2025-05-27 21:15:59 -04:00
Greg Johnston
ff81d34084 fix: fix <select> value by ensuring HTML children are mounted before building attributes (closes #4005) (#4008) 2025-05-27 21:15:41 -04:00
Soso
40a7aba3bc feat: impl AttributeValue for Cow<'_, str> (#4013) 2025-05-27 21:15:12 -04:00
Greg Johnston
d4dcafd908 Merge pull request #4015 from leptos-rs/3042v2
Fix context issues with nesting routing
2025-05-27 21:12:58 -04:00
Greg Johnston
82ccbbf806 copy-paste errors 2025-05-25 15:32:33 -04:00
Greg Johnston
5ba45bb1ed fix: allow Outlet to access context provided in parent view (closes #3042) 2025-05-24 17:51:22 -04:00
Greg Johnston
06dfa37eee feat: allow joining two context trees 2025-05-24 17:50:51 -04:00
Nicolas Cura
e82a0bbc7f chore: handle_response_inner public to be used in custom file_and_error_handler (closes #3996) (#3998) 2025-05-23 14:51:08 -04:00
Álvaro Mondéjar Rubio
4a972fc09e Merge pull request #4003 from mondeja/document-prop-attrs
docs: document `#[prop(default = ...)]` and `#[prop(name = ...)]`
2025-05-23 14:49:59 -04:00
Greg Johnston
07cf649e3b Merge pull request #3994 from leptos-rs/3983
fix: don't use `Arc::ptr_eq` for string comparison (closes #3983)
2025-05-19 19:15:17 -04:00
Greg Johnston
0e9598b799 fix: don't assume classList is unchanged when rebuilding a class effect for the first time (#3983 part two) 2025-05-19 09:42:33 -04:00
Greg Johnston
82303d7e33 fix: don't use Arc::ptr_eq for string comparison (closes #3983) 2025-05-19 09:27:17 -04:00
Álvaro Mondéjar Rubio
c4354ac965 fix: forward missing lint attributes passed to #[component] macro (#3989) 2025-05-18 20:38:16 -04:00
Dennis Waldherr
7de550685a docs: provide error message if file hashing is enabled but no hash file is present (#3990)
Co-authored-by: Dennis Waldherr <bytekeeper@mailbox.org>
2025-05-18 20:30:53 -04:00
mskorkowski
b1f3f6023e fix: smooshed static segments no longer matches the path #3968 (#3973)
* fix: smooshed static segments no longer matches the path #3968

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-05-18 20:29:40 -04:00
Greg Johnston
c189c3a45d fix: meta tags not properly rendered inside synchronously-available Suspend (closes #3976) (#3991) 2025-05-18 20:29:14 -04:00
Eric Roman
3903867f82 Fix spelling typos. (#3965) 2025-05-17 08:49:57 +02:00
Greg Johnston
a42fa452fc feat: add missing Resource::write() and similar functions (see #3959) (#3984) 2025-05-16 09:23:28 -04:00
Saber Haj Rabiee
cd48a6ac8c fix: remove non-existent feature dep in leptos_macro (#3985) 2025-05-16 09:23:04 -04:00
Greg Johnston
34c14adcb8 fix: render identical branch structure for out-of-order and async streaming of Suspense (closes #3970) (#3977) 2025-05-15 19:44:46 -04:00
Saber Haj Rabiee
50cee1d614 chore: upgrade rand and getrandom (#3840)
* chore: update rand and getrandom

* fix: use rng instead of thread_rng

* fix: enable getrandom wasm js backend in build.rs
2025-05-15 11:17:32 +02:00
Saber Haj Rabiee
7ca691305f chore: unify all deps with min occurrences of 2 (#3854) 2025-05-14 20:34:33 -04:00
Greg Johnston
830882f330 fix: allow rebuilding Vec<_> before it is mounted (closes #3962) (#3966) 2025-05-12 15:26:05 -04:00
Scott Little
13110a35e2 fix: add namespace to g in svg portals (closes #3958) (#3960) 2025-05-09 16:44:40 -04:00
Marcus Whybrow
304dc081a2 fix: correct doc comment for SsrMode::PartiallyBlocked (closes #3963) (#3964) 2025-05-09 09:39:00 -07:00
Serhii Shliakhov
14f6bc658e fix: deprecated parameters js warning (#3956) 2025-05-09 08:04:15 -04:00
Eric Roman
09894aaca9 Remove unnecessary "crate::" prefix in a documentation example. (#3952) 2025-05-08 07:43:27 -07:00
Greg Johnston
2ee4444bb4 v0.8.2 2025-05-06 14:09:35 -04:00
Luxalpa
03a1c1e7a6 fix: ensure unique style caching hashes (#3947) 2025-05-06 14:00:29 -04:00
Greg Johnston
12e49ed996 Merge pull request #3950 from leptos-rs/3945
fix: correct order of meta content relative to surrounding tags (closes #3945)
2025-05-06 13:59:28 -04:00
Greg Johnston
1e281e9e74 fix(examples): bugfix revealed a pre-existing bug with meta tags in the hackernews demo! 2025-05-06 11:49:43 -04:00
Greg Johnston
bd475f89d0 fix: correct order of meta content relative to surrounding tags (closes #3945) 2025-05-06 11:19:19 -04:00
Greg Johnston
3d91b5e90f v0.8.1 2025-05-05 21:39:43 -04:00
Greg Johnston
96d8d5218c Merge pull request #3942 from leptos-rs/3907
Some `islands_router` improvements
2025-05-05 21:33:57 -04:00
Greg Johnston
84caa35cef feat: add .map() and .and_then() on LocalResource (#3941) 2025-05-05 21:20:34 -04:00
Greg Johnston
fc8b55161c fix: remove extra marker node after text node when marking a branch (closes #3936) (#3940) 2025-05-05 21:20:16 -04:00
Greg Johnston
657052466b fix: use a runtime check rather than an unnecessary Either to determine how to render islands (see #3896; closes #3929) (#3938) 2025-05-05 19:41:29 -04:00
william light
efe8336363 reactive_stores: implement PartialEq and Eq for Store (#3915)
StoredValue also has these implemented and does the same thing.
2025-05-05 14:32:42 -04:00
Greg Johnston
770881842c fix: correctly provide context through islands to children (closes #3928) (#3933) 2025-05-05 13:00:40 -04:00
Greg Johnston
0d540ef02f fix: ensure that nested children of a RenderEffect are dropped while dropped a RenderEffect (closes #3922) (#3926) 2025-05-05 13:00:20 -04:00
Saber Haj Rabiee
dc1885ad92 feat: check the counter_isomorphic release build with the leptos_debuginfo flag (#3918) 2025-05-04 15:22:04 -04:00
Eric Roman
61bf87439a Fix some typos in the documentation/examples for reactive store. (#3924) 2025-05-03 20:50:13 -04:00
Greg Johnston
308568e520 fix(CI): prevent regreession from nightly clippy in autofix (#3917)
* fix(CI): prevent regreession from nightly clippy in autofix

* chore: format

* chore: update nightly to 2025-04-16 (proc-macro span, #3852)

* chore: improve the autofix ci workflow

* fix: adjust ServerFn macro test stderr based on nightly-2025-04-16

* fix: limit server_fn server macro trybuild tests nightly only
2025-05-03 20:47:42 -04:00
Greg Johnston
1b0f32dc4c fix: clear and re-throw errors in correct order (#3923) 2025-05-03 20:46:57 -04:00
Greg Johnston
2e0b3011d9 fix: correct issues with StaticVec::rebuild() by aligning implementation with Vec::rebuild() (closes #3906) (#3920) 2025-05-03 08:56:36 -04:00
Greg Johnston
680d4ccd07 fix: do not diff islands in islands_router mode (see #3907) 2025-05-02 21:20:16 -04:00
Greg Johnston
325f9cbe33 fix: don't handle ActionForm (et al) with the islands_router default <form> behavior (see #3907) 2025-05-02 21:12:20 -04:00
Greg Johnston
26ab392c95 fix: allow nested Suspense > ErrorBoundary > Suspense (closes #3908) (#3913) 2025-05-02 16:59:04 -04:00
Saber Haj Rabiee
3a4e2a19aa fix: limit server_fn server macro trybuild tests nightly only 2025-05-02 08:32:38 -07:00
Saber Haj Rabiee
eed3d21b40 fix: adjust ServerFn macro test stderr based on nightly-2025-04-16 2025-05-02 08:01:15 -07:00
Saber Haj Rabiee
7ae386285d chore: improve the autofix ci workflow 2025-05-02 07:27:45 -07:00
Saber Haj Rabiee
ebcc51136d chore: update nightly to 2025-04-16 (proc-macro span, #3852) 2025-05-02 07:13:34 -07:00
Saber Haj Rabiee
e10ded4fd0 chore: format 2025-05-02 06:58:01 -07:00
Saber Haj Rabiee
67be872f58 fix(CI): prevent regreession from nightly clippy in autofix 2025-05-02 06:31:39 -07:00
LeoniePhiline
e5b21ac0fc fix(docs): correct panic message in copied example code (#3911) 2025-05-02 08:14:36 -04:00
benwis
9b2e313d20 v0.8.0 2025-05-01 15:26:21 -07:00
Greg Johnston
bc79232033 feat: impl From<MappedSignal<T>> for Signal<T> (#3897) 2025-05-01 14:56:27 -04:00
Saber Haj Rabiee
a7bb2565c4 fix(examples): websocket tests fail (occasionally) second attemp (#3910)
do not check the label immediately
2025-05-01 14:54:57 -04:00
zakstucke
2e393aaca0 fix: leptos_debuginfo (#3899) 2025-05-01 14:54:24 -04:00
Greg Johnston
e37711cb85 fix: correct hydration for elements after island children (closes #3904) (#3905) 2025-05-01 09:44:40 -04:00
Greg Johnston
627b553e60 fix: prevent sibling context leakage in islands (closes #3902) (#3903) 2025-04-30 21:33:54 -04:00
Greg Johnston
e2f9aca466 Merge pull request #3901 from leptos-rs/3896
Fix some island-routing issues
2025-04-30 14:27:31 -04:00
Greg Johnston
970544ed0b fix: correctly hydrate branches inside islands when using islands-router (closes #3896) 2025-04-30 08:48:40 -04:00
Greg Johnston
0c3b3c440f chore: remove forgotten log 2025-04-30 08:36:23 -04:00
nickburlett
6578086e09 fix(examples): incorrect routes in hackernews example (closes #3892) (#3894)
* fix(examples): incorrect routes in hackernews example (closes #3892)

1. `Avoid calling category()` twice on the story type.
2. `get_static_file()` returns Err on not found, so don’t
   unconditionally `unwrap()` it

* cargo fmt

---------

Co-authored-by: Greg Johnston <greg.johnston@gmail.com>
2025-04-30 07:43:12 -04:00
Greg Johnston
113aba9666 docs: add note about file hashing in Stylesheet docs (#3898) 2025-04-29 19:03:18 -07:00
Greg Johnston
58d7475193 fix: don't render branching comments inside script/style tags 2025-04-29 20:32:56 -04:00
Greg Johnston
04d80ff8d0 chore: clippy 2025-04-29 20:31:44 -04:00
Greg Johnston
7971a2dccb fix: marking AnyView branches with out-of-order streaming (for islands_router) 2025-04-29 20:30:40 -04:00
Greg Johnston
e2ea4277bc fix(examples): broken favicons in hackernews examples (closes #3890) (#3891) 2025-04-28 09:52:15 -04:00
Greg Johnston
171c8e7ff7 v0.8.0-rc3 2025-04-26 15:53:20 -04:00
Greg Johnston
53ffbeeb67 chore: fmt 2025-04-26 15:49:32 -04:00
Greg Johnston
ee86844077 Merge pull request #3882 from leptos-rs/3871
fix: remove event listeners from Suspense fallback during SSR (closes #3871)
2025-04-26 12:48:45 -04:00
Greg Johnston
1cee3f2f52 Merge branch 'main' into 3871 2025-04-26 12:48:40 -04:00
Greg Johnston
23c89dbfe1 Merge pull request #3878 from leptos-rs/3828
fix: correctly calculate starting index for first new key (closes #3828)
2025-04-26 12:48:12 -04:00
Greg Johnston
9f71f39f89 Merge branch 'main' into 3828 2025-04-26 12:47:56 -04:00
Greg Johnston
ef1d0f108a Merge pull request #3880 from sabify/websocket-example
fix(examples): websocket example tests fail on latency
2025-04-26 12:47:17 -04:00
Greg Johnston
a7a78317b7 Merge pull request #3879 from huuff/prelude-textprop
chore: put `TextProp` in the prelude (closes #3877)
2025-04-26 12:46:53 -04:00
autofix-ci[bot]
5005cc3587 [autofix.ci] apply automated fixes (attempt 2/3) 2025-04-26 16:15:06 +00:00
autofix-ci[bot]
08708f3388 [autofix.ci] apply automated fixes 2025-04-26 16:03:45 +00:00
Greg Johnston
c19c1b32f1 fix: remove event listeners from Suspense fallback during SSR (closes #3871) 2025-04-26 11:48:59 -04:00
Saber Haj Rabiee
e70cc08e96 fix(examples): websocket example tests fail on latency 2025-04-26 07:06:50 -07:00
autofix-ci[bot]
97175663ef [autofix.ci] apply automated fixes 2025-04-26 00:05:33 +00:00
Haf
92524a93cd chore: put TextProp in the prelude (closes #3877) 2025-04-25 23:50:08 +02:00
autofix-ci[bot]
9449f41ca9 [autofix.ci] apply automated fixes (attempt 2/3) 2025-04-25 20:31:56 +00:00
autofix-ci[bot]
d979055b70 [autofix.ci] apply automated fixes 2025-04-25 20:18:03 +00:00
Greg Johnston
97686f71a5 fix: improve support for keyed fields in ArcField<_> 2025-04-25 16:04:01 -04:00
Greg Johnston
06a0c768dc fix: correctly calculate starting index for first new key (closes #3828) 2025-04-25 15:57:30 -04:00
Saber Haj Rabiee
fff6a508fc feat(examples): add WebSocket example (#3853) 2025-04-24 20:17:08 -04:00
Greg Johnston
e65fc23fc7 fix: prevent infinite loop when sending Result over websocket, remove Display bound (#3848)
* chore: easing `Display` bound on `FromServerFnError`, #3811 follow-up

* fix: send/receive websocket data

* fix: clippy warnings

* fix: server_fn_axum example

* fix: make de/serialize_result functions public

* fix: make websocket result ser/de private

* chore: make the doc a comment and remove allow dead_code
2025-04-23 07:52:42 -04:00
Saber Haj Rabiee
f83b14d76c feat: enhancing ByteStream error handling (#3869)
* feat: enhancing `ByteStream` error handling

* fix: easing unnecessary trait bound over some `TextStream` methods
2025-04-23 07:38:39 -04:00
zakstucke
62dac6fb8a fix: prevent ScopedFuture stopping owner cleanup (#3863) 2025-04-23 07:37:12 -04:00
Rakshith Ravi
b36dec8269 feat: add header generation method to BrowserResponse (#3873) 2025-04-23 07:33:18 -04:00
Fea
0c50852251 fix: Use stabilized ClipboardEvent (#3849) 2025-04-23 07:27:24 -04:00
Nikolai Morin
50cb6005a8 chore(examples): complete the migration to Tailwind 4 (#3861)
The tailwind_csr example was not working yet with version 4, and the
tailwind_actix example still had the no-longer-needed config file.

This also brings the three tailwind examples back in sync, mostly. I
don't know why the axum example has a packages.json and
packages-lock.json file, to be honest.
2025-04-23 07:26:56 -04:00
Sathish
b725291ce9 chore: forward lint attributes used with #[component] macro (#3864)
* Forward lint attributes used with #[component] macro

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-04-23 07:26:26 -04:00
Greg Johnston
ed6d45d92d Merge pull request #3870 from leptos-rs/3851
Error boundary fixes
2025-04-23 07:25:41 -04:00
Greg Johnston
73b5587738 fix: correctly insert out-of-order stream chunks when Ok 2025-04-21 13:12:30 -04:00
Greg Johnston
68813a5918 fix: clear old errors when going from error state to error state (closes #3850) 2025-04-21 12:10:47 -04:00
Greg Johnston
8f6a96341e fix: wait for any inner Suspense before rendering ErrorBoundary (closes #3851) 2025-04-21 09:29:33 -04:00
Greg Johnston
046d5286c3 fix: correctly flush synchronous parts of SSR stream when appending another StreamBuilder 2025-04-21 09:28:14 -04:00
Greg Johnston
b45f982feb fix: close Actix websocket stream when browser disconnects (closes #3865) (#3866) 2025-04-19 16:47:09 -04:00
Greg Johnston
2b50ddc0db v0.8.0-rc2 2025-04-18 08:43:16 -04:00
Saber Haj Rabiee
c743f0641c chore: make the doc a comment and remove allow dead_code 2025-04-17 06:45:40 -07:00
Saber Haj Rabiee
078c252e2e fix: make websocket result ser/de private 2025-04-17 03:38:04 -07:00
martin frances
410aedbba8 chore: ran cargo outdated. (#3722)
* chore: ran cargo outdated fixed issues.

In bumping the rand crate, calls to thread_rng() becomes rng().

* backed out changes to rand.
2025-04-16 16:42:01 -07:00
Saber Haj Rabiee
00e474599f fix: reactive_graph keymap impl and clippy warnings (#3843) 2025-04-16 16:39:07 -07:00
Greg Johnston
8f38559aa2 chore(nightly): update proc-macro span file name method name (#3852) 2025-04-16 16:35:59 -07:00
Greg Johnston
3934c8b162 Update issue templates 2025-04-16 09:15:10 -04:00
Saber Haj Rabiee
de3a558203 fix: make de/serialize_result functions public 2025-04-15 09:24:43 -07:00
Saber Haj Rabiee
4d20105760 fix: server_fn_axum example 2025-04-15 09:23:32 -07:00
Saber Haj Rabiee
b95e827b8b fix: clippy warnings 2025-04-15 08:31:42 -07:00
Saber Haj Rabiee
30c445a419 fix: send/receive websocket data 2025-04-15 07:43:47 -07:00
Saber Haj Rabiee
6d5ab73594 chore: easing Display bound on FromServerFnError, #3811 follow-up 2025-04-14 23:52:02 -07:00
Greg Johnston
e0bf5ec480 fix: don't try to move keyed elements within the DOM if they're not yet mounted (closes #3844) (#3846) 2025-04-14 20:06:31 -04:00
164 changed files with 2932 additions and 1418 deletions

View File

@@ -29,11 +29,15 @@ Steps to reproduce the behavior:
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Next Steps**
- [ ] I will make a PR
- [ ] I would like to make a PR, but need help getting started
- [ ] I want someone else to take the time to fix this
- [ ] This is a low priority for me and is just shared for your information
**Additional context**
Add any other context about the problem here.

View File

@@ -21,33 +21,19 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: {toolchain: nightly, components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
with: {toolchain: "nightly-2025-04-16", components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
- name: Install Glib
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- name: Install cargo-all-features
run: cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
- name: Install jq
run: sudo apt-get install jq
- run: |
echo "Formatting the workspace"
cargo fmt --all
echo "Running Clippy against each member's features (default features included)"
for member in $(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | .name'); do
echo "Working on member $member":
echo -e "\tdefault-features/no-features:"
# this will also run on members with no features or default features
cargo clippy --allow-dirty --fix --lib --package "$member"
features=$(cargo metadata --no-deps --format-version 1 | jq -r ".packages[] | select(.name == \"$member\") | .features | keys[]")
for feature in $features; do
if [ "$feature" = "default" ]; then
continue
fi
echo -e "\tfeature $feature"
cargo clippy --allow-dirty --fix --lib --package "$member" --features "$feature"
done
done
- name: Format the workspace
run: cargo fmt --all
- name: Clippy the workspace
run: cargo all-features clippy --allow-dirty --fix --lib --no-deps
- uses: autofix-ci/action@v1.3.1
if: ${{ always() }}
with:

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
toolchain: [stable, nightly-2025-03-05]
toolchain: [stable, nightly-2025-04-16]
erased_mode: [true, false]
steps:
- name: Free Disk Space
@@ -160,6 +160,12 @@ jobs:
run: |
cd '${{ inputs.directory }}'
cargo make --no-workspace --profile=github-actions ci
# Check if the counter_isomorphic can be built with leptos_debuginfo cfg flag in release mode
- name: ${{ inputs.cargo_make_task }} with --cfg=leptos_debuginfo
if: contains(inputs.directory, 'counter_isomorphic')
run: |
cd '${{ inputs.directory }}'
RUSTFLAGS="$RUSTFLAGS --cfg leptos_debuginfo" cargo leptos build --release
- name: Clean up ${{ inputs.directory }}
if: always()
run: |

724
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,41 +40,81 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.8.0-rc1"
version = "0.8.2"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
# members
throw_error = { path = "./any_error/", version = "0.3.0" }
any_spawner = { path = "./any_spawner/", version = "0.3.0-rc1" }
any_spawner = { path = "./any_spawner/", version = "0.3.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
either_of = { path = "./either_of/", version = "0.1.5" }
hydration_context = { path = "./hydration_context", version = "0.3.0" }
itertools = "0.14.0"
leptos = { path = "./leptos", version = "0.8.0-rc1" }
leptos_config = { path = "./leptos_config", version = "0.8.0-rc1" }
leptos_dom = { path = "./leptos_dom", version = "0.8.0-rc1" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-rc1" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-rc1" }
leptos_macro = { path = "./leptos_macro", version = "0.8.0-rc1" }
leptos_router = { path = "./router", version = "0.8.0-rc1" }
leptos_router_macro = { path = "./router_macro", version = "0.8.0-rc1" }
leptos_server = { path = "./leptos_server", version = "0.8.0-rc1" }
leptos_meta = { path = "./meta", version = "0.8.0-rc1" }
leptos = { path = "./leptos", version = "0.8.2" }
leptos_config = { path = "./leptos_config", version = "0.8.2" }
leptos_dom = { path = "./leptos_dom", version = "0.8.2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.2" }
leptos_macro = { path = "./leptos_macro", version = "0.8.2" }
leptos_router = { path = "./router", version = "0.8.2" }
leptos_router_macro = { path = "./router_macro", version = "0.8.2" }
leptos_server = { path = "./leptos_server", version = "0.8.2" }
leptos_meta = { path = "./meta", version = "0.8.2" }
next_tuple = { path = "./next_tuple", version = "0.1.0" }
oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.2.0-rc1" }
reactive_stores = { path = "./reactive_stores", version = "0.2.0-rc1" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-rc1" }
rustversion = "1"
serde_json = "1.0.0"
server_fn = { path = "./server_fn", version = "0.8.0-rc1" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-rc1" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-rc1" }
tachys = { path = "./tachys", version = "0.2.0-rc1" }
trybuild = "1"
wasm-bindgen = { version = "0.2.100" }
reactive_graph = { path = "./reactive_graph", version = "0.2.0" }
reactive_stores = { path = "./reactive_stores", version = "0.2.0" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0" }
server_fn = { path = "./server_fn", version = "0.8.2" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.2" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.2" }
tachys = { path = "./tachys", version = "0.2.0" }
# common deps
itertools = { default-features = false, version = "0.14.0" }
convert_case = { default-features = false, version = "0.8.0" }
serde_json = { default-features = false, version = "1.0" }
trybuild = { default-features = false, version = "1.0" }
typed-builder = { default-features = false, version = "0.21.0" }
thiserror = { default-features = false, version = "2.0" }
wasm-bindgen = { default-features = false, version = "0.2.100" }
indexmap = { default-features = false, version = "2.9" }
rstml = { default-features = false, version = "0.12.1" }
rustc_version = { default-features = false, version = "0.4.1" }
guardian = { default-features = false, version = "1.3" }
rustc-hash = { default-features = false, version = "2.1" }
once_cell = { default-features = false, version = "1.21" }
actix-web = { default-features = false, version = "4.10" }
tracing = { default-features = false, version = "0.1.41" }
slotmap = { default-features = false, version = "1.0" }
futures = { default-features = false, version = "0.3.31" }
dashmap = { default-features = false, version = "6.1" }
pin-project-lite = { default-features = false, version = "0.2.16" }
send_wrapper = { default-features = false, version = "0.6.0" }
tokio-test = { default-features = false, version = "0.4.4" }
html-escape = { default-features = false, version = "0.2.13" }
proc-macro-error2 = { default-features = false, version = "2.0" }
const_format = { default-features = false, version = "0.2.34" }
gloo-net = { default-features = false, version = "0.6.0" }
url = { default-features = false, version = "2.5" }
tokio = { default-features = false, version = "1.44" }
base64 = { default-features = false, version = "0.22.1" }
cfg-if = { default-features = false, version = "1.0" }
wasm-bindgen-futures = { default-features = false, version = "0.4.50" }
tower = { default-features = false, version = "0.5.2" }
proc-macro2 = { default-features = false, version = "1.0" }
serde = { default-features = false, version = "1.0" }
parking_lot = { default-features = false, version = "0.12.3" }
axum = { default-features = false, version = "0.8.3" }
serde_qs = { default-features = false, version = "0.15.0" }
syn = { default-features = false, version = "2.0" }
xxhash-rust = { default-features = false, version = "0.8.15" }
paste = { default-features = false, version = "1.0" }
quote = { default-features = false, version = "1.0" }
web-sys = { default-features = false, version = "0.3.77" }
js-sys = { default-features = false, version = "0.3.77" }
[profile.release]
codegen-units = 1

View File

@@ -10,4 +10,4 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
pin-project-lite = "0.2.15"
pin-project-lite = { workspace = true, default-features = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "any_spawner"
version = "0.3.0-rc1"
version = "0.3.0"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -10,22 +10,22 @@ edition.workspace = true
[dependencies]
async-executor = { version = "1.13.1", optional = true }
futures = "0.3.31"
glib = { version = "0.20.6", optional = true }
thiserror = "2.0"
tokio = { version = "1.41", optional = true, default-features = false, features = [
futures = { workspace = true, default-features = true }
glib = { version = "0.20.9", optional = true }
thiserror = { workspace = true , default-features = true }
tokio = { optional = true, default-features = false, features = [
"rt",
] }
tracing = { version = "0.1.41", optional = true }
wasm-bindgen-futures = { version = "0.4.50", optional = true }
] , workspace = true }
tracing = { optional = true , workspace = true, default-features = true }
wasm-bindgen-futures = { optional = true , workspace = true, default-features = true }
[dev-dependencies]
futures-lite = { version = "2.6.0", default-features = false }
tokio = { version = "1.41", default-features = false, features = [
tokio = { default-features = false, features = [
"rt",
"macros",
"time",
] }
] , workspace = true }
wasm-bindgen-test = { version = "0.3.50" }
serial_test = "3.2.0"

View File

@@ -472,9 +472,8 @@ fn handle_uninitialized_spawn(_fut: PinnedFuture<()>) {
#[cfg(all(debug_assertions, not(feature = "tracing")))]
{
panic!(
"At {}, tried to spawn a Future with Executor::spawn() before a \
global executor was initialized.",
caller
"At {caller}, tried to spawn a Future with Executor::spawn() \
before a global executor was initialized."
);
}
// In release builds (without tracing), call the specific no-op function.
@@ -503,9 +502,8 @@ fn handle_uninitialized_spawn_local(_fut: PinnedLocalFuture<()>) {
#[cfg(all(debug_assertions, not(feature = "tracing")))]
{
panic!(
"At {}, tried to spawn a Future with Executor::spawn_local() \
before a global executor was initialized.",
caller
"At {caller}, tried to spawn a Future with \
Executor::spawn_local() before a global executor was initialized."
);
}
// In release builds (without tracing), call the specific no-op function (which usually panics).

View File

@@ -31,7 +31,7 @@ pub const fn const_concat(
let mut i = 0;
// have it iterate over bytes manually, because, again,
// no mutable refernces in const fns
// no mutable references in const fns
while i < x.len() {
buffer[position] = x[i];
position += 1;
@@ -59,7 +59,7 @@ pub const fn const_concat_with_prefix(
let mut i = 0;
// have it iterate over bytes manually, because, again,
// no mutable refernces in const fns
// no mutable references in const fns
while i < x.len() {
buffer[position] = x[i];
position += 1;
@@ -116,7 +116,7 @@ pub const fn const_concat_with_separator(
let mut i = 0;
// have it iterate over bytes manually, because, again,
// no mutable refernces in const fns
// no mutable references in const fns
while i < x.len() {
buffer[position] = x[i];
position += 1;

View File

@@ -10,8 +10,8 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
pin-project-lite = "0.2.16"
paste = "1.0.15"
pin-project-lite = { workspace = true, default-features = true }
paste = { workspace = true, default-features = true }
[features]
default = ["no_std"]

View File

@@ -7,7 +7,7 @@ pub fn main() {
fmt()
.with_writer(
// To avoide trace events in the browser from showing their
// To avoid trace events in the browser from showing their
// JS backtrace, which is very annoying, in my opinion
MakeConsoleWriter::default()
.map_trace_level_to(tracing::Level::DEBUG),

View File

@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new(
let story = Resource::new_blocking(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {

View File

@@ -1,7 +1,7 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use axum::{routing::get, Router};
use hackernews_axum::{shell, App};
use leptos::config::get_configuration;
use leptos_axum::{generate_route_list, LeptosRoutes};
@@ -13,6 +13,15 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route(
"/favicon.ico",
get(|| async {
(
[("content-type", "image/x-icon")],
include_bytes!("../public/favicon.ico"),
)
}),
)
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())

View File

@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new(
let story = Resource::new_blocking(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {

View File

@@ -26,12 +26,17 @@ pub async fn file_and_error_handler(
.map(|h| h.to_str().unwrap_or("none"))
.unwrap_or("none")
.to_string();
let res = get_static_file(uri.clone(), accept_encoding).await.unwrap();
let static_result = get_static_file(uri.clone(), accept_encoding).await;
if res.status() == StatusCode::OK {
res.into_response()
} else {
(StatusCode::NOT_FOUND, "Not found.").into_response()
match static_result {
Ok(res) => {
if res.status() == StatusCode::OK {
res.into_response()
} else {
(StatusCode::NOT_FOUND, "Not found.").into_response()
}
}
Err(e) => e.into_response(),
}
}

View File

@@ -1,6 +1,7 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::routing::get;
pub use axum::Router;
use hackernews_islands::*;
pub use leptos::config::get_configuration;
@@ -25,6 +26,7 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/favicon.ico", get(fallback::file_and_error_handler))
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())

View File

@@ -47,7 +47,7 @@ pub fn Stories() -> impl IntoView {
let stories = Resource::new(
move || (page(), story_type()),
move |(page, story_type)| async move {
fetch_stories(category(&story_type), page).await.ok()
fetch_stories(story_type, page).await.ok()
},
);
let (pending, set_pending) = signal(false);

View File

@@ -13,7 +13,7 @@ pub async fn fetch_story(
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new(
let story = Resource::new_blocking(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {

View File

@@ -7,7 +7,7 @@ use send_wrapper::SendWrapper;
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new(
let story = Resource::new_blocking(
move || params.read().get("id").unwrap_or_default(),
move |id| {
SendWrapper::new(async move {

View File

@@ -10,7 +10,11 @@ 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",
"islands-router",
] }
leptos_router = { path = "../../router" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", features = [

View File

@@ -945,9 +945,7 @@ pub fn CustomClientExample() -> impl IntoView {
Item = Result<server_fn::Bytes, server_fn::Bytes>,
> + Send
+ 'static,
impl Sink<Result<server_fn::Bytes, server_fn::Bytes>>
+ Send
+ 'static,
impl Sink<server_fn::Bytes> + Send + 'static,
),
E,
>,

View File

@@ -1,6 +1,6 @@
# Leptos Counter Example
# Stores Example
This example creates a simple counter in a client side rendered app with Rust and WASM!
This example shows how to use reactive stores, by building a client-side rendered TODO application.
## Getting Started

View File

@@ -561,7 +561,7 @@ fn ShowCounters() -> impl IntoView {
//
// However, upon `Reset Counters`, the mode from which the reset
// was issued will result in the rendering be reflected as such, so
// if the intial state was SSR, resetting under CSR will result in
// if the initial state was SSR, resetting under CSR will result in
// the CSR counters be rendered after. However for the intents and
// purpose for the testing only the CSR is cared for.
//

View File

@@ -57,7 +57,7 @@ site-pkg-dir = "pkg"
# The tailwind input file.
#
# Optional, Activates the tailwind build
tailwind-input-file = "style/tailwind.css"
tailwind-input-file = "input.css"
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"

View File

@@ -1,13 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: ["*.html", "./src/**/*.rs"],
transform: {
rs: (content) => content.replace(/(?:^|\s)class:/g, ' '),
},
},
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8" />
<link data-trunk rel="rust" data-wasm-opt="z" />
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico" />
<link data-trunk rel="tailwind-css" href="/style/tailwind.css" />
<link data-trunk rel="tailwind-css" href="input.css" />
<title>Leptos • Counter with Tailwind</title>
</head>

View File

@@ -0,0 +1 @@
@import "tailwindcss";

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,13 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: ["*.html", "./src/**/*.rs"],
transform: {
rs: (content) => content.replace(/(?:^|\s)class:/g, ' '),
},
},
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -0,0 +1,74 @@
[package]
name = "websocket"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "1.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.30"
leptos = { path = "../../leptos", features = ["tracing"] }
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 }
tokio = { version = "1.39", features = ["full"], optional = true }
thiserror = "2.0"
wasm-bindgen = "0.2.100"
[features]
hydrate = ["leptos/hydrate"]
ssr = ["dep:axum", "dep:tokio", "leptos/ssr", "dep:leptos_axum"]
[package.metadata.cargo-all-features]
denylist = ["axum", "tokio", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"], []]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "websocket"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "cargo make test-ui"
end2end-dir = "e2e"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,12 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
]
[env]
CLIENT_PROCESS_NAME = "websocket"
[tasks.test-ui]
cwd = "./e2e"
command = "cargo"
args = ["make", "test-ui", "${@}"]

View File

@@ -0,0 +1,19 @@
# Leptos WebSocket
This example creates a basic WebSocket echo app.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## E2E Testing
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
## Rendering
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

@@ -0,0 +1,18 @@
[package]
name = "websocket_e2e"
version = "0.1.0"
edition = "2021"
[dev-dependencies]
anyhow = "1.0"
async-trait = "0.1.81"
cucumber = "0.21.1"
fantoccini = "0.21.1"
pretty_assertions = "1.4"
serde_json = "1.0"
tokio = { version = "1.39", features = ["macros", "rt-multi-thread", "time"] }
url = "2.5"
[[test]]
name = "app_suite"
harness = false # Allow Cucumber to print output instead of libtest

View File

@@ -0,0 +1,20 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.test]
env = { RUN_AUTOMATICALLY = false }
condition = { env_true = ["RUN_AUTOMATICALLY"] }
[tasks.ci]
[tasks.test-ui]
command = "cargo"
args = [
"test",
"--test",
"app_suite",
"--",
"--retry",
"2",
"--fail-fast",
"${@}",
]

View File

@@ -0,0 +1,34 @@
# E2E Testing
This example demonstrates e2e testing with Rust using executable requirements.
## Testing Stack
| | Role | Description |
|---|---|---|
| [Cucumber](https://github.com/cucumber-rs/cucumber/tree/main) | Test Runner | Run [Gherkin](https://cucumber.io/docs/gherkin/reference/) specifications as Rust tests |
| [Fantoccini](https://github.com/jonhoo/fantoccini/tree/main) | Browser Client | Interact with web pages through WebDriver |
| [Cargo Leptos ](https://github.com/leptos-rs/cargo-leptos) | Build Tool | Compile example and start the server and end-2-end tests |
| [chromedriver](https://chromedriver.chromium.org/downloads) | WebDriver | Provide WebDriver for Chrome
## Testing Organization
Testing is organized around what a user can do and see/not see. Test scenarios are grouped by the **user action** and the **object** of that action. This makes it easier to locate and reason about requirements.
Here is a brief overview of how things fit together.
```bash
features
└── {action}_{object}.feature # Specify test scenarios
tests
├── fixtures
│ ├── action.rs # Perform a user action (click, type, etc.)
│ ├── check.rs # Assert what a user can see/not see
│ ├── find.rs # Query page elements
│ ├── mod.rs
│ └── world
│ ├── action_steps.rs # Map Gherkin steps to user actions
│ ├── check_steps.rs # Map Gherkin steps to user expectations
│ └── mod.rs
└── app_suite.rs # Test main
```

View File

@@ -0,0 +1,10 @@
@echo_client_error
Feature: Echo Client Error
Background:
Given I see the app
@echo_client_error-see-fifth-input-error
Scenario: Should see the client error
Given I add a text as abcde
Then I see the label of the input is Error(ServerFnErrorWrapper(Registration("Error generated from client")))

View File

@@ -0,0 +1,10 @@
@echo_server_error
Feature: Echo Server Error
Background:
Given I see the app
@echo_server_error-see-third-input-error
Scenario: Should see the server error
Given I add a text as abc
Then I see the label of the input is Error(ServerFnErrorWrapper(Registration("Error generated from server")))

View File

@@ -0,0 +1,17 @@
@echo_text
Feature: Echo Text
Background:
Given I see the app
@echo_text-see-first-input
Scenario: Should see the label
Given I add a text as a
Then I see the label of the input is A
@add_text-see-second-input
Scenario: Should see the label
Given I add a text as ab
Then I see the label of the input is AB

View File

@@ -0,0 +1,7 @@
@open_app
Feature: Open App
@open_app-title
Scenario: Should see the home page title
When I open the app
Then I see the page title is Simple Echo WebSocket Communication

View File

@@ -0,0 +1,14 @@
mod fixtures;
use anyhow::Result;
use cucumber::World;
use fixtures::world::AppWorld;
#[tokio::main]
async fn main() -> Result<()> {
AppWorld::cucumber()
.fail_on_skipped()
.run_and_exit("./features")
.await;
Ok(())
}

View File

@@ -0,0 +1,18 @@
use super::{find, world::HOST};
use anyhow::Result;
use fantoccini::Client;
use std::result::Result::Ok;
pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
let url = format!("{}{}", HOST, path);
client.goto(&url).await?;
Ok(())
}
pub async fn fill_input(client: &Client, text: &str) -> Result<()> {
let textbox = find::input(client).await;
textbox.send_keys(text).await?;
Ok(())
}

View File

@@ -0,0 +1,22 @@
use anyhow::{Ok, Result};
use fantoccini::{Client, Locator};
use pretty_assertions::assert_eq;
pub async fn text_on_element(
client: &Client,
selector: &str,
expected_text: &str,
) -> Result<()> {
let element = client
.wait()
.for_element(Locator::Css(selector))
.await
.unwrap_or_else(|_| {
panic!("Element not found by Css selector `{}`", selector)
});
let actual = element.text().await?;
assert_eq!(&actual, expected_text);
Ok(())
}

View File

@@ -0,0 +1,11 @@
use fantoccini::{elements::Element, Client, Locator};
pub async fn input(client: &Client) -> Element {
let textbox = client
.wait()
.for_element(Locator::Css("input"))
.await
.expect("websocket textbox not found");
textbox
}

View File

@@ -0,0 +1,4 @@
pub mod action;
pub mod check;
pub mod find;
pub mod world;

View File

@@ -0,0 +1,20 @@
use crate::fixtures::{action, world::AppWorld};
use anyhow::{Ok, Result};
use cucumber::{given, when};
#[given("I see the app")]
#[when("I open the app")]
async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
action::goto_path(client, "").await?;
Ok(())
}
#[given(regex = "^I add a text as (.*)$")]
async fn i_add_a_text(world: &mut AppWorld, text: String) -> Result<()> {
let client = &world.client;
action::fill_input(client, text.as_str()).await?;
Ok(())
}

View File

@@ -0,0 +1,28 @@
use crate::fixtures::{check, world::AppWorld};
use anyhow::{Ok, Result};
use cucumber::then;
use std::time::Duration;
use tokio::time::sleep;
#[then(regex = "^I see the page title is (.*)$")]
async fn i_see_the_page_title_is(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::text_on_element(client, "h1", &text).await?;
Ok(())
}
#[then(regex = "^I see the label of the input is (.*)$")]
async fn i_see_the_label_of_the_input_is(
world: &mut AppWorld,
text: String,
) -> Result<()> {
sleep(Duration::from_millis(500)).await;
let client = &world.client;
check::text_on_element(client, "p", &text).await?;
Ok(())
}

View File

@@ -0,0 +1,39 @@
pub mod action_steps;
pub mod check_steps;
use anyhow::Result;
use cucumber::World;
use fantoccini::{
error::NewSessionError, wd::Capabilities, Client, ClientBuilder,
};
pub const HOST: &str = "http://127.0.0.1:3000";
#[derive(Debug, World)]
#[world(init = Self::new)]
pub struct AppWorld {
pub client: Client,
}
impl AppWorld {
async fn new() -> Result<Self, anyhow::Error> {
let webdriver_client = build_client().await?;
Ok(Self {
client: webdriver_client,
})
}
}
async fn build_client() -> Result<Client, NewSessionError> {
let mut cap = Capabilities::new();
let arg = serde_json::from_str("{\"args\": [\"-headless\"]}").unwrap();
cap.insert("goog:chromeOptions".to_string(), arg);
let client = ClientBuilder::native()
.capabilities(cap)
.connect("http://localhost:4444")
.await?;
Ok(client)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,9 @@
pub mod websocket;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::websocket::App;
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(App);
}

View File

@@ -0,0 +1,44 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use websocket::websocket::{shell, App};
simple_logger::init_with_level(log::Level::Error)
.expect("couldn't initialize logging");
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
// build our application with a route
let app = Router::new()
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
println!("listening on http://{}", &addr);
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
use leptos::mount::mount_to_body;
use websocket::websocket::App;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(App);
}

View File

@@ -0,0 +1,123 @@
use leptos::{prelude::*, task::spawn_local};
use server_fn::{codec::JsonEncoding, BoxedStream, ServerFnError, Websocket};
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<AutoReload options=options.clone() />
<HydrationScripts options />
<link rel="shortcut icon" type="image/ico" href="/favicon.ico" />
</head>
<body>
<App />
</body>
</html>
}
}
// The websocket protocol can be used on any server function that accepts and returns a [`BoxedStream`]
// with items that can be encoded by the input and output encoding generics.
//
// In this case, the input and output encodings are [`Json`] and [`Json`], respectively which requires
// the items to implement [`Serialize`] and [`Deserialize`].
#[server(protocol = Websocket<JsonEncoding, JsonEncoding>)]
async fn echo_websocket(
input: BoxedStream<String, ServerFnError>,
) -> Result<BoxedStream<String, ServerFnError>, ServerFnError> {
use futures::{channel::mpsc, SinkExt, StreamExt};
let mut input = input; // FIXME :-) server fn fields should pass mut through to destructure
// create a channel of outgoing websocket messages
// we'll return rx, so sending a message to tx will send a message to the client via the websocket
let (mut tx, rx) = mpsc::channel(1);
// spawn a task to listen to the input stream of messages coming in over the websocket
tokio::spawn(async move {
let mut x = 0;
while let Some(msg) = input.next().await {
// do some work on each message, and then send our responses
x += 1;
println!("In server: {} {:?}", x, msg);
if x % 3 == 0 {
let _ = tx
.send(Err(ServerFnError::Registration(
"Error generated from server".to_string(),
)))
.await;
} else {
let _ = tx.send(msg.map(|msg| msg.to_ascii_uppercase())).await;
}
}
});
Ok(rx.into())
}
#[component]
pub fn App() -> impl IntoView {
use futures::{channel::mpsc, StreamExt};
let (mut tx, rx) = mpsc::channel(1);
let latest = RwSignal::new(Ok("".into()));
// we'll only listen for websocket messages on the client
if cfg!(feature = "hydrate") {
spawn_local(async move {
match echo_websocket(rx.into()).await {
Ok(mut messages) => {
while let Some(msg) = messages.next().await {
leptos::logging::log!("{:?}", msg);
latest.set(msg);
}
}
Err(e) => leptos::logging::warn!("{e}"),
}
});
}
let mut x = 0;
view! {
<h1>Simple Echo WebSocket Communication</h1>
<input
type="text"
on:input:target=move |ev| {
x += 1;
let msg = ev.target().value();
leptos::logging::log!("In client: {} {:?}", x, msg);
if x % 5 == 0 {
let _ = tx
.try_send(
Err(
ServerFnError::Registration(
"Error generated from client".to_string(),
),
),
);
} else {
let _ = tx.try_send(Ok(msg));
}
}
/>
<div>
<ErrorBoundary fallback=|errors| {
view! {
<p>
{move || {
errors
.get()
.into_iter()
.map(|(_, e)| format!("{e:?}"))
.collect::<Vec<String>>()
.join(" ")
}}
</p>
}
}>
<p>{latest}</p>
</ErrorBoundary>
</div>
}
}

View File

View File

@@ -12,12 +12,12 @@ edition.workspace = true
[dependencies]
throw_error = { workspace = true }
or_poisoned = { workspace = true }
futures = "0.3.31"
serde = { version = "1.0", features = ["derive"] }
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"
futures = { workspace = true, default-features = true }
serde = { features = ["derive"] , workspace = true, default-features = true }
wasm-bindgen = { workspace = true, optional = true , default-features = true }
js-sys = { optional = true , workspace = true, default-features = true }
once_cell = { workspace = true, default-features = true }
pin-project-lite = { workspace = true, default-features = true }
[features]
browser = ["dep:wasm-bindgen", "dep:js-sys"]

View File

@@ -9,10 +9,10 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
actix-http = "3.9"
actix-http = "3.10"
actix-files = "0.6"
actix-web = "4.9"
futures = "0.3.31"
actix-web = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
any_spawner = { workspace = true, features = ["tokio"] }
hydration_context = { workspace = true }
leptos = { workspace = true, features = ["nonce", "ssr"] }
@@ -22,13 +22,13 @@ leptos_meta = { workspace = true, features = ["nonce"] }
leptos_router = { workspace = true, features = ["ssr"] }
server_fn = { workspace = true, features = ["actix"] }
tachys = { workspace = true }
serde_json = { workspace = true }
parking_lot = "0.12.3"
tracing = { version = "0.1", optional = true }
tokio = { version = "1.43", features = ["rt", "fs"] }
send_wrapper = "0.6.0"
dashmap = "6"
once_cell = "1"
serde_json = { workspace = true , default-features = true }
parking_lot = { workspace = true, default-features = true }
tracing = { optional = true , workspace = true, default-features = true }
tokio = { features = ["rt", "fs"] , workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
dashmap = { workspace = true, default-features = true }
once_cell = { workspace = true, default-features = true }
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]

View File

@@ -1230,7 +1230,7 @@ fn static_path(options: &LeptosOptions, path: &str) -> String {
// If the path ends with a trailing slash, we generate the path
// as a directory with a index.html file inside.
if path != "/" && path.ends_with("/") {
static_file_path(options, &format!("{}index", path))
static_file_path(options, &format!("{path}index"))
} else {
static_file_path(options, path)
}

View File

@@ -4,18 +4,18 @@ authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Axum integrations for the Leptos web framework."
version = "0.8.0-rc1"
version = "0.8.2"
rust-version.workspace = true
edition.workspace = true
[dependencies]
any_spawner = { workspace = true, features = ["tokio"] }
hydration_context = { workspace = true }
axum = { version = "0.8.1", default-features = false, features = [
axum = { default-features = false, features = [
"matched-path",
] }
dashmap = "6"
futures = "0.3.31"
] , workspace = true }
dashmap = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
leptos = { workspace = true, features = ["nonce", "ssr"] }
server_fn = { workspace = true, features = ["axum-no-default"] }
leptos_macro = { workspace = true, features = ["axum"] }
@@ -23,16 +23,16 @@ leptos_meta = { workspace = true, features = ["ssr", "nonce"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
tachys = { workspace = true }
once_cell = "1"
parking_lot = "0.12.3"
tokio = { version = "1.43", default-features = false }
tower = { version = "0.5.1", features = ["util"] }
once_cell = { workspace = true, default-features = true }
parking_lot = { workspace = true, default-features = true }
tokio = { default-features = false , workspace = true }
tower = { features = ["util"] , workspace = true, default-features = true }
tower-http = "0.6.2"
tracing = { version = "0.1.41", optional = true }
tracing = { optional = true , workspace = true, default-features = true }
[dev-dependencies]
axum = "0.8.1"
tokio = { version = "1.43", features = ["net", "rt-multi-thread"] }
axum = { workspace = true, default-features = true }
tokio = { features = ["net", "rt-multi-thread"] , workspace = true, default-features = true }
[features]
wasm = []

View File

@@ -590,7 +590,7 @@ where
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```
@@ -796,7 +796,7 @@ where
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```
@@ -879,7 +879,8 @@ where
}
}
fn handle_response_inner<IV>(
/// Can be used in conjunction with a custom [file_and_error_handler_with_context] to process an Axum [Request](axum::extract::Request) into an Axum [Response](axum::response::Response)
pub fn handle_response_inner<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl FnOnce() -> IV + Send + 'static,
req: Request<Body>,
@@ -1022,7 +1023,7 @@ where
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` resources have loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```
@@ -1089,7 +1090,7 @@ where
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` resources have loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```
@@ -1543,7 +1544,7 @@ fn static_path(options: &LeptosOptions, path: &str) -> String {
// If the path ends with a trailing slash, we generate the path
// as a directory with a index.html file inside.
if path != "/" && path.ends_with("/") {
static_file_path(options, &format!("{}index", path))
static_file_path(options, &format!("{path}index"))
} else {
static_file_path(options, path)
}

View File

@@ -9,7 +9,7 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
futures = "0.3.31"
futures = { workspace = true, default-features = true }
hydration_context = { workspace = true }
leptos = { workspace = true, features = ["nonce"] }
leptos_meta = { workspace = true, features = ["ssr"] }

View File

@@ -15,8 +15,8 @@ any_spawner = { workspace = true, features = [
"wasm-bindgen",
"futures-executor",
] }
base64 = { version = "0.22.1", optional = true }
cfg-if = "1.0"
base64 = { optional = true, workspace = true, default-features = true }
cfg-if = { workspace = true, default-features = true }
hydration_context = { workspace = true }
either_of = { workspace = true }
leptos_dom = { workspace = true }
@@ -27,35 +27,35 @@ leptos_config = { workspace = true }
leptos-spin-macro = { version = "0.2.0", optional = true }
oco_ref = { workspace = true }
or_poisoned = { workspace = true }
paste = "1.0"
rand = { version = "0.8.5", optional = true }
# NOTE: While not used directly, `getrandom`'s `js` feature is needed when `rand` is used on WASM to
paste = { workspace = true, default-features = true }
rand = { version = "0.9.1", optional = true }
# NOTE: While not used directly, `getrandom`'s `wasm_js` feature is needed when `rand` is used on WASM to
# avoid a compilation error
getrandom = { version = "0.2", optional = true }
getrandom = { version = "0.3.3", optional = true }
reactive_graph = { workspace = true, features = ["serde"] }
rustc-hash = "2.0"
rustc-hash = { workspace = true, default-features = true }
tachys = { workspace = true, features = [
"reactive_graph",
"reactive_stores",
"oco",
] }
thiserror = "2.0"
tracing = { version = "0.1.41", optional = true }
typed-builder = "0.20.0"
typed-builder-macro = "0.20.0"
serde = "1.0"
serde_json = { version = "1.0", optional = true }
thiserror = { workspace = true, default-features = true }
tracing = { optional = true, workspace = true, default-features = true }
typed-builder = { workspace = true, default-features = true }
typed-builder-macro = "0.21.0"
serde = { workspace = true, default-features = true }
serde_json = { optional = true, workspace = true, default-features = true }
server_fn = { workspace = true, features = ["form-redirects", "browser"] }
web-sys = { version = "0.3.72", features = [
web-sys = { features = [
"ShadowRoot",
"ShadowRootInit",
"ShadowRootMode",
] }
wasm-bindgen = { workspace = true }
serde_qs = "0.13.0"
slotmap = "1.0"
futures = "0.3.31"
send_wrapper = "0.6.0"
], workspace = true, default-features = true }
wasm-bindgen = { workspace = true, default-features = true }
serde_qs = { workspace = true, default-features = true }
slotmap = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
[features]
hydration = [
@@ -64,13 +64,13 @@ hydration = [
"hydration_context/browser",
"leptos_dom/hydration",
]
csr = ["leptos_macro/csr", "reactive_graph/effects", "getrandom?/js"]
csr = ["leptos_macro/csr", "reactive_graph/effects", "getrandom?/wasm_js"]
hydrate = [
"leptos_macro/hydrate",
"hydration",
"tachys/hydrate",
"reactive_graph/effects",
"getrandom?/js",
"getrandom?/wasm_js",
]
default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls"]
@@ -99,15 +99,16 @@ trace-component-props = [
"leptos_dom/trace-component-props",
]
delegation = ["tachys/delegation"]
islands-router = ["tachys/mark_branches"]
[build-dependencies]
rustc_version = "0.4.1"
rustc_version = { workspace = true, default-features = true }
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
# https://github.com/rust-lang/cargo/issues/4423
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
# This is automatic as long as the leptos crate is depended upon,
# This is automatic as long as the leptos crate is depended upon,
# downstream usage should never manually enable this feature.
[target.'cfg(erase_components)'.dependencies]
leptos_macro = { workspace = true, features = ["__internal_erase_components"] }

View File

@@ -1,8 +1,15 @@
use rustc_version::{version_meta, Channel};
fn main() {
let target = std::env::var("TARGET").unwrap_or_default();
// Set cfg flags depending on release channel
if matches!(version_meta().unwrap().channel, Channel::Nightly) {
println!("cargo:rustc-cfg=rustc_nightly");
}
// Set cfg flag for getrandom wasm_js
if target == "wasm32-unknown-unknown" {
// Set a custom cfg flag for wasm builds
println!("cargo:rustc-cfg=getrandom_backend=\"wasm_js\"");
}
}

View File

@@ -47,7 +47,7 @@ type BoxedChildrenFn = Box<dyn Fn() -> AnyView + Send>;
///
/// Different component types take different types for their `children` prop, some of which cannot
/// be directly constructed. Using `ToChildren` allows the component user to pass children without
/// explicity constructing the correct type.
/// explicitly constructing the correct type.
///
/// ## Examples
///

View File

@@ -1,20 +1,21 @@
use crate::{children::TypedChildren, IntoView};
use futures::{channel::oneshot, future::join_all};
use hydration_context::{SerializedDataId, SharedContext};
use leptos_macro::component;
use reactive_graph::{
computed::ArcMemo,
effect::RenderEffect,
owner::{provide_context, Owner},
owner::{provide_context, ArcStoredValue, Owner},
signal::ArcRwSignal,
traits::{Get, Update, With, WithUntracked},
traits::{Get, Update, With, WithUntracked, WriteValue},
};
use rustc_hash::FxHashMap;
use std::{fmt::Debug, sync::Arc};
use std::{collections::VecDeque, fmt::Debug, mem, sync::Arc};
use tachys::{
html::attribute::{any_attribute::AnyAttribute, Attribute},
hydration::Cursor,
reactive_graph::OwnedView,
ssr::StreamBuilder,
ssr::{StreamBuilder, StreamChunk},
view::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
@@ -96,10 +97,12 @@ where
let hook = hook as Arc<dyn ErrorHook>;
let _guard = throw_error::set_error_hook(Arc::clone(&hook));
let suspended_children = ErrorBoundarySuspendedChildren::default();
let owner = Owner::new();
let children = owner.with(|| {
provide_context(Arc::clone(&hook));
provide_context(suspended_children.clone());
children.into_inner()()
});
@@ -111,11 +114,15 @@ where
children,
errors,
fallback,
suspended_children,
},
owner,
)
}
pub(crate) type ErrorBoundarySuspendedChildren =
ArcStoredValue<Vec<oneshot::Receiver<()>>>;
struct ErrorBoundaryView<Chil, FalFn> {
hook: Arc<dyn ErrorHook>,
boundary_id: SerializedDataId,
@@ -123,6 +130,7 @@ struct ErrorBoundaryView<Chil, FalFn> {
children: Chil,
fallback: FalFn,
errors: ArcRwSignal<Errors>,
suspended_children: ErrorBoundarySuspendedChildren,
}
struct ErrorBoundaryViewState<Chil, Fal> {
@@ -257,6 +265,7 @@ where
children,
fallback,
errors,
suspended_children,
} = self;
ErrorBoundaryView {
hook,
@@ -265,6 +274,7 @@ where
children: children.add_any_attr(attr.into_cloneable_owned()),
fallback,
errors,
suspended_children,
}
}
}
@@ -292,6 +302,7 @@ where
children,
fallback,
errors,
suspended_children,
..
} = self;
ErrorBoundaryView {
@@ -301,6 +312,7 @@ where
children: children.resolve().await,
fallback,
errors,
suspended_children,
}
}
@@ -349,7 +361,8 @@ where
) where
Self: Sized,
{
let _hook = throw_error::set_error_hook(self.hook);
let _hook = throw_error::set_error_hook(Arc::clone(&self.hook));
// first, attempt to serialize the children to HTML, then check for errors
let mut new_buf = StreamBuilder::new(buf.clone_id());
let mut new_pos = *position;
@@ -361,20 +374,76 @@ where
extra_attrs.clone(),
);
// any thrown errors would've been caught here
if self.errors.with_untracked(|map| map.is_empty()) {
buf.append(new_buf);
let suspense_children =
mem::take(&mut *self.suspended_children.write_value());
// not waiting for any suspended children: just render
if suspense_children.is_empty() {
// any thrown errors would've been caught here
if self.errors.with_untracked(|map| map.is_empty()) {
buf.append(new_buf);
} else {
// otherwise, serialize the fallback instead
let mut fallback = String::with_capacity(Fal::MIN_LENGTH);
(self.fallback)(self.errors).to_html_with_buf(
&mut fallback,
position,
escape,
mark_branches,
extra_attrs,
);
buf.push_sync(&fallback);
}
} else {
// otherwise, serialize the fallback instead
let mut fallback = String::with_capacity(Fal::MIN_LENGTH);
(self.fallback)(self.errors).to_html_with_buf(
&mut fallback,
position,
escape,
mark_branches,
extra_attrs,
);
buf.push_sync(&fallback);
let mut position = *position;
// if we're waiting for suspended children, we'll first wait for them to load
// in this implementation, an ErrorBoundary that *contains* Suspense essentially acts
// like a Suspense: it will wait for (all top-level) child Suspense to load before rendering anything
let mut view_buf = StreamBuilder::new(new_buf.clone_id());
view_buf.next_id();
let hook = Arc::clone(&self.hook);
view_buf.push_async(async move {
let _hook = throw_error::set_error_hook(Arc::clone(&hook));
let _ = join_all(suspense_children).await;
let mut my_chunks = VecDeque::new();
for chunk in new_buf.take_chunks() {
match chunk {
StreamChunk::Sync(data) => {
my_chunks.push_back(StreamChunk::Sync(data))
}
StreamChunk::Async { chunks } => {
let chunks = chunks.await;
my_chunks.extend(chunks);
}
StreamChunk::OutOfOrder { chunks } => {
let chunks = chunks.await;
my_chunks.push_back(StreamChunk::OutOfOrder {
chunks: Box::pin(async move { chunks }),
});
}
}
}
if self.errors.with_untracked(|map| map.is_empty()) {
// if no errors, just go ahead with the stream
my_chunks
} else {
// otherwise, serialize the fallback instead
let mut fallback = String::with_capacity(Fal::MIN_LENGTH);
(self.fallback)(self.errors).to_html_with_buf(
&mut fallback,
&mut position,
escape,
mark_branches,
extra_attrs,
);
my_chunks.clear();
my_chunks.push_back(StreamChunk::Sync(fallback));
my_chunks
}
});
buf.append(view_buf);
}
}

View File

@@ -255,7 +255,7 @@ where
) -> Result<Self, serde_qs::Error>;
}
/// Errors that can arise when coverting from an HTML event or form into a Rust data type.
/// Errors that can arise when converting from an HTML event or form into a Rust data type.
#[derive(Error, Debug)]
pub enum FromFormDataError {
/// Could not find a `<form>` connected to the event.

View File

@@ -1,5 +1,5 @@
((root, pkg_path, output_name, wasm_output_name) => {
let MOST_RECENT_CHILDREN_CB;
let MOST_RECENT_CHILDREN_CB = [];
function idle(c) {
if ("requestIdleCallback" in window) {
@@ -22,12 +22,18 @@
traverse(child, children);
}
} else {
if(tag === 'leptos-children') {
MOST_RECENT_CHILDREN_CB = node.$$on_hydrate;
if (tag === 'leptos-children') {
MOST_RECENT_CHILDREN_CB.push(node.$$on_hydrate);
for(const child of node.children) {
traverse(child);
};
// un-set the "most recent children"
MOST_RECENT_CHILDREN_CB.pop();
} else {
for(const child of node.children) {
traverse(child);
};
}
for(const child of node.children) {
traverse(child);
};
}
}
}
@@ -37,8 +43,9 @@
function hydrateIsland(el, id, mod) {
const islandFn = mod[id];
if (islandFn) {
if (MOST_RECENT_CHILDREN_CB) {
MOST_RECENT_CHILDREN_CB();
const children_cb = MOST_RECENT_CHILDREN_CB[MOST_RECENT_CHILDREN_CB.length-1];
if (children_cb) {
children_cb();
}
islandFn(el);
} else {
@@ -48,7 +55,7 @@
idle(() => {
import(`${root}/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default(`${root}/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.default({module_or_path: `${root}/${pkg_path}/${wasm_output_name}.wasm`}).then(() => {
mod.hydrate();
hydrateIslands(document.body, mod);
});

View File

@@ -17,6 +17,10 @@ window.addEventListener("popstate", async (ev) => {
});
window.addEventListener("submit", async (ev) => {
if (ev.defaultPrevented) {
return;
}
const req = submitToReq(ev);
if(!req) {
return;
@@ -194,6 +198,15 @@ function diffRange(oldDocument, oldRoot, newDocument, newRoot, oldEnd, newEnd) {
else if (oldNode.nodeType === Node.TEXT_NODE) {
oldNode.textContent = newNode.textContent;
}
// islands should not be diffed on the client, because we do not want to overwrite client-side state
// but their children should be diffed still, because they could contain new server content
else if (oldNode.nodeType === Node.ELEMENT_NODE && oldNode.tagName === "LEPTOS-ISLAND") {
// TODO: diff the leptos-children
// skip over leptos-island otherwise
oldDocWalker.nextSibling();
newDocWalker.nextSibling();
}
// if it's an element, replace if it's a different tag, or update attributes
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
diffElement(oldNode, newNode);

View File

@@ -83,6 +83,10 @@ pub fn HydrationScripts(
}
}
}
} else {
leptos::logging::error!(
"File hashing is active but no hash file was found"
);
}
} else if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
wasm_file_name.push_str("_bg");
@@ -107,21 +111,19 @@ pub fn HydrationScripts(
.unwrap_or_default();
let root = root.unwrap_or_default();
use_context::<IslandsRouterNavigation>().is_none().then(|| {
view! {
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link
rel="preload"
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
r#as="fetch"
r#type="application/wasm"
crossorigin=nonce.clone().unwrap_or_default()
/>
<script type="module" nonce=nonce>
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
</script>
}
})
view! {
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link
rel="preload"
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
r#as="fetch"
r#type="application/wasm"
crossorigin=nonce.clone().unwrap_or_default()
/>
<script type="module" nonce=nonce>
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
</script>
}
}
/// If this is provided via context, it means that you are using the islands router and

View File

@@ -7,7 +7,7 @@ ws.onmessage = (ev) => {
let found = false;
document.querySelectorAll("link").forEach((link) => {
if (link.getAttribute('href').includes(msg.css)) {
let newHref = '/' + msg.css + '?version=' + new Date().getMilliseconds();
let newHref = '/' + msg.css + '?version=' + Date.now();
link.setAttribute('href', newHref);
found = true;
}

View File

@@ -162,6 +162,7 @@ pub mod prelude {
pub use crate::{
callback::*, children::*, component::*, control_flow::*, error::*,
form::*, hydration::*, into_view::*, mount::*, suspense::*,
text_prop::*,
};
pub use leptos_config::*;
pub use leptos_dom::helpers::*;

View File

@@ -4,7 +4,7 @@ use base64::{
engine::{self, general_purpose},
Engine,
};
use rand::{thread_rng, RngCore};
use rand::{rng, RngCore};
use std::{fmt::Display, ops::Deref, sync::Arc};
use tachys::html::attribute::AttributeValue;
@@ -171,9 +171,9 @@ const NONCE_ENGINE: engine::GeneralPurpose =
impl Nonce {
/// Generates a new nonce from 16 bytes (128 bits) of random data.
pub fn new() -> Self {
let mut thread_rng = thread_rng();
let mut rng = rng();
let mut bytes = [0; 16];
thread_rng.fill_bytes(&mut bytes);
rng.fill_bytes(&mut bytes);
Nonce(NONCE_ENGINE.encode(bytes).into())
}
}

View File

@@ -8,7 +8,7 @@ use std::sync::Arc;
///
/// Useful for inserting modals and tooltips outside of a cropping layout.
/// If no mount point is given, the portal is inserted in `document.body`;
/// it is wrapped in a `<div>` unless `is_svg` is `true` in which case it's wrappend in a `<g>`.
/// it is wrapped in a `<div>` unless `is_svg` is `true` in which case it's wrapped in a `<g>`.
/// Setting `use_shadow` to `true` places the element in a shadow root to isolate styles.
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
@@ -42,11 +42,15 @@ where
let children = children.into_inner();
Effect::new(move |_| {
let tag = if is_svg { "g" } else { "div" };
let container = document()
.create_element(tag)
.expect("element creation to work");
let container = if is_svg {
document()
.create_element_ns(Some("http://www.w3.org/2000/svg"), "g")
.expect("SVG element creation to work")
} else {
document()
.create_element("div")
.expect("HTML element creation to work")
};
let render_root = if use_shadow {
container

View File

@@ -1,8 +1,9 @@
use crate::{
children::{TypedChildren, ViewFnOnce},
error::ErrorBoundarySuspendedChildren,
IntoView,
};
use futures::{select, FutureExt};
use futures::{channel::oneshot, select, FutureExt};
use hydration_context::SerializedDataId;
use leptos_macro::component;
use reactive_graph::{
@@ -13,7 +14,7 @@ use reactive_graph::{
effect::RenderEffect,
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
traits::{Dispose, Get, Read, Track, With},
traits::{Dispose, Get, Read, Track, With, WriteValue},
};
use slotmap::{DefaultKey, SlotMap};
use std::sync::Arc;
@@ -99,6 +100,8 @@ pub fn Suspense<Chil>(
where
Chil: IntoView + Send + 'static,
{
let error_boundary_parent = use_context::<ErrorBoundarySuspendedChildren>();
let owner = Owner::new();
owner.with(|| {
let (starts_local, id) = {
@@ -129,6 +132,7 @@ where
none_pending,
fallback,
children,
error_boundary_parent,
})
})
}
@@ -150,6 +154,7 @@ pub(crate) struct SuspenseBoundary<const TRANSITION: bool, Fal, Chil> {
pub none_pending: ArcMemo<bool>,
pub fallback: Fal,
pub children: Chil,
pub error_boundary_parent: Option<ErrorBoundarySuspendedChildren>,
}
impl<const TRANSITION: bool, Fal, Chil> Render
@@ -228,12 +233,14 @@ where
none_pending,
fallback,
children,
error_boundary_parent,
} = self;
SuspenseBoundary {
id,
none_pending,
fallback,
children: children.add_any_attr(attr),
error_boundary_parent,
}
}
}
@@ -288,6 +295,13 @@ where
let suspense_context = use_context::<SuspenseContext>().unwrap();
let owner = Owner::current().unwrap();
let mut notify_error_boundary =
self.error_boundary_parent.map(|children| {
let (tx, rx) = oneshot::channel();
children.write_value().push(rx);
tx
});
// we need to wait for one of two things: either
// 1. all tasks are finished loading, or
// 2. we read from a local resource, meaning this Suspense can never resolve on the server
@@ -318,6 +332,9 @@ where
// dropped, so it doesn't matter if we manage to send this.
_ = tx.send(());
}
if let Some(tx) = notify_error_boundary.take() {
_ = tx.send(());
}
}
}
}
@@ -413,6 +430,11 @@ where
extra_attrs,
);
} else {
// calling this will walk over the tree, removing all event listeners
// and other single-threaded values from the view tree. this needs to be
// done because the fallback can be shifted to another thread in push_async below.
self.fallback.dry_resolve();
buf.push_async({
let mut position = *position;
async move {

View File

@@ -1,5 +1,6 @@
use crate::{
children::{TypedChildren, ViewFnOnce},
error::ErrorBoundarySuspendedChildren,
suspense_component::SuspenseBoundary,
IntoView,
};
@@ -7,7 +8,7 @@ use leptos_macro::component;
use reactive_graph::{
computed::{suspense::SuspenseContext, ArcMemo},
effect::Effect,
owner::{provide_context, Owner},
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
traits::{Get, Set, Track, With},
wrappers::write::SignalSetter,
@@ -85,6 +86,8 @@ pub fn Transition<Chil>(
where
Chil: IntoView + Send + 'static,
{
let error_boundary_parent = use_context::<ErrorBoundarySuspendedChildren>();
let owner = Owner::new();
owner.with(|| {
let (starts_local, id) = {
@@ -123,6 +126,7 @@ where
none_pending,
fallback,
children,
error_boundary_parent,
})
})
}

View File

@@ -10,18 +10,18 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
config = { version = "0.15.8", default-features = false, features = [
config = { version = "0.15.11", default-features = false, features = [
"toml",
"convert-case",
] }
regex = "1.11"
serde = { version = "1.0", features = ["derive", "rc"] }
thiserror = "2.0"
typed-builder = "0.20.0"
serde = { features = ["derive", "rc"] , workspace = true, default-features = true }
thiserror = { workspace = true , default-features = true }
typed-builder = { workspace = true , default-features = true }
[dev-dependencies]
tokio = { version = "1.43", features = ["rt", "macros"] }
tempfile = "3.14"
tokio = { features = ["rt", "macros"] , workspace = true, default-features = true }
tempfile = "3.19"
temp-env = { version = "0.3.6", features = ["async_closure"] }
[package.metadata.docs.rs]

View File

@@ -12,19 +12,20 @@ edition.workspace = true
tachys = { workspace = true }
reactive_graph = { workspace = true }
or_poisoned = { workspace = true }
js-sys = "0.3.74"
send_wrapper = "0.6.0"
tracing = { version = "0.1.41", optional = true }
wasm-bindgen = { workspace = true }
serde_json = { version = "1.0", optional = true }
serde = { version = "1.0", optional = true }
js-sys = { workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
tracing = { optional = true , workspace = true, default-features = true }
wasm-bindgen = { workspace = true , default-features = true }
serde_json = { optional = true , workspace = true, default-features = true }
serde = { optional = true , workspace = true, default-features = true }
[dev-dependencies]
leptos = { path = "../leptos" }
[dependencies.web-sys]
version = "0.3.72"
features = ["Location"]
workspace = true
default-features = true
[features]
default = []

View File

@@ -11,18 +11,18 @@ edition.workspace = true
[dependencies]
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
syn = { version = "2.0", features = [
serde = { features = ["derive"] , workspace = true, default-features = true }
syn = { features = [
"full",
"parsing",
"extra-traits",
"visit",
"printing",
] }
quote = "1.0"
rstml = "0.12.0"
proc-macro2 = { version = "1.0", features = ["span-locations", "nightly"] }
parking_lot = "0.12.3"
] , workspace = true, default-features = true }
quote = { workspace = true, default-features = true }
rstml = { workspace = true, default-features = true }
proc-macro2 = { features = ["span-locations", "nightly"] , workspace = true, default-features = true }
parking_lot = { workspace = true, default-features = true }
walkdir = "2.5"
camino = "1.1"
indexmap = "2.6"
indexmap = { workspace = true, default-features = true }

View File

@@ -14,38 +14,38 @@ proc-macro = true
[dependencies]
attribute-derive = { version = "0.10.3", features = ["syn-full"] }
cfg-if = "1.0"
html-escape = "0.2.13"
itertools = { workspace = true }
prettyplease = "0.2.25"
proc-macro-error2 = { version = "2.0", default-features = false }
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
rstml = "0.12.0"
cfg-if = { workspace = true, default-features = true }
html-escape = { workspace = true, default-features = true }
itertools = { workspace = true , default-features = true }
prettyplease = "0.2.32"
proc-macro-error2 = { default-features = false , workspace = true }
proc-macro2 = { workspace = true, default-features = true }
quote = { workspace = true, default-features = true }
syn = { features = ["full"] , workspace = true, default-features = true }
rstml = { workspace = true, default-features = true }
leptos_hot_reload = { workspace = true }
server_fn_macro = { workspace = true }
convert_case = "0.7"
uuid = { version = "1.11", features = ["v4"] }
tracing = { version = "0.1.41", optional = true }
convert_case = { workspace = true , default-features = true }
uuid = { version = "1.16", features = ["v4"] }
tracing = { optional = true , workspace = true, default-features = true }
[dev-dependencies]
log = "0.4.22"
typed-builder = "0.20.0"
trybuild = { workspace = true }
log = "0.4.27"
typed-builder = { workspace = true, default-features = true }
trybuild = { workspace = true , default-features = true }
leptos = { path = "../leptos" }
leptos_router = { path = "../router", features = ["ssr"] }
server_fn = { path = "../server_fn", features = ["cbor"] }
insta = "1.41"
serde = "1.0"
insta = "1.42"
serde = { workspace = true, default-features = true }
[build-dependencies]
rustc_version = "0.4.1"
rustc_version = { workspace = true, default-features = true }
[features]
csr = []
hydrate = []
ssr = ["server_fn_macro/ssr", "leptos/ssr"]
ssr = ["server_fn_macro/ssr"]
nightly = ["server_fn_macro/nightly"]
tracing = ["dep:tracing"]
islands = []

View File

@@ -359,23 +359,11 @@ impl ToTokens for Model {
let component = if is_island {
let hydrate_fn_name = hydrate_fn_name.as_ref().unwrap();
quote! {
{
if ::leptos::context::use_context::<::leptos::reactive::owner::IsHydrating>()
.map(|h| h.0)
.unwrap_or(false) {
::leptos::either::Either::Left(
#component
)
} else {
::leptos::either::Either::Right(
::leptos::tachys::html::islands::Island::new(
stringify!(#hydrate_fn_name),
#component
)
#island_serialized_props
)
}
}
::leptos::tachys::html::islands::Island::new(
stringify!(#hydrate_fn_name),
#component
)
#island_serialized_props
}
} else {
component
@@ -645,7 +633,15 @@ impl Parse for DummyModel {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut attrs = input.call(Attribute::parse_outer)?;
// Drop unknown attributes like #[deprecated]
drain_filter(&mut attrs, |attr| !attr.path().is_ident("doc"));
drain_filter(&mut attrs, |attr| {
let path = attr.path();
!(path.is_ident("doc")
|| path.is_ident("allow")
|| path.is_ident("expect")
|| path.is_ident("warn")
|| path.is_ident("deny")
|| path.is_ident("forbid"))
});
let vis: Visibility = input.parse()?;
let mut sig: Signature = input.parse()?;
@@ -933,12 +929,23 @@ impl UnknownAttrs {
let attrs = attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("doc") {
let path = attr.path();
if path.is_ident("doc") {
if let Meta::NameValue(_) = &attr.meta {
return None;
}
}
if path.is_ident("allow")
|| path.is_ident("expect")
|| path.is_ident("warn")
|| path.is_ident("deny")
|| path.is_ident("forbid")
{
return None;
}
Some((attr.into_token_stream(), attr.span()))
})
.collect_vec();

View File

@@ -361,7 +361,7 @@ fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
cfg_if::cfg_if! {
if #[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))] {
Some(leptos_hot_reload::span_to_stable_id(
site.source_file().path(),
site.file(),
site.start().line()
))
} else {
@@ -409,6 +409,7 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// generate documentation for the component.
///
/// Heres how you would define and use a simple Leptos component which can accept custom properties for a name and age:
///
/// ```rust
/// # use leptos::prelude::*;
/// use std::time::Duration;
@@ -446,6 +447,7 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// ```
///
/// Here are some important details about how Leptos components work within the framework:
///
/// * **The component function only runs once.** Your component function is not a “render” function
/// that re-runs whenever changes happen in the state. Its a “setup” function that runs once to
/// create the user interface, and sets up a reactive system to update it. This means its okay
@@ -458,7 +460,6 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
///
/// ```
/// # use leptos::prelude::*;
///
/// // PascalCase: Generated component will be called MyComponent
/// #[component]
/// fn MyComponent() -> impl IntoView {}
@@ -500,8 +501,10 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// ```
///
/// ## Customizing Properties
///
/// You can use the `#[prop]` attribute on individual component properties (function arguments) to
/// customize the types that component property can receive. You can use the following attributes:
///
/// * `#[prop(into)]`: This will call `.into()` on any value passed into the component prop. (For example,
/// you could apply `#[prop(into)]` to a prop that takes
/// [Signal](https://docs.rs/leptos/latest/leptos/struct.Signal.html), which would
@@ -514,6 +517,11 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// * `#[prop(optional_no_strip)]`: The same as `optional`, but requires values to be passed as `None` or
/// `Some(T)` explicitly. This means that the optional property can be omitted (and be `None`), or explicitly
/// specified as either `None` or `Some(T)`.
/// * `#[prop(default = <expr>)]`: Optional property that specifies a default value, which is used when the
/// property is not specified.
/// * `#[prop(name = "new_name")]`: Specifiy a different name for the property. Can be used to destructure
/// fields in component function parameters (see example below).
///
/// ```rust
/// # use leptos::prelude::*;
///
@@ -522,6 +530,8 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// #[prop(into)] name: String,
/// #[prop(optional)] optional_value: Option<i32>,
/// #[prop(optional_no_strip)] optional_no_strip: Option<i32>,
/// #[prop(default = 7)] optional_default: i32,
/// #[prop(name = "data")] UserInfo { email, user_id }: UserInfo,
/// ) -> impl IntoView {
/// // whatever UI you need
/// }
@@ -530,16 +540,24 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// pub fn App() -> impl IntoView {
/// view! {
/// <MyComponent
/// name="Greg" // automatically converted to String with `.into()`
/// optional_value=42 // received as `Some(42)`
/// optional_no_strip=Some(42) // received as `Some(42)`
/// name="Greg" // automatically converted to String with `.into()`
/// optional_value=42 // received as `Some(42)`
/// optional_no_strip=Some(42) // received as `Some(42)`
/// optional_default=42 // received as `42`
/// data=UserInfo {email: "foo", user_id: "bar" }
/// />
/// <MyComponent
/// name="Bob" // automatically converted to String with `.into()`
/// // optional values can both be omitted, and received as `None`
/// data=UserInfo {email: "foo", user_id: "bar" }
/// // optional values can be omitted
/// />
/// }
/// }
///
/// pub struct UserInfo {
/// pub email: &'static str,
/// pub user_id: &'static str,
/// }
/// ```
#[proc_macro_error2::proc_macro_error]
#[proc_macro_attribute]

View File

@@ -1,8 +1,6 @@
[package]
name = "leptos_server"
# TODO revert to { workspace = true } before 0.8.0 release
# this is a hack because I missing bumping the hydration_context version number before publishing
version = "0.8.0-rc1"
version = { workspace = true }
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -12,24 +10,24 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
base64 = "0.22.1"
base64 = { workspace = true, default-features = true }
codee = { version = "0.3.0", features = ["json_serde"] }
hydration_context = { workspace = true }
reactive_graph = { workspace = true, features = ["hydration"] }
server_fn = { workspace = true }
tracing = { version = "0.1.41", optional = true }
futures = "0.3.31"
tracing = { optional = true , workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
any_spawner = { workspace = true }
or_poisoned = { workspace = true }
tachys = { workspace = true, optional = true, features = ["reactive_graph"] }
send_wrapper = "0.6"
send_wrapper = { workspace = true, default-features = true }
# serialization formats
serde = { version = "1.0" }
js-sys = { version = "0.3.74", optional = true }
wasm-bindgen = { version = "0.2.100", optional = true }
serde_json = { workspace = true }
serde = { workspace = true, default-features = true }
js-sys = { optional = true , workspace = true, default-features = true }
wasm-bindgen = { workspace = true, optional = true , default-features = true }
serde_json = { workspace = true , default-features = true }
[features]
ssr = []

View File

@@ -14,11 +14,13 @@ use reactive_graph::{
ArcRwSignal, RwSignal,
},
traits::{
DefinedAt, IsDisposed, ReadUntracked, Track, Update, With, Write,
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Update, With, Write,
},
};
use std::{
future::{pending, Future, IntoFuture},
ops::{Deref, DerefMut},
panic::Location,
};
@@ -41,6 +43,14 @@ impl<T> Clone for ArcLocalResource<T> {
}
}
impl<T> Deref for ArcLocalResource<T> {
type Target = ArcAsyncDerived<T>;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl<T> ArcLocalResource<T> {
/// Creates the resource.
///
@@ -62,7 +72,7 @@ impl<T> ArcLocalResource<T> {
pending().await
} else {
// LocalResources that are immediately available can cause a hydration error,
// because the future *looks* like it is alredy ready (and therefore would
// because the future *looks* like it is already ready (and therefore would
// already have been rendered to html on the server), but in fact was ignored
// on the server. the simplest way to avoid this is to ensure that we always
// wait a tick before resolving any value for a localresource.
@@ -157,6 +167,32 @@ impl<T> DefinedAt for ArcLocalResource<T> {
}
}
impl<T> Notify for ArcLocalResource<T>
where
T: 'static,
{
fn notify(&self) {
self.data.notify()
}
}
impl<T> Write for ArcLocalResource<T>
where
T: 'static,
{
type Value = Option<T>;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.data.try_write()
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.data.try_write_untracked()
}
}
impl<T> ReadUntracked for ArcLocalResource<T>
where
T: 'static,
@@ -241,6 +277,14 @@ pub struct LocalResource<T> {
defined_at: &'static Location<'static>,
}
impl<T> Deref for LocalResource<T> {
type Target = AsyncDerived<T>;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl<T> Clone for LocalResource<T> {
fn clone(&self) -> Self {
*self
@@ -270,7 +314,7 @@ impl<T> LocalResource<T> {
pending().await
} else {
// LocalResources that are immediately available can cause a hydration error,
// because the future *looks* like it is alredy ready (and therefore would
// because the future *looks* like it is already ready (and therefore would
// already have been rendered to html on the server), but in fact was ignored
// on the server. the simplest way to avoid this is to ensure that we always
// wait a tick before resolving any value for a localresource.
@@ -300,6 +344,34 @@ impl<T> LocalResource<T> {
pub fn refetch(&self) {
self.refetch.try_update(|n| *n += 1);
}
/// Synchronously, reactively reads the current value of the resource and applies the function
/// `f` to its value if it is `Some(_)`.
#[track_caller]
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
where
T: 'static,
{
self.data.try_with(|n| n.as_ref().map(f))?
}
}
impl<T, E> LocalResource<Result<T, E>>
where
T: 'static,
E: Clone + 'static,
{
/// Applies the given function when a resource that returns `Result<T, E>`
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
/// calls over the `Option<Result<_, _>>` returned by the resource.
///
/// This is useful when used with features like server functions, in conjunction
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
/// left to handle the `None` and `Err(_)` states.
#[track_caller]
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
}
}
impl<T> IntoFuture for LocalResource<T>
@@ -336,6 +408,32 @@ impl<T> DefinedAt for LocalResource<T> {
}
}
impl<T> Notify for LocalResource<T>
where
T: 'static,
{
fn notify(&self) {
self.data.notify()
}
}
impl<T> Write for LocalResource<T>
where
T: 'static,
{
type Value = Option<T>;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.data.try_write()
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.data.try_write_untracked()
}
}
impl<T> ReadUntracked for LocalResource<T>
where
T: 'static,

View File

@@ -26,7 +26,7 @@ use reactive_graph::{
};
use std::{
future::{pending, IntoFuture},
ops::Deref,
ops::{Deref, DerefMut},
panic::Location,
sync::{
atomic::{AtomicBool, Ordering},
@@ -162,6 +162,32 @@ where
}
}
impl<T, Ser> Notify for ArcResource<T, Ser>
where
T: 'static,
{
fn notify(&self) {
self.data.notify()
}
}
impl<T, Ser> Write for ArcResource<T, Ser>
where
T: 'static,
{
type Value = Option<T>;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.data.try_write()
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.data.try_write_untracked()
}
}
impl<T, Ser> ReadUntracked for ArcResource<T, Ser>
where
T: 'static,
@@ -842,6 +868,32 @@ where
}
}
impl<T, Ser> Notify for Resource<T, Ser>
where
T: Send + Sync + 'static,
{
fn notify(&self) {
self.data.notify()
}
}
impl<T, Ser> Write for Resource<T, Ser>
where
T: Send + Sync + 'static,
{
type Value = Option<T>;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.data.try_write()
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.data.try_write_untracked()
}
}
impl<T, Ser> ReadUntracked for Resource<T, Ser>
where
T: Send + Sync + 'static,

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.8.0-rc1"
version = "0.8.2"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -10,17 +10,18 @@ edition.workspace = true
[dependencies]
leptos = { workspace = true }
once_cell = "1.20"
once_cell = { workspace = true, default-features = true }
or_poisoned = { workspace = true }
indexmap = "2.6"
send_wrapper = "0.6.0"
tracing = { version = "0.1.41", optional = true }
wasm-bindgen = { workspace = true }
futures = "0.3.31"
indexmap = { workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
tracing = { optional = true , workspace = true, default-features = true }
wasm-bindgen = { workspace = true , default-features = true }
futures = { workspace = true, default-features = true }
[dependencies.web-sys]
version = "0.3.72"
features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
workspace = true
default-features = true
[features]
default = []

View File

@@ -216,6 +216,13 @@ impl ServerMetaContextOutput {
self,
mut stream: impl Stream<Item = String> + Send + Unpin,
) -> impl Stream<Item = String> + Send {
// if the first chunk consists of a synchronously-available Suspend,
// inject_meta_context can accidentally run a tick before it, but the Suspend
// when both are available. waiting a tick before awaiting the first chunk
// in the Stream ensures that this always runs after that first chunk
// see https://github.com/leptos-rs/leptos/issues/3976 for the original issue
leptos::task::tick().await;
// wait for the first chunk of the stream, to ensure our components hve run
let mut first_chunk = stream.next().await.unwrap_or_default();
@@ -242,23 +249,22 @@ impl ServerMetaContextOutput {
let head_loc = first_chunk
.find("</head>")
.expect("you are using leptos_meta without a </head> tag");
let marker_loc =
first_chunk.find("<!--HEAD-->").unwrap_or_else(|| {
let marker_loc = first_chunk
.find("<!--HEAD-->")
.map(|pos| pos + "<!--HEAD-->".len())
.unwrap_or_else(|| {
first_chunk.find("</head>").unwrap_or(head_loc)
});
let (before_marker, after_marker) =
first_chunk.split_at_mut(marker_loc);
let (before_head_close, after_head) =
after_marker.split_at_mut(head_loc - marker_loc);
buf.push_str(before_marker);
buf.push_str(&meta_buf);
if let Some(title) = title {
buf.push_str("<title>");
buf.push_str(&title);
buf.push_str("</title>");
}
buf.push_str(before_head_close);
buf.push_str(&meta_buf);
buf.push_str(after_head);
buf.push_str(after_marker);
buf
};
@@ -446,7 +452,7 @@ where
tracing::warn!("{}", msg);
#[cfg(not(feature = "tracing"))]
eprintln!("{}", msg);
eprintln!("{msg}");
}
}

View File

@@ -7,6 +7,9 @@ use leptos::{
/// Injects an [`HTMLLinkElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
/// head that loads a stylesheet from the URL given by the `href` property.
///
/// Note that this does *not* work with the `cargo-leptos` `hash-files` feature: if you are using file
/// hashing, you should use [`HashedStylesheet`](crate::HashedStylesheet).
///
/// ```
/// use leptos::prelude::*;
/// use leptos_meta::*;

View File

@@ -9,8 +9,8 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
serde = "1.0"
thiserror = "2.0"
serde = { workspace = true, default-features = true }
thiserror = { workspace = true , default-features = true }
[dev-dependencies]
serde_json = { workspace = true }
serde_json = { workspace = true , default-features = true }

View File

@@ -19,9 +19,9 @@ leptos_router = { path = "../../router" }
log = "0.4.0"
simple_logger = "5.0"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7.0", optional = true, features = ["macros"] }
tower = { version = "0.4.0", optional = true }
tower-http = { version = "0.5.0", features = ["fs"], optional = true }
axum = { version = "0.8.0", optional = true, features = ["macros"] }
tower = { version = "0.5.0", optional = true }
tower-http = { version = "0.6.0", features = ["fs"], optional = true }
tokio = { version = "1.0", features = ["full"], optional = true }
http = { version = "1.0" }
sqlx = { version = "0.8.0", features = [
@@ -30,10 +30,13 @@ sqlx = { version = "0.8.0", features = [
], optional = true }
thiserror = "1.0"
wasm-bindgen = "0.2.0"
axum_session_auth = { version = "0.14.0", features = [], optional = true }
axum_session = { version = "0.14.0", features = [], optional = true }
axum_session_sqlx = { version = "0.3.0", features = [ "sqlite", "tls-rustls"], optional = true }
bcrypt = { version = "0.15.0", optional = true }
axum_session_auth = { version = "0.16.0", features = [], optional = true }
axum_session = { version = "0.16.0", features = [], optional = true }
axum_session_sqlx = { version = "0.5.0", features = [
"sqlite",
"tls-rustls",
], optional = true }
bcrypt = { version = "0.17.0", optional = true }
async-trait = { version = "0.1.0", optional = true }
[features]

View File

@@ -185,7 +185,7 @@ pub async fn foo() -> Result<String, ServerFnError> {
pub async fn get_user() -> Result<Option<User>, ServerFnError> {
use crate::todo::ssr::auth;
let auth = auth()?;
let auth = auth().await?;
Ok(auth.current_user)
}
@@ -199,7 +199,7 @@ pub async fn login(
use self::ssr::*;
let pool = pool()?;
let auth = auth()?;
let auth = auth().await?;
let (user, UserPasshash(expected_passhash)) =
User::get_from_username_with_passhash(username, &pool)
@@ -229,7 +229,7 @@ pub async fn signup(
use self::ssr::*;
let pool = pool()?;
let auth = auth()?;
let auth = auth().await?;
if password != password_confirmation {
return Err(ServerFnError::ServerError(
@@ -264,7 +264,7 @@ pub async fn signup(
pub async fn logout() -> Result<(), ServerFnError> {
use self::ssr::*;
let auth = auth()?;
let auth = auth().await?;
auth.logout_user();
leptos_axum::redirect("/");

View File

@@ -1,62 +1,12 @@
use axum::{
body::Body as AxumBody,
extract::{Path, State},
http::Request,
response::{IntoResponse, Response},
routing::get,
Router,
};
use axum::Router;
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::{AuthConfig, AuthSessionLayer};
use axum_session_sqlx::SessionSqlitePool;
use leptos::{
config::get_configuration, logging::log, prelude::provide_context,
};
use leptos_axum::{
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
};
use session_auth_axum::{
auth::{ssr::AuthSession, User},
state::AppState,
todo::*,
};
use leptos::{config::get_configuration, logging::log};
use leptos_axum::{generate_route_list, LeptosRoutes};
use session_auth_axum::{auth::User, state::AppState, todo::*};
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
async fn server_fn_handler(
State(app_state): State<AppState>,
auth_session: AuthSession,
path: Path<String>,
request: Request<AxumBody>,
) -> impl IntoResponse {
log!("{:?}", path);
handle_server_fns_with_context(
move || {
provide_context(auth_session.clone());
provide_context(app_state.pool.clone());
},
request,
)
.await
}
async fn leptos_routes_handler(
auth_session: AuthSession,
state: State<AppState>,
req: Request<AxumBody>,
) -> Response {
let State(app_state) = state.clone();
let handler = leptos_axum::render_route_with_context(
app_state.routes.clone(),
move || {
provide_context(auth_session.clone());
provide_context(app_state.pool.clone());
},
move || shell(app_state.leptos_options.clone()),
);
handler(state, req).await.into_response()
}
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Info)
@@ -82,18 +32,6 @@ async fn main() {
eprintln!("{e:?}");
}
// Explicit server function registration is no longer required
// on the main branch. On 0.3.0 and earlier, uncomment the lines
// below to register the server functions.
// _ = GetTodos::register();
// _ = AddTodo::register();
// _ = DeleteTodo::register();
// _ = Login::register();
// _ = Logout::register();
// _ = Signup::register();
// _ = GetUser::register();
// _ = Foo::register();
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).unwrap();
let leptos_options = conf.leptos_options;
@@ -108,11 +46,10 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route(
"/api/*fn_name",
get(server_fn_handler).post(server_fn_handler),
)
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
.leptos_routes(&app_state, routes, {
let options = app_state.leptos_options.clone();
move || shell(options.clone())
})
.fallback(leptos_axum::file_and_error_handler::<AppState, _>(shell))
.layer(
AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(

View File

@@ -1,7 +1,7 @@
use crate::{auth::*, error_template::ErrorTemplate};
use leptos::prelude::*;
use leptos_meta::*;
use leptos_router::{components::*, *};
use leptos_router::{components::*, path};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -16,19 +16,21 @@ pub struct Todo {
#[cfg(feature = "ssr")]
pub mod ssr {
use super::Todo;
use crate::auth::{ssr::AuthSession, User};
use crate::{
auth::{ssr::AuthSession, User},
state::AppState,
};
use leptos::prelude::*;
use sqlx::SqlitePool;
pub fn pool() -> Result<SqlitePool, ServerFnError> {
use_context::<SqlitePool>()
with_context::<AppState, _>(|state| state.pool.clone())
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
}
pub fn auth() -> Result<AuthSession, ServerFnError> {
use_context::<AuthSession>().ok_or_else(|| {
ServerFnError::ServerError("Auth session missing.".into())
})
pub async fn auth() -> Result<AuthSession, ServerFnError> {
let auth = leptos_axum::extract().await?;
Ok(auth)
}
#[derive(sqlx::FromRow, Clone)]
@@ -165,7 +167,7 @@ pub fn TodoApp() -> impl IntoView {
", "
<A href="/login">"Login"</A>
", "
<span>{format!("Login error: {}", e)}</span>
<span>{format!("Login error: {e}")}</span>
}
.into_any()
}

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.2.0-rc1"
version = "0.2.2"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -12,28 +12,28 @@ edition.workspace = true
[dependencies]
any_spawner = { workspace = true }
or_poisoned = { workspace = true }
futures = "0.3.31"
futures = { workspace = true, default-features = true }
hydration_context = { workspace = true, optional = true }
pin-project-lite = "0.2.15"
rustc-hash = "2.0"
serde = { version = "1.0", features = ["derive"], optional = true }
slotmap = "1.0"
thiserror = "2.0"
tracing = { version = "0.1.41", optional = true }
guardian = "1.2"
pin-project-lite = { workspace = true, default-features = true }
rustc-hash = { workspace = true, default-features = true }
serde = { features = ["derive"], optional = true , workspace = true, default-features = true }
slotmap = { workspace = true, default-features = true }
thiserror = { workspace = true , default-features = true }
tracing = { optional = true , workspace = true, default-features = true }
guardian = { workspace = true, default-features = true }
async-lock = "3.4.0"
send_wrapper = { version = "0.6.0", features = ["futures"] }
send_wrapper = { features = ["futures"] , workspace = true, default-features = true }
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
web-sys = { version = "0.3.72", features = ["console"] }
web-sys = { version = "0.3.77", features = ["console"] }
[dev-dependencies]
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
tokio-test = { version = "0.4.4" }
tokio = { features = ["rt-multi-thread", "macros"] , workspace = true, default-features = true }
tokio-test = { workspace = true, default-features = true }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
[build-dependencies]
rustc_version = "0.4.1"
rustc_version = { workspace = true, default-features = true }
[features]
nightly = []

View File

@@ -234,7 +234,8 @@ macro_rules! spawn_derived {
subscribers: SubscriberSet::new(),
state: AsyncDerivedState::Clean,
version: 0,
suspenses: Vec::new()
suspenses: Vec::new(),
pending_suspenses: Vec::new()
}));
let value = Arc::new(AsyncRwLock::new($initial));
let wakers = Arc::new(RwLock::new(Vec::new()));
@@ -364,7 +365,7 @@ macro_rules! spawn_derived {
// generate and assign new value
loading.store(true, Ordering::Relaxed);
let (this_version, suspense_ids) = {
let this_version = {
let mut guard = inner.write().or_poisoned();
guard.version += 1;
let version = guard.version;
@@ -372,14 +373,17 @@ macro_rules! spawn_derived {
.into_iter()
.map(|sc| sc.task_id())
.collect::<Vec<_>>();
(version, suspense_ids)
guard.pending_suspenses.extend(suspense_ids);
version
};
let new_value = fut.await;
drop(suspense_ids);
let latest_version = inner.read().or_poisoned().version;
let latest_version = {
let mut guard = inner.write().or_poisoned();
drop(mem::take(&mut guard.pending_suspenses));
guard.version
};
if latest_version == this_version {
Self::set_inner_value(new_value, value, wakers, inner, loading, Some(ready_tx)).await;
@@ -514,7 +518,9 @@ impl<T: 'static> ArcAsyncDerived<T> {
{
let fun = move || {
let fut = fun();
async move { SendOption::new(Some(fut.await)) }
ScopedFuture::new_untracked(async move {
SendOption::new(Some(fut.await))
})
};
let initial_value = SendOption::new(initial_value);
let (this, _) = spawn_derived!(
@@ -560,7 +566,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
};
let initial_value = SendOption::new_local(initial_value);
let (this, _) = spawn_derived!(
crate::spawn_local_scoped,
crate::spawn_local,
initial_value,
fun,
true,
@@ -595,7 +601,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
async move { SendOption::new_local(Some(fut.await)) }
};
let (this, _) = spawn_derived!(
crate::spawn_local_scoped,
crate::spawn_local,
initial,
fun,
false,
@@ -641,6 +647,14 @@ impl<T: 'static> Write for ArcAsyncDerived<T> {
type Value = Option<T>;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
// increment the version, such that a rerun triggered previously does not overwrite this
// new value
let mut guard = self.inner.write().or_poisoned();
guard.version += 1;
// tell any suspenses to stop waiting for this
drop(mem::take(&mut guard.pending_suspenses));
Some(MappedMut::new(
WriteGuard::new(self.clone(), self.value.blocking_write()),
|v| v.deref(),
@@ -651,6 +665,14 @@ impl<T: 'static> Write for ArcAsyncDerived<T> {
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
// increment the version, such that a rerun triggered previously does not overwrite this
// new value
let mut guard = self.inner.write().or_poisoned();
guard.version += 1;
// tell any suspenses to stop waiting for this
drop(mem::take(&mut guard.pending_suspenses));
Some(MappedMut::new(
self.value.blocking_write(),
|v| v.deref(),

View File

@@ -14,8 +14,10 @@ use crate::{
unwrap_signal,
};
use core::fmt::Debug;
use or_poisoned::OrPoisoned;
use std::{
future::Future,
mem,
ops::{Deref, DerefMut},
panic::Location,
};
@@ -27,7 +29,7 @@ use std::{
/// values that depend on it that it has changed.
///
/// This is an arena-allocated type, which is `Copy` and is disposed when its reactive
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that livesas
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that lives as
/// as long as a reference to it is alive, see [`ArcAsyncDerived`].
///
/// ## Examples
@@ -349,6 +351,17 @@ where
let guard = self
.inner
.try_with_value(|n| n.value.blocking_write_arc())?;
self.inner.try_with_value(|n| {
let mut guard = n.inner.write().or_poisoned();
// increment the version, such that a rerun triggered previously does not overwrite this
// new value
guard.version += 1;
// tell any suspenses to stop waiting for this
drop(mem::take(&mut guard.pending_suspenses));
});
Some(MappedMut::new(
WriteGuard::new(*self, guard),
|v| v.deref(),
@@ -359,6 +372,16 @@ where
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.inner.try_with_value(|n| {
let mut guard = n.inner.write().or_poisoned();
// increment the version, such that a rerun triggered previously does not overwrite this
// new value
guard.version += 1;
// tell any suspenses to stop waiting for this
drop(mem::take(&mut guard.pending_suspenses));
});
self.inner
.try_with_value(|n| n.value.blocking_write_arc())
.map(|inner| {

View File

@@ -1,3 +1,4 @@
use super::suspense::TaskHandle;
use crate::{
channel::Sender,
computed::suspense::SuspenseContext,
@@ -22,6 +23,7 @@ pub(crate) struct ArcAsyncDerivedInner {
pub state: AsyncDerivedState,
pub version: usize,
pub suspenses: Vec<SuspenseContext>,
pub pending_suspenses: Vec<TaskHandle>,
}
#[derive(Debug, PartialEq, Eq)]

View File

@@ -42,6 +42,18 @@ impl<Fut> ScopedFuture<Fut> {
fut,
}
}
/// Wraps the given `Future` by taking the current [`Owner`] re-setting it as the
/// active owner every time the inner `Future` is polled. Always untracks, i.e., clears
/// the active [`Observer`] when polled.
pub fn new_untracked(fut: Fut) -> Self {
let owner = Owner::current().unwrap_or_default();
Self {
owner,
observer: None,
fut,
}
}
}
impl<Fut: Future> Future for ScopedFuture<Fut> {

View File

@@ -15,7 +15,7 @@ pub struct MemoInner<T, S>
where
S: Storage<T>,
{
/// Must always be aquired *after* the reactivity lock
/// Must always be acquired *after* the reactivity lock
pub(crate) value: Arc<RwLock<Option<S::Wrapped>>>,
#[allow(clippy::type_complexity)]
pub(crate) fun: Arc<dyn Fn(Option<T>) -> (T, bool) + Send + Sync>,
@@ -137,7 +137,7 @@ where
})
});
// Two locks are aquired, so order matters.
// Two locks are acquired, so order matters.
let reactivity_lock = self.reactivity.write().or_poisoned();
{
// Safety: Can block endlessly if the user is has a ReadGuard on the value

View File

@@ -27,7 +27,7 @@ use std::{fmt::Debug, hash::Hash, panic::Location};
/// not re-run the calculation when a source signal changes until they are read again.
///
/// This is an arena-allocated type, which is `Copy` and is disposed when its reactive
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that livesas
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that lives as
/// as long as a reference to it is alive, see [`ArcMemo`].
///
/// ```

View File

@@ -14,7 +14,7 @@ pub struct SpecialNonReactiveZone;
/// Exits the "special non-reactive zone" when dropped.
#[derive(Debug)]
pub struct SpecialNonReactiveZoneGuard;
pub struct SpecialNonReactiveZoneGuard(bool);
use pin_project_lite::pin_project;
use std::{
@@ -31,8 +31,8 @@ thread_local! {
impl SpecialNonReactiveZone {
/// Suppresses warnings about non-reactive accesses until the guard is dropped.
pub fn enter() -> SpecialNonReactiveZoneGuard {
IS_SPECIAL_ZONE.set(true);
SpecialNonReactiveZoneGuard
let prev = IS_SPECIAL_ZONE.replace(true);
SpecialNonReactiveZoneGuard(prev)
}
#[cfg(all(debug_assertions, feature = "effects"))]
@@ -48,7 +48,7 @@ impl SpecialNonReactiveZone {
impl Drop for SpecialNonReactiveZoneGuard {
fn drop(&mut self) {
IS_SPECIAL_ZONE.set(false);
IS_SPECIAL_ZONE.set(self.0);
}
}

View File

@@ -369,7 +369,7 @@ mod inner {
const MSG: &str = "ImmediateEffect recursed more than once.";
match effect.defined_at() {
Some(defined_at) => {
log_warning(format_args!("{MSG} Defined at: {}", defined_at));
log_warning(format_args!("{MSG} Defined at: {defined_at}"));
}
None => {
log_warning(format_args!("{MSG}"));

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