Compare commits

...

98 Commits

Author SHA1 Message Date
Greg Johnston
10db2f83eb v0.7.3 2025-01-04 08:55:44 -05:00
Greg Johnston
357f3be393 fix: erase_components with AttributeInterceptor (#3435) 2025-01-04 08:46:17 -05:00
Soso
a1f5d6f79f feat: impl IntoClass for Cow<'_, str> (#3420)
I also thought about making the implementation more generic, for example over
T: AsRef<str>, but that would change the associated types and be a breaking change
it would also conflict with the `Option` impl due to the orphan rules.

Maybe there is something to be done with Borrow or Deref in a future release?
2025-01-03 15:42:42 -05:00
Michael Scofield
34ecd2d541 feat: add Dispose for Store (#3429) 2025-01-03 15:37:59 -05:00
Michael Scofield
df38b075d8 feat: add Default to stores (#3432) 2025-01-03 15:37:41 -05:00
Konstantin Stepanov
46233e3097 fix: avoid failure on guard try_new() functions (#3436) 2025-01-03 15:37:06 -05:00
Greg Johnston
46f6d9ae26 fix: correct ownership in redirect route hook (closes #3425) (#3428) 2024-12-31 11:48:52 -05:00
Chris
8264d478e4 docs: remove islands mention from leptos_axum (#3423) 2024-12-31 10:35:20 -05:00
Greg Johnston
9c54da304e Merge pull request #3424 from leptos-rs/3419
Fix issues with island hydration when nested in a closure, and with context (closes #3419)
2024-12-31 10:35:02 -05:00
Michael Scofield
1c002a2df8 chore: add missing #[track_caller]s (#3422) 2024-12-31 10:34:47 -05:00
Greg Johnston
6e4dac91bf perf: optimize and simplify island walk 2024-12-31 09:02:36 -05:00
Greg Johnston
6493b0b359 fix: a more substantial island context system that restores ownership properly 2024-12-30 19:13:55 -05:00
Greg Johnston
d58042b7ba fix: establish hydration status via ownership and context 2024-12-30 16:49:43 -05:00
Georg Vienna
7a5b27ad20 feat: allow prop destructuring via new #[prop(name = ...)] (#3382) 2024-12-27 16:57:34 -05:00
Spencer Ferris
78d0dbdfa2 fix: provide custom state in file_and_error_handler (#3408)
The `file_and_error_handler` provided by leptos_axum does not provide
the app's custom state in the context. This means that when the fallback
handler tries to render the app, the app will not be able to access the
custom state (and will panic if it uses `expect_context`).

The handle defined by `file_and_error_handler` has access to the custom
state from Axum, so we can pass the custom state from Axum into the
Leptos handler using the `additional_context` parameter of
`handle_response_inner`.
2024-12-27 13:44:39 -05:00
Greg Johnston
1c30a4c131 fix: getrandom needs js feature (used when nonce feature is active) (closes #3409) (#3410) 2024-12-26 07:29:53 -05:00
Greg Johnston
b91429d4f0 fix: correctly track updates to keyed fields in keyed iterator (closes #3401) (#3403) 2024-12-24 08:14:14 -05:00
Alexis Fontaine
6663fddf76 fix: enable console feature of web-sys (#3406) 2024-12-24 08:14:01 -05:00
Greg Johnston
28af33d511 v0.7.2 2024-12-21 14:09:01 -05:00
Greg Johnston
5a3413d3b3 fix: correct hydration position for static text nodes in nightly (closes #3395) (#3396) 2024-12-21 14:02:38 -05:00
Saber Haj Rabiee
702d2e247b fix(ci): add missing glib for semver checks (#3393) 2024-12-21 09:07:07 -05:00
Greg Johnston
71f698f322 fix: correct span for let: syntax (closes #3387) (#3391) 2024-12-20 15:35:48 -05:00
Michael Scofield
8d4776bf5f feat: add a From<ArcStore<T>> for Store<T, S> (#3389) 2024-12-20 13:02:48 -05:00
Oliver
65798e430f docs: showcase let syntax in for_loop (closes #3059) (#3383)
* docs: showcase let syntax in for_loop

* fix: doctests on for_loop

---------

Co-authored-by: Oliver Nordh <oliver.nordh@proton.me>
2024-12-20 13:00:46 -05:00
Greg Johnston
d46d1a4fab Merge pull request #3376 from sabify/fix-ci
fix(ci): missing glib in ci
2024-12-20 12:59:45 -05:00
Alyx Mote
f9533ab75b Update elements.rs (#3379)
fix: add missing `popovertarget` and `popovertargetaction` attributes for `<button>`
2024-12-17 19:26:04 -05:00
Saber Haj Rabiee
d08f8822c0 fix(ci): add missing glib for autofix ci 2024-12-16 23:35:05 -08:00
Saber Haj Rabiee
7357839efb fix(ci): missing glib in ci 2024-12-16 23:34:33 -08:00
Greg Johnston
1b8ad58114 chore: update lockfile 2024-12-16 20:57:56 -05:00
Greg Johnston
ef72f1ce96 v0.7.1 2024-12-16 20:52:00 -05:00
Gary Coady
6a5bfe9a5d feat: impl IntoSplitSignal for Field (closes #3362) (#3364) 2024-12-16 20:48:13 -05:00
stefnotch
1661fe2412 feat: add signal.into_inner() (#3343) 2024-12-16 20:47:25 -05:00
Greg Johnston
2324853155 chore: reenable cargo-semver-checks on PRs (#3375) 2024-12-16 19:48:52 -05:00
Greg Johnston
28a3859365 fix: rebuilding of InertElement (closes #3368) (#3370) 2024-12-16 19:23:15 -05:00
Greg Johnston
6b50179189 docs: clarify Signal::derive() behavior (#3351) 2024-12-16 09:22:30 -05:00
Paul Hansen
0bb825f6bd feat: AttributeInterceptor component to allow passing attributes to other elements (#3340) 2024-12-16 09:18:10 -05:00
Greg Johnston
a6aa111122 fix: only mark memos Check if they aren't already Dirty (closes #3339) (#3356) 2024-12-16 09:10:20 -05:00
Marcus Whybrow
753cfe8e44 chore: implement PatchField for usize (#3346) 2024-12-16 09:09:50 -05:00
zakstucke
bc9c3add87 feat: accessing context by reference (with_context and update_context) (#3279) 2024-12-16 09:09:22 -05:00
Michael Scofield
d0bb45dbc4 chore: update leptos_meta feature docs (#3350) 2024-12-16 08:33:24 -05:00
zakstucke
2a4b80cf22 feat: opt-in locations in release mode with --cfg locations (#3281) 2024-12-16 08:32:57 -05:00
redforks
881734b43a chore: impl Clone trait for TypedChildrenFn (#3349) 2024-12-16 08:32:17 -05:00
bicarlsen
68fdc8d9c4 docs: typo fix in ToChildren::to_children docs. (#3352) 2024-12-14 10:58:49 -05:00
sutantodadang
03ed805137 implemented actix multipart (#3361) 2024-12-14 10:58:29 -05:00
Greg Johnston
5d4603e988 fix: don't try to hydrate inside <noscript> (closes #3360) (#3363) 2024-12-14 10:57:51 -05:00
Greg Johnston
e65969cfcd fix: nested keyed fields in stores (closes #3338) (#3344) 2024-12-11 20:03:41 -05:00
Greg Johnston
e1e90b8595 Merge pull request #3341 from FreezyLemon/fix-some-doc-typos
Fix a doc typo and one broken intra-doc link
2024-12-10 16:27:30 -05:00
bicarlsen
71a136c7af feat: increase number of branch arms for either! macro (#3337) 2024-12-10 16:26:41 -05:00
Greg Johnston
49366be2a5 feat: add scroll prop to <A/> component to control scrolling behavior (closes #2666) (#3333) 2024-12-10 16:26:11 -05:00
FreezyLemon
6f5bdcc675 Fix rustdoc link to ArenaItem on Sandboxed::new 2024-12-09 23:01:27 +01:00
FreezyLemon
726a99441f Fix typo in Signal/ArcSignal 2024-12-09 22:49:07 +01:00
Greg Johnston
775bea46d9 fix: correctly mount/unmount hydrated static text nodes in nightly mode (closes #3334) (#3336) 2024-12-08 12:39:16 -05:00
Greg Johnston
2aa9827a1f fix: correctly support !Send Actix APIs in server functions (#3326)
* fix: correctly support `!Send` Actix APIs in server functions

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2024-12-07 15:41:32 -05:00
Greg Johnston
be3c1811fc fix: correctly swap Vec<_> in the DOM (closes #3321) (#3324)
* fix: correctly swap `Vec<_>` in the DOM (closes #3321)

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2024-12-05 21:08:05 -05:00
Greg Johnston
8df4f3c71d publish 0.1.1 to include either! macro 2024-12-04 18:53:18 -05:00
dependabot[bot]
3028a9acd0 chore(deps): bump rustls in the cargo group across 1 directory (#3289)
Bumps the cargo group with 1 update in the / directory: [rustls](https://github.com/rustls/rustls).


Updates `rustls` from 0.23.16 to 0.23.18
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.16...v/0.23.18)

---
updated-dependencies:
- dependency-name: rustls
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 15:21:55 -08:00
dependabot[bot]
4b167fb809 chore(deps): bump hyper from 1.5.0 to 1.5.1 (#3265)
Bumps [hyper](https://github.com/hyperium/hyper) from 1.5.0 to 1.5.1.
- [Release notes](https://github.com/hyperium/hyper/releases)
- [Changelog](https://github.com/hyperium/hyper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper/compare/v1.5.0...v1.5.1)

---
updated-dependencies:
- dependency-name: hyper
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 15:21:42 -08:00
dependabot[bot]
46ae8ef9b2 chore(deps): bump rkyv from 0.8.8 to 0.8.9 (#3290)
Bumps [rkyv](https://github.com/rkyv/rkyv) from 0.8.8 to 0.8.9.
- [Release notes](https://github.com/rkyv/rkyv/releases)
- [Commits](https://github.com/rkyv/rkyv/commits)

---
updated-dependencies:
- dependency-name: rkyv
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 15:21:26 -08:00
benwis
21f26e7a6b Remove dependa 2024-12-03 09:00:34 -08:00
dependabot[bot]
204648d388 chore(deps): bump wasm-bindgen from 0.2.95 to 0.2.97 (#3317)
Bumps [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) from 0.2.95 to 0.2.97.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/compare/0.2.95...0.2.97)

---
updated-dependencies:
- dependency-name: wasm-bindgen
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ben Wishovich <benwis@users.noreply.github.com>
2024-12-03 08:54:32 -08:00
dependabot[bot]
a04bca55a2 chore(deps): bump tracing from 0.1.40 to 0.1.41 (#3294)
Bumps [tracing](https://github.com/tokio-rs/tracing) from 0.1.40 to 0.1.41.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-0.1.40...tracing-0.1.41)

---
updated-dependencies:
- dependency-name: tracing
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ben Wishovich <benwis@users.noreply.github.com>
2024-12-03 08:50:46 -08:00
dependabot[bot]
1fec678174 chore(deps): bump url from 2.5.3 to 2.5.4 (#3291)
Bumps [url](https://github.com/servo/rust-url) from 2.5.3 to 2.5.4.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.5.3...v2.5.4)

---
updated-dependencies:
- dependency-name: url
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 08:49:04 -08:00
dependabot[bot]
4dbb8d1a42 chore(deps): bump bytes from 1.8.0 to 1.9.0 (#3297)
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: bytes
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 08:48:33 -08:00
dependabot[bot]
fc2c52eb04 chore(deps): bump js-sys from 0.3.72 to 0.3.74 (#3306)
Bumps [js-sys](https://github.com/rustwasm/wasm-bindgen) from 0.3.72 to 0.3.74.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: js-sys
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 08:48:03 -08:00
dependabot[bot]
1ae35ce5b3 chore(deps): bump wasm-bindgen-futures from 0.4.45 to 0.4.47 (#3318)
Bumps [wasm-bindgen-futures](https://github.com/rustwasm/wasm-bindgen) from 0.4.45 to 0.4.47.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: wasm-bindgen-futures
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 08:47:49 -08:00
dependabot[bot]
8edb11f324 chore(deps): bump postcard from 1.0.10 to 1.1.1 (#3319)
Bumps [postcard](https://github.com/jamesmunns/postcard) from 1.0.10 to 1.1.1.
- [Release notes](https://github.com/jamesmunns/postcard/releases)
- [Changelog](https://github.com/jamesmunns/postcard/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jamesmunns/postcard/compare/v1.0.10...postcard/v1.1.1)

---
updated-dependencies:
- dependency-name: postcard
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 08:47:36 -08:00
mahdi739
5a01a7f2ed chore: fix typo (#3320) 2024-12-03 08:47:25 -08:00
Greg Johnston
f252460d02 fix: wait for blocking resources before sending subsequent chunks (close #3280) (#3282) 2024-12-02 16:44:36 -08:00
rjmac
6331b488e4 Remove the Send requirement on a local Action's future (fixes #3309) (#3310)
Co-authored-by: Robert Macomber <robertm@mox>
2024-12-02 09:19:42 -08:00
Gabriel Lopes Veiga
fcba8b3b17 fix: prevent multiple location headers on redirect (#3298) (#3311) 2024-12-01 18:54:35 -08:00
Greg Johnston
d665dd4b89 v0.7.0 2024-11-30 12:09:41 -05:00
Greg Johnston
be740b38ee feat: add NodeRef::on_load() and writeable NodeRef (#3305) 2024-11-30 11:55:13 -05:00
Rakshith Ravi
d2803c938c chore: remove pre-release message from README (#3075) 2024-11-30 11:48:50 -05:00
Greg Johnston
f29224415a fix: sort tuple-syntax class and style in addition to colon-syntax (closes #3296) (#3303) 2024-11-29 19:28:11 -05:00
Greg Johnston
292772c4d6 fix: allow a deprecated wasm-bindgen struct 2024-11-29 15:50:57 -05:00
benwis
5947aa299e Release rc3 2024-11-28 11:28:20 -08:00
Greg Johnston
6098836cf7 docs: improve line location of hydration error message when using view macro (#3293) 2024-11-27 14:36:31 -05:00
Niklas Eicker
2dfa61ff6a docs: fix help message for island macro (#3287) 2024-11-24 14:45:10 -05:00
Darwin Boersma
4f39b0b0ef Version any_spawner alongside other crates, reexport CustomExecutor (#3284)
* rc2 version any_spawner

Signed-off-by: Darwin Boersma <darwin@sadlark.com>

* reexport full any_spawner crate

Signed-off-by: Darwin Boersma <darwin@sadlark.com>

---------

Signed-off-by: Darwin Boersma <darwin@sadlark.com>
2024-11-23 14:49:43 -08:00
dependabot[bot]
980595f1f0 chore(deps): bump proc-macro2 from 1.0.91 to 1.0.92 (#3276)
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.91 to 1.0.92.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.91...1.0.92)

---
updated-dependencies:
- dependency-name: proc-macro2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-23 08:29:52 -05:00
Greg Johnston
14eb707e82 rc2 2024-11-22 15:27:00 -05:00
Greg Johnston
3de0414ed5 docs: adds more helpful text for hydration errors (closes #3267) (#3275) 2024-11-22 15:10:37 -05:00
Greg Johnston
75cae91661 chore: expose some internals of routing to make wrappers easier (#3232) 2024-11-22 15:10:23 -05:00
Greg Johnston
8b258b0d26 fix: notify Suspense on subsequent reloads (closes #3277) (#3278) 2024-11-22 14:55:24 -05:00
dependabot[bot]
84bdd6b568 chore(deps): bump serde_json from 1.0.132 to 1.0.133 (#3261)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.132 to 1.0.133.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.132...v1.0.133)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-21 19:24:47 -05:00
dependabot[bot]
809023a2ad chore(deps): bump attribute-derive from 0.10.2 to 0.10.3 (#3260)
Bumps [attribute-derive](https://github.com/ModProg/attribute-derive) from 0.10.2 to 0.10.3.
- [Release notes](https://github.com/ModProg/attribute-derive/releases)
- [Changelog](https://github.com/ModProg/attribute-derive/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ModProg/attribute-derive/commits)

---
updated-dependencies:
- dependency-name: attribute-derive
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-21 19:24:39 -05:00
dependabot[bot]
1477ae2cfb chore(deps): bump axum from 0.7.8 to 0.7.9 (#3259)
Bumps [axum](https://github.com/tokio-rs/axum) from 0.7.8 to 0.7.9.
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.7.8...axum-v0.7.9)

---
updated-dependencies:
- dependency-name: axum
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-21 19:24:31 -05:00
dependabot[bot]
26995f8efd chore(deps): bump tower-http from 0.6.1 to 0.6.2 (#3266)
Bumps [tower-http](https://github.com/tower-rs/tower-http) from 0.6.1 to 0.6.2.
- [Release notes](https://github.com/tower-rs/tower-http/releases)
- [Commits](https://github.com/tower-rs/tower-http/compare/tower-http-0.6.1...tower-http-0.6.2)

---
updated-dependencies:
- dependency-name: tower-http
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-21 19:24:23 -05:00
dependabot[bot]
3c174b26a5 chore(deps): bump syn from 2.0.87 to 2.0.89 (#3272)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.87 to 2.0.89.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.87...2.0.89)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-21 19:24:13 -05:00
dependabot[bot]
0c7e77800a chore(deps): bump proc-macro2 from 1.0.89 to 1.0.91 (#3273)
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.89 to 1.0.91.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.89...1.0.91)

---
updated-dependencies:
- dependency-name: proc-macro2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-21 19:24:04 -05:00
Greg Johnston
fee2421047 chore: fix broken links, etc. in docs (#3269) 2024-11-21 19:23:55 -05:00
Ari Seyhun
89f26f6e9b feat: add fallback prop to ProtectedRoute and ProtectedParentRoute (#3264) 2024-11-19 22:32:45 -08:00
Silver
e76b22bec8 Remove unused shell() function definition (#3254) 2024-11-19 12:17:45 -08:00
Greg Johnston
cff277b3db fix: add Write implementatations for Field<T> and ArcField<T> (closes #3257) (#3262) 2024-11-19 10:04:40 -05:00
Ari Seyhun
0258ac6df4 feat(reactive_stores): add map_untracked to OptionStoreExt (#3245) 2024-11-18 09:41:04 -05:00
Greg Johnston
36132a5823 Merge pull request #3249 from metatoaster/pr3213_e2e_tests
test: aria-current end-to-end tests (#3213)
2024-11-17 15:50:13 -05:00
autofix-ci[bot]
6f35f8d197 [autofix.ci] apply automated fixes 2024-11-17 01:51:40 +00:00
Tommy Yu
3973c4f420 test: aria-current end-to-end tests (#3213) 2024-11-17 14:40:47 +13:00
137 changed files with 2648 additions and 1090 deletions

View File

@@ -1,12 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "cargo"
directories:
- "/"
schedule:
interval: "daily"
open-pull-requests-limit: 10

View File

@@ -13,6 +13,7 @@ concurrency:
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
DEBIAN_FRONTEND: noninteractive
jobs:
autofix:
runs-on: ubuntu-latest
@@ -21,6 +22,10 @@ jobs:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: {toolchain: nightly, 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 jq
run: sudo apt-get install jq
- run: |

View File

@@ -8,15 +8,21 @@ on:
branches:
- main
- leptos_0.6
env:
DEBIAN_FRONTEND: noninteractive
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
test:
needs: [get-leptos-changed]
if: github.event.pull_request.labels[0].name == 'semver' # needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
if: needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
name: Run semver check (nightly-2024-08-01)
runs-on: ubuntu-latest
steps:
- name: Install Glib
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- name: Checkout
uses: actions/checkout@v4
- name: Semver Checks

View File

@@ -14,6 +14,7 @@ on:
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
DEBIAN_FRONTEND: noninteractive
jobs:
test:
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
@@ -33,6 +34,10 @@ jobs:
echo "Disk space after cleanup:"
df -h
# Setup environment
- name: Install Glib
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@master

651
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,36 +40,36 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.7.0-rc1"
version = "0.7.3"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.2.0-rc1" }
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
throw_error = { path = "./any_error/", version = "0.2.0" }
any_spawner = { path = "./any_spawner/", version = "0.2.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
either_of = { path = "./either_of/", version = "0.1.0" }
hydration_context = { path = "./hydration_context", version = "0.2.0-rc1" }
leptos = { path = "./leptos", version = "0.7.0-rc1" }
leptos_config = { path = "./leptos_config", version = "0.7.0-rc1" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-rc1" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-rc1" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-rc1" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-rc1" }
leptos_router = { path = "./router", version = "0.7.0-rc1" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-rc1" }
leptos_server = { path = "./leptos_server", version = "0.7.0-rc1" }
leptos_meta = { path = "./meta", version = "0.7.0-rc1" }
next_tuple = { path = "./next_tuple", version = "0.1.0-rc1" }
hydration_context = { path = "./hydration_context", version = "0.2.0" }
leptos = { path = "./leptos", version = "0.7.3" }
leptos_config = { path = "./leptos_config", version = "0.7.3" }
leptos_dom = { path = "./leptos_dom", version = "0.7.3" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.3" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.3" }
leptos_macro = { path = "./leptos_macro", version = "0.7.3" }
leptos_router = { path = "./router", version = "0.7.3" }
leptos_router_macro = { path = "./router_macro", version = "0.7.3" }
leptos_server = { path = "./leptos_server", version = "0.7.3" }
leptos_meta = { path = "./meta", version = "0.7.3" }
next_tuple = { path = "./next_tuple", version = "0.1.0" }
oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-rc1" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-rc1" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-rc1" }
server_fn = { path = "./server_fn", version = "0.7.0-rc1" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-rc1" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-rc1" }
tachys = { path = "./tachys", version = "0.1.0-rc1" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0" }
server_fn = { path = "./server_fn", version = "0.7.3" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.3" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.3" }
tachys = { path = "./tachys", version = "0.1.0" }
[profile.release]
codegen-units = 1
@@ -78,3 +78,9 @@ opt-level = 'z'
[workspace.metadata.cargo-all-features]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(leptos_debuginfo)',
'cfg(erase_components)',
] }

View File

@@ -12,8 +12,6 @@
You can find a list of useful libraries and example projects at [`awesome-leptos`](https://github.com/leptos-rs/awesome-leptos).
# The `main` branch is currently undergoing major changes in preparation for the [0.7](https://github.com/leptos-rs/leptos/milestone/4) release. For a stable version, please use the [v0.6.13 tag](https://github.com/leptos-rs/leptos/tree/v0.6.13)
# Leptos
```rust

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "any_spawner"
version = "0.1.1"
version = "0.2.1"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -16,8 +16,8 @@ thiserror = "2.0"
tokio = { version = "1.41", optional = true, default-features = false, features = [
"rt",
] }
tracing = { version = "0.1.40", optional = true }
wasm-bindgen-futures = { version = "0.4.45", optional = true }
tracing = { version = "0.1.41", optional = true }
wasm-bindgen-futures = { version = "0.4.47", optional = true }
[features]
async-executor = ["dep:async-executor"]

View File

@@ -1,6 +1,6 @@
[package]
name = "either_of"
version = "0.1.0"
version = "0.1.3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -134,7 +134,7 @@ tuples!(EitherOf14 + EitherOf14Future + EitherOf14FutureProj => A, B, C, D, E, F
tuples!(EitherOf15 + EitherOf15Future + EitherOf15FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
tuples!(EitherOf16 + EitherOf16Future + EitherOf16FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
/// Matches over the first expression and returns an either ([`Either`], [`EitherOf3`], ... [`EitherOf6`])
/// Matches over the first expression and returns an either ([`Either`], [`EitherOf3`], ... [`EitherOf8`])
/// composed of the values returned by the match arms.
///
/// The pattern syntax is exactly the same as found in a match arm.
@@ -197,6 +197,29 @@ macro_rules! either {
$e_pattern => $crate::EitherOf6::E($e_expression),
$f_pattern => $crate::EitherOf6::F($f_expression),
}
};
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr, $g_pattern:pat => $g_expression:expr,) => {
match $match {
$a_pattern => $crate::EitherOf7::A($a_expression),
$b_pattern => $crate::EitherOf7::B($b_expression),
$c_pattern => $crate::EitherOf7::C($c_expression),
$d_pattern => $crate::EitherOf7::D($d_expression),
$e_pattern => $crate::EitherOf7::E($e_expression),
$f_pattern => $crate::EitherOf7::F($f_expression),
$g_pattern => $crate::EitherOf7::G($g_expression),
}
};
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr, $g_pattern:pat => $g_expression:expr, $h_pattern:pat => $h_expression:expr,) => {
match $match {
$a_pattern => $crate::EitherOf8::A($a_expression),
$b_pattern => $crate::EitherOf8::B($b_expression),
$c_pattern => $crate::EitherOf8::C($c_expression),
$d_pattern => $crate::EitherOf8::D($d_expression),
$e_pattern => $crate::EitherOf8::E($e_expression),
$f_pattern => $crate::EitherOf8::F($f_expression),
$g_pattern => $crate::EitherOf8::G($g_expression),
$h_pattern => $crate::EitherOf8::H($h_expression),
}
}; // if you need more eithers feel free to open a PR ;-)
}
@@ -233,4 +256,23 @@ fn either_macro() {
16 => 24u8,
_ => 12,
);
let _: EitherOf7<&str, f64, char, f32, u8, i8, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
17 => 2i8,
_ => 12,
);
let _: EitherOf8<&str, f64, char, f32, u8, i8, u32, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
17 => 2i8,
18 => 42u32,
_ => 12,
);
}

View File

@@ -6,7 +6,116 @@ Feature: Check aria-current being applied to make links bolded
Given I see the app
Scenario: Should see the base case working
Then I see the link Out-of-Order being bolded
Then I see the following links being bolded
Then I see the Out-of-Order link being bolded
And I see the following links being bolded
| Out-of-Order |
| Nested |
And I see the In-Order link not being bolded
And I see the following links not being bolded
| In-Order |
| Single |
Scenario: Should see client-side render the correct bolded links
When I select the link In-Order
And I select the link Single
Then I see the following links being bolded
| In-Order |
| Single |
And I see the following links not being bolded
| Out-of-Order |
| Nested |
Scenario: Should see server-side render the correct bolded links
When I select the link In-Order
And I select the link Single
And I reload the page
Then I see the following links being bolded
| In-Order |
| Single |
And I see the following links not being bolded
| Out-of-Order |
| Nested |
Scenario: Check that the base nested route links are working
When I select the link Instrumented
Then I see the Instrumented link being bolded
And I see the Item Listing link not being bolded
Scenario: Should see going deep down into nested routes bold links
When I select the link Instrumented
And I select the link Target 421
Then I see the following links being bolded
| Instrumented |
| Item Listing |
| Target 4## |
| Target 42# |
| Target 421 |
| field1 |
Scenario: Should see going deep down into nested routes in SSR bold links
When I select the link Instrumented
And I select the link Target 421
And I reload the page
Then I see the following links being bolded
| Instrumented |
| Item Listing |
| Target 4## |
| Target 42# |
| Target 421 |
| field1 |
Scenario: Going deep down navigate around nested links bold correctly
When I select the link Instrumented
And I select the link Target 421
And I select the link Inspect path2/field3
Then I see the following links being bolded
| Instrumented |
| Item Listing |
| Target 4## |
| Target 42# |
| field3 |
And I see the following links not being bolded
| Target 421 |
| field1 |
Scenario: Going deep down navigate around nested links bold correctly, SSR
When I select the link Instrumented
And I select the link Target 421
And I select the link Inspect path2/field3
And I reload the page
Then I see the following links being bolded
| Instrumented |
| Item Listing |
| Target 4## |
| Target 42# |
| field3 |
And I see the following links not being bolded
| Target 421 |
| field1 |
Scenario: Going deep down back out nested routes reset bolded states
When I select the link Instrumented
And I select the link Target 421
And I select the link Counters
Then I see the following links being bolded
| Instrumented |
| Counters |
And I see the following links not being bolded
| Item Listing |
| Target 4## |
| Target 42# |
| Target 421 |
Scenario: Going deep down back out nested routes reset bolded states, SSR
When I select the link Instrumented
And I select the link Target 421
And I select the link Counters
And I reload the page
Then I see the following links being bolded
| Instrumented |
| Counters |
And I see the following links not being bolded
| Item Listing |
| Target 4## |
| Target 42# |
| Target 421 |

View File

@@ -90,3 +90,11 @@ pub async fn link_text_is_aria_current(client: &Client, text: &str) -> Result<()
Ok(())
}
pub async fn link_text_is_not_aria_current(client: &Client, text: &str) -> Result<()> {
let link = find::link_with_text(client, text).await?;
link.attr("aria-current").await?
.map(|_| anyhow::bail!("aria-current mistakenly set for {text}"))
.unwrap_or(Ok(()))
}

View File

@@ -80,7 +80,7 @@ async fn i_see_the_second_count_is(
Ok(())
}
#[then(regex = r"^I see the link (.*) being bolded$")]
#[then(regex = r"^I see the (.*) link being bolded$")]
async fn i_see_the_link_being_bolded(
world: &mut AppWorld,
text: String,
@@ -106,6 +106,32 @@ async fn i_see_the_following_links_being_bolded(
Ok(())
}
#[then(regex = r"^I see the (.*) link not being bolded$")]
async fn i_see_the_link_being_not_bolded(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::link_text_is_not_aria_current(client, &text).await?;
Ok(())
}
#[then(expr = "I see the following links not being bolded")]
async fn i_see_the_following_links_not_being_bolded(
world: &mut AppWorld,
step: &Step,
) -> Result<()> {
let client = &world.client;
if let Some(table) = step.table.as_ref() {
for row in table.rows.iter() {
check::link_text_is_not_aria_current(client, &row[0]).await?;
}
}
Ok(())
}
#[then(expr = "I see the following counters under section")]
#[then(expr = "the following counters under section")]
async fn i_see_the_following_counters_under_section(

View File

@@ -4,25 +4,6 @@ use leptos::prelude::*;
use serde::{Deserialize, Serialize};
use server_fn::ServerFnError;
pub fn shell(leptos_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=leptos_options.clone() />
<HydrationScripts options=leptos_options.clone()/>
<link rel="stylesheet" id="leptos" href="/pkg/todo_app_sqlite_csr.css"/>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
</head>
<body>
<TodoApp/>
</body>
</html>
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Todo {

View File

@@ -1,6 +1,6 @@
[package]
name = "hydration_context"
version = "0.2.0-rc1"
version = "0.2.1"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -14,8 +14,8 @@ throw_error = { workspace = true }
or_poisoned = { workspace = true }
futures = "0.3.31"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = { version = "0.2.95", optional = true }
js-sys = { version = "0.3.72", optional = true }
wasm-bindgen = { version = "0.2.97", optional = true }
js-sys = { version = "0.3.74", optional = true }
once_cell = "1.20"
pin-project-lite = "0.2.15"
@@ -25,3 +25,6 @@ browser = ["dep:wasm-bindgen", "dep:js-sys"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -1,3 +1,8 @@
// #[wasm_bindgen(thread_local)] is deprecated in wasm-bindgen 0.2.96
// but the replacement is also only shipped in that version
// as a result, we'll just allow deprecated for now
#![allow(deprecated)]
use super::{SerializedDataId, SharedContext};
use crate::{PinnedFuture, PinnedStream};
use core::fmt::Debug;

View File

@@ -83,15 +83,14 @@ pub trait SharedContext: Debug {
/// Reads the current value of some data from the shared context, if it has been
/// sent from the server. This returns the serialized data as a `String` that should
/// be deserialized using [`Serializable::de`].
/// be deserialized.
///
/// On the server and in client-side rendered implementations, this should
/// always return [`None`].
fn read_data(&self, id: &SerializedDataId) -> Option<String>;
/// Returns a [`Future`] that resolves with a `String` that should
/// be deserialized using [`Serializable::de`] once the given piece of server
/// data has resolved.
/// be deserialized once the given piece of server data has resolved.
///
/// On the server and in client-side rendered implementations, this should
/// return a [`Future`] that is immediately ready with [`None`].
@@ -148,8 +147,8 @@ pub trait SharedContext: Debug {
/// Adds a `Future` to the set of “blocking resources” that should prevent the servers
/// response stream from beginning until all are resolved. The `Future` returned by
/// [`blocking_resources`](Self::blocking_resources) will not resolve until every `Future`
/// added by this method has resolved.
/// blocking resources will not resolve until every `Future` added by this method
/// has resolved.
///
/// In browser implementations, this should be a no-op.
fn defer_stream(&self, wait_for: PinnedFuture<()>);

View File

@@ -82,7 +82,7 @@ impl ResponseParts {
}
}
/// A wrapper for an Actix [`HttpRequest`](actix_web::HttpRequest) that allows it to be used in an
/// A wrapper for an Actix [`HttpRequest`] that allows it to be used in an
/// `Send`/`Sync` setting like Leptos's Context API.
#[derive(Debug, Clone)]
pub struct Request(SendWrapper<HttpRequest>);
@@ -375,8 +375,8 @@ pub fn handle_server_fns_with_context(
.take(),
);
// it it accepts text/html (i.e., is a plain form post) and doesn't already have a
// Location set, then redirect to to Referer
// if it accepts text/html (i.e., is a plain form post) and doesn't already have a
// Location set, then redirect to the Referer
if accepts_html {
if let Some(referrer) = referrer {
let has_location =
@@ -390,7 +390,20 @@ pub fn handle_server_fns_with_context(
}
}
// apply status code and headers if used changed them
// the Location header may have been set to Referer, so any redirection by the
// user must overwrite it
{
let mut res_options = res_options.0.write();
let headers = res.0.headers_mut();
for location in
res_options.headers.remove(header::LOCATION)
{
headers.insert(header::LOCATION, location);
}
}
// apply status code and headers if user changed them
res.extend_response(&res_options);
res.0
})
@@ -419,12 +432,6 @@ pub fn handle_server_fns_with_context(
/// will include fallback content for any `<Suspense/>` nodes, and be immediately interactive,
/// but requires some client-side JavaScript.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
/// includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{App, HttpServer};
@@ -465,7 +472,6 @@ pub fn handle_server_fns_with_context(
/// - [ResponseOptions]
/// - [Request]
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -485,13 +491,6 @@ 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.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using
/// [render_to_stream_in_order](leptos::ssr::render_to_stream_in_order),
/// and includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{App, HttpServer};
@@ -534,7 +533,6 @@ where
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [Request]
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -551,13 +549,7 @@ where
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` [Resource](leptos::Resource)s have loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to the apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_string_async](leptos::ssr::render_to_string_async), and
/// includes everything described in the documentation for that function.
/// `async` resources have loaded.
///
/// This can then be set up at an appropriate route in your application:
/// ```
@@ -690,7 +682,6 @@ where
/// - [ResponseOptions]
/// - [Request]
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -713,7 +704,7 @@ where
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously serving the page once all `async`
/// [Resource](leptos::Resource)s have loaded.
/// resources have loaded.
///
/// This function allows you to provide additional information to Leptos for your route.
/// It could be used to pass in Path Info, Connection Info, or anything your heart desires.

View File

@@ -11,7 +11,7 @@ edition.workspace = true
[dependencies]
any_spawner = { workspace = true, features = ["tokio"] }
hydration_context = { workspace = true }
axum = { version = "0.7.8", default-features = false, features = [
axum = { version = "0.7.9", default-features = false, features = [
"matched-path",
] }
dashmap = "6"
@@ -26,11 +26,11 @@ once_cell = "1"
parking_lot = "0.12.3"
tokio = { version = "1.41", default-features = false }
tower = { version = "0.5.1", features = ["util"] }
tower-http = "0.6.1"
tracing = { version = "0.1.40", optional = true }
tower-http = "0.6.2"
tracing = { version = "0.1.41", optional = true }
[dev-dependencies]
axum = "0.7.8"
axum = "0.7.9"
tokio = { version = "1.41", features = ["net", "rt-multi-thread"] }
[features]

View File

@@ -16,7 +16,6 @@
//! - `default`: supports running in a typical native Tokio/Axum environment
//! - `wasm`: with `default-features = false`, supports running in a JS Fetch-based
//! environment
//! - `islands`: activates Leptos [islands mode](https://leptos-rs.github.io/leptos/islands.html)
//!
//! ### Important Note
//! Prior to 0.5, using `default-features = false` on `leptos_axum` simply did nothing. Now, it actively
@@ -197,9 +196,9 @@ impl ExtendResponse for AxumResponse {
/// 2. A server function that is called from WASM running in the client (e.g., a dispatched action
/// or a spawned `Future`).
/// 3. A `<form>` submitted to the server function endpoint using default browser APIs (often due
/// to using [`ActionForm`](leptos::form::ActionForm) without JS/WASM present.)
/// to using [`ActionForm`] without JS/WASM present.)
///
/// Using it with a non-blocking [`Resource`](leptos::server::Resource) will not work if you are using streaming rendering,
/// Using it with a non-blocking [`Resource`] will not work if you are using streaming rendering,
/// as the response's headers will already have been sent by the time the server function calls `redirect()`.
///
/// ### Implementation
@@ -399,8 +398,8 @@ async fn handle_server_fns_inner(
// actually run the server fn
let mut res = AxumResponse(service.run(req).await);
// it it accepts text/html (i.e., is a plain form post) and doesn't already have a
// Location set, then redirect to to Referer
// if it accepts text/html (i.e., is a plain form post) and doesn't already have a
// Location set, then redirect to the Referer
if accepts_html {
if let Some(referrer) = referrer {
let has_location =
@@ -412,7 +411,7 @@ async fn handle_server_fns_inner(
}
}
// apply status code and headers if used changed them
// apply status code and headers if user changed them
res.extend_response(&res_options);
Ok(res.0)
})
@@ -442,12 +441,6 @@ pub type PinnedHtmlStream =
/// 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.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
/// includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use axum::{handler::Handler, Router};
@@ -485,8 +478,7 @@ pub type PinnedHtmlStream =
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
/// - [`ServerMetaContext`]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -537,12 +529,6 @@ 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.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream_in_order], and includes everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use axum::{handler::Handler, Router};
@@ -580,8 +566,7 @@ where
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
/// - [`ServerMetaContext`]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -634,8 +619,7 @@ where
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
/// - [`ServerMetaContext`]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -766,8 +750,7 @@ where
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
/// - [`ServerMetaContext`]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -834,8 +817,7 @@ where
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
/// - [`ServerMetaContext`]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -952,13 +934,7 @@ fn provide_contexts(
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` [Resource](leptos::Resource)s have loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_string_async], and includes everything described in
/// the documentation for that function.
/// `async` resources have loaded.
///
/// This can then be set up at an appropriate route in your application:
/// ```
@@ -998,8 +974,7 @@ fn provide_contexts(
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
/// - [`ServerMetaContext`]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -1020,7 +995,7 @@ where
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` [Resource](leptos::Resource)s have loaded.
/// `async` resources have loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
@@ -1053,8 +1028,7 @@ where
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
/// - [`ServerMetaContext`]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -1088,7 +1062,7 @@ where
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` [Resource](leptos::Resource)s have loaded.
/// `async` resources have loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
@@ -1121,8 +1095,7 @@ where
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
/// - [`ServerMetaContext`]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
@@ -2020,12 +1993,12 @@ pub fn file_and_error_handler<S, IV>(
+ 'static
where
IV: IntoView + 'static,
S: Send + 'static,
S: Send + Sync + Clone + 'static,
LeptosOptions: FromRef<S>,
{
move |uri: Uri, State(options): State<S>, req: Request<Body>| {
move |uri: Uri, State(state): State<S>, req: Request<Body>| {
Box::pin(async move {
let options = LeptosOptions::from_ref(&options);
let options = LeptosOptions::from_ref(&state);
let res = get_static_file(uri, &options.site_root, req.headers());
let res = res.await.unwrap();
@@ -2033,7 +2006,9 @@ where
res.into_response()
} else {
let mut res = handle_response_inner(
|| {},
move || {
provide_context(state.clone());
},
move || shell(options),
req,
|app, chunks| {

View File

@@ -40,15 +40,28 @@ pub trait ExtendResponse: Sized {
let (owner, stream) =
build_response(app_fn, additional_context, stream_builder);
let sc = owner.shared_context().unwrap();
let stream = stream.await.ready_chunks(32).map(|n| n.join(""));
let sc = owner.shared_context().unwrap();
while let Some(pending) = sc.await_deferred() {
pending.await;
}
let mut stream =
Box::pin(meta_context.inject_meta_context(stream).await);
let mut stream = Box::pin(
meta_context.inject_meta_context(stream).await.then({
let sc = Arc::clone(&sc);
move |chunk| {
let sc = Arc::clone(&sc);
async move {
while let Some(pending) = sc.await_deferred() {
pending.await;
}
chunk
}
}
}),
);
// wait for the first chunk of the stream, then set the status and headers
let first_chunk = stream.next().await.unwrap_or_default();

View File

@@ -11,7 +11,10 @@ edition.workspace = true
[dependencies]
throw_error = { workspace = true }
any_spawner = { workspace = true, features = ["wasm-bindgen", "futures-executor"] }
any_spawner = { workspace = true, features = [
"wasm-bindgen",
"futures-executor",
] }
base64 = { version = "0.22.1", optional = true }
cfg-if = "1.0"
hydration_context = { workspace = true }
@@ -28,9 +31,13 @@ paste = "1.0"
rand = { version = "0.8.5", optional = true }
reactive_graph = { workspace = true, features = ["serde"] }
rustc-hash = "2.0"
tachys = { workspace = true, features = ["reactive_graph", "reactive_stores", "oco"] }
tachys = { workspace = true, features = [
"reactive_graph",
"reactive_stores",
"oco",
] }
thiserror = "2.0"
tracing = { version = "0.1.40", optional = true }
tracing = { version = "0.1.41", optional = true }
typed-builder = "0.20.0"
typed-builder-macro = "0.20.0"
serde = "1.0"
@@ -45,25 +52,27 @@ web-sys = { version = "0.3.72", features = [
"ShadowRootInit",
"ShadowRootMode",
] }
wasm-bindgen = "0.2.95"
wasm-bindgen = "0.2.97"
serde_qs = "0.13.0"
slotmap = "1.0"
futures = "0.3.31"
send_wrapper = "0.6.0"
getrandom = { version = "0.2", features = ["js"], optional = true }
[features]
hydration = [
"reactive_graph/hydration",
"leptos_server/hydration",
"hydration_context/browser",
"leptos_dom/hydration"
"leptos_dom/hydration",
]
csr = ["leptos_macro/csr", "reactive_graph/effects"]
csr = ["leptos_macro/csr", "reactive_graph/effects", "dep:getrandom"]
hydrate = [
"leptos_macro/hydrate",
"hydration",
"tachys/hydrate",
"reactive_graph/effects",
"dep:getrandom",
]
default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls"]
@@ -89,7 +98,7 @@ spin = ["leptos-spin-macro"]
islands = ["leptos_macro/islands", "dep:serde_json"]
trace-component-props = [
"leptos_macro/trace-component-props",
"leptos_dom/trace-component-props"
"leptos_dom/trace-component-props",
]
delegation = ["tachys/delegation"]
@@ -101,23 +110,56 @@ denylist = [
"rustls",
"default-tls",
"wasm-bindgen",
"rkyv", # was causing clippy issues on nightly
"rkyv", # was causing clippy issues on nightly
"trace-component-props",
"spin",
"islands",
]
skip_feature_sets = [
["csr", "ssr"],
["csr", "hydrate"],
["ssr", "hydrate"],
["serde", "serde-lite"],
["serde-lite", "miniserde"],
["serde", "miniserde"],
["serde", "rkyv"],
["miniserde", "rkyv"],
["serde-lite", "rkyv"],
["default-tls", "rustls"],
[
"csr",
"ssr",
],
[
"csr",
"hydrate",
],
[
"ssr",
"hydrate",
],
[
"serde",
"serde-lite",
],
[
"serde-lite",
"miniserde",
],
[
"serde",
"miniserde",
],
[
"serde",
"rkyv",
],
[
"miniserde",
"rkyv",
],
[
"serde-lite",
"rkyv",
],
[
"default-tls",
"rustls",
],
]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -0,0 +1,150 @@
use crate::attr::{
any_attribute::{AnyAttribute, IntoAnyAttribute},
Attribute, NextAttribute,
};
use leptos::prelude::*;
/// Function stored to build/rebuild the wrapped children when attributes are added.
type ChildBuilder<T> = dyn Fn(AnyAttribute) -> T + Send + Sync + 'static;
/// Intercepts attributes passed to your component, allowing passing them to any element.
///
/// By default, Leptos passes any attributes passed to your component (e.g. `<MyComponent
/// attr:class="some-class"/>`) to the top-level element in the view returned by your component.
/// [`AttributeInterceptor`] allows you to intercept this behavior and pass it onto any element in
/// your component instead.
///
/// Must be the top level element in your component's view.
///
/// ## Example
///
/// Any attributes passed to MyComponent will be passed to the #inner element.
///
/// ```
/// # use leptos::prelude::*;
/// use leptos::attribute_interceptor::AttributeInterceptor;
///
/// #[component]
/// pub fn MyComponent() -> impl IntoView {
/// view! {
/// <AttributeInterceptor let:attrs>
/// <div id="wrapper">
/// <div id="inner" {..attrs} />
/// </div>
/// </AttributeInterceptor>
/// }
/// }
/// ```
#[component(transparent)]
pub fn AttributeInterceptor<Chil, T>(
/// The elements that will be rendered, with the attributes this component received as a
/// parameter.
children: Chil,
) -> impl IntoView
where
Chil: Fn(AnyAttribute) -> T + Send + Sync + 'static,
T: IntoView,
{
AttributeInterceptorInner::new(children)
}
/// Wrapper to intercept attributes passed to a component so you can apply them to a different
/// element.
struct AttributeInterceptorInner<T: IntoView, A> {
children_builder: Box<ChildBuilder<T>>,
children: T,
attributes: A,
}
impl<T: IntoView> AttributeInterceptorInner<T, ()> {
/// Use this as the returned view from your component to collect the attributes that are passed
/// to your component so you can manually handle them.
pub fn new<F>(children: F) -> Self
where
F: Fn(AnyAttribute) -> T + Send + Sync + 'static,
{
let children_builder = Box::new(children);
let children = children_builder(().into_any_attr());
Self {
children_builder,
children,
attributes: (),
}
}
}
impl<T: IntoView, A: Attribute> Render for AttributeInterceptorInner<T, A> {
type State = <T as Render>::State;
fn build(self) -> Self::State {
self.children.build()
}
fn rebuild(self, state: &mut Self::State) {
self.children.rebuild(state);
}
}
impl<T: IntoView, A> AddAnyAttr for AttributeInterceptorInner<T, A>
where
A: Attribute,
{
type Output<SomeNewAttr: leptos::attr::Attribute> =
AttributeInterceptorInner<T, <<A as NextAttribute>::Output<SomeNewAttr> as Attribute>::CloneableOwned>;
fn add_any_attr<NewAttr: leptos::attr::Attribute>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
{
let attributes =
self.attributes.add_any_attr(attr).into_cloneable_owned();
let children =
(self.children_builder)(attributes.clone().into_any_attr());
AttributeInterceptorInner {
children_builder: self.children_builder,
children,
attributes,
}
}
}
impl<T: IntoView, A: Attribute> RenderHtml for AttributeInterceptorInner<T, A> {
type AsyncOutput = T::AsyncOutput;
const MIN_LENGTH: usize = T::MIN_LENGTH;
fn dry_resolve(&mut self) {
self.children.dry_resolve()
}
fn resolve(
self,
) -> impl std::future::Future<Output = Self::AsyncOutput> + Send {
self.children.resolve()
}
fn to_html_with_buf(
self,
buf: &mut String,
position: &mut leptos::tachys::view::Position,
escape: bool,
mark_branches: bool,
) {
self.children
.to_html_with_buf(buf, position, escape, mark_branches)
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &leptos::tachys::hydration::Cursor,
position: &leptos::tachys::view::PositionState,
) -> Self::State {
self.children.hydrate::<FROM_SERVER>(cursor, position)
}
}

View File

@@ -31,13 +31,13 @@
//! *Notes*:
//! - The `render_number` prop can receive any type that implements `Fn(i32) -> String`.
//! - Callbacks are most useful when you want optional generic props.
//! - All callbacks implement the [`Callable`] trait, and can be invoked with `my_callback.run(input)`.
//! - All callbacks implement the [`Callable`](leptos::callback::Callable) trait, and can be invoked with `my_callback.run(input)`.
//! - The callback types implement [`Copy`], so they can easily be moved into and out of other closures, just like signals.
//!
//! # Types
//! This modules implements 2 callback types:
//! - [`Callback`]
//! - [`UnsyncCallback`]
//! - [`Callback`](leptos::callback::Callback)
//! - [`UnsyncCallback`](leptos::callback::UnsyncCallback)
//!
//! Use `SyncCallback` if the function is not `Sync` and `Send`.

View File

@@ -85,7 +85,7 @@ type BoxedChildrenFn = Box<dyn Fn() -> AnyView + Send>;
/// )
/// }
pub trait ToChildren<F> {
/// Convert the provided type to (generally a closure) to Self (generally a "children" type,
/// Convert the provided type (generally a closure) to Self (generally a "children" type,
/// e.g., [Children]). See the implementations to see exactly which input types are supported
/// and which "children" type they are converted to.
fn to_children(f: F) -> Self;
@@ -246,7 +246,7 @@ where
}
}
/// A typed equivalent to [`ChildrenMut`], which takes a generic but preserves type information to
/// A typed equivalent to [`ChildrenFnMut`], which takes a generic but preserves type information to
/// allow the compiler to optimize the view more effectively.
pub struct TypedChildrenMut<T>(Box<dyn FnMut() -> View<T> + Send>);
@@ -285,6 +285,13 @@ impl<T> Debug for TypedChildrenFn<T> {
}
}
impl<T> Clone for TypedChildrenFn<T> {
// Manual implementation to avoid the `T: Clone` bound.
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> TypedChildrenFn<T> {
/// Extracts the inner `children` function.
pub fn into_inner(self) -> Arc<dyn Fn() -> View<T> + Send + Sync> {

View File

@@ -44,6 +44,67 @@ use tachys::{reactive_graph::OwnedView, view::keyed::keyed};
/// }
/// }
/// ```
///
/// For convenience, you can also choose to write template code directly in the `<For>`
/// component, using the `let` syntax:
///
/// ```
/// # use leptos::prelude::*;
///
/// # #[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// # struct Counter {
/// # id: usize,
/// # count: RwSignal<i32>
/// # }
/// #
/// # #[component]
/// # fn Counters() -> impl IntoView {
/// # let (counters, set_counters) = create_signal::<Vec<Counter>>(vec![]);
/// #
/// view! {
/// <div>
/// <For
/// each=move || counters.get()
/// key=|counter| counter.id
/// let(counter)
/// >
/// <button>"Value: " {move || counter.count.get()}</button>
/// </For>
/// </div>
/// }
/// # }
/// ```
///
/// The `let` syntax also supports destructuring the pattern of your data.
/// `let((one, two))` in the case of tuples, and `let(Struct { field_one, field_two })`
/// in the case of structs.
///
/// ```
/// # use leptos::prelude::*;
///
/// # #[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// # struct Counter {
/// # id: usize,
/// # count: RwSignal<i32>
/// # }
/// #
/// # #[component]
/// # fn Counters() -> impl IntoView {
/// # let (counters, set_counters) = create_signal::<Vec<Counter>>(vec![]);
/// #
/// view! {
/// <div>
/// <For
/// each=move || counters.get()
/// key=|counter| counter.id
/// let(Counter { id, count })
/// >
/// <button>"Value: " {move || count.get()}</button>
/// </For>
/// </div>
/// }
/// # }
/// ```
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
pub fn For<IF, I, T, EF, N, KF, K>(

View File

@@ -72,9 +72,7 @@ use web_sys::{
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
pub fn ActionForm<ServFn>(
/// The action from which to build the form. This should include a URL, which can be generated
/// by default using [`create_server_action`](leptos_server::create_server_action) or added
/// manually using [`using_server_fn`](leptos_server::Action::using_server_fn).
/// The action from which to build the form.
action: ServerAction<ServFn>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
#[prop(optional)]
@@ -149,9 +147,7 @@ where
/// progressively enhanced to use client-side routing.
#[component]
pub fn MultiActionForm<ServFn>(
/// The action from which to build the form. This should include a URL, which can be generated
/// by default using [create_server_action](leptos_server::create_server_action) or added
/// manually using [leptos_server::Action::using_server_fn].
/// The action from which to build the form.
action: ServerMultiAction<ServFn>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
#[prop(optional)]

View File

@@ -1,4 +1,6 @@
((root, pkg_path, output_name, wasm_output_name) => {
let MOST_RECENT_CHILDREN_CB;
function idle(c) {
if ("requestIdleCallback" in window) {
window.requestIdleCallback(c);
@@ -6,55 +8,49 @@
c();
}
}
function islandTree(rootNode) {
const tree = [];
function traverse(node, parent) {
function hydrateIslands(rootNode, mod) {
function traverse(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
if(node.tagName.toLowerCase() === 'leptos-island') {
const tag = node.tagName.toLowerCase();
if(tag === 'leptos-island') {
const children = [];
const id = node.dataset.component || null;
const data = { id, node, children };
hydrateIsland(node, id, mod);
for(const child of node.children) {
traverse(child, children);
}
(parent || tree).push(data);
} else {
if(tag === 'leptos-children') {
MOST_RECENT_CHILDREN_CB = node.$$on_hydrate;
}
for(const child of node.children) {
traverse(child, parent);
traverse(child);
};
}
}
}
traverse(rootNode, null);
return { el: null, id: null, children: tree };
traverse(rootNode);
}
function hydrateIsland(el, id, mod) {
const islandFn = mod[id];
if (islandFn) {
if (MOST_RECENT_CHILDREN_CB) {
MOST_RECENT_CHILDREN_CB();
}
islandFn(el);
} else {
console.warn(`Could not find WASM function for the island ${id}.`);
}
}
function hydrateIslands(entry, mod) {
if(entry.node) {
hydrateIsland(entry.node, entry.id, mod);
}
for (const island of entry.children) {
hydrateIslands(island, mod);
}
}
idle(() => {
import(`${root}/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default(`${root}/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.hydrate();
hydrateIslands(islandTree(document.body, null), mod);
hydrateIslands(document.body, mod);
});
})
});

View File

@@ -39,7 +39,7 @@
//! server actions, forms, and server-sent events (SSE).
//! - **[`todomvc`]** shows the basics of building an isomorphic web app. Both the server and the client import the same app code.
//! The server renders the app directly to an HTML string, and the client hydrates that HTML to make it interactive.
//! You might also want to see how we use [`Effect::new`](leptos::prelude::Effect::new) to
//! You might also want to see how we use [`Effect::new`](leptos::prelude::Effect) to
//! [serialize JSON to `localStorage`](https://github.com/leptos-rs/leptos/blob/20af4928b2fffe017408d3f4e7330db22cf68277/examples/todomvc/src/lib.rs#L191-L209)
//! and [reactively call DOM methods](https://github.com/leptos-rs/leptos/blob/16f084a71268ac325fbc4a5e50c260df185eadb6/examples/todomvc/src/lib.rs#L292-L296)
//! on [references to elements](https://github.com/leptos-rs/leptos/blob/20af4928b2fffe017408d3f4e7330db22cf68277/examples/todomvc/src/lib.rs#L228).
@@ -78,7 +78,7 @@
//! + `async` interop: [`Resource`](leptos::prelude::Resource) for loading data using `async` functions
//! and [`Action`](leptos::prelude::Action) to mutate data or imperatively call `async` functions.
//! + reactions: [`Effect`](leptos::prelude::Effect) and [`RenderEffect`](leptos::prelude::RenderEffect).
//! - **Templating/Views**: the [`view`] macro and [`IntoView`](leptos::IntoView) trait.
//! - **Templating/Views**: the [`view`] macro and [`IntoView`] trait.
//! - **Routing**: the [`leptos_router`](https://docs.rs/leptos_router/latest/leptos_router/) crate
//! - **Server Functions**: the [`server`](macro@leptos::prelude::server) macro and [`ServerAction`](leptos::prelude::ServerAction).
//!
@@ -192,6 +192,9 @@ pub mod callback;
/// Types that can be passed as the `children` prop of a component.
pub mod children;
/// Wrapper for intercepting component attributes.
pub mod attribute_interceptor;
#[doc(hidden)]
/// Traits used to implement component constructors.
pub mod component;
@@ -290,7 +293,7 @@ pub mod logging {
/// Utilities for working with asynchronous tasks.
pub mod task {
pub use any_spawner::Executor;
pub use any_spawner::{self, CustomExecutor, Executor};
use std::future::Future;
/// Spawns a thread-safe [`Future`].

View File

@@ -1535,7 +1535,7 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.7.1"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
@@ -1851,7 +1851,7 @@ dependencies = [
[[package]]
name = "quote-use"
version = "0.7.1"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58e9a38ef862d7fec635661503289062bc5b3035e61859a8de3d3f81823accd2"
dependencies = [
@@ -1953,7 +1953,7 @@ dependencies = [
[[package]]
name = "ron"
version = "0.7.1"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
dependencies = [
@@ -2104,7 +2104,7 @@ dependencies = [
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [

View File

@@ -26,3 +26,6 @@ temp-env = { version = "0.3.6", features = ["async_closure"] }
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -12,10 +12,10 @@ edition.workspace = true
tachys = { workspace = true }
reactive_graph = { workspace = true }
or_poisoned = { workspace = true }
js-sys = "0.3.72"
js-sys = "0.3.74"
send_wrapper = "0.6.0"
tracing = { version = "0.1.40", optional = true }
wasm-bindgen = "0.2.95"
tracing = { version = "0.1.41", optional = true }
wasm-bindgen = "0.2.97"
serde_json = { version = "1.0", optional = true }
serde = { version = "1.0", optional = true }
@@ -37,3 +37,6 @@ rustdoc-args = ["--generate-link-to-definition"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.7.0-rc1"
version = "0.7.3"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -13,7 +13,7 @@ edition.workspace = true
proc-macro = true
[dependencies]
attribute-derive = { version = "0.10.2", features = ["syn-full"] }
attribute-derive = { version = "0.10.3", features = ["syn-full"] }
cfg-if = "1.0"
html-escape = "0.2.13"
itertools = "0.13.0"
@@ -27,7 +27,7 @@ leptos_hot_reload = { workspace = true }
server_fn_macro = { workspace = true }
convert_case = "0.6.0"
uuid = { version = "1.11", features = ["v4"] }
tracing = { version = "0.1.40", optional = true }
tracing = { version = "0.1.41", optional = true }
[dev-dependencies]
log = "0.4.22"
@@ -83,4 +83,7 @@ skip_feature_sets = [
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(erase_components)'] }
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(leptos_debuginfo)',
'cfg(erase_components)',
] }

View File

@@ -260,8 +260,12 @@ impl ToTokens for Model {
let body_name = unmodified_fn_name_from_fn_name(&body_name);
let body_expr = if is_island {
quote! {
::leptos::reactive::owner::Owner::with_hydration(move || {
#body_name(#prop_names)
::leptos::reactive::owner::Owner::new().with(|| {
::leptos::reactive::owner::Owner::with_hydration(move || {
::leptos::tachys::reactive_graph::OwnedView::new({
#body_name(#prop_names)
})
})
})
}
} else {
@@ -301,8 +305,8 @@ impl ToTokens for Model {
let hydrate_fn_name = hydrate_fn_name.as_ref().unwrap();
quote! {
{
if ::leptos::reactive::owner::Owner::current_shared_context()
.map(|sc| sc.get_is_hydrating())
if ::leptos::context::use_context::<::leptos::reactive::owner::IsHydrating>()
.map(|h| h.0)
.unwrap_or(false) {
::leptos::either::Either::Left(
#component
@@ -339,9 +343,23 @@ impl ToTokens for Model {
let children = Box::new(|| {
let sc = ::leptos::reactive::owner::Owner::current_shared_context().unwrap();
let prev = sc.get_is_hydrating();
let value = ::leptos::reactive::owner::Owner::with_no_hydration(||
::leptos::tachys::html::islands::IslandChildren::new(children()).into_any()
);
let owner = ::leptos::reactive::owner::Owner::new();
let value = owner.clone().with(|| {
::leptos::reactive::owner::Owner::with_no_hydration(move || {
::leptos::tachys::reactive_graph::OwnedView::new({
::leptos::tachys::html::islands::IslandChildren::new_with_on_hydrate(
children(),
{
let owner = owner.clone();
move || {
owner.set()
}
}
)
}).into_any()
})
});
sc.set_is_hydrating(prev);
value
});
@@ -424,20 +442,21 @@ impl ToTokens for Model {
};
let children = if is_island_with_children {
quote! {
.children({Box::new(|| {
.children({
let owner = leptos::reactive::owner::Owner::current();
Box::new(move || {
use leptos::tachys::view::any_view::IntoAny;
::leptos::tachys::html::islands::IslandChildren::new(
// TODO owner restoration for context
()
::leptos::tachys::html::islands::IslandChildren::new_with_on_hydrate(
(),
{
let owner = owner.clone();
move || {
if let Some(owner) = &owner {
owner.set()
}
}
}
).into_any()})})
//.children(children)
/*.children(Box::new(|| {
use leptos::tachys::view::any_view::IntoAny;
::leptos::tachys::html::islands::IslandChildren::new(
// TODO owner restoration for context
()
).into_any()
}))*/
}
} else {
quote! {}
@@ -655,14 +674,44 @@ impl Prop {
abort!(e.span(), e.to_string());
});
let name = if let Pat::Ident(i) = *typed.pat {
i
} else {
abort!(
typed.pat,
"only `prop: bool` style types are allowed within the \
`#[component]` macro"
);
let name = match *typed.pat {
Pat::Ident(i) => {
if let Some(name) = &prop_opts.name {
PatIdent {
attrs: vec![],
by_ref: None,
mutability: None,
ident: Ident::new(name, i.span()),
subpat: None,
}
} else {
i
}
}
Pat::Struct(_) | Pat::Tuple(_) | Pat::TupleStruct(_) => {
if let Some(name) = &prop_opts.name {
PatIdent {
attrs: vec![],
by_ref: None,
mutability: None,
ident: Ident::new(name, typed.pat.span()),
subpat: None,
}
} else {
abort!(
typed.pat,
"destructured props must be given a name e.g. \
#[prop(name = \"data\")]"
);
}
}
_ => {
abort!(
typed.pat,
"only `prop: bool` style types are allowed within the \
`#[component]` macro"
);
}
};
Self {
@@ -865,6 +914,7 @@ struct PropOpt {
default: Option<syn::Expr>,
into: bool,
attrs: bool,
name: Option<String>,
}
struct TypedBuilderOpts {

View File

@@ -1,4 +1,4 @@
//! Macros for use with the [`leptos`] framework.
//! Macros for use with the Leptos framework.
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
#![forbid(unsafe_code)]
@@ -272,8 +272,8 @@ pub fn view(tokens: TokenStream) -> TokenStream {
view_macro_impl(tokens, false)
}
/// The `template` macro behaves like [`view`], except that it wraps the entire tree in a
/// [`ViewTemplate`](leptos::prelude::ViewTemplate). This optimizes creation speed by rendering
/// The `template` macro behaves like [`view`](view!), except that it wraps the entire tree in a
/// [`ViewTemplate`](https://docs.rs/leptos/0.7.0-gamma3/leptos/prelude/struct.ViewTemplate.html). This optimizes creation speed by rendering
/// most of the view into a `<template>` tag with HTML rendered at compile time, then hydrating it.
/// In exchange, there is a small binary size overhead.
#[proc_macro_error2::proc_macro_error]
@@ -366,7 +366,7 @@ fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
}
}
/// This behaves like the [`view`] macro, but loads the view from an external file instead of
/// This behaves like the [`view`](view!) macro, but loads the view from an external file instead of
/// parsing it inline.
///
/// This is designed to allow editing views in a separate file, if this improves a user's workflow.
@@ -639,7 +639,7 @@ pub fn island(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
abort!(
transparent,
"only `transparent` is supported";
help = "try `#[component(transparent)]` or `#[component]`"
help = "try `#[island(transparent)]` or `#[island]`"
);
}

View File

@@ -108,9 +108,12 @@ pub(crate) fn component_to_tokens(
let KeyedAttributeValue::Binding(binding) = &attr.possible_value
else {
if let Some(ident) = attr.key.to_string().strip_prefix("let:") {
let ident1 =
format_ident!("{ident}", span = attr.key.span());
return Some(quote! { #ident1 });
let span = match &attr.key {
NodeName::Punctuated(path) => path[1].span(),
_ => unreachable!(),
};
let ident1 = format_ident!("{ident}", span = span);
return Some(quote_spanned! { span => #ident1 });
} else {
return None;
}

View File

@@ -652,6 +652,18 @@ pub(crate) fn element_to_tokens(
},
_ => None,
};
if let NodeAttribute::Attribute(a) = a {
if let Some(Tuple(_)) = a.value() {
return Ordering::Greater;
}
}
if let NodeAttribute::Attribute(b) = b {
if let Some(Tuple(_)) = b.value() {
return Ordering::Less;
}
}
match (key_a.as_deref(), key_b.as_deref()) {
(Some("class"), Some("class")) | (Some("style"), Some("style")) => {
Ordering::Equal
@@ -755,7 +767,7 @@ pub(crate) fn element_to_tokens(
let name = node.name().to_string();
// link custom ident to name span for IDE docs
let custom = Ident::new("custom", name.span());
quote! { ::leptos::tachys::html::element::#custom(#name) }
quote_spanned! { node.name().span() => ::leptos::tachys::html::element::#custom(#name) }
} else if is_svg_element(&tag) {
parent_type = TagType::Svg;
let name = if tag == "use" || tag == "use_" {
@@ -763,33 +775,33 @@ pub(crate) fn element_to_tokens(
} else {
name.to_token_stream()
};
quote! { ::leptos::tachys::svg::#name() }
quote_spanned! { node.name().span() => ::leptos::tachys::svg::#name() }
} else if is_math_ml_element(&tag) {
parent_type = TagType::Math;
quote! { ::leptos::tachys::mathml::#name() }
quote_spanned! { node.name().span() => ::leptos::tachys::mathml::#name() }
} else if is_ambiguous_element(&tag) {
match parent_type {
TagType::Unknown => {
// We decided this warning was too aggressive, but I'll leave it here in case we want it later
/* proc_macro_error2::emit_warning!(name.span(), "The view macro is assuming this is an HTML element, \
but it is ambiguous; if it is an SVG or MathML element, prefix with svg:: or math::"); */
quote! {
quote_spanned! { node.name().span() =>
::leptos::tachys::html::element::#name()
}
}
TagType::Html => {
quote! { ::leptos::tachys::html::element::#name() }
quote_spanned! { node.name().span() => ::leptos::tachys::html::element::#name() }
}
TagType::Svg => {
quote! { ::leptos::tachys::svg::#name() }
quote_spanned! { node.name().span() => ::leptos::tachys::svg::#name() }
}
TagType::Math => {
quote! { ::leptos::tachys::math::#name() }
quote_spanned! { node.name().span() => ::leptos::tachys::math::#name() }
}
}
} else {
parent_type = TagType::Html;
quote! { ::leptos::tachys::html::element::#name() }
quote_spanned! { name.span() => ::leptos::tachys::html::element::#name() }
};
/* TODO restore this
@@ -1710,7 +1722,7 @@ fn tuple_name(name: &str, node: &KeyedAttribute) -> TupleName {
TupleName::None
}
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
enum TupleName {
None,
Str(String),

View File

@@ -1,6 +1,15 @@
use core::num::NonZeroUsize;
use leptos::prelude::*;
#[derive(PartialEq, Debug)]
struct UserInfo {
user_id: String,
email: String,
}
#[derive(PartialEq, Debug)]
struct Admin(bool);
#[component]
fn Component(
#[prop(optional)] optional: bool,
@@ -10,6 +19,10 @@ fn Component(
#[prop(default = NonZeroUsize::new(10).unwrap())] default: NonZeroUsize,
#[prop(into)] into: String,
impl_trait: impl Fn() -> i32 + 'static,
#[prop(name = "data")] UserInfo { email, user_id }: UserInfo,
#[prop(name = "tuple")] (name, id): (String, i32),
#[prop(name = "tuple_struct")] Admin(is_admin): Admin,
#[prop(name = "outside_name")] inside_name: i32,
) -> impl IntoView {
_ = optional;
_ = optional_into;
@@ -18,6 +31,12 @@ fn Component(
_ = default;
_ = into;
_ = impl_trait;
_ = email;
_ = user_id;
_ = id;
_ = name;
_ = is_admin;
_ = inside_name;
}
#[test]
@@ -26,6 +45,13 @@ fn component() {
.into("")
.strip_option(9)
.impl_trait(|| 42)
.data(UserInfo {
email: "em@il".into(),
user_id: "1".into(),
})
.tuple(("Joe".into(), 12))
.tuple_struct(Admin(true))
.outside_name(1)
.build();
assert!(!cp.optional);
assert_eq!(cp.optional_into, None);
@@ -34,6 +60,16 @@ fn component() {
assert_eq!(cp.default, NonZeroUsize::new(10).unwrap());
assert_eq!(cp.into, "");
assert_eq!((cp.impl_trait)(), 42);
assert_eq!(
cp.data,
UserInfo {
email: "em@il".into(),
user_id: "1".into(),
}
);
assert_eq!(cp.tuple, ("Joe".into(), 12));
assert_eq!(cp.tuple_struct, Admin(true));
assert_eq!(cp.outside_name, 1);
}
#[test]
@@ -45,12 +81,26 @@ fn component_nostrip() {
strip_option=9
into=""
impl_trait=|| 42
data=UserInfo {
email: "em@il".into(),
user_id: "1".into(),
}
tuple=("Joe".into(), 12)
tuple_struct=Admin(true)
outside_name=1
/>
<Component
nostrip:optional_into=Some("foo")
strip_option=9
into=""
impl_trait=|| 42
data=UserInfo {
email: "em@il".into(),
user_id: "1".into(),
}
tuple=("Joe".into(), 12)
tuple_struct=Admin(true)
outside_name=1
/>
};
}

View File

@@ -44,4 +44,10 @@ fn default_with_invalid_value(
_ = default;
}
#[component]
fn destructure_without_name((default, value): (bool, i32)) -> impl IntoView {
_ = default;
_ = value;
}
fn main() {}

View File

@@ -1,4 +1,4 @@
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default`, `into` and `attrs`
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default`, `into`, `attrs` and `name`
--> tests/ui/component.rs:10:31
|
10 | fn unknown_prop_option(#[prop(hello)] test: bool) -> impl IntoView {
@@ -41,3 +41,9 @@ error: unexpected end of input, expected one of: identifier, `::`, `<`, `_`, lit
| ^^^^^^^^^^^^
|
= note: this error originates in the attribute macro `component` (in Nightly builds, run with -Z macro-backtrace for more info)
error: destructured props must be given a name e.g. #[prop(name = "data")]
--> tests/ui/component.rs:48:29
|
48 | fn destructure_without_name((default, value): (bool, i32)) -> impl IntoView {
| ^^^^^^^^^^^^^^^^

View File

@@ -1,4 +1,4 @@
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default`, `into` and `attrs`
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default`, `into`, `attrs` and `name`
--> tests/ui/component_absolute.rs:5:31
|
5 | fn unknown_prop_option(#[prop(hello)] test: bool) -> impl ::leptos::IntoView {

View File

@@ -15,7 +15,7 @@ codee = { version = "0.2.0", features = ["json_serde"] }
hydration_context = { workspace = true }
reactive_graph = { workspace = true, features = ["hydration"] }
server_fn = { workspace = true }
tracing = { version = "0.1.40", optional = true }
tracing = { version = "0.1.41", optional = true }
futures = "0.3.31"
any_spawner = { workspace = true }
@@ -25,8 +25,8 @@ send_wrapper = "0.6"
# serialization formats
serde = { version = "1.0" }
js-sys = { version = "0.3.72", optional = true }
wasm-bindgen = { version = "0.2.95", optional = true }
js-sys = { version = "0.3.74", optional = true }
wasm-bindgen = { version = "0.2.97", optional = true }
serde_json = { version = "1.0" }
[features]
@@ -44,3 +44,6 @@ denylist = ["tracing"]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -43,7 +43,7 @@ where
S::Output: 'static,
{
inner: ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -65,7 +65,7 @@ where
inner: ArcAction::new_with_value(err, |input: &S| {
S::run_on_client(input.clone())
}),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -91,7 +91,7 @@ where
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
}
}
@@ -114,11 +114,11 @@ where
S::Output: 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -132,7 +132,7 @@ where
S::Output: 'static,
{
inner: Action<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -153,7 +153,7 @@ where
inner: Action::new_with_value(err, |input: &S| {
S::run_on_client(input.clone())
}),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -217,11 +217,11 @@ where
S::Output: 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}

View File

@@ -1,4 +1,4 @@
//! Utilities for communicating between the server and the client with [`leptos`].
//! Utilities for communicating between the server and the client with Leptos.
#![deny(missing_docs)]
#![forbid(unsafe_code)]

View File

@@ -20,7 +20,7 @@ use std::{
/// A reference-counted resource that only loads its data locally on the client.
pub struct ArcLocalResource<T> {
data: ArcAsyncDerived<SendWrapper<T>>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -28,7 +28,7 @@ impl<T> Clone for ArcLocalResource<T> {
fn clone(&self) -> Self {
Self {
data: self.data.clone(),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
}
}
@@ -70,7 +70,7 @@ impl<T> ArcLocalResource<T> {
let fut = fetcher();
SendWrapper::new(async move { SendWrapper::new(fut.await) })
}),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -104,11 +104,11 @@ where
impl<T> DefinedAt for ArcLocalResource<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -200,7 +200,7 @@ impl<T> Subscriber for ArcLocalResource<T> {
/// A resource that only loads its data locally on the client.
pub struct LocalResource<T> {
data: AsyncDerived<SendWrapper<T>>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -253,7 +253,7 @@ impl<T> LocalResource<T> {
SendWrapper::new(async move { SendWrapper::new(fut.await) })
})
},
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -287,11 +287,11 @@ where
impl<T> DefinedAt for LocalResource<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -398,7 +398,7 @@ impl<T: 'static> From<ArcLocalResource<T>> for LocalResource<T> {
fn from(arc: ArcLocalResource<T>) -> Self {
Self {
data: arc.data.into(),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: arc.defined_at,
}
}
@@ -408,7 +408,7 @@ impl<T: 'static> From<LocalResource<T>> for ArcLocalResource<T> {
fn from(local: LocalResource<T>) -> Self {
Self {
data: local.data.into(),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: local.defined_at,
}
}

View File

@@ -12,7 +12,7 @@ where
S::Output: 'static,
{
inner: ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -29,7 +29,7 @@ where
inner: ArcMultiAction::new(|input: &S| {
S::run_on_client(input.clone())
}),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -55,7 +55,7 @@ where
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
}
}
@@ -78,11 +78,11 @@ where
S::Output: 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -96,7 +96,7 @@ where
S::Output: 'static,
{
inner: MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -123,7 +123,7 @@ where
inner: MultiAction::new(|input: &S| {
S::run_on_client(input.clone())
}),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -176,11 +176,11 @@ where
S::Output: 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}

View File

@@ -60,7 +60,7 @@ pub struct ArcOnceResource<T, Ser = JsonSerdeCodec> {
suspenses: Arc<RwLock<Vec<SuspenseContext>>>,
loading: Arc<AtomicBool>,
ser: PhantomData<fn() -> Ser>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -73,7 +73,7 @@ impl<T, Ser> Clone for ArcOnceResource<T, Ser> {
suspenses: self.suspenses.clone(),
loading: self.loading.clone(),
ser: self.ser,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
}
}
@@ -140,7 +140,7 @@ where
wakers,
suspenses,
ser: PhantomData,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
};
@@ -183,11 +183,11 @@ impl<T, Ser> ArcOnceResource<T, Ser> {
impl<T, Ser> DefinedAt for ArcOnceResource<T, Ser> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
@@ -253,7 +253,8 @@ where
}
}
/// A [`Future`] that is ready when an [`ArcAsyncDerived`] is finished loading or reloading,
/// A [`Future`] that is ready when an
/// [`ArcAsyncDerived`](reactive_graph::computed::ArcAsyncDerived) is finished loading or reloading,
/// and contains its value. `.await`ing this clones the value `T`.
pub struct OnceResourceFuture<T> {
source: AnySource,
@@ -271,7 +272,7 @@ where
#[track_caller]
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
let _guard = SpecialNonReactiveZone::enter();
let waker = cx.waker();
self.source.track();
@@ -490,7 +491,7 @@ where
#[derive(Debug)]
pub struct OnceResource<T, Ser = JsonSerdeCodec> {
inner: ArenaItem<ArcOnceResource<T, Ser>>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -523,13 +524,13 @@ where
fut: impl Future<Output = T> + Send + 'static,
blocking: bool,
) -> Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
let defined_at = Location::caller();
Self {
inner: ArenaItem::new(ArcOnceResource::new_with_options(
fut, blocking,
)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at,
}
}
@@ -550,11 +551,11 @@ where
impl<T, Ser> DefinedAt for OnceResource<T, Ser> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}

View File

@@ -74,7 +74,7 @@ pub struct ArcResource<T, Ser = JsonSerdeCodec> {
ser: PhantomData<Ser>,
refetch: ArcRwSignal<usize>,
data: ArcAsyncDerived<T>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -82,7 +82,7 @@ impl<T, Ser> Debug for ArcResource<T, Ser> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut d = f.debug_struct("ArcResource");
d.field("ser", &self.ser).field("data", &self.data);
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
d.field("defined_at", self.defined_at);
d.finish_non_exhaustive()
}
@@ -98,7 +98,7 @@ where
ser: PhantomData,
data: arc_resource.data.into(),
refetch: arc_resource.refetch.into(),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -114,7 +114,7 @@ where
ser: PhantomData,
data: resource.data.into(),
refetch: resource.refetch.into(),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -122,11 +122,11 @@ where
impl<T, Ser> DefinedAt for ArcResource<T, Ser> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -139,7 +139,7 @@ impl<T, Ser> Clone for ArcResource<T, Ser> {
ser: self.ser,
refetch: self.refetch.clone(),
data: self.data.clone(),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
}
}
@@ -300,7 +300,7 @@ where
ser: PhantomData,
data,
refetch,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -781,7 +781,7 @@ where
ser: PhantomData<Ser>,
data: AsyncDerived<T>,
refetch: RwSignal<usize>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -792,7 +792,7 @@ where
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut d = f.debug_struct("ArcResource");
d.field("ser", &self.ser).field("data", &self.data);
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
d.field("defined_at", self.defined_at);
d.finish_non_exhaustive()
}
@@ -803,11 +803,11 @@ where
T: Send + Sync + 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -1270,7 +1270,7 @@ where
ser: PhantomData,
data: data.into(),
refetch: refetch.into(),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.7.0-rc1"
version = "0.7.3"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -14,8 +14,8 @@ once_cell = "1.20"
or_poisoned = { workspace = true }
indexmap = "2.6"
send_wrapper = "0.6.0"
tracing = { version = "0.1.40", optional = true }
wasm-bindgen = "0.2.95"
tracing = { version = "0.1.41", optional = true }
wasm-bindgen = "0.2.97"
futures = "0.3.31"
[dependencies.web-sys]
@@ -32,3 +32,6 @@ rustdoc-args = ["--generate-link-to-definition"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -7,8 +7,7 @@
//! using the [`Leptos`](https://github.com/leptos-rs/leptos) web framework.
//!
//! Document metadata is updated automatically when running in the browser. For server-side
//! rendering, after the component tree is rendered to HTML, [`MetaContext::dehydrate`] can generate
//! HTML that should be injected into the `<head>` of the HTML document being rendered.
//! rendering, after the component tree is rendered to HTML, [`ServerMetaContextOutput::inject_meta_context`] will inject meta tags into a stream of HTML inside the `<head>`.
//!
//! ```
//! use leptos::prelude::*;
@@ -38,14 +37,10 @@
//! }
//! ```
//! # Feature Flags
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
//! - `stable` By default, Leptos requires `nightly` Rust, which is what allows the ergonomics
//! of calling signals as functions. Enable this feature to support `stable` Rust.
//! - `tracing` Adds integration with the `tracing` crate.
//!
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in.
//! **Important Note:** If youre using server-side rendering, you should enable `ssr`.
use futures::{Stream, StreamExt};
use leptos::{

View File

@@ -1,6 +1,6 @@
[package]
name = "next_tuple"
version = "0.1.0-rc1"
version = "0.1.0"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.1.0-rc1"
version = "0.1.3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -19,13 +19,13 @@ rustc-hash = "2.0"
serde = { version = "1.0", features = ["derive"], optional = true }
slotmap = "1.0"
thiserror = "2.0"
tracing = { version = "0.1.40", optional = true }
tracing = { version = "0.1.41", optional = true }
guardian = "1.2"
async-lock = "3.4.0"
send_wrapper = { version = "0.6.0", features = ["futures"] }
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
web-sys = "0.3.72"
web-sys = { version = "0.3.72", features = ["console"] }
[dev-dependencies]
tokio = { version = "1.41", features = ["rt-multi-thread", "macros"] }
@@ -47,3 +47,6 @@ rustdoc-args = ["--cfg", "docsrs"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -101,7 +101,7 @@ pub struct ArcAction<I, O> {
action_fn: Arc<
dyn Fn(&I) -> Pin<Box<dyn Future<Output = O> + Send>> + Send + Sync,
>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -114,7 +114,7 @@ impl<I, O> Clone for ArcAction<I, O> {
version: self.version.clone(),
dispatched: self.dispatched.clone(),
action_fn: self.action_fn.clone(),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
}
}
@@ -198,7 +198,7 @@ where
version: Default::default(),
dispatched: Default::default(),
action_fn: Arc::new(move |input| Box::pin(action_fn(input))),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -370,7 +370,7 @@ where
action_fn: Arc::new(move |input| {
Box::pin(SendWrapper::new(action_fn(input)))
}),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -502,11 +502,11 @@ where
O: 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -592,7 +592,7 @@ where
/// ```
pub struct Action<I, O, S = SyncStorage> {
inner: ArenaItem<ArcAction<I, O>, S>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -656,7 +656,7 @@ where
{
Self {
inner: ArenaItem::new(ArcAction::new(action_fn)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -681,7 +681,7 @@ where
{
Self {
inner: ArenaItem::new(ArcAction::new_with_value(value, action_fn)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -715,11 +715,11 @@ where
pub fn new_local<F, Fu>(action_fn: F) -> Self
where
F: Fn(&I) -> Fu + 'static,
Fu: Future<Output = O> + Send + 'static,
Fu: Future<Output = O> + 'static,
{
Self {
inner: ArenaItem::new_local(ArcAction::new_unsync(action_fn)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -737,7 +737,7 @@ where
inner: ArenaItem::new_local(ArcAction::new_unsync_with_value(
value, action_fn,
)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -979,7 +979,7 @@ where
inner: ArenaItem::new_with_storage(ArcAction::new_unsync(
action_fn,
)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -998,7 +998,7 @@ where
inner: ArenaItem::new_with_storage(
ArcAction::new_unsync_with_value(value, action_fn),
),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -1006,11 +1006,11 @@ where
impl<I, O, S> DefinedAt for Action<I, O, S> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}

View File

@@ -46,7 +46,7 @@ use std::{fmt::Debug, future::Future, panic::Location, pin::Pin, sync::Arc};
/// ```
pub struct MultiAction<I, O, S = SyncStorage> {
inner: ArenaItem<ArcMultiAction<I, O>, S>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -62,11 +62,11 @@ where
O: 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -130,7 +130,7 @@ where
{
Self {
inner: ArenaItem::new_with_storage(ArcMultiAction::new(action_fn)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}

View File

@@ -19,7 +19,7 @@ pub(crate) use inner::MemoInner;
pub use memo::*;
pub use selector::*;
/// Derives a reactive slice of an [`RwSignal`](crate::signal::RwSignal).
/// Derives a reactive slice of an [`RwSignal`].
///
/// Slices have the same guarantees as [`Memo`s](crate::computed::Memo):
/// they only emit their value when it has actually been changed.

View File

@@ -93,7 +93,7 @@ pub struct ArcMemo<T, S = SyncStorage>
where
S: Storage<T>,
{
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: Arc<RwLock<MemoInner<T, S>>>,
}
@@ -164,7 +164,7 @@ where
RwLock::new(MemoInner::new(Arc::new(fun), subscriber))
});
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner,
}
@@ -177,7 +177,7 @@ where
{
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
inner: Arc::clone(&self.inner),
}
@@ -190,11 +190,11 @@ where
{
#[inline(always)]
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -272,7 +272,7 @@ where
AnySource(
Arc::as_ptr(&self.inner) as usize,
Arc::downgrade(&self.inner) as Weak<dyn Source + Send + Sync>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
self.defined_at,
)
}

View File

@@ -107,7 +107,7 @@ use std::{
/// - [`IntoFuture`](std::future::Future) allows you to create a [`Future`] that resolves
/// when this resource is done loading.
pub struct ArcAsyncDerived<T> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
pub(crate) defined_at: &'static Location<'static>,
// the current state of this signal
pub(crate) value: Arc<AsyncRwLock<Option<T>>>,
@@ -117,6 +117,7 @@ pub struct ArcAsyncDerived<T> {
pub(crate) loading: Arc<AtomicBool>,
}
#[allow(dead_code)]
pub(crate) trait BlockingLock<T> {
fn blocking_read_arc(self: &Arc<Self>)
-> async_lock::RwLockReadGuardArc<T>;
@@ -183,7 +184,7 @@ impl<T> BlockingLock<T> for AsyncRwLock<T> {
impl<T> Clone for ArcAsyncDerived<T> {
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
value: Arc::clone(&self.value),
wakers: Arc::clone(&self.wakers),
@@ -196,7 +197,7 @@ impl<T> Clone for ArcAsyncDerived<T> {
impl<T> Debug for ArcAsyncDerived<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut f = f.debug_struct("ArcAsyncDerived");
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
f.field("defined_at", &self.defined_at);
f.finish_non_exhaustive()
}
@@ -205,11 +206,11 @@ impl<T> Debug for ArcAsyncDerived<T> {
impl<T> DefinedAt for ArcAsyncDerived<T> {
#[inline(always)]
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -240,7 +241,7 @@ macro_rules! spawn_derived {
let wakers = Arc::new(RwLock::new(Vec::new()));
let this = ArcAsyncDerived {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
value: Arc::clone(&value),
wakers,
@@ -583,19 +584,17 @@ impl<T: 'static> ReadUntracked for ArcAsyncDerived<T> {
fn try_read_untracked(&self) -> Option<Self::Value> {
if let Some(suspense_context) = use_context::<SuspenseContext>() {
if self.value.blocking_read().is_none() {
let handle = suspense_context.task_id();
let ready = SpecialNonReactiveFuture::new(self.ready());
crate::spawn(async move {
ready.await;
drop(handle);
});
self.inner
.write()
.or_poisoned()
.suspenses
.push(suspense_context);
}
let handle = suspense_context.task_id();
let ready = SpecialNonReactiveFuture::new(self.ready());
crate::spawn(async move {
ready.await;
drop(handle);
});
self.inner
.write()
.or_poisoned()
.suspenses
.push(suspense_context);
}
AsyncPlain::try_new(&self.value).map(ReadGuard::new)
}
@@ -633,7 +632,7 @@ impl<T: 'static> ToAnySource for ArcAsyncDerived<T> {
AnySource(
Arc::as_ptr(&self.inner) as usize,
Arc::downgrade(&self.inner) as Weak<dyn Source + Send + Sync>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
self.defined_at,
)
}

View File

@@ -83,7 +83,7 @@ use std::{future::Future, ops::DerefMut, panic::Location};
/// - [`IntoFuture`](std::future::Future) allows you to create a [`Future`] that resolves
/// when this resource is done loading.
pub struct AsyncDerived<T, S = SyncStorage> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
pub(crate) inner: ArenaItem<ArcAsyncDerived<T>, S>,
}
@@ -99,10 +99,10 @@ where
T: Send + Sync + 'static,
{
fn from(value: ArcAsyncDerived<T>) -> Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
let defined_at = value.defined_at;
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at,
inner: ArenaItem::new_with_storage(value),
}
@@ -127,10 +127,10 @@ where
T: 'static,
{
fn from_local(value: ArcAsyncDerived<T>) -> Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
let defined_at = value.defined_at;
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at,
inner: ArenaItem::new_with_storage(value),
}
@@ -152,7 +152,7 @@ where
Fut: Future<Output = T> + Send + 'static,
{
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcAsyncDerived::new(fun)),
}
@@ -170,7 +170,7 @@ where
Fut: Future<Output = T> + Send + 'static,
{
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(
ArcAsyncDerived::new_with_initial(initial_value, fun),
@@ -187,7 +187,7 @@ impl<T> AsyncDerived<SendWrapper<T>> {
Fut: Future<Output = T> + 'static,
{
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcAsyncDerived::new_mock(fun)),
}
@@ -209,7 +209,7 @@ where
Fut: Future<Output = T> + 'static,
{
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcAsyncDerived::new_unsync(
fun,
@@ -230,7 +230,7 @@ where
Fut: Future<Output = T> + 'static,
{
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(
ArcAsyncDerived::new_unsync_with_initial(initial_value, fun),
@@ -278,11 +278,11 @@ where
impl<T, S> DefinedAt for AsyncDerived<T, S> {
#[inline(always)]
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}

View File

@@ -67,7 +67,9 @@ where
fn mark_check(&self) {
{
let mut lock = self.write().or_poisoned();
lock.state = ReactiveNodeState::Check;
if lock.state != ReactiveNodeState::Dirty {
lock.state = ReactiveNodeState::Check;
}
}
for sub in (&self.read().or_poisoned().subscribers).into_iter() {
sub.mark_check();

View File

@@ -100,7 +100,7 @@ pub struct Memo<T, S = SyncStorage>
where
S: Storage<T>,
{
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: ArenaItem<ArcMemo<T, S>, S>,
}
@@ -121,7 +121,7 @@ where
#[track_caller]
fn from(value: ArcMemo<T, SyncStorage>) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value),
}
@@ -135,7 +135,7 @@ where
#[track_caller]
fn from_local(value: ArcMemo<T, LocalStorage>) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value),
}
@@ -175,7 +175,7 @@ where
T: PartialEq,
{
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcMemo::new(fun)),
}
@@ -197,7 +197,7 @@ where
changed: fn(Option<&T>, Option<&T>) -> bool,
) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcMemo::new_with_compare(
fun, changed,
@@ -207,7 +207,7 @@ where
/// Creates a new memo by passing a function that computes the value.
///
/// Unlike [`ArcMemo::new`](), this receives ownership of the previous value. As a result, it
/// Unlike [`Memo::new`](), this receives ownership of the previous value. As a result, it
/// must return both the new value and a `bool` that is `true` if the value has changed.
///
/// This is lazy: the function will not be called until the memo's value is read for the first
@@ -221,7 +221,7 @@ where
fun: impl Fn(Option<T>) -> (T, bool) + Send + Sync + 'static,
) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcMemo::new_owning(fun)),
}
@@ -276,11 +276,11 @@ where
S: Storage<T>,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}

View File

@@ -59,7 +59,7 @@ impl<'a> IntoIterator for &'a SourceSet {
self.0.iter()
}
}
#[derive(Default, Clone)]
#[derive(Debug, Default, Clone)]
pub struct SubscriberSet(Vec<AnySubscriber>);
impl SubscriberSet {

View File

@@ -26,16 +26,17 @@ pub trait Source: ReactiveNode {
pub struct AnySource(
pub(crate) usize,
pub(crate) Weak<dyn Source + Send + Sync>,
#[cfg(debug_assertions)] pub(crate) &'static Location<'static>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
pub(crate) &'static Location<'static>,
);
impl DefinedAt for AnySource {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.2)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}

View File

@@ -124,7 +124,7 @@ pub fn log_warning(text: Arguments) {
}
}
/// Calls [`Executor::spawn`], but ensures that the task also runs in the current arena, if
/// Calls [`Executor::spawn`](any_spawner::Executor), but ensures that the task also runs in the current arena, if
/// multithreaded arena sandboxing is enabled.
pub fn spawn(task: impl Future<Output = ()> + Send + 'static) {
#[cfg(feature = "sandboxed-arenas")]
@@ -133,8 +133,9 @@ pub fn spawn(task: impl Future<Output = ()> + Send + 'static) {
any_spawner::Executor::spawn(task);
}
/// Calls [`Executor::spawn_local`], but ensures that the task runs under the current reactive [`Owner`]
/// and [`Observed`]. Does not cancel the task if the owner is cleaned up.
/// Calls [`Executor::spawn_local`](any_spawner::Executor), but ensures that the task runs under the current reactive [`Owner`](crate::owner::Owner) and observer.
///
/// Does not cancel the task if the owner is cleaned up.
pub fn spawn_local_scoped(task: impl Future<Output = ()> + 'static) {
let task = ScopedFuture::new(task);
@@ -144,8 +145,9 @@ pub fn spawn_local_scoped(task: impl Future<Output = ()> + 'static) {
any_spawner::Executor::spawn_local(task);
}
/// Calls [`Executor::spawn_local`], but ensures that the task runs under the current reactive [`Owner`]
/// and [`Observed`]. Cancels the task if the owner is cleaned up.
/// Calls [`Executor::spawn_local`](any_spawner::Executor), but ensures that the task runs under the current reactive [`Owner`](crate::owner::Owner) and observer.
///
/// Cancels the task if the owner is cleaned up.
pub fn spawn_local_scoped_with_cancellation(
task: impl Future<Output = ()> + 'static,
) {

View File

@@ -292,6 +292,8 @@ impl Owner {
#[cfg(feature = "hydration")]
pub fn with_hydration<T>(fun: impl FnOnce() -> T + 'static) -> T {
fn inner<T>(fun: Box<dyn FnOnce() -> T>) -> T {
provide_context(IsHydrating(true));
let sc = OWNER.with_borrow(|o| {
o.as_ref()
.and_then(|current| current.shared_context.clone())
@@ -315,6 +317,8 @@ impl Owner {
#[cfg(feature = "hydration")]
pub fn with_no_hydration<T>(fun: impl FnOnce() -> T + 'static) -> T {
fn inner<T>(fun: Box<dyn FnOnce() -> T>) -> T {
provide_context(IsHydrating(false));
let sc = OWNER.with_borrow(|o| {
o.as_ref()
.and_then(|current| current.shared_context.clone())
@@ -335,6 +339,10 @@ impl Owner {
}
}
#[doc(hidden)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct IsHydrating(pub bool);
/// Registers a function to be run the next time the current owner is cleaned up.
///
/// Because the ownership model is associated with reactive nodes, each "decision point" in an

View File

@@ -1,6 +1,6 @@
use crate::{
signal::guards::{Plain, ReadGuard, UntrackedWriteGuard},
traits::{DefinedAt, IsDisposed, ReadValue, WriteValue},
traits::{DefinedAt, IntoInner, IsDisposed, ReadValue, WriteValue},
};
use std::{
fmt::{Debug, Formatter},
@@ -19,7 +19,7 @@ use std::{
/// accessing it does not cause effects to subscribe, and
/// updating it does not notify anything else.
pub struct ArcStoredValue<T> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
value: Arc<RwLock<T>>,
}
@@ -27,7 +27,7 @@ pub struct ArcStoredValue<T> {
impl<T> Clone for ArcStoredValue<T> {
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
value: Arc::clone(&self.value),
}
@@ -47,7 +47,7 @@ impl<T: Default> Default for ArcStoredValue<T> {
#[track_caller]
fn default() -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
value: Arc::new(RwLock::new(T::default())),
}
@@ -70,11 +70,11 @@ impl<T> Hash for ArcStoredValue<T> {
impl<T> DefinedAt for ArcStoredValue<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -90,7 +90,7 @@ impl<T> ArcStoredValue<T> {
#[track_caller]
pub fn new(value: T) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
value: Arc::new(RwLock::new(value)),
}
@@ -124,3 +124,12 @@ impl<T> IsDisposed for ArcStoredValue<T> {
false
}
}
impl<T> IntoInner for ArcStoredValue<T> {
type Value = T;
#[inline(always)]
fn into_inner(self) -> Option<Self::Value> {
Some(Arc::into_inner(self.value)?.into_inner().unwrap())
}
}

View File

@@ -121,8 +121,11 @@ pub mod sandboxed {
}
impl<T> Sandboxed<T> {
/// Wraps the given [`Future`], ensuring that any [`ArenaItem`] created while it is being
/// polled will be associated with the same arena that was active when this was called.
/// Wraps the given [`Future`], ensuring that any [`ArenaItem`][item] created while it is
/// being polled will be associated with the same arena that was active when this was
/// called.
///
/// [item]:[crate::owner::ArenaItem]
pub fn new(inner: T) -> Self {
let arena = MAP.with_borrow(|n| n.as_ref().and_then(Weak::upgrade));
Self { arena, inner }

View File

@@ -2,7 +2,7 @@ use super::{
arena::{Arena, NodeId},
LocalStorage, Storage, SyncStorage, OWNER,
};
use crate::traits::{Dispose, IsDisposed};
use crate::traits::{Dispose, IntoInner, IsDisposed};
use send_wrapper::SendWrapper;
use std::{any::Any, hash::Hash, marker::PhantomData};
@@ -134,3 +134,12 @@ impl<T, S> Dispose for ArenaItem<T, S> {
Arena::with_mut(|arena| arena.remove(self.node));
}
}
impl<T, S: Storage<T>> IntoInner for ArenaItem<T, S> {
type Value = T;
#[inline(always)]
fn into_inner(self) -> Option<Self::Value> {
S::take(self.node)
}
}

View File

@@ -15,35 +15,14 @@ impl Owner {
}
fn use_context<T: Clone + 'static>(&self) -> Option<T> {
let ty = TypeId::of::<T>();
let inner = self.inner.read().or_poisoned();
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let contexts = &self.inner.read().or_poisoned().contexts;
if let Some(context) = contexts.get(&ty) {
context.downcast_ref::<T>().cloned()
} else {
while let Some(ref this_parent) = parent.clone() {
let this_parent = this_parent.read().or_poisoned();
let contexts = &this_parent.contexts;
let value = contexts.get(&ty);
let downcast = value
.and_then(|context| context.downcast_ref::<T>().cloned());
if let Some(value) = downcast {
return Some(value);
} else {
parent =
this_parent.parent.as_ref().and_then(|p| p.upgrade());
}
}
None
}
self.with_context(Clone::clone)
}
fn take_context<T: 'static>(&self) -> Option<T> {
let ty = TypeId::of::<T>();
let inner = self.inner.read().or_poisoned();
let mut inner = self.inner.write().or_poisoned();
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let contexts = &mut self.inner.write().or_poisoned().contexts;
let contexts = &mut inner.contexts;
if let Some(context) = contexts.remove(&ty) {
context.downcast::<T>().ok().map(|n| *n)
} else {
@@ -64,6 +43,64 @@ impl Owner {
}
}
fn with_context<T: 'static, R>(
&self,
cb: impl FnOnce(&T) -> R,
) -> Option<R> {
let ty = TypeId::of::<T>();
let inner = self.inner.read().or_poisoned();
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let contexts = &inner.contexts;
let reference = if let Some(context) = contexts.get(&ty) {
context.downcast_ref::<T>()
} else {
while let Some(ref this_parent) = parent.clone() {
let this_parent = this_parent.read().or_poisoned();
let contexts = &this_parent.contexts;
let value = contexts.get(&ty);
let downcast =
value.and_then(|context| context.downcast_ref::<T>());
if let Some(value) = downcast {
return Some(cb(value));
} else {
parent =
this_parent.parent.as_ref().and_then(|p| p.upgrade());
}
}
None
};
reference.map(cb)
}
fn update_context<T: 'static, R>(
&self,
cb: impl FnOnce(&mut T) -> R,
) -> Option<R> {
let ty = TypeId::of::<T>();
let mut inner = self.inner.write().or_poisoned();
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let contexts = &mut inner.contexts;
let reference = if let Some(context) = contexts.get_mut(&ty) {
context.downcast_mut::<T>()
} else {
while let Some(ref this_parent) = parent.clone() {
let mut this_parent = this_parent.write().or_poisoned();
let contexts = &mut this_parent.contexts;
let value = contexts.get_mut(&ty);
let downcast =
value.and_then(|context| context.downcast_mut::<T>());
if let Some(value) = downcast {
return Some(cb(value));
} else {
parent =
this_parent.parent.as_ref().and_then(|p| p.upgrade());
}
}
None
};
reference.map(cb)
}
/// Searches for items stored in context in either direction, either among parents or among
/// descendants.
pub fn use_context_bidirectional<T: Clone + 'static>(&self) -> Option<T> {
@@ -319,3 +356,86 @@ pub fn expect_context<T: Clone + 'static>() -> T {
pub fn take_context<T: 'static>() -> Option<T> {
Owner::current().and_then(|owner| owner.take_context())
}
/// Access a reference to a context value of type `T` in the reactive system.
///
/// This traverses the reactive ownership graph, beginning from the current reactive
/// [`Owner`] and iterating through its parents, if any. When the value is found,
/// the function that you pass is applied to an immutable reference to it.
///
/// The context value should have been provided elsewhere using
/// [`provide_context`](provide_context).
///
/// This is useful for passing values down to components or functions lower in a
/// hierarchy without needs to “prop drill” by passing them through each layer as
/// arguments to a function or properties of a component.
///
/// Context works similarly to variable scope: a context that is provided higher in
/// the reactive graph can be used lower down, but a context that is provided lower
/// in the tree cannot be used higher up.
///
/// ```rust
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::owner::*;
/// # let owner = Owner::new(); owner.set();
/// # use reactive_graph::effect::Effect;
/// # futures::executor::block_on(async move {
/// # any_spawner::Executor::init_futures_executor();
/// Effect::new(move |_| {
/// provide_context(String::from("foo"));
///
/// Effect::new(move |_| {
/// let value = with_context::<String, _>(|val| val.to_string())
/// .expect("could not find String in context");
/// assert_eq!(value, "foo");
/// });
/// });
/// # });
/// ```
pub fn with_context<T: 'static, R>(cb: impl FnOnce(&T) -> R) -> Option<R> {
Owner::current().and_then(|owner| owner.with_context(cb))
}
/// Update a context value of type `T` in the reactive system.
///
/// This traverses the reactive ownership graph, beginning from the current reactive
/// [`Owner`] and iterating through its parents, if any. When the value is found,
/// the function that you pass is applied to a mutable reference to it.
///
/// The context value should have been provided elsewhere using
/// [`provide_context`](provide_context).
///
/// This is useful for passing values down to components or functions lower in a
/// hierarchy without needs to “prop drill” by passing them through each layer as
/// arguments to a function or properties of a component.
///
/// Context works similarly to variable scope: a context that is provided higher in
/// the reactive graph can be used lower down, but a context that is provided lower
/// in the tree cannot be used higher up.
///
/// ```rust
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::owner::*;
/// # let owner = Owner::new(); owner.set();
/// # use reactive_graph::effect::Effect;
/// # futures::executor::block_on(async move {
/// # any_spawner::Executor::init_futures_executor();
/// Effect::new(move |_| {
/// provide_context(String::from("foo"));
///
/// Effect::new(move |_| {
/// let value = update_context::<String, _>(|val| {
/// std::mem::replace(val, "bar".to_string())
/// })
/// .expect("could not find String in context");
/// assert_eq!(value, "foo");
/// assert_eq!(expect_context::<String>(), "bar");
/// });
/// });
/// # });
/// ```
pub fn update_context<T: 'static, R>(
cb: impl FnOnce(&mut T) -> R,
) -> Option<R> {
Owner::current().and_then(|owner| owner.update_context(cb))
}

View File

@@ -30,7 +30,7 @@ impl<T> StorageAccess<T> for SendWrapper<T> {
}
}
/// A way of storing a [`ArenaItem`], either as itself or with a wrapper to make it threadsafe.
/// A way of storing an [`ArenaItem`](super::arena_item::ArenaItem), either as itself or with a wrapper to make it threadsafe.
///
/// This exists because all items stored in the arena must be `Send + Sync`, but in single-threaded
/// environments you might want or need to use thread-unsafe types.
@@ -54,6 +54,10 @@ pub trait Storage<T>: Send + Sync + 'static {
/// Sets a new value for the stored value. If it has been disposed, returns `Some(T)`.
fn try_set(node: NodeId, value: T) -> Option<T>;
/// Takes an item from the arena if it exists and can be accessed from this thread.
/// If it cannot be casted, it will still be removed from the arena.
fn take(node: NodeId) -> Option<T>;
}
/// A form of [`Storage`] that stores the type as itself, with no wrapper.
@@ -100,6 +104,16 @@ where
}
})
}
fn take(node: NodeId) -> Option<T> {
Arena::with_mut(|arena| {
let m = arena.remove(node)?;
match m.downcast::<T>() {
Ok(inner) => Some(*inner),
Err(_) => None,
}
})
}
}
/// A form of [`Storage`] that stores the type with a wrapper that makes it `Send + Sync`, but only
@@ -148,4 +162,14 @@ where
}
})
}
fn take(node: NodeId) -> Option<T> {
Arena::with_mut(|arena| {
let m = arena.remove(node)?;
match m.downcast::<SendWrapper<T>>() {
Ok(inner) => Some(inner.take()),
Err(_) => None,
}
})
}
}

View File

@@ -4,7 +4,9 @@ use super::{
};
use crate::{
signal::guards::{Plain, ReadGuard, UntrackedWriteGuard},
traits::{DefinedAt, Dispose, IsDisposed, ReadValue, WriteValue},
traits::{
DefinedAt, Dispose, IntoInner, IsDisposed, ReadValue, WriteValue,
},
unwrap_signal,
};
use std::{
@@ -22,7 +24,7 @@ use std::{
/// updating it does not notify anything else.
pub struct StoredValue<T, S = SyncStorage> {
value: ArenaItem<ArcStoredValue<T>, S>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -62,11 +64,11 @@ impl<T, S> Hash for StoredValue<T, S> {
impl<T, S> DefinedAt for StoredValue<T, S> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -83,7 +85,7 @@ where
pub fn new_with_storage(value: T) -> Self {
Self {
value: ArenaItem::new_with_storage(ArcStoredValue::new(value)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
@@ -162,6 +164,19 @@ impl<T, S> Dispose for StoredValue<T, S> {
}
}
impl<T, S> IntoInner for StoredValue<T, S>
where
T: 'static,
S: Storage<ArcStoredValue<T>>,
{
type Value = T;
#[inline(always)]
fn into_inner(self) -> Option<Self::Value> {
self.value.into_inner()?.into_inner()
}
}
impl<T> From<ArcStoredValue<T>> for StoredValue<T>
where
T: Send + Sync + 'static,
@@ -169,7 +184,7 @@ where
#[track_caller]
fn from(value: ArcStoredValue<T>) -> Self {
StoredValue {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
value: ArenaItem::new(value),
}

View File

@@ -113,7 +113,8 @@ pub fn arc_signal<T>(value: T) -> (ArcReadSignal<T>, ArcWriteSignal<T>) {
pub fn signal<T: Send + Sync + 'static>(
value: T,
) -> (ReadSignal<T>, WriteSignal<T>) {
RwSignal::new(value).split()
let (r, w) = arc_signal(value);
(r.into(), w.into())
}
/// Creates an arena-allocated signal.

View File

@@ -4,7 +4,7 @@ use super::{
};
use crate::{
graph::SubscriberSet,
traits::{DefinedAt, IsDisposed, ReadUntracked},
traits::{DefinedAt, IntoInner, IsDisposed, ReadUntracked},
};
use core::fmt::{Debug, Formatter, Result};
use std::{
@@ -54,7 +54,7 @@ use std::{
/// assert_eq!(count.read(), 0);
/// ```
pub struct ArcReadSignal<T> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
pub(crate) defined_at: &'static Location<'static>,
pub(crate) value: Arc<RwLock<T>>,
pub(crate) inner: Arc<RwLock<SubscriberSet>>,
@@ -64,7 +64,7 @@ impl<T> Clone for ArcReadSignal<T> {
#[track_caller]
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
value: Arc::clone(&self.value),
inner: Arc::clone(&self.inner),
@@ -85,7 +85,7 @@ impl<T: Default> Default for ArcReadSignal<T> {
#[track_caller]
fn default() -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
value: Arc::new(RwLock::new(T::default())),
inner: Arc::new(RwLock::new(SubscriberSet::new())),
@@ -110,11 +110,11 @@ impl<T> Hash for ArcReadSignal<T> {
impl<T> DefinedAt for ArcReadSignal<T> {
#[inline(always)]
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -128,6 +128,15 @@ impl<T> IsDisposed for ArcReadSignal<T> {
}
}
impl<T> IntoInner for ArcReadSignal<T> {
type Value = T;
#[inline(always)]
fn into_inner(self) -> Option<Self::Value> {
Some(Arc::into_inner(self.value)?.into_inner().unwrap())
}
}
impl<T> AsSubscriberSet for ArcReadSignal<T> {
type Output = Arc<RwLock<SubscriberSet>>;

View File

@@ -6,7 +6,7 @@ use super::{
use crate::{
graph::{ReactiveNode, SubscriberSet},
prelude::{IsDisposed, Notify},
traits::{DefinedAt, ReadUntracked, UntrackableGuard, Write},
traits::{DefinedAt, IntoInner, ReadUntracked, UntrackableGuard, Write},
};
use core::fmt::{Debug, Formatter, Result};
use std::{
@@ -94,7 +94,7 @@ use std::{
/// assert_eq!(double_count(), 2);
/// ```
pub struct ArcRwSignal<T> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
pub(crate) defined_at: &'static Location<'static>,
pub(crate) value: Arc<RwLock<T>>,
pub(crate) inner: Arc<RwLock<SubscriberSet>>,
@@ -104,7 +104,7 @@ impl<T> Clone for ArcRwSignal<T> {
#[track_caller]
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
value: Arc::clone(&self.value),
inner: Arc::clone(&self.inner),
@@ -154,7 +154,7 @@ impl<T> ArcRwSignal<T> {
#[track_caller]
pub fn new(value: T) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
value: Arc::new(RwLock::new(value)),
inner: Arc::new(RwLock::new(SubscriberSet::new())),
@@ -165,7 +165,7 @@ impl<T> ArcRwSignal<T> {
#[track_caller]
pub fn read_only(&self) -> ArcReadSignal<T> {
ArcReadSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
value: Arc::clone(&self.value),
inner: Arc::clone(&self.inner),
@@ -176,7 +176,7 @@ impl<T> ArcRwSignal<T> {
#[track_caller]
pub fn write_only(&self) -> ArcWriteSignal<T> {
ArcWriteSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
value: Arc::clone(&self.value),
inner: Arc::clone(&self.inner),
@@ -198,7 +198,7 @@ impl<T> ArcRwSignal<T> {
) -> Option<Self> {
if Arc::ptr_eq(&read.inner, &write.inner) {
Some(Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
value: read.value,
inner: read.inner,
@@ -212,11 +212,11 @@ impl<T> ArcRwSignal<T> {
impl<T> DefinedAt for ArcRwSignal<T> {
#[inline(always)]
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -230,6 +230,15 @@ impl<T> IsDisposed for ArcRwSignal<T> {
}
}
impl<T> IntoInner for ArcRwSignal<T> {
type Value = T;
#[inline(always)]
fn into_inner(self) -> Option<Self::Value> {
Some(Arc::into_inner(self.value)?.into_inner().unwrap())
}
}
impl<T> AsSubscriberSet for ArcRwSignal<T> {
type Output = Arc<RwLock<SubscriberSet>>;

View File

@@ -13,7 +13,7 @@ use std::{
///
/// This can be useful for when using external data not stored in signals, for example.
pub struct ArcTrigger {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
pub(crate) defined_at: &'static Location<'static>,
pub(crate) inner: Arc<RwLock<SubscriberSet>>,
}
@@ -23,7 +23,7 @@ impl ArcTrigger {
#[track_caller]
pub fn new() -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: Default::default(),
}
@@ -40,7 +40,7 @@ impl Clone for ArcTrigger {
#[track_caller]
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
inner: Arc::clone(&self.inner),
}
@@ -72,11 +72,11 @@ impl AsSubscriberSet for ArcTrigger {
impl DefinedAt for ArcTrigger {
#[inline(always)]
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}

View File

@@ -2,7 +2,7 @@ use super::guards::{UntrackedWriteGuard, WriteGuard};
use crate::{
graph::{ReactiveNode, SubscriberSet},
prelude::{IsDisposed, Notify},
traits::{DefinedAt, UntrackableGuard, Write},
traits::{DefinedAt, IntoInner, UntrackableGuard, Write},
};
use core::fmt::{Debug, Formatter, Result};
use std::{
@@ -54,7 +54,7 @@ use std::{
/// assert_eq!(count.get(), 3);
/// ```
pub struct ArcWriteSignal<T> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
pub(crate) defined_at: &'static Location<'static>,
pub(crate) value: Arc<RwLock<T>>,
pub(crate) inner: Arc<RwLock<SubscriberSet>>,
@@ -64,7 +64,7 @@ impl<T> Clone for ArcWriteSignal<T> {
#[track_caller]
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
value: Arc::clone(&self.value),
inner: Arc::clone(&self.inner),
@@ -98,11 +98,11 @@ impl<T> Hash for ArcWriteSignal<T> {
impl<T> DefinedAt for ArcWriteSignal<T> {
#[inline(always)]
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -116,6 +116,15 @@ impl<T> IsDisposed for ArcWriteSignal<T> {
}
}
impl<T> IntoInner for ArcWriteSignal<T> {
type Value = T;
#[inline(always)]
fn into_inner(self) -> Option<Self::Value> {
Some(Arc::into_inner(self.value)?.into_inner().unwrap())
}
}
impl<T> Notify for ArcWriteSignal<T> {
fn notify(&self) {
self.inner.mark_dirty();

View File

@@ -104,7 +104,7 @@ impl<T: 'static> Debug for Plain<T> {
impl<T: 'static> Plain<T> {
/// Takes a reference-counted read guard on the given lock.
pub fn try_new(inner: Arc<RwLock<T>>) -> Option<Self> {
ArcRwLockReadGuardian::take(inner)
ArcRwLockReadGuardian::try_take(inner)?
.ok()
.map(|guard| Plain { guard })
}
@@ -331,7 +331,7 @@ pub struct UntrackedWriteGuard<T: 'static>(ArcRwLockWriteGuardian<T>);
impl<T: 'static> UntrackedWriteGuard<T> {
/// Creates a write guard from the given lock.
pub fn try_new(inner: Arc<RwLock<T>>) -> Option<Self> {
ArcRwLockWriteGuardian::take(inner)
ArcRwLockWriteGuardian::try_take(inner)?
.ok()
.map(UntrackedWriteGuard)
}

View File

@@ -6,7 +6,7 @@ use super::{
use crate::{
graph::SubscriberSet,
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
traits::{DefinedAt, Dispose, IsDisposed, ReadUntracked},
traits::{DefinedAt, Dispose, IntoInner, IsDisposed, ReadUntracked},
unwrap_signal,
};
use core::fmt::Debug;
@@ -58,7 +58,7 @@ use std::{
/// assert_eq!(count.read(), 0);
/// ```
pub struct ReadSignal<T, S = SyncStorage> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
pub(crate) defined_at: &'static Location<'static>,
pub(crate) inner: ArenaItem<ArcReadSignal<T>, S>,
}
@@ -105,11 +105,11 @@ impl<T, S> Hash for ReadSignal<T, S> {
impl<T, S> DefinedAt for ReadSignal<T, S> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -122,6 +122,18 @@ impl<T, S> IsDisposed for ReadSignal<T, S> {
}
}
impl<T, S> IntoInner for ReadSignal<T, S>
where
S: Storage<ArcReadSignal<T>>,
{
type Value = T;
#[inline(always)]
fn into_inner(self) -> Option<Self::Value> {
self.inner.into_inner()?.into_inner()
}
}
impl<T, S> AsSubscriberSet for ReadSignal<T, S>
where
S: Storage<ArcReadSignal<T>>,
@@ -156,7 +168,7 @@ where
#[track_caller]
fn from(value: ArcReadSignal<T>) -> Self {
ReadSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value),
}
@@ -170,7 +182,7 @@ where
#[track_caller]
fn from_local(value: ArcReadSignal<T>) -> Self {
ReadSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value),
}

View File

@@ -8,7 +8,7 @@ use crate::{
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
signal::guards::{UntrackedWriteGuard, WriteGuard},
traits::{
DefinedAt, Dispose, IsDisposed, Notify, ReadUntracked,
DefinedAt, Dispose, IntoInner, IsDisposed, Notify, ReadUntracked,
UntrackableGuard, Write,
},
unwrap_signal,
@@ -100,7 +100,7 @@ use std::{
/// assert_eq!(double_count(), 2);
/// ```
pub struct RwSignal<T, S = SyncStorage> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: ArenaItem<ArcRwSignal<T>, S>,
}
@@ -139,7 +139,7 @@ where
#[track_caller]
pub fn new_with_storage(value: T) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcRwSignal::new(value)),
}
@@ -172,7 +172,7 @@ where
#[track_caller]
pub fn read_only(&self) -> ReadSignal<T, S> {
ReadSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(
self.inner
@@ -194,7 +194,7 @@ where
#[track_caller]
pub fn write_only(&self) -> WriteSignal<T, S> {
WriteSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(
self.inner
@@ -231,10 +231,10 @@ where
(Some(read), Some(write)) => {
if Arc::ptr_eq(&read.inner, &write.inner) {
Some(Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcRwSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
value: Arc::clone(&read.value),
inner: Arc::clone(&read.inner),
@@ -296,11 +296,11 @@ impl<T, S> Hash for RwSignal<T, S> {
impl<T, S> DefinedAt for RwSignal<T, S> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -313,6 +313,18 @@ impl<T: 'static, S> IsDisposed for RwSignal<T, S> {
}
}
impl<T, S> IntoInner for RwSignal<T, S>
where
S: Storage<ArcRwSignal<T>>,
{
type Value = T;
#[inline(always)]
fn into_inner(self) -> Option<Self::Value> {
self.inner.into_inner()?.into_inner()
}
}
impl<T, S> AsSubscriberSet for RwSignal<T, S>
where
S: Storage<ArcRwSignal<T>>,
@@ -378,7 +390,7 @@ where
#[track_caller]
fn from(value: ArcRwSignal<T>) -> Self {
RwSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value),
}
@@ -402,7 +414,7 @@ where
#[track_caller]
fn from_local(value: ArcRwSignal<T>) -> Self {
RwSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value),
}

View File

@@ -105,7 +105,7 @@ where
AnySource(
Arc::as_ptr(subs) as usize,
Arc::downgrade(subs) as Weak<dyn Source + Send + Sync>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
self.defined_at().expect("no DefinedAt in debug mode"),
)
})

View File

@@ -18,7 +18,7 @@ use std::{
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted trigger that lives
/// as long as a reference to it is alive, see [`ArcTrigger`].
pub struct Trigger {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
pub(crate) defined_at: &'static Location<'static>,
pub(crate) inner: ArenaItem<ArcTrigger>,
}
@@ -28,7 +28,7 @@ impl Trigger {
#[track_caller]
pub fn new() -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new(ArcTrigger::new()),
}
@@ -83,11 +83,11 @@ impl AsSubscriberSet for Trigger {
impl DefinedAt for Trigger {
#[inline(always)]
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}

View File

@@ -1,7 +1,10 @@
use super::{guards::WriteGuard, ArcWriteSignal};
use crate::{
owner::{ArenaItem, Storage, SyncStorage},
traits::{DefinedAt, Dispose, IsDisposed, Notify, UntrackableGuard, Write},
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
traits::{
DefinedAt, Dispose, IntoInner, IsDisposed, Notify, UntrackableGuard,
Write,
},
};
use core::fmt::Debug;
use guardian::ArcRwLockWriteGuardian;
@@ -50,7 +53,7 @@ use std::{hash::Hash, ops::DerefMut, panic::Location, sync::Arc};
/// assert_eq!(count.get(), 3);
/// ```
pub struct WriteSignal<T, S = SyncStorage> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
pub(crate) defined_at: &'static Location<'static>,
pub(crate) inner: ArenaItem<ArcWriteSignal<T>, S>,
}
@@ -97,23 +100,63 @@ impl<T, S> Hash for WriteSignal<T, S> {
impl<T, S> DefinedAt for WriteSignal<T, S> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
}
}
impl<T> From<ArcWriteSignal<T>> for WriteSignal<T>
where
T: Send + Sync + 'static,
{
#[track_caller]
fn from(value: ArcWriteSignal<T>) -> Self {
WriteSignal {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value),
}
}
}
impl<T> FromLocal<ArcWriteSignal<T>> for WriteSignal<T, LocalStorage>
where
T: 'static,
{
#[track_caller]
fn from_local(value: ArcWriteSignal<T>) -> Self {
WriteSignal {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value),
}
}
}
impl<T, S> IsDisposed for WriteSignal<T, S> {
fn is_disposed(&self) -> bool {
self.inner.is_disposed()
}
}
impl<T, S> IntoInner for WriteSignal<T, S>
where
S: Storage<ArcWriteSignal<T>>,
{
type Value = T;
#[inline(always)]
fn into_inner(self) -> Option<Self::Value> {
self.inner.into_inner()?.into_inner()
}
}
impl<T, S> Notify for WriteSignal<T, S>
where
T: 'static,

View File

@@ -220,7 +220,7 @@ where
}
}
/// Helper trait to implement flatten() on Option<&Option<T>>.
/// Helper trait to implement flatten() on `Option<&Option<T>>`.
pub trait FlattenOptionRefOption {
/// The type of the value contained in the double option.
type Value;

View File

@@ -16,7 +16,7 @@
//! | Trait | Mode | Description |
//! |-------------------|-------|---------------------------------------------------------------------------------------|
//! | [`Track`] | — | Tracks changes to this value, adding it as a source of the current reactive observer. |
//! | [`Trigger`] | — | Notifies subscribers that this value has changed. |
//! | [`Notify`] | — | Notifies subscribers that this value has changed. |
//! | [`ReadUntracked`] | Guard | Gives immutable access to the value of this signal. |
//! | [`Write`] | Guard | Gives mutable access to the value of this signal.
//!
@@ -34,7 +34,7 @@
//! | Trait | Mode | Composition | Description
//! |---------------------|---------------|-----------------------------------|------------
//! | [`UpdateUntracked`] | `fn(&mut T)` | [`Write`] | Applies closure to the current value to update it, but doesn't notify subscribers.
//! | [`Update`] | `fn(&mut T)` | [`UpdateUntracked`] + [`Trigger`] | Applies closure to the current value to update it, and notifies subscribers.
//! | [`Update`] | `fn(&mut T)` | [`UpdateUntracked`] + [`Notify`] | Applies closure to the current value to update it, and notifies subscribers.
//! | [`Set`] | `T` | [`Update`] | Sets the value to a new value, and notifies subscribers.
//!
//! ## Using the Traits
@@ -67,10 +67,10 @@ use std::{
#[macro_export]
macro_rules! unwrap_signal {
($signal:ident) => {{
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
let location = std::panic::Location::caller();
|| {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
panic!(
"{}",
@@ -80,7 +80,7 @@ macro_rules! unwrap_signal {
)
);
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
panic!(
"Tried to access a reactive value that has already been \
@@ -103,6 +103,7 @@ pub trait Dispose {
/// Allows tracking the value of some reactive data.
pub trait Track {
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
#[track_caller]
fn track(&self);
}
@@ -404,6 +405,7 @@ where
/// Notifies subscribers of a change in this signal.
pub trait Notify {
/// Notifies subscribers of a change in this signal.
#[track_caller]
fn notify(&self);
}
@@ -630,6 +632,18 @@ pub trait IsDisposed {
fn is_disposed(&self) -> bool;
}
/// Turns a signal back into a raw value.
pub trait IntoInner {
/// The type of the value contained in the signal.
type Value;
/// Returns the inner value if this is the only reference to to the signal.
/// Otherwise, returns `None` and drops this reference.
/// # Panics
/// Panics if the inner lock is poisoned.
fn into_inner(self) -> Option<Self::Value>;
}
/// Describes where the signal was defined. This is used for diagnostic warnings and is purely a
/// debug-mode tool.
pub trait DefinedAt {

View File

@@ -93,17 +93,35 @@ pub mod read {
}
/// A wrapper for any kind of reference-counted reactive signal:
/// an [`ArcReadSignal`], [`ArcMemo`], [`ArcRwSignal`],
/// or derived signal closure.
/// an [`ArcReadSignal`], [`ArcMemo`], [`ArcRwSignal`], or derived signal closure,
/// or a plain value of the same type
///
/// This allows you to create APIs that take any kind of `ArcSignal<T>` as an argument,
/// rather than adding a generic `F: Fn() -> T`. Values can be access with the same
/// function call, `with()`, and `get()` APIs as other signals.
/// This allows you to create APIs that take `T` or any reactive value that returns `T`
/// as an argument, rather than adding a generic `F: Fn() -> T`.
///
/// Values can be accessed with the same function call, `read()`, `with()`, and `get()`
/// APIs as other signals.
///
/// ## Important Notes about Derived Signals
///
/// `Signal::derive()` is simply a way to box and type-erase a “derived signal,” which
/// is a plain closure that accesses one or more signals. It does *not* cache the value
/// of that computation. Accessing the value of a `Signal<_>` that is created using `Signal::derive()`
/// will run the closure again every time you call `.read()`, `.with()`, or `.get()`.
///
/// If you want the closure to run the minimal number of times necessary to update its state,
/// and then to cache its value, you should use a [`Memo`] (and convert it into a `Signal<_>`)
/// rather than using `Signal::derive()`.
///
/// Note that for many computations, it is nevertheless less expensive to use a derived signal
/// than to create a separate memo and to cache the value: creating a new reactive node and
/// taking the lock on that cached value whenever you access the signal is *more* expensive than
/// simply re-running the calculation in many cases.
pub struct ArcSignal<T: 'static, S = SyncStorage>
where
S: Storage<T>,
{
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: SignalTypes<T, S>,
}
@@ -114,7 +132,7 @@ pub mod read {
{
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
inner: self.inner.clone(),
}
@@ -128,7 +146,7 @@ pub mod read {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let mut s = f.debug_struct("ArcSignal");
s.field("inner", &self.inner);
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
s.field("defined_at", &self.defined_at);
s.finish()
}
@@ -184,7 +202,7 @@ pub mod read {
Self {
inner: SignalTypes::DerivedSignal(Arc::new(derived_signal)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -194,7 +212,7 @@ pub mod read {
pub fn stored(value: T) -> Self {
Self {
inner: SignalTypes::Stored(ArcStoredValue::new(value)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -214,7 +232,7 @@ pub mod read {
fn from(value: ArcReadSignal<T>) -> Self {
Self {
inner: SignalTypes::ReadSignal(value),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -225,7 +243,7 @@ pub mod read {
fn from(value: ArcRwSignal<T>) -> Self {
Self {
inner: SignalTypes::ReadSignal(value.read_only()),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -239,7 +257,7 @@ pub mod read {
fn from(value: ArcMemo<T, S>) -> Self {
Self {
inner: SignalTypes::Memo(value),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -250,11 +268,11 @@ pub mod read {
S: Storage<T>,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -330,16 +348,35 @@ pub mod read {
}
/// A wrapper for any kind of arena-allocated reactive signal:
/// an [`ReadSignal`], [`Memo`], [`RwSignal`], or derived signal closure.
/// a [`ReadSignal`], [`Memo`], [`RwSignal`], or derived signal closure,
/// or a plain value of the same type
///
/// This allows you to create APIs that take any kind of `Signal<T>` as an argument,
/// rather than adding a generic `F: Fn() -> T`. Values can be access with the same
/// function call, `with()`, and `get()` APIs as other signals.
/// This allows you to create APIs that take `T` or any reactive value that returns `T`
/// as an argument, rather than adding a generic `F: Fn() -> T`.
///
/// Values can be accessed with the same function call, `read()`, `with()`, and `get()`
/// APIs as other signals.
///
/// ## Important Notes about Derived Signals
///
/// `Signal::derive()` is simply a way to box and type-erase a “derived signal,” which
/// is a plain closure that accesses one or more signals. It does *not* cache the value
/// of that computation. Accessing the value of a `Signal<_>` that is created using `Signal::derive()`
/// will run the closure again every time you call `.read()`, `.with()`, or `.get()`.
///
/// If you want the closure to run the minimal number of times necessary to update its state,
/// and then to cache its value, you should use a [`Memo`] (and convert it into a `Signal<_>`)
/// rather than using `Signal::derive()`.
///
/// Note that for many computations, it is nevertheless less expensive to use a derived signal
/// than to create a separate memo and to cache the value: creating a new reactive node and
/// taking the lock on that cached value whenever you access the signal is *more* expensive than
/// simply re-running the calculation in many cases.
pub struct Signal<T, S = SyncStorage>
where
S: Storage<T>,
{
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: ArenaItem<SignalTypes<T, S>, S>,
}
@@ -371,7 +408,7 @@ pub mod read {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let mut s = f.debug_struct("Signal");
s.field("inner", &self.inner);
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
s.field("defined_at", &self.defined_at);
s.finish()
}
@@ -393,11 +430,11 @@ pub mod read {
S: Storage<T>,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -530,7 +567,7 @@ pub mod read {
inner: ArenaItem::new_with_storage(SignalTypes::DerivedSignal(
Arc::new(derived_signal),
)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -542,7 +579,7 @@ pub mod read {
inner: ArenaItem::new_with_storage(SignalTypes::Stored(
ArcStoredValue::new(value),
)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -569,7 +606,7 @@ pub mod read {
inner: ArenaItem::new_local(SignalTypes::DerivedSignal(
Arc::new(derived_signal),
)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -582,7 +619,7 @@ pub mod read {
inner: ArenaItem::new_local(SignalTypes::Stored(
ArcStoredValue::new(value),
)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -640,7 +677,7 @@ pub mod read {
#[track_caller]
fn from(value: ArcSignal<T, SyncStorage>) -> Self {
Signal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new(value.inner),
}
@@ -654,7 +691,7 @@ pub mod read {
#[track_caller]
fn from_local(value: ArcSignal<T, LocalStorage>) -> Self {
Signal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_local(value.inner),
}
@@ -668,7 +705,7 @@ pub mod read {
#[track_caller]
fn from(value: Signal<T, S>) -> Self {
ArcSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: value
.inner
@@ -686,7 +723,7 @@ pub mod read {
fn from(value: ReadSignal<T>) -> Self {
Self {
inner: ArenaItem::new(SignalTypes::ReadSignal(value.into())),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -702,7 +739,7 @@ pub mod read {
inner: ArenaItem::new_local(SignalTypes::ReadSignal(
value.into(),
)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -716,7 +753,7 @@ pub mod read {
fn from(value: ArcReadSignal<T>) -> Self {
Self {
inner: ArenaItem::new(SignalTypes::ReadSignal(value)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -730,7 +767,7 @@ pub mod read {
fn from(value: ArcReadSignal<T>) -> Self {
Self {
inner: ArenaItem::new_local(SignalTypes::ReadSignal(value)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -746,7 +783,7 @@ pub mod read {
inner: ArenaItem::new(SignalTypes::ReadSignal(
value.read_only().into(),
)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -762,7 +799,7 @@ pub mod read {
inner: ArenaItem::new_local(SignalTypes::ReadSignal(
value.read_only().into(),
)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -778,7 +815,7 @@ pub mod read {
inner: ArenaItem::new(SignalTypes::ReadSignal(
value.read_only(),
)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -794,7 +831,7 @@ pub mod read {
inner: ArenaItem::new_local(SignalTypes::ReadSignal(
value.read_only(),
)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -808,7 +845,7 @@ pub mod read {
fn from(value: Memo<T>) -> Self {
Self {
inner: ArenaItem::new(SignalTypes::Memo(value.into())),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -822,7 +859,7 @@ pub mod read {
fn from(value: Memo<T, LocalStorage>) -> Self {
Self {
inner: ArenaItem::new_local(SignalTypes::Memo(value.into())),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -836,7 +873,7 @@ pub mod read {
fn from(value: ArcMemo<T>) -> Self {
Self {
inner: ArenaItem::new(SignalTypes::Memo(value)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -850,7 +887,7 @@ pub mod read {
fn from(value: ArcMemo<T, LocalStorage>) -> Self {
Self {
inner: ArenaItem::new_local(SignalTypes::Memo(value)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -1831,7 +1868,7 @@ pub mod write {
T: 'static,
{
inner: SignalSetterTypes<T, S>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static std::panic::Location<'static>,
}
@@ -1846,7 +1883,7 @@ pub mod write {
fn default() -> Self {
Self {
inner: SignalSetterTypes::Default,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -1899,7 +1936,7 @@ pub mod write {
inner: SignalSetterTypes::Mapped(ArenaItem::new_with_storage(
Box::new(mapped_setter),
)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -1910,7 +1947,7 @@ pub mod write {
fn from(value: WriteSignal<T, S>) -> Self {
Self {
inner: SignalSetterTypes::Write(value),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}
@@ -1925,7 +1962,7 @@ pub mod write {
fn from(value: RwSignal<T, S>) -> Self {
Self {
inner: SignalSetterTypes::Write(value.write_only()),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: std::panic::Location::caller(),
}
}

View File

@@ -443,3 +443,43 @@ fn unsync_derived_signal_and_memo() {
assert_eq!(f.with(|n| *n), 6);
assert_eq!(f.get_untracked(), 6);
}
#[test]
fn memo_updates_even_if_not_read_until_later() {
#![allow(clippy::bool_assert_comparison)]
let owner = Owner::new();
owner.set();
// regression test for https://github.com/leptos-rs/leptos/issues/3339
let input = RwSignal::new(0);
let first_memo = Memo::new(move |_| input.get() == 1);
let second_memo = Memo::new(move |_| first_memo.get());
assert_eq!(input.get(), 0);
assert_eq!(first_memo.get(), false);
println!("update to 1");
input.set(1);
assert_eq!(input.get(), 1);
println!("read memo 1");
assert_eq!(first_memo.get(), true);
println!("read memo 2");
assert_eq!(second_memo.get(), true);
// this time, we don't read the memo
println!("\nupdate to 2");
input.set(2);
assert_eq!(input.get(), 2);
println!("read memo 1");
assert_eq!(first_memo.get(), false);
println!("\nupdate to 3");
input.set(3);
assert_eq!(input.get(), 3);
println!("read memo 1");
assert_eq!(first_memo.get(), false);
println!("read memo 2");
assert_eq!(second_memo.get(), false);
}

View File

@@ -2,8 +2,8 @@ use reactive_graph::{
owner::Owner,
signal::{arc_signal, signal, ArcRwSignal, RwSignal},
traits::{
Get, GetUntracked, Read, Set, Update, UpdateUntracked, With,
WithUntracked, Write,
Dispose, Get, GetUntracked, IntoInner, Read, Set, Update,
UpdateUntracked, With, WithUntracked, Write,
},
};
@@ -108,3 +108,35 @@ fn update_signal() {
set_a.set(4);
assert_eq!(a.get(), 4);
}
#[test]
fn into_inner_signal() {
let owner = Owner::new();
owner.set();
let rw_signal = RwSignal::new(1);
assert_eq!(rw_signal.get(), 1);
assert_eq!(rw_signal.into_inner(), Some(1));
}
#[test]
fn into_inner_arc_signal() {
let owner = Owner::new();
owner.set();
let (a, b) = arc_signal(2);
assert_eq!(a.get(), 2);
std::mem::drop(b);
assert_eq!(a.into_inner(), Some(2));
}
#[test]
fn into_inner_non_arc_signal() {
let owner = Owner::new();
owner.set();
let (a, b) = signal(2);
assert_eq!(a.get(), 2);
b.dispose();
assert_eq!(a.into_inner(), Some(2));
}

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores"
version = "0.1.0-rc1"
version = "0.1.3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -23,3 +23,6 @@ tokio = { version = "1.41", features = ["rt-multi-thread", "macros"] }
tokio-test = { version = "0.4.4" }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
reactive_graph = { workspace = true, features = ["effects"] }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -7,6 +7,7 @@ use reactive_graph::{
owner::Storage,
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Write,
},
};
use std::{
@@ -25,13 +26,14 @@ pub struct ArcField<T>
where
T: 'static,
{
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
path: StorePath,
trigger: StoreFieldTrigger,
get_trigger: Arc<dyn Fn(StorePath) -> StoreFieldTrigger + Send + Sync>,
read: Arc<dyn Fn() -> Option<StoreFieldReader<T>> + Send + Sync>,
write: Arc<dyn Fn() -> Option<StoreFieldWriter<T>> + Send + Sync>,
pub(crate) write:
Arc<dyn Fn() -> Option<StoreFieldWriter<T>> + Send + Sync>,
keys: Arc<dyn Fn() -> Option<KeyMap> + Send + Sync>,
track_field: Arc<dyn Fn() + Send + Sync>,
}
@@ -114,7 +116,7 @@ where
#[track_caller]
fn from(value: Store<T, S>) -> Self {
ArcField {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
path: value.path().into_iter().collect(),
trigger: value.get_trigger(value.path().into_iter().collect()),
@@ -134,7 +136,7 @@ where
#[track_caller]
fn from(value: ArcStore<T>) -> Self {
ArcField {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
path: value.path().into_iter().collect(),
trigger: value.get_trigger(value.path().into_iter().collect()),
@@ -172,7 +174,7 @@ where
#[track_caller]
fn from(value: Subfield<Inner, Prev, T>) -> Self {
ArcField {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
path: value.path().into_iter().collect(),
trigger: value.get_trigger(value.path().into_iter().collect()),
@@ -210,7 +212,7 @@ where
#[track_caller]
fn from(value: AtIndex<Inner, Prev>) -> Self {
ArcField {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
path: value.path().into_iter().collect(),
trigger: value.get_trigger(value.path().into_iter().collect()),
@@ -252,7 +254,7 @@ where
#[track_caller]
fn from(value: AtKeyed<Inner, Prev, K, T>) -> Self {
ArcField {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
path: value.path().into_iter().collect(),
trigger: value.get_trigger(value.path().into_iter().collect()),
@@ -283,7 +285,7 @@ where
impl<T> Clone for ArcField<T> {
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
path: self.path.clone(),
trigger: self.trigger.clone(),
@@ -298,11 +300,11 @@ impl<T> Clone for ArcField<T> {
impl<T> DefinedAt for ArcField<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -329,6 +331,22 @@ impl<T> ReadUntracked for ArcField<T> {
}
}
impl<T> Write for ArcField<T> {
type Value = T;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
(self.write)()
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
let mut guard = (self.write)()?;
guard.untrack();
Some(guard)
}
}
impl<T> IsDisposed for ArcField<T> {
fn is_disposed(&self) -> bool {
false

View File

@@ -6,10 +6,18 @@ use crate::{
};
use reactive_graph::{
owner::{ArenaItem, Storage, SyncStorage},
traits::{DefinedAt, IsDisposed, Notify, ReadUntracked, Track},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Write,
},
unwrap_signal,
};
use std::{fmt::Debug, hash::Hash, ops::IndexMut, panic::Location};
use std::{
fmt::Debug,
hash::Hash,
ops::{DerefMut, IndexMut},
panic::Location,
};
/// Wraps access to a single field of type `T`.
///
@@ -19,7 +27,7 @@ pub struct Field<T, S = SyncStorage>
where
T: 'static,
{
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: ArenaItem<ArcField<T>, S>,
}
@@ -67,7 +75,7 @@ where
#[track_caller]
fn from(value: Store<T, S>) -> Self {
Field {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value.into()),
}
@@ -82,7 +90,7 @@ where
#[track_caller]
fn from(value: ArcStore<T>) -> Self {
Field {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value.into()),
}
@@ -100,7 +108,7 @@ where
#[track_caller]
fn from(value: Subfield<Inner, Prev, T>) -> Self {
Field {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value.into()),
}
@@ -118,7 +126,7 @@ where
#[track_caller]
fn from(value: AtIndex<Inner, Prev>) -> Self {
Field {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value.into()),
}
@@ -141,7 +149,7 @@ where
#[track_caller]
fn from(value: AtKeyed<Inner, Prev, K, T>) -> Self {
Field {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value.into()),
}
@@ -158,11 +166,11 @@ impl<T, S> Copy for Field<T, S> {}
impl<T, S> DefinedAt for Field<T, S> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -204,6 +212,24 @@ where
}
}
impl<T> Write for Field<T> {
type Value = T;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.inner.try_get_value().and_then(|inner| (inner.write)())
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.inner.try_get_value().and_then(|inner| {
let mut guard = (inner.write)()?;
guard.untrack();
Some(guard)
})
}
}
impl<T, S> IsDisposed for Field<T, S> {
fn is_disposed(&self) -> bool {
self.inner.is_disposed()

View File

@@ -23,7 +23,7 @@ use std::{
/// Provides access to the data at some index in another collection.
#[derive(Debug)]
pub struct AtIndex<Inner, Prev> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: Inner,
index: usize,
@@ -36,7 +36,7 @@ where
{
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
inner: self.inner.clone(),
index: self.index,
@@ -52,7 +52,7 @@ impl<Inner, Prev> AtIndex<Inner, Prev> {
#[track_caller]
pub fn new(inner: Inner, index: usize) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner,
index,
@@ -115,11 +115,11 @@ where
Inner: StoreField<Value = Prev>,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}

View File

@@ -28,7 +28,7 @@ pub struct KeyedSubfield<Inner, Prev, K, T>
where
for<'a> &'a T: IntoIterator,
{
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
path_segment: StorePathSegment,
inner: Inner,
@@ -44,7 +44,7 @@ where
{
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
path_segment: self.path_segment,
inner: self.inner.clone(),
@@ -76,7 +76,7 @@ where
write: fn(&mut Prev) -> &mut T,
) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner,
path_segment,
@@ -254,11 +254,11 @@ where
Inner: StoreField<Value = Prev>,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -356,7 +356,7 @@ pub struct AtKeyed<Inner, Prev, K, T>
where
for<'a> &'a T: IntoIterator,
{
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: KeyedSubfield<Inner, Prev, K, T>,
key: K,
@@ -370,7 +370,7 @@ where
{
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
inner: self.inner.clone(),
key: self.key.clone(),
@@ -394,7 +394,7 @@ where
#[track_caller]
pub fn new(inner: KeyedSubfield<Inner, Prev, K, T>, key: K) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner,
key,
@@ -450,10 +450,7 @@ where
let inner = self.inner.reader()?;
let inner_path = self.inner.path().into_iter().collect();
let keys = self
.inner
.keys()
.expect("using keys on a store with no keys");
let keys = self.inner.keys()?;
let index = keys
.with_field_keys(
inner_path,
@@ -461,8 +458,7 @@ where
|| self.inner.latest_keys(),
)
.flatten()
.map(|(_, idx)| idx)
.expect("reading from a keyed field that has not yet been created");
.map(|(_, idx)| idx)?;
Some(MappedMutArc::new(
inner,
@@ -511,11 +507,11 @@ where
for<'a> &'a T: IntoIterator,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -623,10 +619,15 @@ where
.keys()
.expect("updating keys on a store with no keys");
// generating the latest keys out here means that if we have
// nested keyed fields, the second field will not try to take a
// read-lock on the key map to get the field while the first field
// is still holding the write-lock in the closure below
let latest = self.latest_keys();
keys.with_field_keys(
inner_path,
|keys| {
keys.update(self.latest_keys());
keys.update(latest);
},
|| self.latest_keys(),
);
@@ -649,8 +650,8 @@ where
#[track_caller]
fn into_iter(self) -> StoreFieldKeyedIter<Inner, Prev, K, T> {
// reactively track changes to this field
let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.this.track();
self.update_keys();
self.track_field();
// get the current length of the field by accessing slice
let reader = self

View File

@@ -114,8 +114,8 @@ use reactive_graph::{
ArcTrigger,
},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Write,
DefinedAt, Dispose, IsDisposed, Notify, ReadUntracked, Track,
UntrackableGuard, Write,
},
};
pub use reactive_stores_macro::{Patch, Store};
@@ -315,7 +315,7 @@ impl KeyMap {
/// This adds a getter method for each field to `Store<T>`, which allow accessing reactive versions
/// of each individual field of the struct.
pub struct ArcStore<T> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
pub(crate) value: Arc<RwLock<T>>,
signals: Arc<RwLock<TriggerMap>>,
@@ -326,7 +326,7 @@ impl<T> ArcStore<T> {
/// Creates a new store from the initial value.
pub fn new(value: T) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
value: Arc::new(RwLock::new(value)),
signals: Default::default(),
@@ -335,10 +335,16 @@ impl<T> ArcStore<T> {
}
}
impl<T: Default> Default for ArcStore<T> {
fn default() -> Self {
Self::new(T::default())
}
}
impl<T: Debug> Debug for ArcStore<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut f = f.debug_struct("ArcStore");
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
let f = f.field("defined_at", &self.defined_at);
f.field("value", &self.value)
.field("signals", &self.signals)
@@ -349,7 +355,7 @@ impl<T: Debug> Debug for ArcStore<T> {
impl<T> Clone for ArcStore<T> {
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
value: Arc::clone(&self.value),
signals: Arc::clone(&self.signals),
@@ -360,11 +366,11 @@ impl<T> Clone for ArcStore<T> {
impl<T> DefinedAt for ArcStore<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -433,7 +439,7 @@ impl<T: 'static> Notify for ArcStore<T> {
/// This follows the same ownership rules as arena-allocated types like
/// [`RwSignal`](reactive_graph::signal::RwSignal).
pub struct Store<T, S = SyncStorage> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: ArenaItem<ArcStore<T>, S>,
}
@@ -445,7 +451,7 @@ where
/// Creates a new store with the initial value.
pub fn new(value: T) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcStore::new(value)),
}
@@ -461,20 +467,38 @@ where
/// This pins the value to the current thread. Accessing it from any other thread will panic.
pub fn new_local(value: T) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcStore::new(value)),
}
}
}
impl<T> Default for Store<T>
where
T: Default + Send + Sync + 'static,
{
fn default() -> Self {
Self::new(T::default())
}
}
impl<T> Default for Store<T, LocalStorage>
where
T: Default + 'static,
{
fn default() -> Self {
Self::new_local(T::default())
}
}
impl<T: Debug, S> Debug for Store<T, S>
where
S: Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut f = f.debug_struct("Store");
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
let f = f.field("defined_at", &self.defined_at);
f.field("inner", &self.inner).finish()
}
@@ -490,11 +514,11 @@ impl<T, S> Copy for Store<T, S> {}
impl<T, S> DefinedAt for Store<T, S> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -511,6 +535,15 @@ where
}
}
impl<T, S> Dispose for Store<T, S>
where
T: 'static,
{
fn dispose(self) {
self.inner.dispose();
}
}
impl<T, S> ReadUntracked for Store<T, S>
where
T: 'static,
@@ -569,6 +602,20 @@ where
}
}
impl<T, S> From<ArcStore<T>> for Store<T, S>
where
T: 'static,
S: Storage<ArcStore<T>>,
{
fn from(value: ArcStore<T>) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: value.defined_at,
inner: ArenaItem::new_with_storage(value),
}
}
}
#[cfg(test)]
mod tests {
use crate::{self as reactive_stores, Patch, Store, StoreFieldIterator};

View File

@@ -1,5 +1,5 @@
use crate::{StoreField, Subfield};
use reactive_graph::traits::Read;
use reactive_graph::traits::{Read, ReadUntracked};
use std::ops::Deref;
/// Extends optional store fields, with the ability to unwrap or map over them.
@@ -23,12 +23,23 @@ where
self,
map_fn: impl FnOnce(Subfield<Self, Option<Self::Output>, Self::Output>) -> U,
) -> Option<U>;
/// Unreactively maps over the field.
///
/// This returns `None` if the subfield is currently `None`,
/// and a new store subfield with the inner value if it is `Some`. This is an unreactive variant of
/// `[OptionStoreExt::map]`, and will not cause the reactive context to re-run if the field changes.
fn map_untracked<U>(
self,
map_fn: impl FnOnce(Subfield<Self, Option<Self::Output>, Self::Output>) -> U,
) -> Option<U>;
}
impl<T, S> OptionStoreExt for S
where
S: StoreField<Value = Option<T>> + Read,
S: StoreField<Value = Option<T>> + Read + ReadUntracked,
<S as Read>::Value: Deref<Target = Option<T>>,
<S as ReadUntracked>::Value: Deref<Target = Option<T>>,
{
type Output = T;
@@ -51,6 +62,17 @@ where
None
}
}
fn map_untracked<U>(
self,
map_fn: impl FnOnce(Subfield<S, Option<T>, T>) -> U,
) -> Option<U> {
if self.read_untracked().is_some() {
Some(map_fn(self.unwrap()))
} else {
None
}
}
}
#[cfg(test)]

View File

@@ -78,6 +78,7 @@ patch_primitives! {
Arc<str>,
Rc<str>,
Cow<'_, str>,
usize,
u8,
u16,
u32,

View File

@@ -24,12 +24,15 @@ pub trait StoreField: Sized {
type Writer: UntrackableGuard<Target = Self::Value>;
/// Returns the trigger that tracks access and updates for this field.
#[track_caller]
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger;
/// The path of this field (see [`StorePath`]).
#[track_caller]
fn path(&self) -> impl IntoIterator<Item = StorePathSegment>;
/// Reactively tracks this field.
#[track_caller]
fn track_field(&self) {
let path = self.path().into_iter().collect();
let trigger = self.get_trigger(path);
@@ -38,12 +41,15 @@ pub trait StoreField: Sized {
}
/// Returns a read guard to access this field.
#[track_caller]
fn reader(&self) -> Option<Self::Reader>;
/// Returns a write guard to update this field.
#[track_caller]
fn writer(&self) -> Option<Self::Writer>;
/// The keys for this field, if it is a keyed field.
#[track_caller]
fn keys(&self) -> Option<KeyMap>;
}
@@ -55,26 +61,31 @@ where
type Reader = Plain<T>;
type Writer = WriteGuard<ArcTrigger, UntrackedWriteGuard<T>>;
#[track_caller]
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
let triggers = &self.signals;
let trigger = triggers.write().or_poisoned().get_or_insert(path);
trigger
}
#[track_caller]
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
iter::empty()
}
#[track_caller]
fn reader(&self) -> Option<Self::Reader> {
Plain::try_new(Arc::clone(&self.value))
}
#[track_caller]
fn writer(&self) -> Option<Self::Writer> {
let trigger = self.get_trigger(Default::default());
let guard = UntrackedWriteGuard::try_new(Arc::clone(&self.value))?;
Some(WriteGuard::new(trigger.children, guard))
}
#[track_caller]
fn keys(&self) -> Option<KeyMap> {
Some(self.keys.clone())
}
@@ -89,6 +100,7 @@ where
type Reader = Plain<T>;
type Writer = WriteGuard<ArcTrigger, UntrackedWriteGuard<T>>;
#[track_caller]
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
self.inner
.try_get_value()
@@ -96,6 +108,7 @@ where
.unwrap_or_else(unwrap_signal!(self))
}
#[track_caller]
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
.try_get_value()
@@ -103,14 +116,17 @@ where
.unwrap_or_else(unwrap_signal!(self))
}
#[track_caller]
fn reader(&self) -> Option<Self::Reader> {
self.inner.try_get_value().and_then(|n| n.reader())
}
#[track_caller]
fn writer(&self) -> Option<Self::Writer> {
self.inner.try_get_value().and_then(|n| n.writer())
}
#[track_caller]
fn keys(&self) -> Option<KeyMap> {
self.inner.try_get_value().and_then(|inner| inner.keys())
}

View File

@@ -18,7 +18,7 @@ use std::{iter, marker::PhantomData, ops::DerefMut, panic::Location};
/// Accesses a single field of a reactive structure.
#[derive(Debug)]
pub struct Subfield<Inner, Prev, T> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
path_segment: StorePathSegment,
inner: Inner,
@@ -33,7 +33,7 @@ where
{
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
path_segment: self.path_segment,
inner: self.inner.clone(),
@@ -56,7 +56,7 @@ impl<Inner, Prev, T> Subfield<Inner, Prev, T> {
write: fn(&mut Prev) -> &mut T,
) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner,
path_segment,
@@ -103,6 +103,7 @@ where
self.inner.keys()
}
#[track_caller]
fn track_field(&self) {
let inner = self
.inner
@@ -119,11 +120,11 @@ where
Inner: StoreField<Value = Prev>,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -144,6 +145,7 @@ where
Inner: StoreField<Value = Prev>,
Prev: 'static,
{
#[track_caller]
fn notify(&self) {
let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.this.notify();
@@ -157,6 +159,7 @@ where
Prev: 'static,
T: 'static,
{
#[track_caller]
fn track(&self) {
self.track_field();
}

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores_macro"
version = "0.1.0-rc1"
version = "0.1.0"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.7.0-rc1"
version = "0.7.3"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
@@ -19,9 +19,9 @@ reactive_graph = { workspace = true }
tachys = { workspace = true, features = ["reactive_graph"] }
futures = "0.3.31"
url = "2.5"
js-sys = { version = "0.3.72" }
wasm-bindgen = { version = "0.2.95" }
tracing = { version = "0.1.40", optional = true }
js-sys = { version = "0.3.74" }
wasm-bindgen = { version = "0.2.97" }
tracing = { version = "0.1.41", optional = true }
once_cell = "1.20"
send_wrapper = "0.6.0"
thiserror = "2.0"

View File

@@ -13,8 +13,8 @@ use crate::{
resolve_path::resolve_path,
ChooseView, MatchNestedRoutes, NestedRoute, RouteDefs, SsrMode,
};
use either_of::Either;
use leptos::prelude::*;
use either_of::EitherOf3;
use leptos::{children, prelude::*};
use reactive_graph::{
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
@@ -64,8 +64,8 @@ pub fn Router<Chil>(
//#[prop(optional)]
//trailing_slash: TrailingSlash,
/// The `<Router/>` should usually wrap your whole page. It can contain
/// any elements, and should include a [`Routes`](crate::Routes) component somewhere
/// to define and display [`Route`](crate::Route)s.
/// any elements, and should include a [`Routes`] component somewhere
/// to define and display [`Route`]s.
children: TypedChildren<Chil>,
) -> impl IntoView
where
@@ -82,13 +82,18 @@ where
#[cfg(not(feature = "ssr"))]
let (location_provider, current_url, redirect_hook) = {
let owner = Owner::current();
let location =
BrowserUrl::new().expect("could not access browser navigation"); // TODO options here
location.init(base.clone());
provide_context(location.clone());
let current_url = location.as_url().clone();
let redirect_hook = Box::new(|loc: &str| BrowserUrl::redirect(loc));
let redirect_hook = Box::new(move |loc: &str| {
if let Some(owner) = &owner {
owner.with(|| BrowserUrl::redirect(loc));
}
});
(Some(location), current_url, redirect_hook)
};
@@ -328,9 +333,10 @@ where
/// and the element it should display.
#[component(transparent)]
pub fn Route<Segments, View>(
/// The path fragment that this route should match. This can be created using the [`path`]
/// macro, or path segments ([`StaticSegment`], [`ParamSegment`], [`WildcardSegment`], and
/// [`OptionalParamSegment`]).
/// The path fragment that this route should match. This can be created using the
/// [`path`](crate::path) macro, or path segments ([`StaticSegment`](crate::StaticSegment),
/// [`ParamSegment`](crate::ParamSegment), [`WildcardSegment`](crate::WildcardSegment), and
/// [`OptionalParamSegment`](crate::OptionalParamSegment)).
path: Segments,
/// The view for this route.
view: View,
@@ -349,9 +355,10 @@ where
/// and the element it should display.
#[component(transparent)]
pub fn ParentRoute<Segments, View, Children>(
/// The path fragment that this route should match. This can be created using the [`path`]
/// macro, or path segments ([`StaticSegment`], [`ParamSegment`], [`WildcardSegment`], and
/// [`OptionalParamSegment`]).
/// The path fragment that this route should match. This can be created using the
/// [`path`](crate::path) macro, or path segments ([`StaticSegment`](crate::StaticSegment),
/// [`ParamSegment`](crate::ParamSegment), [`WildcardSegment`](crate::WildcardSegment), and
/// [`OptionalParamSegment`](crate::OptionalParamSegment)).
path: Segments,
/// The view for this route.
view: View,
@@ -374,9 +381,10 @@ where
/// redirects to `redirect_path` instead of displaying its `view`.
#[component(transparent)]
pub fn ProtectedRoute<Segments, ViewFn, View, C, PathFn, P>(
/// The path fragment that this route should match. This can be created using the [`path`]
/// macro, or path segments ([`StaticSegment`], [`ParamSegment`], [`WildcardSegment`], and
/// [`OptionalParamSegment`]).
/// The path fragment that this route should match. This can be created using the
/// [`path`](crate::path) macro, or path segments ([`StaticSegment`](crate::StaticSegment),
/// [`ParamSegment`](crate::ParamSegment), [`WildcardSegment`](crate::WildcardSegment), and
/// [`OptionalParamSegment`](crate::OptionalParamSegment)).
path: Segments,
/// The view for this route.
view: ViewFn,
@@ -386,6 +394,9 @@ pub fn ProtectedRoute<Segments, ViewFn, View, C, PathFn, P>(
condition: C,
/// The path that will be redirected to if the condition is `Some(false)`.
redirect_path: PathFn,
/// Will be displayed while the condition is pending. By default this is the empty view.
#[prop(optional, into)]
fallback: children::ViewFn,
/// The mode that this route prefers during server-side rendering.
/// Defaults to out-of-order streaming.
#[prop(optional)]
@@ -398,23 +409,26 @@ where
PathFn: Fn() -> P + Send + Clone + 'static,
P: Display + 'static,
{
let fallback = move || fallback.run();
let view = move || {
let condition = condition.clone();
let redirect_path = redirect_path.clone();
let view = view.clone();
let fallback = fallback.clone();
(view! {
<Transition>
<Transition fallback=fallback.clone()>
{move || {
let condition = condition();
let view = view.clone();
let redirect_path = redirect_path.clone();
let fallback = fallback.clone();
Unsuspend::new(move || match condition {
Some(true) => Either::Left(view()),
Some(true) => EitherOf3::A(view()),
#[allow(clippy::unit_arg)]
Some(false) => {
Either::Right(view! { <Redirect path=redirect_path()/> }.into_inner())
EitherOf3::B(view! { <Redirect path=redirect_path()/> }.into_inner())
}
None => Either::Right(()),
None => EitherOf3::C(fallback()),
})
}}
@@ -427,9 +441,10 @@ where
#[component(transparent)]
pub fn ProtectedParentRoute<Segments, ViewFn, View, C, PathFn, P, Children>(
/// The path fragment that this route should match. This can be created using the [`path`]
/// macro, or path segments ([`StaticSegment`], [`ParamSegment`], [`WildcardSegment`], and
/// [`OptionalParamSegment`]).
/// The path fragment that this route should match. This can be created using the
/// [`path`](crate::path) macro, or path segments ([`StaticSegment`](crate::StaticSegment),
/// [`ParamSegment`](crate::ParamSegment), [`WildcardSegment`](crate::WildcardSegment), and
/// [`OptionalParamSegment`](crate::OptionalParamSegment)).
path: Segments,
/// The view for this route.
view: ViewFn,
@@ -437,6 +452,9 @@ pub fn ProtectedParentRoute<Segments, ViewFn, View, C, PathFn, P, Children>(
/// the page, `Some(false)` means the user cannot access the page, and `None` means this
/// information is still loading.
condition: C,
/// Will be displayed while the condition is pending. By default this is the empty view.
#[prop(optional, into)]
fallback: children::ViewFn,
/// The path that will be redirected to if the condition is `Some(false)`.
redirect_path: PathFn,
/// Nested child routes.
@@ -453,17 +471,21 @@ where
PathFn: Fn() -> P + Send + Clone + 'static,
P: Display + 'static,
{
let fallback = move || fallback.run();
let children = children.into_inner();
let view = move || {
let condition = condition.clone();
let redirect_path = redirect_path.clone();
let fallback = fallback.clone();
let view = view.clone();
let owner = Owner::current().unwrap();
let view = {
let fallback = fallback.clone();
move || {
let condition = condition();
let view = view.clone();
let redirect_path = redirect_path.clone();
let fallback = fallback.clone();
let owner = owner.clone();
Unsuspend::new(move || match condition {
// reset the owner so that things like providing context work
@@ -472,16 +494,16 @@ where
//
// clippy: not redundant, a FnOnce vs FnMut issue
#[allow(clippy::redundant_closure)]
Some(true) => Either::Left(owner.with(|| view())),
Some(true) => EitherOf3::A(owner.with(|| view())),
#[allow(clippy::unit_arg)]
Some(false) => Either::Right(
Some(false) => EitherOf3::B(
view! { <Redirect path=redirect_path()/> }.into_inner(),
),
None => Either::Right(()),
None => EitherOf3::C(fallback()),
})
}
};
(view! { <Transition>{view}</Transition> }).into_any()
(view! { <Transition fallback>{view}</Transition> }).into_any()
};
NestedRoute::new(path, view).ssr_mode(ssr).child(children)
}

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