Compare commits

...

24 Commits

Author SHA1 Message Date
autofix-ci[bot]
0dc6b7b56b [autofix.ci] apply automated fixes (attempt 2/3) 2026-01-02 21:30:57 +00:00
autofix-ci[bot]
3a2700ac56 [autofix.ci] apply automated fixes 2026-01-02 20:24:10 +00:00
Greg Johnston
421ed40e9d chore: remove conditional code for wasm32 from stores 2026-01-02 14:46:16 -05:00
Greg Johnston
588f6db557 chore: remove dashmap (cf. #4408) 2026-01-02 14:45:59 -05:00
Greg Johnston
dd507168fa chore: update syntax in COMMON_BUGS.md doc
Updated documentation to reflect changes in signal handling and effects in Rust.
2026-01-02 12:35:50 -05:00
Greg Johnston
8438b1633f chore: remove outdated bug from "common bugs" 2026-01-02 12:34:32 -05:00
Saber Haj Rabiee
b87c310046 fix: auto re-connect dev websocket live reload on close (#4517) 2026-01-02 12:30:59 -05:00
dependabot[bot]
692153ded2 chore(deps): bump the rust-dependencies group across 1 directory with 23 updates (#4515)
Bumps the rust-dependencies group with 20 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [serde_json](https://github.com/serde-rs/json) | `1.0.145` | `1.0.148` |
| [proc-macro2](https://github.com/dtolnay/proc-macro2) | `1.0.103` | `1.0.104` |
| [axum](https://github.com/tokio-rs/axum) | `0.8.7` | `0.8.8` |
| [tempfile](https://github.com/Stebalien/tempfile) | `3.23.0` | `3.24.0` |
| [rmp-serde](https://github.com/3Hren/msgpack-rust) | `1.3.0` | `1.3.1` |
| [reqwest](https://github.com/seanmonstar/reqwest) | `0.12.26` | `0.12.28` |
| [insta](https://github.com/mitsuhiko/insta) | `1.45.0` | `1.45.1` |
| [async-lock](https://github.com/smol-rs/async-lock) | `3.4.1` | `3.4.2` |
| [axum-core](https://github.com/tokio-rs/axum) | `0.5.5` | `0.5.6` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.50` | `1.2.51` |
| [derive_more](https://github.com/JelteF/derive_more) | `2.1.0` | `2.1.1` |
| [iri-string](https://github.com/lo48576/iri-string) | `0.7.9` | `0.7.10` |
| [itoa](https://github.com/dtolnay/itoa) | `1.0.15` | `1.0.17` |
| [miniserde](https://github.com/dtolnay/miniserde) | `0.1.43` | `0.1.45` |
| [rmp](https://github.com/3Hren/msgpack-rust) | `0.8.14` | `0.8.15` |
| [ryu](https://github.com/dtolnay/ryu) | `1.0.20` | `1.0.22` |
| [serde_spanned](https://github.com/toml-rs/toml) | `1.0.3` | `1.0.4` |
| [signal-hook-registry](https://github.com/vorner/signal-hook) | `1.4.7` | `1.4.8` |
| [toml_datetime](https://github.com/toml-rs/toml) | `0.7.3` | `0.7.5+spec-1.1.0` |
| [toml_edit](https://github.com/toml-rs/toml) | `0.23.7` | `0.23.10+spec-1.0.0` |



Updates `serde_json` from 1.0.145 to 1.0.148
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.145...v1.0.148)

Updates `proc-macro2` from 1.0.103 to 1.0.104
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.103...1.0.104)

Updates `axum` from 0.8.7 to 0.8.8
- [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.8.7...axum-v0.8.8)

Updates `tempfile` from 3.23.0 to 3.24.0
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.23.0...v3.24.0)

Updates `rmp-serde` from 1.3.0 to 1.3.1
- [Release notes](https://github.com/3Hren/msgpack-rust/releases)
- [Commits](https://github.com/3Hren/msgpack-rust/commits)

Updates `reqwest` from 0.12.26 to 0.12.28
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.26...v0.12.28)

Updates `insta` from 1.45.0 to 1.45.1
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.45.0...1.45.1)

Updates `async-lock` from 3.4.1 to 3.4.2
- [Release notes](https://github.com/smol-rs/async-lock/releases)
- [Changelog](https://github.com/smol-rs/async-lock/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/async-lock/compare/v3.4.1...v3.4.2)

Updates `axum-core` from 0.5.5 to 0.5.6
- [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-core-v0.5.5...axum-core-v0.5.6)

Updates `cc` from 1.2.50 to 1.2.51
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.50...cc-v1.2.51)

Updates `derive_more` from 2.1.0 to 2.1.1
- [Release notes](https://github.com/JelteF/derive_more/releases)
- [Changelog](https://github.com/JelteF/derive_more/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JelteF/derive_more/compare/v2.1.0...v2.1.1)

Updates `derive_more-impl` from 2.1.0 to 2.1.1
- [Release notes](https://github.com/JelteF/derive_more/releases)
- [Changelog](https://github.com/JelteF/derive_more/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JelteF/derive_more/compare/v2.1.0...v2.1.1)

Updates `find-msvc-tools` from 0.1.5 to 0.1.6
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/find-msvc-tools-v0.1.5...find-msvc-tools-v0.1.6)

Updates `iri-string` from 0.7.9 to 0.7.10
- [Changelog](https://github.com/lo48576/iri-string/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/lo48576/iri-string/compare/v0.7.9...v0.7.10)

Updates `itoa` from 1.0.15 to 1.0.17
- [Release notes](https://github.com/dtolnay/itoa/releases)
- [Commits](https://github.com/dtolnay/itoa/compare/1.0.15...1.0.17)

Updates `miniserde` from 0.1.43 to 0.1.45
- [Release notes](https://github.com/dtolnay/miniserde/releases)
- [Commits](https://github.com/dtolnay/miniserde/compare/0.1.43...0.1.45)

Updates `rmp` from 0.8.14 to 0.8.15
- [Release notes](https://github.com/3Hren/msgpack-rust/releases)
- [Commits](https://github.com/3Hren/msgpack-rust/commits)

Updates `rustix` from 1.1.2 to 1.1.3
- [Release notes](https://github.com/bytecodealliance/rustix/releases)
- [Changelog](https://github.com/bytecodealliance/rustix/blob/main/CHANGES.md)
- [Commits](https://github.com/bytecodealliance/rustix/compare/v1.1.2...v1.1.3)

Updates `ryu` from 1.0.20 to 1.0.22
- [Release notes](https://github.com/dtolnay/ryu/releases)
- [Commits](https://github.com/dtolnay/ryu/compare/1.0.20...1.0.22)

Updates `serde_spanned` from 1.0.3 to 1.0.4
- [Commits](https://github.com/toml-rs/toml/compare/serde_spanned-v1.0.3...serde_spanned-v1.0.4)

Updates `signal-hook-registry` from 1.4.7 to 1.4.8
- [Changelog](https://github.com/vorner/signal-hook/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vorner/signal-hook/compare/registry-v1.4.7...registry-v1.4.8)

Updates `toml_datetime` from 0.7.3 to 0.7.5+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/toml_datetime-v0.7.3...toml_datetime-v0.7.5)

Updates `toml_edit` from 0.23.7 to 0.23.10+spec-1.0.0
- [Commits](https://github.com/toml-rs/toml/compare/v0.23.7...v0.23.10)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.148
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: proc-macro2
  dependency-version: 1.0.104
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: axum
  dependency-version: 0.8.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tempfile
  dependency-version: 3.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: rmp-serde
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: reqwest
  dependency-version: 0.12.28
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: insta
  dependency-version: 1.45.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: async-lock
  dependency-version: 3.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: axum-core
  dependency-version: 0.5.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.51
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: derive_more
  dependency-version: 2.1.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: derive_more-impl
  dependency-version: 2.1.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: find-msvc-tools
  dependency-version: 0.1.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: iri-string
  dependency-version: 0.7.10
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: itoa
  dependency-version: 1.0.17
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: miniserde
  dependency-version: 0.1.45
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rmp
  dependency-version: 0.8.15
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rustix
  dependency-version: 1.1.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: ryu
  dependency-version: 1.0.22
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: serde_spanned
  dependency-version: 1.0.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: signal-hook-registry
  dependency-version: 1.4.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: toml_datetime
  dependency-version: 0.7.5+spec-1.1.0
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: toml_edit
  dependency-version: 0.23.10+spec-1.0.0
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 12:29:51 -05:00
fbdb
bbb4e698ba chore: fix broken links in docs (#4505) 2026-01-02 12:29:39 -05:00
Luxalpa
0c44199bc9 feat: implement Params derive macro without nightly (#4514) 2026-01-02 12:29:10 -05:00
Ai Suzuki
4f0daada69 chore: pass lint attributes (allow, warn deny and forbid) to generated struct (closes #4498) (#4496) 2026-01-02 12:19:00 -05:00
Greg Johnston
b45eb8f39f chore: fix nonsensical example in reactive_stores (#4516) 2025-12-30 08:07:02 -05:00
Greg Johnston
46f16ec9cf fix: preserve existing attributes when rebuilding AnyViewWithAttrs (closes #4512) (#4513) 2025-12-29 13:55:11 -05:00
Greg Johnston
bfdf0bf4d1 fix: track parents when tracking keyed subfield (second half of #4475) (#4510) 2025-12-29 13:54:59 -05:00
Greg Johnston
e7e18b1995 fix: correctly read resources inside <For/> children (closes #4503) (#4504) 2025-12-26 11:26:05 -05:00
dependabot[bot]
81cff63455 chore(deps): bump actions/cache from 4 to 5 (#4489)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 15:48:06 -05:00
dependabot[bot]
61186c2432 chore(deps): bump the rust-dependencies group across 1 directory with 19 updates (#4499)
Bumps the rust-dependencies group with 14 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [tracing](https://github.com/tokio-rs/tracing) | `0.1.43` | `0.1.44` |
| [bitcode](https://github.com/SoftbearStudios/bitcode) | `0.6.7` | `0.6.9` |
| [insta](https://github.com/mitsuhiko/insta) | `1.44.3` | `1.45.0` |
| [bumpalo](https://github.com/fitzgen/bumpalo) | `3.19.0` | `3.19.1` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.49` | `1.2.50` |
| [glam](https://github.com/bitshifter/glam-rs) | `0.30.8` | `0.30.9` |
| [half](https://github.com/VoidStarKat/half-rs) | `2.6.0` | `2.7.1` |
| [minicov](https://github.com/Amanieu/minicov) | `0.3.7` | `0.3.8` |
| [rustls-pki-types](https://github.com/rustls/pki-types) | `1.13.1` | `1.13.2` |
| [system-deps](https://github.com/gdesmott/system-deps) | `7.0.5` | `7.0.7` |
| [toml_parser](https://github.com/toml-rs/toml) | `1.0.4` | `1.0.6+spec-1.1.0` |
| [toml_writer](https://github.com/toml-rs/toml) | `1.0.4` | `1.0.6+spec-1.1.0` |
| [utf8-width](https://github.com/magiclen/utf8-width) | `0.1.7` | `0.1.8` |
| [version-compare](https://gitlab.com/timvisee/version-compare) | `0.2.0` | `0.2.1` |



Updates `tracing` from 0.1.43 to 0.1.44
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-0.1.43...tracing-0.1.44)

Updates `bitcode` from 0.6.7 to 0.6.9
- [Commits](https://github.com/SoftbearStudios/bitcode/commits)

Updates `insta` from 1.44.3 to 1.45.0
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.44.3...1.45.0)

Updates `bitcode_derive` from 0.6.7 to 0.6.9
- [Commits](https://github.com/SoftbearStudios/bitcode/commits)

Updates `bumpalo` from 3.19.0 to 3.19.1
- [Changelog](https://github.com/fitzgen/bumpalo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fitzgen/bumpalo/compare/v3.19.0...v3.19.1)

Updates `cc` from 1.2.49 to 1.2.50
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.49...cc-v1.2.50)

Updates `glam` from 0.30.8 to 0.30.9
- [Changelog](https://github.com/bitshifter/glam-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitshifter/glam-rs/compare/0.30.8...0.30.9)

Updates `half` from 2.6.0 to 2.7.1
- [Release notes](https://github.com/VoidStarKat/half-rs/releases)
- [Changelog](https://github.com/VoidStarKat/half-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/VoidStarKat/half-rs/compare/v2.6.0...v2.7.1)

Updates `minicov` from 0.3.7 to 0.3.8
- [Changelog](https://github.com/Amanieu/minicov/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Amanieu/minicov/compare/v0.3.7...v0.3.8)

Updates `rustls-pki-types` from 1.13.1 to 1.13.2
- [Release notes](https://github.com/rustls/pki-types/releases)
- [Commits](https://github.com/rustls/pki-types/compare/v/1.13.1...v/1.13.2)

Updates `system-deps` from 7.0.5 to 7.0.7
- [Release notes](https://github.com/gdesmott/system-deps/releases)
- [Changelog](https://github.com/gdesmott/system-deps/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gdesmott/system-deps/compare/v7.0.5...v7.0.7)

Updates `toml` from 0.8.23 to 0.9.8
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.23...toml-v0.9.8)

Updates `toml_datetime` from 0.6.11 to 0.7.3
- [Commits](https://github.com/toml-rs/toml/compare/toml_datetime-v0.6.11...toml_datetime-v0.7.3)

Updates `toml_edit` from 0.22.27 to 0.23.7
- [Commits](https://github.com/toml-rs/toml/compare/v0.22.27...v0.23.7)

Updates `toml_parser` from 1.0.4 to 1.0.6+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/toml_parser-v1.0.4...toml_parser-v1.0.6)

Updates `toml_writer` from 1.0.4 to 1.0.6+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/toml_writer-v1.0.4...toml_writer-v1.0.6)

Updates `tracing-core` from 0.1.35 to 0.1.36
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-core-0.1.35...tracing-core-0.1.36)

Updates `utf8-width` from 0.1.7 to 0.1.8
- [Commits](https://github.com/magiclen/utf8-width/compare/v0.1.7...v0.1.8)

Updates `version-compare` from 0.2.0 to 0.2.1
- [Changelog](https://gitlab.com/timvisee/version-compare/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/timvisee/version-compare/compare/v0.2.0...v0.2.1)

---
updated-dependencies:
- dependency-name: tracing
  dependency-version: 0.1.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: bitcode
  dependency-version: 0.6.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: insta
  dependency-version: 1.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: bitcode_derive
  dependency-version: 0.6.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: bumpalo
  dependency-version: 3.19.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.50
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: glam
  dependency-version: 0.30.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: half
  dependency-version: 2.7.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: minicov
  dependency-version: 0.3.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rustls-pki-types
  dependency-version: 1.13.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: system-deps
  dependency-version: 7.0.7
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: toml
  dependency-version: 0.9.8
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: toml_datetime
  dependency-version: 0.7.3
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: toml_edit
  dependency-version: 0.23.7
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: toml_parser
  dependency-version: 1.0.6+spec-1.1.0
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: toml_writer
  dependency-version: 1.0.6+spec-1.1.0
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tracing-core
  dependency-version: 0.1.36
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: utf8-width
  dependency-version: 0.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: version-compare
  dependency-version: 0.2.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 15:47:50 -05:00
Greg Johnston
75e42ccea5 Merge pull request #4501 from leptos-rs/4488v2
Fix untracked writes on keyed store fields
2025-12-19 15:47:29 -05:00
Greg Johnston
85c7cc94ad Merge pull request #4500 from leptos-rs/fix-4481
fix: wrong path for `IntoAnyAttribute` in macro expansion from #4481
2025-12-19 15:46:56 -05:00
Greg Johnston
270536adb1 fix: prevent untracked writes on keyed subfields from notifying parent
(closes #4488)
2025-12-19 11:41:13 -05:00
Greg Johnston
cec0fb8d85 chore: add regression test to make sure untracked writes on store fields
don't notify anything
2025-12-19 11:40:44 -05:00
Greg Johnston
764b9cd57d leptos_macro-v0.8.14 2025-12-19 11:32:43 -05:00
Greg Johnston
6de2b4006a fix: wrong path for IntoAnyAttribute in macro expansion from #4481 2025-12-19 11:31:25 -05:00
Greg Johnston
65940cbefa fix: do not show Transition fallback on 2nd change if 1st change resolved synchronously (closes #4492, closes #3868) (#4495) 2025-12-19 10:42:44 -05:00
38 changed files with 796 additions and 453 deletions

View File

@@ -103,7 +103,7 @@ jobs:
id: pnpm-cache
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
- uses: actions/cache@v5
if: contains(inputs.directory, 'examples')
name: Setup pnpm cache
with:

254
Cargo.lock generated
View File

@@ -296,9 +296,9 @@ dependencies = [
[[package]]
name = "async-lock"
version = "3.4.1"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [
"event-listener",
"event-listener-strategy",
@@ -403,9 +403,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "axum"
version = "0.8.7"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"axum-core",
"base64",
@@ -440,9 +440,9 @@ dependencies = [
[[package]]
name = "axum-core"
version = "0.5.5"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
@@ -471,9 +471,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitcode"
version = "0.6.7"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "648bd963d2e5d465377acecfb4b827f9f553b6bc97a8f61715779e9ed9e52b74"
checksum = "0a6ed1b54d8dc333e7be604d00fa9262f4635485ffea923647b6521a5fff045d"
dependencies = [
"arrayvec",
"bitcode_derive",
@@ -484,9 +484,9 @@ dependencies = [
[[package]]
name = "bitcode_derive"
version = "0.6.7"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffebfc2d28a12b262c303cb3860ee77b91bd83b1f20f0bd2a9693008e2f55a9e"
checksum = "238b90427dfad9da4a9abd60f3ec1cdee6b80454bde49ed37f1781dd8e9dc7f9"
dependencies = [
"proc-macro2",
"quote",
@@ -531,9 +531,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.19.0"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "bytecheck"
@@ -599,9 +599,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.2.49"
version = "1.2.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -708,7 +708,7 @@ dependencies = [
"convert_case 0.6.0",
"pathdiff",
"serde_core",
"toml 0.9.8",
"toml",
"winnow",
]
@@ -845,20 +845,6 @@ dependencies = [
"typenum",
]
[[package]]
name = "dashmap"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
@@ -887,18 +873,18 @@ dependencies = [
[[package]]
name = "derive_more"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618"
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [
"convert_case 0.10.0",
"proc-macro2",
@@ -1106,9 +1092,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
[[package]]
name = "flate2"
@@ -1321,9 +1307,9 @@ dependencies = [
[[package]]
name = "glam"
version = "0.30.8"
version = "0.30.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e12d847aeb25f41be4c0ec9587d624e9cd631bc007a8fd7ce3f5851e064c6460"
checksum = "bd47b05dddf0005d850e5644cae7f2b14ac3df487979dbfff3b56f20b1a6ae46"
[[package]]
name = "glib"
@@ -1447,12 +1433,13 @@ dependencies = [
[[package]]
name = "half"
version = "2.6.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
@@ -1464,12 +1451,6 @@ dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -1797,13 +1778,14 @@ dependencies = [
[[package]]
name = "insta"
version = "1.44.3"
version = "1.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698"
checksum = "983e3b24350c84ab8a65151f537d67afbbf7153bb9f1110e03e9fa9b07f67a5c"
dependencies = [
"console",
"once_cell",
"similar",
"tempfile",
]
[[package]]
@@ -1829,9 +1811,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "iri-string"
version = "0.7.9"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
dependencies = [
"memchr",
"serde",
@@ -1848,9 +1830,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.15"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jobserver"
@@ -1945,7 +1927,6 @@ dependencies = [
"actix-http",
"actix-web",
"any_spawner",
"dashmap",
"futures",
"hydration_context",
"leptos",
@@ -1953,6 +1934,7 @@ dependencies = [
"leptos_macro",
"leptos_meta",
"leptos_router",
"or_poisoned",
"parking_lot",
"send_wrapper",
"serde_json",
@@ -1968,7 +1950,6 @@ version = "0.8.7"
dependencies = [
"any_spawner",
"axum",
"dashmap",
"futures",
"hydration_context",
"leptos",
@@ -1976,6 +1957,7 @@ dependencies = [
"leptos_macro",
"leptos_meta",
"leptos_router",
"or_poisoned",
"parking_lot",
"server_fn",
"tachys",
@@ -2047,7 +2029,7 @@ dependencies = [
[[package]]
name = "leptos_macro"
version = "0.8.13"
version = "0.8.14"
dependencies = [
"attribute-derive",
"cfg-if",
@@ -2302,9 +2284,9 @@ dependencies = [
[[package]]
name = "mini-internal"
version = "0.1.43"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "560f32b6891d8d9bade8942c45a27694f16d789d3b4b8e6b7135a5240de0a8af"
checksum = "2a8df435db5df1dd82a74f77e3c3addf6ab7665079c31e222a64f34f7475d87e"
dependencies = [
"proc-macro2",
"quote",
@@ -2313,9 +2295,9 @@ dependencies = [
[[package]]
name = "minicov"
version = "0.3.7"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b"
checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d"
dependencies = [
"cc",
"walkdir",
@@ -2323,13 +2305,13 @@ dependencies = [
[[package]]
name = "miniserde"
version = "0.1.43"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac79f4123c070de643a7a93b9339abf18c30005c622bf4b1c29c2c0960f52d39"
checksum = "8c48e83ec09ab8a51d4e6be46608bf2a1293a79e2f7ea60246a2ce50eaef44ba"
dependencies = [
"itoa",
"mini-internal",
"ryu",
"zmij",
]
[[package]]
@@ -2654,7 +2636,7 @@ version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
dependencies = [
"toml_edit 0.23.7",
"toml_edit",
]
[[package]]
@@ -2716,9 +2698,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.103"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
dependencies = [
"unicode-ident",
]
@@ -2918,7 +2900,6 @@ name = "reactive_stores"
version = "0.3.1"
dependencies = [
"any_spawner",
"dashmap",
"guardian",
"itertools",
"leptos",
@@ -2998,9 +2979,9 @@ dependencies = [
[[package]]
name = "reqwest"
version = "0.12.26"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"bytes",
@@ -3087,22 +3068,19 @@ dependencies = [
[[package]]
name = "rmp"
version = "0.8.14"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
dependencies = [
"byteorder",
"num-traits",
"paste",
]
[[package]]
name = "rmp-serde"
version = "1.3.0"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
dependencies = [
"byteorder",
"rmp",
"serde",
]
@@ -3145,9 +3123,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.1.2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [
"bitflags",
"errno",
@@ -3172,9 +3150,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.13.1"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c"
checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
dependencies = [
"web-time",
"zeroize",
@@ -3199,9 +3177,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.20"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
[[package]]
name = "same-file"
@@ -3344,15 +3322,15 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.145"
version = "1.0.148"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
"zmij",
]
[[package]]
@@ -3379,18 +3357,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.9"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
dependencies = [
"serde_core",
]
@@ -3445,7 +3414,6 @@ dependencies = [
"ciborium",
"const-str",
"const_format",
"dashmap",
"futures",
"gloo-net",
"http 1.4.0",
@@ -3454,6 +3422,7 @@ dependencies = [
"inventory",
"js-sys",
"multer",
"or_poisoned",
"pin-project-lite",
"postcard",
"reqwest",
@@ -3547,10 +3516,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.7"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
@@ -3747,14 +3717,14 @@ dependencies = [
[[package]]
name = "system-deps"
version = "7.0.5"
version = "7.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb"
checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f"
dependencies = [
"cfg-expr",
"heck",
"pkg-config",
"toml 0.8.23",
"toml",
"version-compare",
]
@@ -3821,9 +3791,9 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.23.0"
version = "3.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
dependencies = [
"fastrand",
"getrandom 0.3.4",
@@ -4043,18 +4013,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_edit 0.22.27",
]
[[package]]
name = "toml"
version = "0.9.8"
@@ -4063,8 +4021,8 @@ checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned 1.0.3",
"toml_datetime 0.7.3",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
@@ -4072,61 +4030,39 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.11"
version = "0.7.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
version = "0.23.10+spec-1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
dependencies = [
"indexmap",
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"winnow",
]
[[package]]
name = "toml_edit"
version = "0.23.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
dependencies = [
"indexmap",
"toml_datetime 0.7.3",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.0.4"
version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.4"
version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
[[package]]
name = "tower"
@@ -4186,9 +4122,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.43"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
@@ -4209,9 +4145,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.35"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
]
@@ -4234,7 +4170,7 @@ dependencies = [
"serde_json",
"target-triple",
"termcolor",
"toml 0.9.8",
"toml",
]
[[package]]
@@ -4347,9 +4283,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8-width"
version = "0.1.7"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
[[package]]
name = "utf8_iter"
@@ -4382,9 +4318,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version-compare"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
[[package]]
name = "version_check"
@@ -4918,6 +4854,12 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "zmij"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d"
[[package]]
name = "zstd"
version = "0.13.3"

View File

@@ -74,7 +74,7 @@ tachys = { path = "./tachys", version = "0.2.11" }
async-once-cell = { default-features = false, version = "0.5.4" }
itertools = { default-features = false, version = "0.14.0" }
convert_case = { default-features = false, version = "0.10.0" }
serde_json = { default-features = false, version = "1.0.145" }
serde_json = { default-features = false, version = "1.0.148" }
trybuild = { default-features = false, version = "1.0.114" }
typed-builder = { default-features = false, version = "0.23.2" }
typed-builder-macro = { default-features = false, version = "0.23.2" }
@@ -86,10 +86,9 @@ rustc_version = { default-features = false, version = "0.4.1" }
guardian = { default-features = false, version = "1.3.0" }
rustc-hash = { default-features = false, version = "2.1.1" }
actix-web = { default-features = false, version = "4.12.1" }
tracing = { default-features = false, version = "0.1.43" }
tracing = { default-features = false, version = "0.1.44" }
slotmap = { default-features = false, version = "1.1.1" }
futures = { default-features = false, version = "0.3.31" }
dashmap = { default-features = false, version = "6.1.0" }
pin-project-lite = { default-features = false, version = "0.2.16" }
send_wrapper = { default-features = false, version = "0.6.0" }
tokio-test = { default-features = false, version = "0.4.4" }
@@ -103,10 +102,10 @@ base64 = { default-features = false, version = "0.22.1" }
cfg-if = { default-features = false, version = "1.0.4" }
wasm-bindgen-futures = { default-features = false, version = "0.4.56" }
tower = { default-features = false, version = "0.5.2" }
proc-macro2 = { default-features = false, version = "1.0.103" }
proc-macro2 = { default-features = false, version = "1.0.104" }
serde = { default-features = false, version = "1.0.228" }
parking_lot = { default-features = false, version = "0.12.5" }
axum = { default-features = false, version = "0.8.7" }
axum = { default-features = false, version = "0.8.8" }
serde_qs = { default-features = false, version = "0.15.0" }
syn = { default-features = false, version = "2.0.111" }
xxhash-rust = { default-features = false, version = "0.8.15" }
@@ -131,7 +130,7 @@ inventory = { default-features = false, version = "0.3.21" }
config = { default-features = false, version = "0.15.19" }
camino = { default-features = false, version = "1.2.2" }
ciborium = { default-features = false, version = "0.2.2" }
bitcode = { default-features = false, version = "0.6.7" }
bitcode = { default-features = false, version = "0.6.9" }
multer = { default-features = false, version = "3.1.0" }
leptos-spin-macro = { default-features = false, version = "0.2.0" }
sledgehammer_utils = { default-features = false, version = "0.3.1" }
@@ -144,7 +143,7 @@ bytes = { default-features = false, version = "1.11.0" }
http = { default-features = false, version = "1.4.0" }
regex = { default-features = false, version = "1.12.2" }
drain_filter_polyfill = { default-features = false, version = "0.1.3" }
tempfile = { default-features = false, version = "3.23.0" }
tempfile = { default-features = false, version = "3.24.0" }
futures-lite = { default-features = false, version = "2.6.1" }
log = { default-features = false, version = "0.4.29" }
percent-encoding = { default-features = false, version = "2.3.2" }
@@ -153,18 +152,18 @@ const-str = { default-features = false, version = "0.7.1" }
http-body-util = { default-features = false, version = "0.1.3" }
hyper = { default-features = false, version = "1.8.1" }
postcard = { default-features = false, version = "1.1.3" }
rmp-serde = { default-features = false, version = "1.3.0" }
reqwest = { default-features = false, version = "0.12.26" }
rmp-serde = { default-features = false, version = "1.3.1" }
reqwest = { default-features = false, version = "0.12.28" }
tower-layer = { default-features = false, version = "0.3.3" }
attribute-derive = { default-features = false, version = "0.10.5" }
insta = { default-features = false, version = "1.44.3" }
insta = { default-features = false, version = "1.45.1" }
codee = { default-features = false, version = "0.3.5" }
actix-http = { default-features = false, version = "3.11.2" }
wasm-bindgen-test = { default-features = false, version = "0.3.56" }
rustversion = { default-features = false, version = "1.0.22" }
getrandom = { default-features = false, version = "0.3.4" }
actix-files = { default-features = false, version = "0.6.9" }
async-lock = { default-features = false, version = "3.4.1" }
async-lock = { default-features = false, version = "3.4.2" }
base16 = { default-features = false, version = "0.2.1" }
digest = { default-features = false, version = "0.10.7" }
sha2 = { default-features = false, version = "0.10.9" }

View File

@@ -74,8 +74,8 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
## What does that mean?
- **Full-stack**: Leptos can be used to build apps that run in the browser (client-side rendering), on the server (server-side rendering), or by rendering HTML on the server and then adding interactivity in the browser (server-side rendering with hydration). This includes support for HTTP streaming of both data ([`Resource`s](https://docs.rs/leptos/latest/leptos/struct.Resource.html)) and HTML (out-of-order or in-order streaming of [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) components.)
- **Isomorphic**: Leptos provides primitives to write isomorphic [server functions](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html), i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser, without needing to create and maintain a separate REST or other API.
- **Full-stack**: Leptos can be used to build apps that run in the browser (client-side rendering), on the server (server-side rendering), or by rendering HTML on the server and then adding interactivity in the browser (server-side rendering with hydration). This includes support for HTTP streaming of both data ([`Resource`s](https://docs.rs/leptos/latest/leptos/prelude/struct.Resource.html)) and HTML (out-of-order or in-order streaming of [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/suspense/fn.Suspense.html) components.)
- **Isomorphic**: Leptos provides primitives to write isomorphic [server functions](https://docs.rs/server_fn/latest/server_fn/), i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser, without needing to create and maintain a separate REST or other API.
- **Web**: Leptos is built on the Web platform and Web standards. The [router](https://docs.rs/leptos_router/latest/leptos_router/) is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
- **Framework**: Leptos provides most of what you need to build a modern web app: a reactive system, templating library, and a router that works on both the server and client side.
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signals value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (So, no virtual DOM overhead!)

View File

@@ -9,12 +9,12 @@ This document is intended as a running list of common issues, with example code
**Issue**: Sometimes you want to update a reactive signal in a way that depends on another signal.
```rust
let (a, set_a) = create_signal(0);
let (b, set_b) = create_signal(false);
let (a, set_a) = signal(0);
let (b, set_b) = signal(false);
create_effect(move |_| {
if a() > 5 {
set_b(true);
Effect::new(move |_| {
if a.get() > 5 {
set_b.set(true);
}
});
```
@@ -24,56 +24,10 @@ This creates an inefficient chain of updates, and can easily lead to infinite lo
**Solution**: Follow the rule, _What can be derived, should be derived._ In this case, this has the benefit of massively reducing the code size, too!
```rust
let (a, set_a) = create_signal(0);
let b = move || a () > 5;
let (a, set_a) = signal(0);
let b = move || a.get() > 5;
```
### Nested signal updates/reads triggering panic
Sometimes you have nested signals: for example, hash-map that can change over time, each of whose values can also change over time:
```rust
#[component]
pub fn App() -> impl IntoView {
let resources = create_rw_signal(HashMap::new());
let update = move |id: usize| {
resources.update(|resources| {
resources
.entry(id)
.or_insert_with(|| create_rw_signal(0))
.update(|amount| *amount += 1)
})
};
view! {
<div>
<pre>{move || format!("{:#?}", resources.get().into_iter().map(|(id, resource)| (id, resource.get())).collect::<Vec<_>>())}</pre>
<button on:click=move |_| update(1)>"+"</button>
</div>
}
}
```
Clicking the button twice will cause a panic, because of the nested signal _read_. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
You can fix this fairly easily by using the [`batch()`](https://docs.rs/leptos/latest/leptos/fn.batch.html) method:
```rust
let update = move |id: usize| {
batch(move || {
resources.update(|resources| {
resources
.entry(id)
.or_insert_with(|| create_rw_signal(0))
.update(|amount| *amount += 1)
})
});
};
```
This delays running any effects until after both updates are made, preventing the conflict entirely without requiring any other restructuring.
## Templates and the DOM
### `<input value=...>` doesn't update or stops updating
@@ -83,8 +37,8 @@ Many DOM attributes can be updated either by setting an attribute on the DOM nod
This means that in practice, attributes like `value` or `checked` on an `<input/>` element only update the _default_ value for the `<input/>`. If you want to reactively update the value, you should use `prop:value` instead to set the `value` property.
```rust
let (a, set_a) = create_signal("Starting value".to_string());
let on_input = move |ev| set_a(event_target_value(&ev));
let (a, set_a) = signal("Starting value".to_string());
let on_input = move |ev| set_a.set(event_target_value(&ev));
view! {
@@ -97,8 +51,8 @@ view! {
```
```rust
let (a, set_a) = create_signal("Starting value".to_string());
let on_input = move |ev| set_a(event_target_value(&ev));
let (a, set_a) = signal("Starting value".to_string());
let on_input = move |ev| set_a.set(event_target_value(&ev));
view! {

View File

@@ -17,7 +17,7 @@ leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0.12"
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
wasm-bindgen = "0.2.92"
wasm-bindgen = "0.2.106"
[features]
hydrate = [

View File

@@ -0,0 +1,38 @@
@check_issue_4492
Feature: Regression test for issue #4492
Scenario: Scenario A should show Loading once on first load.
Given I see the app
And I can access regression test 4492
When I click the button a-toggle
Then I see a-result has the text Loading...
When I wait 100ms
Then I see a-result has the text 0
When I click the button a-button
Then I see a-result has the text 0
When I wait 100ms
Then I see a-result has the text 1
Scenario: Scenario B should never show Loading
Given I see the app
And I can access regression test 4492
When I click the button b-toggle
Then I see b-result has the text 0
When I click the button b-button
Then I see b-result has the text 0
When I wait 100ms
Then I see b-result has the text 1
When I click the button b-button
Then I see b-result has the text 1
When I wait 100ms
Then I see b-result has the text 2
Scenario: Scenario C should never show Loading
Given I see the app
And I can access regression test 4492
When I click the button c-toggle
Then I see c-result has the text 0
When I click the button c-button
Then I see c-result has the text 42
When I wait 100ms
Then I see c-result has the text 1

View File

@@ -15,3 +15,9 @@ pub async fn click_link(client: &Client, text: &str) -> Result<()> {
link.click().await?;
Ok(())
}
pub async fn click_button(client: &Client, id: &str) -> Result<()> {
let btn = find::element_by_id(&client, &id).await?;
btn.click().await?;
Ok(())
}

View File

@@ -7,7 +7,15 @@ pub async fn result_text_is(
client: &Client,
expected_text: &str,
) -> Result<()> {
let actual = find::text_at_id(client, "result").await?;
element_text_is(client, "result", expected_text).await
}
pub async fn element_text_is(
client: &Client,
id: &str,
expected_text: &str,
) -> Result<()> {
let actual = find::text_at_id(client, id).await?;
assert_eq!(&actual, expected_text);
Ok(())
}

View File

@@ -20,6 +20,14 @@ async fn i_select_the_link(world: &mut AppWorld, text: String) -> Result<()> {
Ok(())
}
#[when(regex = "^I click the button (.*)$")]
async fn i_click_the_button(world: &mut AppWorld, id: String) -> Result<()> {
let client = &world.client;
action::click_button(client, &id).await?;
Ok(())
}
#[given(expr = "I select the following links")]
#[when(expr = "I select the following links")]
async fn i_select_the_following_links(
@@ -54,3 +62,10 @@ async fn i_go_back(world: &mut AppWorld) -> Result<()> {
Ok(())
}
#[when(regex = r"^I wait (\d+)ms$")]
async fn i_wait_ms(_world: &mut AppWorld, ms: u64) -> Result<()> {
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
Ok(())
}

View File

@@ -19,6 +19,17 @@ async fn i_see_the_result_is_the_string(
Ok(())
}
#[then(regex = r"^I see ([\w-]+) has the text (.*)$")]
async fn i_see_element_has_text(
world: &mut AppWorld,
id: String,
text: String,
) -> Result<()> {
let client = &world.client;
check::element_text_is(client, &id, &text).await?;
Ok(())
}
#[then(regex = r"^I see the navbar$")]
async fn i_see_the_navbar(world: &mut AppWorld) -> Result<()> {
let client = &world.client;

View File

@@ -1,7 +1,7 @@
use crate::{
issue_4005::Routes4005, issue_4088::Routes4088, issue_4217::Routes4217,
issue_4285::Routes4285, issue_4296::Routes4296, issue_4324::Routes4324,
pr_4015::Routes4015, pr_4091::Routes4091,
issue_4492::Routes4492, pr_4015::Routes4015, pr_4091::Routes4091,
};
use leptos::prelude::*;
use leptos_meta::{MetaTags, *};
@@ -48,6 +48,7 @@ pub fn App() -> impl IntoView {
<Routes4285/>
<Routes4296/>
<Routes4324/>
<Routes4492/>
</Routes>
</main>
</Router>
@@ -75,6 +76,7 @@ fn HomePage() -> impl IntoView {
<li><a href="/4285/">"4285"</a></li>
<li><a href="/4296/">"4296"</a></li>
<li><a href="/4324/">"4324"</a></li>
<li><a href="/4492/">"4492"</a></li>
</ul>
</nav>
}

View File

@@ -0,0 +1,114 @@
use leptos::prelude::*;
#[allow(unused_imports)]
use leptos_router::{
components::Route, path, MatchNestedRoutes, NavigateOptions,
};
#[component]
pub fn Routes4492() -> impl MatchNestedRoutes + Clone {
view! {
<Route path=path!("4492") view=Issue4492/>
}
.into_inner()
}
#[component]
fn Issue4492() -> impl IntoView {
let show_a = RwSignal::new(false);
let show_b = RwSignal::new(false);
let show_c = RwSignal::new(false);
view! {
<button id="a-toggle" on:click=move |_| show_a.set(!show_a.get())>"Toggle A"</button>
<button id="b-toggle" on:click=move |_| show_b.set(!show_b.get())>"Toggle B"</button>
<button id="c-toggle" on:click=move |_| show_c.set(!show_c.get())>"Toggle C"</button>
<Show when=move || show_a.get()>
<ScenarioA/>
</Show>
<Show when=move || show_b.get()>
<ScenarioB/>
</Show>
<Show when=move || show_c.get()>
<ScenarioC/>
</Show>
}
}
#[component]
fn ScenarioA() -> impl IntoView {
// scenario A: one truly-async resource is read on click
let counter = RwSignal::new(0);
let resource = Resource::new(
move || counter.get(),
|count| async move {
sleep(50).await.unwrap();
count
},
);
view! {
<Transition fallback=|| view! { <p id="a-result">"Loading..."</p> }>
<p id="a-result">{resource}</p>
</Transition>
<button id="a-button" on:click=move |_| *counter.write() += 1>"+1"</button>
}
}
#[component]
fn ScenarioB() -> impl IntoView {
// scenario B: resource immediately available first time, then after 250ms
let counter = RwSignal::new(0);
let resource = Resource::new(
move || counter.get(),
|count| async move {
if count == 0 {
count
} else {
sleep(50).await.unwrap();
count
}
},
);
view! {
<Transition fallback=|| view! { <p id="b-result">"Loading..."</p> }>
<p id="b-result">{resource}</p>
</Transition>
<button id="b-button" on:click=move |_| *counter.write() += 1>"+1"</button>
}
}
#[component]
fn ScenarioC() -> impl IntoView {
// scenario C: not even a resource on the first run, just a value
// see https://github.com/leptos-rs/leptos/issues/3868
let counter = RwSignal::new(0);
let s_res = StoredValue::new(None::<ArcLocalResource<i32>>);
let resource = move || {
let count = counter.get();
if count == 0 {
count
} else {
let r = s_res.get_value().unwrap_or_else(|| {
let res = ArcLocalResource::new(move || async move {
sleep(50).await.unwrap();
count
});
s_res.set_value(Some(res.clone()));
res
});
r.get().unwrap_or(42)
}
};
view! {
<Transition fallback=|| view! { <p id="c-result">"Loading..."</p> }>
<p id="c-result">{resource}</p>
</Transition>
<button id="c-button" on:click=move |_| *counter.write() += 1>"+1"</button>
}
}
#[server]
async fn sleep(ms: u64) -> Result<(), ServerFnError> {
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
Ok(())
}

View File

@@ -5,6 +5,7 @@ mod issue_4217;
mod issue_4285;
mod issue_4296;
mod issue_4324;
mod issue_4492;
mod pr_4015;
mod pr_4091;

View File

@@ -37,7 +37,6 @@ web-sys = { version = "0.3.70", features = ["FileList", "File"] }
strum = { version = "0.27.1", features = ["strum_macros", "derive"] }
notify = { version = "8.0", optional = true }
pin-project-lite = "0.2.14"
dashmap = { version = "6.0", optional = true }
async-broadcast = { version = "0.7.1", optional = true }
bytecheck = "0.8.0"
rkyv = { version = "0.8.8" }
@@ -52,7 +51,6 @@ ssr = [
"leptos/ssr",
"dep:leptos_axum",
"dep:notify",
"dep:dashmap",
"dep:async-broadcast",
]

View File

@@ -422,7 +422,6 @@ pub fn FileUploadWithProgress() -> impl IntoView {
#[cfg(feature = "ssr")]
mod progress {
use async_broadcast::{broadcast, Receiver, Sender};
use dashmap::DashMap;
use futures::Stream;
use std::sync::LazyLock;

View File

@@ -27,7 +27,7 @@ parking_lot = { workspace = true, default-features = true }
tracing = { optional = true, workspace = true, default-features = true }
tokio = { features = ["rt", "fs"], workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
dashmap = { workspace = true, default-features = true }
or_poisoned = { workspace = true, default-features = true }
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]

View File

@@ -16,7 +16,6 @@ use actix_web::{
web::{Data, Payload, ServiceConfig},
*,
};
use dashmap::DashMap;
use futures::{stream::once, Stream, StreamExt};
use http::StatusCode;
use hydration_context::SsrSharedContext;
@@ -38,6 +37,7 @@ use leptos_router::{
static_routes::{RegenerationFn, ResolvedStaticPath},
ExpandOptionals, Method, PathSegment, RouteList, RouteListing, SsrMode,
};
use or_poisoned::OrPoisoned;
use parking_lot::RwLock;
use send_wrapper::SendWrapper;
use server_fn::{
@@ -45,7 +45,7 @@ use server_fn::{
request::actix::ActixRequest,
};
use std::{
collections::HashSet,
collections::{HashMap, HashSet},
fmt::{Debug, Display},
future::Future,
ops::{Deref, DerefMut},
@@ -1222,8 +1222,9 @@ impl StaticRouteGenerator {
}
}
static STATIC_HEADERS: LazyLock<DashMap<String, ResponseOptions>> =
LazyLock::new(DashMap::new);
static STATIC_HEADERS: LazyLock<
std::sync::RwLock<HashMap<String, ResponseOptions>>,
> = LazyLock::new(Default::default);
fn was_404(owner: &Owner) -> bool {
let resp = owner.with(|| expect_context::<ResponseOptions>());
@@ -1255,7 +1256,10 @@ async fn write_static_route(
html: &str,
) -> Result<(), std::io::Error> {
if let Some(options) = response_options {
STATIC_HEADERS.insert(path.to_string(), options);
STATIC_HEADERS
.write()
.or_poisoned()
.insert(path.to_string(), options);
}
let path = static_path(options, path);
@@ -1322,8 +1326,11 @@ where
.await;
(owner.with(use_context::<ResponseOptions>), html)
} else {
let headers =
STATIC_HEADERS.get(orig_path).map(|v| v.clone());
let headers = STATIC_HEADERS
.read()
.or_poisoned()
.get(orig_path)
.cloned();
(headers, None)
};

View File

@@ -14,7 +14,6 @@ hydration_context = { workspace = true }
axum = { default-features = false, features = [
"matched-path",
], workspace = true }
dashmap = { workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
leptos = { workspace = true, features = ["nonce", "ssr"] }
server_fn = { workspace = true, features = ["axum-no-default"] }
@@ -28,6 +27,7 @@ tokio = { default-features = false, workspace = true }
tower = { features = ["util"], workspace = true, default-features = true }
tower-http = { workspace = true, default-features = true }
tracing = { optional = true, workspace = true, default-features = true }
or_poisoned = { workspace = true, default-features = true }
[dev-dependencies]
axum = { workspace = true, default-features = true }

View File

@@ -47,8 +47,6 @@ use axum::{
response::IntoResponse,
routing::{delete, get, patch, post, put},
};
#[cfg(feature = "default")]
use dashmap::DashMap;
use futures::{stream::once, Future, Stream, StreamExt};
use hydration_context::SsrSharedContext;
use leptos::{
@@ -69,12 +67,13 @@ use leptos_router::{
static_routes::RegenerationFn, ExpandOptionals, PathSegment, RouteList,
RouteListing, SsrMode,
};
use or_poisoned::OrPoisoned;
use parking_lot::RwLock;
use server_fn::{error::ServerFnErrorErr, redirect::REDIRECT_HEADER};
#[cfg(feature = "default")]
use std::path::Path;
#[cfg(feature = "default")]
use std::sync::LazyLock;
#[cfg(feature = "default")]
use std::{collections::HashMap, path::Path};
use std::{collections::HashSet, fmt::Debug, io, pin::Pin, sync::Arc};
#[cfg(feature = "default")]
use tower::util::ServiceExt;
@@ -1522,8 +1521,9 @@ impl StaticRouteGenerator {
}
#[cfg(feature = "default")]
static STATIC_HEADERS: LazyLock<DashMap<String, ResponseOptions>> =
LazyLock::new(DashMap::new);
static STATIC_HEADERS: LazyLock<
std::sync::RwLock<HashMap<String, ResponseOptions>>,
> = LazyLock::new(Default::default);
#[cfg(feature = "default")]
fn was_404(owner: &Owner) -> bool {
@@ -1558,7 +1558,10 @@ async fn write_static_route(
html: &str,
) -> Result<(), std::io::Error> {
if let Some(options) = response_options {
STATIC_HEADERS.insert(path.to_string(), options);
STATIC_HEADERS
.write()
.or_poisoned()
.insert(path.to_string(), options);
}
let path = static_path(options, path);
@@ -1635,7 +1638,11 @@ where
.await;
(owner.with(use_context::<ResponseOptions>), html)
} else {
let headers = STATIC_HEADERS.get(orig_path).map(|v| v.clone());
let headers = STATIC_HEADERS
.read()
.or_poisoned()
.get(orig_path)
.map(|v| v.clone());
(headers, None)
};

View File

@@ -1,25 +1,42 @@
if (window.location.protocol === 'https:') {
protocol = 'wss://';
if (window.location.protocol === "https:") {
protocol = "wss://";
}
let host = window.location.hostname;
let ws = new WebSocket(`${protocol}${host}:${reload_port}/live_reload`);
ws.onmessage = (ev) => {
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();
if (msg.css) {
let found = false;
document.querySelectorAll("link").forEach((link) => {
if (link.getAttribute('href').includes(msg.css)) {
let newHref = '/' + msg.css + '?version=' + Date.now();
link.setAttribute('href', newHref);
found = true;
}
});
if (!found) console.warn(`CSS hot-reload: Could not find a <link href=/\"${msg.css}\"> element`);
function connect() {
let ws = new WebSocket(`${protocol}${host}:${reload_port}/live_reload`);
ws.onmessage = (ev) => {
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();
if (msg.css) {
let found = false;
document.querySelectorAll("link").forEach((link) => {
if (link.getAttribute("href").includes(msg.css)) {
let newHref = "/" + msg.css + "?version=" + Date.now();
link.setAttribute("href", newHref);
found = true;
}
});
if (!found)
console.warn(
`CSS hot-reload: Could not find a <link href=/\"${msg.css}\"> element`,
);
}
if (msg.view) {
patch(msg.view);
}
};
if(msg.view) {
patch(msg.view);
}
};
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
ws.onclose = () => {
console.warn("Live-reload disconnected. Reconnecting...");
setTimeout(connect, 1000);
};
ws.onerror = () => {
ws.close();
};
}
connect();

View File

@@ -15,7 +15,10 @@ use reactive_graph::{
effect::RenderEffect,
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
traits::{Dispose, Get, Read, ReadUntracked, Track, With, WriteValue},
traits::{
Dispose, Get, Read, ReadUntracked, Track, With, WithUntracked,
WriteValue,
},
};
use slotmap::{DefaultKey, SlotMap};
use std::sync::{Arc, Mutex};
@@ -119,14 +122,19 @@ where
provide_context(SuspenseContext {
tasks: tasks.clone(),
});
let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
tasks.track();
if prev.is_none() && starts_local {
false
} else {
tasks.with(SlotMap::is_empty)
let none_pending = ArcMemo::new({
let tasks = tasks.clone();
move |prev: Option<&bool>| {
tasks.track();
if prev.is_none() && starts_local {
false
} else {
tasks.with(SlotMap::is_empty)
}
}
});
let has_tasks =
Arc::new(move || !tasks.with_untracked(SlotMap::is_empty));
OwnedView::new(SuspenseBoundary::<false, _, _> {
id,
@@ -134,6 +142,7 @@ where
fallback,
children,
error_boundary_parent,
has_tasks,
})
})
}
@@ -156,6 +165,7 @@ pub(crate) struct SuspenseBoundary<const TRANSITION: bool, Fal, Chil> {
pub fallback: Fal,
pub children: Chil,
pub error_boundary_parent: Option<ErrorBoundarySuspendedChildren>,
pub has_tasks: Arc<dyn Fn() -> bool + Send + Sync>,
}
impl<const TRANSITION: bool, Fal, Chil> Render
@@ -192,12 +202,26 @@ where
outer_owner.clone(),
);
if let Some(mut state) = prev {
let state = if let Some(mut state) = prev {
this.rebuild(&mut state);
state
} else {
this.build()
};
if nth_run == 1 && !(self.has_tasks)() {
// if this is the first run, and there are no pending resources at this point,
// it means that there were no actually-async resources read while rendering the children
// this means that we're effectively on the settled second run: none_pending
// won't change false => true and cause this to rerender (and therefore increment nth_run)
//
// we increment it manually here so that future resource changes won't cause the transition fallback
// to be displayed for the first time
// see https://github.com/leptos-rs/leptos/issues/3868, https://github.com/leptos-rs/leptos/issues/4492
nth_run += 1;
}
state
})
}
@@ -235,6 +259,7 @@ where
fallback,
children,
error_boundary_parent,
has_tasks,
} = self;
SuspenseBoundary {
id,
@@ -242,6 +267,7 @@ where
fallback,
children: children.add_any_attr(attr),
error_boundary_parent,
has_tasks,
}
}
}

View File

@@ -10,10 +10,11 @@ use reactive_graph::{
effect::Effect,
owner::{provide_context, use_context, Owner},
signal::ArcRwSignal,
traits::{Get, Set, Track, With},
traits::{Get, Set, Track, With, WithUntracked},
wrappers::write::SignalSetter,
};
use slotmap::{DefaultKey, SlotMap};
use std::sync::Arc;
use tachys::reactive_graph::OwnedView;
/// If any [`Resource`](crate::prelude::Resource) is read in the `children` of this
@@ -104,14 +105,19 @@ where
provide_context(SuspenseContext {
tasks: tasks.clone(),
});
let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
tasks.track();
if prev.is_none() && starts_local {
false
} else {
tasks.with(SlotMap::is_empty)
let none_pending = ArcMemo::new({
let tasks = tasks.clone();
move |prev: Option<&bool>| {
tasks.track();
if prev.is_none() && starts_local {
false
} else {
tasks.with(SlotMap::is_empty)
}
}
});
let has_tasks =
Arc::new(move || !tasks.with_untracked(SlotMap::is_empty));
if let Some(set_pending) = set_pending {
Effect::new_isomorphic({
let none_pending = none_pending.clone();
@@ -127,6 +133,7 @@ where
fallback,
children,
error_boundary_parent,
has_tasks,
})
})
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.8.13"
version = "0.8.14"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"

View File

@@ -199,7 +199,7 @@ mod slot;
/// ```
///
/// 9. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
/// [NodeRef](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to use later.
/// [NodeRef](https://docs.rs/leptos/latest/leptos/prelude/struct.NodeRef.html) to use later.
/// ```rust
/// # use leptos::prelude::*;
///
@@ -505,9 +505,9 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
///
/// * `#[prop(into)]`: This will call `.into()` on any value passed into the component prop. (For example,
/// you could apply `#[prop(into)]` to a prop that takes
/// [Signal](https://docs.rs/leptos/latest/leptos/struct.Signal.html), which would
/// allow users to pass a [ReadSignal](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) or
/// [RwSignal](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html)
/// [Signal](https://docs.rs/leptos/latest/leptos/prelude/struct.Signal.html), which would
/// allow users to pass a [ReadSignal](https://docs.rs/leptos/latest/leptos/prelude/struct.ReadSignal.html) or
/// [RwSignal](https://docs.rs/leptos/latest/leptos/prelude/struct.RwSignal.html)
/// and automatically convert it.)
/// * `#[prop(optional)]`: If the user does not specify this property when they use the component,
/// it will be set to its default value. If the property type is `Option<T>`, values should be passed

View File

@@ -25,7 +25,7 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
let span = field.span();
quote_spanned! {
span=> #ident: <#ty as ::leptos_router::params::IntoParam>::into_param(
span=> #ident: ::leptos_router::params::macro_helpers::Wrapper::<#ty>::__into_param(
map.get_str(#field_name_string),
#field_name_string
)?
@@ -39,6 +39,8 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
let gen = quote! {
impl Params for #name {
fn from_map(map: &::leptos_router::params::ParamsMap) -> ::core::result::Result<Self, ::leptos_router::params::ParamsError> {
use ::leptos_router::params::macro_helpers::Fallback as _;
Ok(Self {
#(#fields,)*
})

View File

@@ -177,7 +177,7 @@ pub(crate) fn component_to_tokens(
if cfg!(feature = "__internal_erase_components") {
quote! {
.add_any_attr({
vec![#(::leptos::html::attribute::any_attribute::IntoAnyAttribute::into_any_attr(#spreads),)*]
vec![#(::leptos::attr::any_attribute::IntoAnyAttribute::into_any_attr(#spreads),)*]
})
}
} else {

View File

@@ -15,7 +15,7 @@ use codee::{
Decoder, Encoder,
};
use core::{fmt::Debug, marker::PhantomData};
use futures::Future;
use futures::{Future, FutureExt};
use or_poisoned::OrPoisoned;
use reactive_graph::{
computed::{
@@ -258,11 +258,17 @@ where
if let Some(suspense_context) = use_context::<SuspenseContext>() {
if self.value.read().or_poisoned().is_none() {
let handle = suspense_context.task_id();
let ready = SpecialNonReactiveFuture::new(self.ready());
reactive_graph::spawn(async move {
ready.await;
drop(handle);
});
let mut ready =
Box::pin(SpecialNonReactiveFuture::new(self.ready()));
match ready.as_mut().now_or_never() {
Some(_) => drop(handle),
None => {
reactive_graph::spawn(async move {
ready.await;
drop(handle);
});
}
}
self.suspenses.write().or_poisoned().push(suspense_context);
}
}

View File

@@ -632,12 +632,29 @@ impl<T: 'static> ReadUntracked for ArcAsyncDerived<T> {
fn try_read_untracked(&self) -> Option<Self::Value> {
if let Some(suspense_context) = use_context::<SuspenseContext>() {
// create a handle to register it with suspense
let handle = suspense_context.task_id();
let ready = SpecialNonReactiveFuture::new(self.ready());
crate::spawn(async move {
ready.await;
drop(handle);
});
// check if the task is *already* ready
let mut ready =
Box::pin(SpecialNonReactiveFuture::new(self.ready()));
match ready.as_mut().now_or_never() {
Some(_) => {
// if it's already ready, drop the handle immediately
// this will immediately notify the suspense context that it's complete
drop(handle);
}
None => {
// otherwise, spawn a task to wait for it to be ready, then drop the handle,
// which will notify the suspense
crate::spawn(async move {
ready.await;
drop(handle);
});
}
}
// register the suspense context with our list of them, to be notified later if this re-runs
self.inner
.write()
.or_poisoned()

View File

@@ -17,7 +17,6 @@ paste = { workspace = true, default-features = true }
reactive_graph = { workspace = true }
rustc-hash = { workspace = true, default-features = true }
reactive_stores_macro = { workspace = true }
dashmap = { workspace = true, default-features = true }
send_wrapper = { workspace = true, default-features = true }
[dev-dependencies]

View File

@@ -140,13 +140,20 @@ where
}
fn track_field(&self) {
let inner = self
.inner
.get_trigger(self.inner.path().into_iter().collect());
inner.this.track();
let mut full_path = self.path().into_iter().collect::<StorePath>();
let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.this.track();
trigger.children.track();
// tracks `this` for all ancestors: i.e., it will track any change that is made
// directly to one of its ancestors, but not a change made to a *child* of an ancestor
// (which would end up with every subfield tracking its own siblings, because they are
// children of its parent)
while !full_path.is_empty() {
full_path.pop();
let inner = self.get_trigger(full_path.clone());
inner.this.track();
}
}
}
@@ -176,6 +183,7 @@ where
{
inner: KeyedSubfield<Inner, Prev, K, T>,
guard: Option<Guard>,
untracked: bool,
}
impl<Inner, Prev, K, T, Guard> Deref
@@ -227,6 +235,7 @@ where
K: Debug + Send + Sync + PartialEq + Eq + Hash + 'static,
{
fn untrack(&mut self) {
self.untracked = true;
if let Some(inner) = self.guard.as_mut() {
inner.untrack();
}
@@ -251,7 +260,10 @@ where
// now that the write lock is release, we can get a read lock to refresh this keyed field
// based on the new value
self.inner.update_keys();
self.inner.notify();
if !self.untracked {
self.inner.notify();
}
// reactive updates happen on the next tick
}
@@ -344,6 +356,7 @@ where
Some(KeyedSubfieldWriteGuard {
inner: self.clone(),
guard: Some(guard),
untracked: false,
})
}
@@ -355,6 +368,7 @@ where
Some(KeyedSubfieldWriteGuard {
inner: self.clone(),
guard: Some(guard),
untracked: true,
})
}
}
@@ -879,4 +893,41 @@ mod tests {
assert_eq!(b_count.load(Ordering::Relaxed), 1);
assert_eq!(c_count.load(Ordering::Relaxed), 1);
}
#[tokio::test]
async fn untracked_write_on_keyed_subfield_shouldnt_notify() {
_ = any_spawner::Executor::init_tokio();
let store = Store::new(data());
assert_eq!(store.read_untracked().todos.len(), 3);
// create an effect to read from the keyed subfield
let todos_count = Arc::new(AtomicUsize::new(0));
Effect::new_sync({
let todos_count = Arc::clone(&todos_count);
move || {
store.todos().track();
todos_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
assert_eq!(todos_count.load(Ordering::Relaxed), 1);
// writing to keyed subfield notifies the iterator
store.todos().write().push(Todo {
id: 13,
label: "D".into(),
});
tick().await;
assert_eq!(todos_count.load(Ordering::Relaxed), 2);
// but an untracked write doesn't
store.todos().write_untracked().push(Todo {
id: 14,
label: "E".into(),
});
tick().await;
assert_eq!(todos_count.load(Ordering::Relaxed), 2);
}
}

View File

@@ -57,11 +57,11 @@
//! # if false { // don't run effect in doctests
//! Effect::new(move |_| {
//! // you can access individual store fields with a getter
//! println!("todos: {:?}", &*store.todos().read());
//! println!("user: {:?}", &*store.user().read());
//! });
//! # }
//!
//! // won't notify the effect that listens to `todos`
//! // won't notify the effect that listens to `user`
//! store.todos().write().push(Todo {
//! label: "Test".to_string(),
//! completed: false,
@@ -239,6 +239,7 @@
//! field in the signal inner `Arc<RwLock<_>>`, and tracks the trigger that corresponds with its
//! path; calling `.write()` returns a writeable guard, and notifies that same trigger.
use or_poisoned::OrPoisoned;
use reactive_graph::{
owner::{ArenaItem, LocalStorage, Storage, SyncStorage},
signal::{
@@ -414,32 +415,15 @@ impl<K> Default for FieldKeys<K> {
}
}
#[cfg(not(target_arch = "wasm32"))]
type HashMap<K, V> = Arc<dashmap::DashMap<K, V>>;
#[cfg(target_arch = "wasm32")]
type HashMap<K, V> = send_wrapper::SendWrapper<
std::rc::Rc<std::cell::RefCell<std::collections::HashMap<K, V>>>,
>;
type Map<K, V> = Arc<std::sync::RwLock<std::collections::HashMap<K, V>>>;
/// A map of the keys for a keyed subfield.
#[derive(Clone)]
#[derive(Clone, Default)]
pub struct KeyMap(
HashMap<StorePath, Box<dyn Any + Send + Sync>>,
HashMap<(StorePath, usize), StorePathSegment>,
Map<StorePath, Box<dyn Any + Send + Sync>>,
Map<(StorePath, usize), StorePathSegment>,
);
impl Default for KeyMap {
fn default() -> Self {
#[cfg(not(target_arch = "wasm32"))]
return Self(Default::default(), Default::default());
#[cfg(target_arch = "wasm32")]
return Self(
send_wrapper::SendWrapper::new(Default::default()),
send_wrapper::SendWrapper::new(Default::default()),
);
}
}
impl KeyMap {
fn with_field_keys<K, T>(
&self,
@@ -452,62 +436,33 @@ impl KeyMap {
{
let initial_keys = initialize();
#[cfg(not(target_arch = "wasm32"))]
let mut entry = self
.0
let mut guard = self.0.write().or_poisoned();
let entry = guard
.entry(path.clone())
.or_insert_with(|| Box::new(FieldKeys::new(initial_keys)));
#[cfg(target_arch = "wasm32")]
let entry = if !self.0.borrow().contains_key(&path) {
Some(Box::new(FieldKeys::new(initial_keys)))
} else {
None
};
#[cfg(target_arch = "wasm32")]
let mut map = self.0.borrow_mut();
#[cfg(target_arch = "wasm32")]
let entry = map.entry(path.clone()).or_insert_with(|| entry.unwrap());
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
let (result, new_keys) = fun(entry);
if !new_keys.is_empty() {
for (idx, segment) in new_keys {
#[cfg(not(target_arch = "wasm32"))]
self.1.insert((path.clone(), idx), segment);
#[cfg(target_arch = "wasm32")]
self.1.borrow_mut().insert((path.clone(), idx), segment);
self.1
.write()
.or_poisoned()
.insert((path.clone(), idx), segment);
}
}
Some(result)
}
fn contains_key(&self, key: &StorePath) -> bool {
#[cfg(not(target_arch = "wasm32"))]
{
self.0.contains_key(key)
}
#[cfg(target_arch = "wasm32")]
{
self.0.borrow_mut().contains_key(key)
}
self.0.read().or_poisoned().contains_key(key)
}
fn get_key_for_index(
&self,
key: &(StorePath, usize),
) -> Option<StorePathSegment> {
#[cfg(not(target_arch = "wasm32"))]
{
self.1.get(key).as_deref().copied()
}
#[cfg(target_arch = "wasm32")]
{
self.1.borrow().get(key).as_deref().copied()
}
self.1.read().or_poisoned().get(key).copied()
}
}
@@ -833,7 +788,7 @@ mod tests {
use reactive_graph::{
effect::Effect,
owner::StoredValue,
traits::{Read, ReadUntracked, Set, Update, Write},
traits::{Read, ReadUntracked, Set, Track, Update, Write},
};
use std::sync::{
atomic::{AtomicUsize, Ordering},
@@ -1375,4 +1330,34 @@ mod tests {
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
}
#[tokio::test]
async fn untracked_write_on_subfield_shouldnt_notify() {
_ = any_spawner::Executor::init_tokio();
let name_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(data());
let tracked_field = store.user();
Effect::new_sync({
let name_count = Arc::clone(&name_count);
move |_| {
tracked_field.track();
name_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
assert_eq!(name_count.load(Ordering::Relaxed), 1);
tracked_field.write().push('!');
tick().await;
assert_eq!(name_count.load(Ordering::Relaxed), 2);
tracked_field.write_untracked().push('!');
tick().await;
assert_eq!(name_count.load(Ordering::Relaxed), 2);
}
}

View File

@@ -199,31 +199,56 @@ where
}
}
// TODO can we support Option<T> and T in a non-nightly way?
#[cfg(all(feature = "nightly", rustc_nightly))]
mod option_param {
use super::{IntoParam, ParamsError};
/// Helpers for the `Params` derive macro to allow specialization without nightly.
pub mod macro_helpers {
use crate::params::{IntoParam, ParamsError};
use std::{str::FromStr, sync::Arc};
auto trait NotOption {}
impl<T> !NotOption for Option<T> {}
/// This struct is never actually created; it just exists so that we can impl associated
/// functions on it.
pub struct Wrapper<T>(T);
impl<T> IntoParam for T
where
T: FromStr + NotOption,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
fn into_param(
impl<T: IntoParam> Wrapper<T> {
/// This is the 'preferred' impl to be used for all `T` that implement `IntoParam`.
/// Because it is directly on the struct, the compiler will pick this over the impl from
/// the `Fallback` trait.
#[inline]
pub fn __into_param(
value: Option<&str>,
name: &str,
) -> Result<Self, ParamsError> {
let value = value
.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
Self::from_str(value).map_err(|e| ParamsError::Params(Arc::new(e)))
) -> Result<T, ParamsError> {
T::into_param(value, name)
}
}
}
/// If the Fallback trait is in scope, then the compiler has two possible implementations for
/// `__into_params`. It will pick the one from this trait if the inherent one doesn't exist.
/// (which it won't if `T` does not implement `IntoParam`)
pub trait Fallback<T>: Sized
where
T: FromStr,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
/// Fallback function in case the inherent impl on the Wrapper struct does not exist for
/// `T`
#[inline]
fn __into_param(
value: Option<&str>,
name: &str,
) -> Result<T, ParamsError> {
let value = value
.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
T::from_str(value).map_err(|e| ParamsError::Params(Arc::new(e)))
}
}
impl<T> Fallback<T> for Wrapper<T>
where
T: FromStr,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
}
}
/// Errors that can occur while parsing params using [`Params`].
#[derive(Error, Debug, Clone)]
pub enum ParamsError {

View File

@@ -25,10 +25,10 @@ send_wrapper = { features = [
"futures",
], optional = true, workspace = true, default-features = true }
thiserror = { workspace = true, default-features = true }
or_poisoned = { workspace = true, default-features = true }
# registration system
inventory = { optional = true, workspace = true, default-features = true }
dashmap = { workspace = true, default-features = true }
## servers
# actix

View File

@@ -145,7 +145,6 @@ use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
pub use const_format;
#[doc(hidden)]
pub use const_str;
use dashmap::DashMap;
pub use error::ServerFnError;
#[cfg(feature = "form-redirects")]
use error::ServerFnUrlError;
@@ -165,12 +164,13 @@ pub use serde;
pub use serde_lite;
use server::Server;
use std::{
collections::HashMap,
fmt::{Debug, Display},
future::Future,
marker::PhantomData,
ops::{Deref, DerefMut},
pin::Pin,
sync::{Arc, LazyLock},
sync::{Arc, LazyLock, RwLock},
};
#[doc(hidden)]
pub use xxhash_rust;
@@ -866,12 +866,14 @@ pub use inventory;
macro_rules! initialize_server_fn_map {
($req:ty, $res:ty) => {
std::sync::LazyLock::new(|| {
$crate::inventory::iter::<ServerFnTraitObj<$req, $res>>
.into_iter()
.map(|obj| {
((obj.path().to_string(), obj.method()), obj.clone())
})
.collect()
std::sync::RwLock::new(
$crate::inventory::iter::<ServerFnTraitObj<$req, $res>>
.into_iter()
.map(|obj| {
((obj.path().to_string(), obj.method()), obj.clone())
})
.collect(),
)
})
};
}
@@ -984,7 +986,7 @@ impl<Req, Res> Clone for ServerFnTraitObj<Req, Res> {
#[allow(unused)] // used by server integrations
type LazyServerFnMap<Req, Res> =
LazyLock<DashMap<(String, Method), ServerFnTraitObj<Req, Res>>>;
LazyLock<RwLock<HashMap<(String, Method), ServerFnTraitObj<Req, Res>>>>;
#[cfg(feature = "ssr")]
impl<Req: 'static, Res: 'static> inventory::Collect
@@ -1006,6 +1008,7 @@ pub mod axum {
};
use axum::body::Body;
use http::{Method, Request, Response, StatusCode};
use or_poisoned::OrPoisoned;
use std::future::Future;
static REGISTERED_SERVER_FUNCTIONS: LazyServerFnMap<
@@ -1066,7 +1069,7 @@ pub mod axum {
>,
> + 'static,
{
REGISTERED_SERVER_FUNCTIONS.insert(
REGISTERED_SERVER_FUNCTIONS.write().or_poisoned().insert(
(T::PATH.into(), T::Protocol::METHOD),
ServerFnTraitObj::new::<T>(|req| Box::pin(T::run_on_server(req))),
);
@@ -1074,9 +1077,14 @@ pub mod axum {
/// The set of all registered server function paths.
pub fn server_fn_paths() -> impl Iterator<Item = (&'static str, Method)> {
REGISTERED_SERVER_FUNCTIONS
.iter()
let paths: Vec<_> = REGISTERED_SERVER_FUNCTIONS
.read()
.unwrap()
.values()
.map(|item| (item.path(), item.method()))
.collect();
paths.into_iter()
}
/// An Axum handler that responds to a server function request.
@@ -1110,14 +1118,18 @@ pub mod axum {
method: Method,
) -> Option<BoxedService<Request<Body>, Response<Body>>> {
let key = (path.into(), method);
REGISTERED_SERVER_FUNCTIONS.get(&key).map(|server_fn| {
let middleware = (server_fn.middleware)();
let mut service = server_fn.clone().boxed();
for middleware in middleware {
service = middleware.layer(service);
}
service
})
REGISTERED_SERVER_FUNCTIONS
.read()
.or_poisoned()
.get(&key)
.map(|server_fn| {
let middleware = (server_fn.middleware)();
let mut service = server_fn.clone().boxed();
for middleware in middleware {
service = middleware.layer(service);
}
service
})
}
}
@@ -1131,6 +1143,7 @@ pub mod actix {
};
use actix_web::{web::Payload, HttpRequest, HttpResponse};
use http::Method;
use or_poisoned::OrPoisoned;
#[doc(hidden)]
pub use send_wrapper::SendWrapper;
use std::future::Future;
@@ -1177,7 +1190,7 @@ pub mod actix {
>,
> + 'static,
{
REGISTERED_SERVER_FUNCTIONS.insert(
REGISTERED_SERVER_FUNCTIONS.write().or_poisoned().insert(
(T::PATH.into(), T::Protocol::METHOD),
ServerFnTraitObj::new::<T>(|req| Box::pin(T::run_on_server(req))),
);
@@ -1185,9 +1198,14 @@ pub mod actix {
/// The set of all registered server function paths.
pub fn server_fn_paths() -> impl Iterator<Item = (&'static str, Method)> {
REGISTERED_SERVER_FUNCTIONS
.iter()
let paths: Vec<_> = REGISTERED_SERVER_FUNCTIONS
.read()
.unwrap()
.values()
.map(|item| (item.path(), item.method()))
.collect();
paths.into_iter()
}
/// An Actix handler that responds to a server function request.
@@ -1236,16 +1254,18 @@ pub mod actix {
ActixMethod::CONNECT => Method::CONNECT,
_ => unreachable!(),
};
REGISTERED_SERVER_FUNCTIONS.get(&(path.into(), method)).map(
|server_fn| {
REGISTERED_SERVER_FUNCTIONS
.read()
.or_poisoned()
.get(&(path.into(), method))
.map(|server_fn| {
let middleware = (server_fn.middleware)();
let mut service = server_fn.clone().boxed();
for middleware in middleware {
service = middleware.layer(service);
}
service
},
)
})
}
}

View File

@@ -309,6 +309,16 @@ impl ServerFnCall {
.collect::<TokenStream2>()
}
/// Get the lint attributes for the server function.
pub fn lint_attrs(&self) -> TokenStream2 {
// pass through lint attributes from the function body
self.body
.lint_attrs
.iter()
.map(|lint_attr| quote!(#lint_attr))
.collect::<TokenStream2>()
}
fn fn_name_as_str(&self) -> String {
self.body.ident.to_string()
}
@@ -388,6 +398,8 @@ impl ServerFnCall {
PathInfo::None => quote! {},
};
let lint_attrs = self.lint_attrs();
let vis = &self.body.vis;
let struct_name = self.struct_name();
let fields = self
@@ -410,6 +422,7 @@ impl ServerFnCall {
#docs
#[derive(Debug, #derives)]
#addl_path
#lint_attrs
#vis struct #struct_name {
#(#fields),*
}
@@ -1465,6 +1478,8 @@ pub struct ServerFnBody {
pub block: TokenStream2,
/// The documentation of the server function.
pub docs: Vec<(String, Span)>,
/// The lint attributes of the server function.
pub lint_attrs: Vec<Attribute>,
/// The middleware attributes applied to the server function.
pub middlewares: Vec<Middleware>,
}
@@ -1522,6 +1537,27 @@ impl Parse for ServerFnBody {
};
!attr.path.is_ident("doc")
});
let lint_attrs = attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("allow")
|| attr.path().is_ident("warn")
|| attr.path().is_ident("deny")
|| attr.path().is_ident("forbid")
{
return Some(attr.clone());
}
None
})
.collect();
attrs.retain(|attr| {
!attr.path().is_ident("allow")
&& !attr.path().is_ident("warn")
&& !attr.path().is_ident("deny")
&& !attr.path().is_ident("forbid")
});
// extract all #[middleware] attributes, removing them from signature of dummy
let mut middlewares: Vec<Middleware> = vec![];
attrs.retain(|attr| {
@@ -1557,6 +1593,7 @@ impl Parse for ServerFnBody {
block,
attrs,
docs,
lint_attrs,
middlewares,
})
}

View File

@@ -693,13 +693,18 @@ impl Render for AnyViewWithAttrs {
fn rebuild(self, state: &mut Self::State) {
self.view.rebuild(&mut state.view);
let elements = state.elements();
// FIXME this seems wrong but I think the previous version was also broken!
if let Some(element) = elements.first() {
self.attrs.rebuild(&mut (
element.clone(),
std::mem::take(&mut state.attrs),
));
// at this point, we have rebuilt the inner view
// now we need to update attributes that were spread onto this
// this approach is not ideal, but it avoids two edge cases:
// 1) merging attributes from two unrelated views (https://github.com/leptos-rs/leptos/issues/4268)
// 2) failing to re-create attributes from the same view (https://github.com/leptos-rs/leptos/issues/4512)
for element in state.elements() {
// first, remove the previous set of attributes
self.attrs
.clone()
.rebuild(&mut (element.clone(), Vec::new()));
// then, add the new set of attributes
self.attrs.clone().build(&element);
}
}
}
@@ -824,6 +829,7 @@ impl AddAnyAttr for AnyViewWithAttrs {
/// State for any view with attributes spread onto it.
pub struct AnyViewWithAttrsState {
view: AnyViewState,
#[allow(dead_code)] // keeps attribute states alive until dropped
attrs: Vec<AnyAttributeState>,
}

View File

@@ -23,14 +23,32 @@ pub fn keyed<T, I, K, KF, VF, VFS, V>(
) -> Keyed<T, I, K, KF, VF, VFS, V>
where
I: IntoIterator<Item = T>,
K: Eq + Hash + 'static,
K: Eq + Hash + SerializableKey + 'static,
KF: Fn(&T) -> K,
V: Render,
VF: Fn(usize, T) -> (VFS, V),
VFS: Fn(usize),
{
Keyed {
items,
#[cfg(not(feature = "ssr"))]
items: Some(items),
#[cfg(feature = "ssr")]
items: None,
#[cfg(feature = "ssr")]
ssr_items: items
.into_iter()
.enumerate()
.map(|(i, t)| {
let key = if cfg!(feature = "islands") {
let key = (key_fn)(&t);
key.ser_key()
} else {
String::new()
};
let (_, view) = (view_fn)(i, t);
(key, view)
})
.collect::<Vec<_>>(),
key_fn,
view_fn,
}
@@ -45,7 +63,9 @@ where
VF: Fn(usize, T) -> (VFS, V),
VFS: Fn(usize),
{
items: I,
items: Option<I>,
#[cfg(feature = "ssr")]
ssr_items: Vec<(String, V)>,
key_fn: KF,
view_fn: VF,
}
@@ -106,14 +126,13 @@ where
VFS: Fn(usize),
{
type State = KeyedState<K, VFS, V>;
// TODO fallible state and try_build()/try_rebuild() here
fn build(self) -> Self::State {
let items = self.items.into_iter();
let items = self.items.into_iter().flatten();
let (capacity, _) = items.size_hint();
let mut hashed_items =
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
let mut rendered_items = Vec::new();
let mut rendered_items = Vec::with_capacity(capacity);
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);
@@ -134,7 +153,7 @@ where
hashed_items,
ref mut rendered_items,
} = state;
let new_items = self.items.into_iter();
let new_items = self.items.into_iter().flatten();
let (capacity, _) = new_items.size_hint();
let mut new_hashed_items =
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
@@ -198,6 +217,8 @@ where
{
let Keyed {
items,
#[cfg(feature = "ssr")]
ssr_items,
key_fn,
view_fn,
} = self;
@@ -205,6 +226,11 @@ where
Keyed {
items,
key_fn,
#[cfg(feature = "ssr")]
ssr_items: ssr_items
.into_iter()
.map(|(k, v)| (k, v.add_any_attr(attr.clone())))
.collect(),
view_fn: Box::new(move |index, item| {
let (index, view) = view_fn(index, item);
(index, view.add_any_attr(attr.clone()))
@@ -229,21 +255,39 @@ where
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {
// TODO...
#[cfg(feature = "ssr")]
for view in &mut self.ssr_items {
view.dry_resolve();
}
}
async fn resolve(self) -> Self::AsyncOutput {
futures::future::join_all(self.items.into_iter().enumerate().map(
|(index, item)| {
let (_, view) = (self.view_fn)(index, item);
view.resolve()
},
))
.await
.into_iter()
.collect::<Vec<_>>()
#[cfg(feature = "ssr")]
{
futures::future::join_all(
self.ssr_items.into_iter().map(|(_, view)| view.resolve()),
)
.await
.into_iter()
.collect::<Vec<_>>()
}
#[cfg(not(feature = "ssr"))]
{
futures::future::join_all(
self.items.into_iter().flatten().enumerate().map(
|(index, item)| {
let (_, view) = (self.view_fn)(index, item);
view.resolve()
},
),
)
.await
.into_iter()
.collect::<Vec<_>>()
}
}
#[allow(unused)]
fn to_html_with_buf(
self,
buf: &mut String,
@@ -255,8 +299,9 @@ where
if mark_branches && escape {
buf.open_branch("for");
}
for (index, item) in self.items.into_iter().enumerate() {
let (_, item) = (self.view_fn)(index, item);
#[cfg(feature = "ssr")]
for item in self.ssr_items {
if mark_branches && escape {
buf.open_branch("item");
}
@@ -278,6 +323,7 @@ where
buf.push_str("<!>");
}
#[allow(unused)]
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder,
@@ -289,13 +335,10 @@ where
if mark_branches && escape {
buf.open_branch("for");
}
for (index, item) in self.items.into_iter().enumerate() {
let branch_name = mark_branches.then(|| {
let key = (self.key_fn)(&item);
let key = key.ser_key();
format!("item-{key}")
});
let (_, item) = (self.view_fn)(index, item);
#[cfg(feature = "ssr")]
for (key, item) in self.ssr_items {
let branch_name = mark_branches.then(|| format!("item-{key}"));
if mark_branches && escape {
buf.open_branch(branch_name.as_ref().unwrap());
}
@@ -311,6 +354,7 @@ where
}
*position = Position::NextChild;
}
if mark_branches && escape {
buf.close_branch("for");
}
@@ -334,11 +378,11 @@ where
.expect("parent of keyed list should be an element");
// build list
let items = self.items.into_iter();
let items = self.items.into_iter().flatten();
let (capacity, _) = items.size_hint();
let mut hashed_items =
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
let mut rendered_items = Vec::new();
let mut rendered_items = Vec::with_capacity(capacity);
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);
@@ -373,11 +417,11 @@ where
.expect("parent of keyed list should be an element");
// build list
let items = self.items.into_iter();
let items = self.items.into_iter().flatten();
let (capacity, _) = items.size_hint();
let mut hashed_items =
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
let mut rendered_items = Vec::new();
let mut rendered_items = Vec::with_capacity(capacity);
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);