mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
88 Commits
proc-macro
...
v0.8.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ee4444bb4 | ||
|
|
03a1c1e7a6 | ||
|
|
12e49ed996 | ||
|
|
1e281e9e74 | ||
|
|
bd475f89d0 | ||
|
|
3d91b5e90f | ||
|
|
96d8d5218c | ||
|
|
84caa35cef | ||
|
|
fc8b55161c | ||
|
|
657052466b | ||
|
|
efe8336363 | ||
|
|
770881842c | ||
|
|
0d540ef02f | ||
|
|
dc1885ad92 | ||
|
|
61bf87439a | ||
|
|
308568e520 | ||
|
|
1b0f32dc4c | ||
|
|
2e0b3011d9 | ||
|
|
680d4ccd07 | ||
|
|
325f9cbe33 | ||
|
|
26ab392c95 | ||
|
|
3a4e2a19aa | ||
|
|
eed3d21b40 | ||
|
|
7ae386285d | ||
|
|
ebcc51136d | ||
|
|
e10ded4fd0 | ||
|
|
67be872f58 | ||
|
|
e5b21ac0fc | ||
|
|
9b2e313d20 | ||
|
|
bc79232033 | ||
|
|
a7bb2565c4 | ||
|
|
2e393aaca0 | ||
|
|
e37711cb85 | ||
|
|
627b553e60 | ||
|
|
e2f9aca466 | ||
|
|
970544ed0b | ||
|
|
0c3b3c440f | ||
|
|
6578086e09 | ||
|
|
113aba9666 | ||
|
|
58d7475193 | ||
|
|
04d80ff8d0 | ||
|
|
7971a2dccb | ||
|
|
e2ea4277bc | ||
|
|
171c8e7ff7 | ||
|
|
53ffbeeb67 | ||
|
|
ee86844077 | ||
|
|
1cee3f2f52 | ||
|
|
23c89dbfe1 | ||
|
|
9f71f39f89 | ||
|
|
ef1d0f108a | ||
|
|
a7a78317b7 | ||
|
|
5005cc3587 | ||
|
|
08708f3388 | ||
|
|
c19c1b32f1 | ||
|
|
e70cc08e96 | ||
|
|
97175663ef | ||
|
|
92524a93cd | ||
|
|
9449f41ca9 | ||
|
|
d979055b70 | ||
|
|
97686f71a5 | ||
|
|
06a0c768dc | ||
|
|
fff6a508fc | ||
|
|
e65fc23fc7 | ||
|
|
f83b14d76c | ||
|
|
62dac6fb8a | ||
|
|
b36dec8269 | ||
|
|
0c50852251 | ||
|
|
50cb6005a8 | ||
|
|
b725291ce9 | ||
|
|
ed6d45d92d | ||
|
|
73b5587738 | ||
|
|
68813a5918 | ||
|
|
8f6a96341e | ||
|
|
046d5286c3 | ||
|
|
b45f982feb | ||
|
|
2b50ddc0db | ||
|
|
c743f0641c | ||
|
|
078c252e2e | ||
|
|
410aedbba8 | ||
|
|
00e474599f | ||
|
|
8f38559aa2 | ||
|
|
3934c8b162 | ||
|
|
de3a558203 | ||
|
|
4d20105760 | ||
|
|
b95e827b8b | ||
|
|
30c445a419 | ||
|
|
6d5ab73594 | ||
|
|
e0bf5ec480 |
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -29,11 +29,14 @@ Steps to reproduce the behavior:
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Next Steps**
|
||||
[ ] I will make a PR
|
||||
[ ] I would like to make a PR, but need help getting started
|
||||
[ ] I want someone else to take the time to fix this
|
||||
[ ] This is a low priority for me and is just shared for your information
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
28
.github/workflows/autofix.yml
vendored
28
.github/workflows/autofix.yml
vendored
@@ -21,33 +21,19 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: {toolchain: nightly, components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
|
||||
with: {toolchain: "nightly-2025-04-16", components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
|
||||
- name: Install Glib
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- name: Install cargo-all-features
|
||||
run: cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- run: |
|
||||
echo "Formatting the workspace"
|
||||
cargo fmt --all
|
||||
|
||||
echo "Running Clippy against each member's features (default features included)"
|
||||
for member in $(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | .name'); do
|
||||
echo "Working on member $member":
|
||||
echo -e "\tdefault-features/no-features:"
|
||||
# this will also run on members with no features or default features
|
||||
cargo clippy --allow-dirty --fix --lib --package "$member"
|
||||
|
||||
features=$(cargo metadata --no-deps --format-version 1 | jq -r ".packages[] | select(.name == \"$member\") | .features | keys[]")
|
||||
for feature in $features; do
|
||||
if [ "$feature" = "default" ]; then
|
||||
continue
|
||||
fi
|
||||
echo -e "\tfeature $feature"
|
||||
cargo clippy --allow-dirty --fix --lib --package "$member" --features "$feature"
|
||||
done
|
||||
done
|
||||
- name: Format the workspace
|
||||
run: cargo fmt --all
|
||||
- name: Clippy the workspace
|
||||
run: cargo all-features clippy --allow-dirty --fix --lib --no-deps
|
||||
- uses: autofix-ci/action@v1.3.1
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
|
||||
8
.github/workflows/run-cargo-make-task.yml
vendored
8
.github/workflows/run-cargo-make-task.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
toolchain: [stable, nightly-2025-03-05]
|
||||
toolchain: [stable, nightly-2025-04-16]
|
||||
erased_mode: [true, false]
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
@@ -160,6 +160,12 @@ jobs:
|
||||
run: |
|
||||
cd '${{ inputs.directory }}'
|
||||
cargo make --no-workspace --profile=github-actions ci
|
||||
# Check if the counter_isomorphic can be built with leptos_debuginfo cfg flag in release mode
|
||||
- name: ${{ inputs.cargo_make_task }} with --cfg=leptos_debuginfo
|
||||
if: contains(inputs.directory, 'counter_isomorphic')
|
||||
run: |
|
||||
cd '${{ inputs.directory }}'
|
||||
RUSTFLAGS="$RUSTFLAGS --cfg leptos_debuginfo" cargo leptos build --release
|
||||
- name: Clean up ${{ inputs.directory }}
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
97
Cargo.lock
generated
97
Cargo.lock
generated
@@ -263,7 +263,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "any_spawner"
|
||||
version = "0.3.0-rc1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"async-executor",
|
||||
"futures",
|
||||
@@ -687,9 +687,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "const-str"
|
||||
version = "0.5.7"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9"
|
||||
checksum = "9e991226a70654b49d34de5ed064885f0bef0348a8e70018b8ff1ac80aa984a2"
|
||||
|
||||
[[package]]
|
||||
name = "const_format"
|
||||
@@ -732,9 +732,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.7.1"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
|
||||
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
@@ -1781,7 +1781,7 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
||||
|
||||
[[package]]
|
||||
name = "leptos"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"any_spawner",
|
||||
"base64",
|
||||
@@ -1813,8 +1813,8 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"throw_error",
|
||||
"tracing",
|
||||
"typed-builder",
|
||||
"typed-builder-macro",
|
||||
"typed-builder 0.21.0",
|
||||
"typed-builder-macro 0.21.0",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
@@ -1833,7 +1833,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_actix"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"actix-files",
|
||||
"actix-http",
|
||||
@@ -1859,7 +1859,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_axum"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"any_spawner",
|
||||
"axum",
|
||||
@@ -1883,7 +1883,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_config"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"config",
|
||||
"regex",
|
||||
@@ -1892,12 +1892,12 @@ dependencies = [
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"typed-builder",
|
||||
"typed-builder 0.21.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leptos_dom"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"leptos",
|
||||
@@ -1914,7 +1914,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_hot_reload"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
@@ -1930,7 +1930,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_integration_utils"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"hydration_context",
|
||||
@@ -1943,11 +1943,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_macro"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"attribute-derive",
|
||||
"cfg-if",
|
||||
"convert_case 0.7.1",
|
||||
"convert_case 0.8.0",
|
||||
"html-escape",
|
||||
"insta",
|
||||
"itertools",
|
||||
@@ -1963,17 +1963,17 @@ dependencies = [
|
||||
"rustc_version",
|
||||
"serde",
|
||||
"server_fn",
|
||||
"server_fn_macro 0.8.0-rc1",
|
||||
"server_fn_macro 0.8.2",
|
||||
"syn 2.0.100",
|
||||
"tracing",
|
||||
"trybuild",
|
||||
"typed-builder",
|
||||
"typed-builder 0.20.1",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leptos_meta"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"indexmap",
|
||||
@@ -1988,7 +1988,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_router"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"any_spawner",
|
||||
"either_of",
|
||||
@@ -2013,7 +2013,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_router_macro"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"leptos_macro",
|
||||
"leptos_router",
|
||||
@@ -2025,7 +2025,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_server"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"any_spawner",
|
||||
"base64",
|
||||
@@ -2759,7 +2759,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reactive_graph"
|
||||
version = "0.2.0-rc1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"any_spawner",
|
||||
"async-lock",
|
||||
@@ -2782,9 +2782,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reactive_stores"
|
||||
version = "0.2.0-rc1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"any_spawner",
|
||||
"dashmap",
|
||||
"guardian",
|
||||
"itertools",
|
||||
"leptos",
|
||||
@@ -2793,15 +2794,16 @@ dependencies = [
|
||||
"reactive_graph",
|
||||
"reactive_stores_macro",
|
||||
"rustc-hash 2.1.1",
|
||||
"send_wrapper",
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reactive_stores_macro"
|
||||
version = "0.2.0-rc1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"convert_case 0.7.1",
|
||||
"convert_case 0.8.0",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3239,13 +3241,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_qs"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6"
|
||||
checksum = "8b417bedc008acbdf6d6b4bc482d29859924114bbe2650b7921fb68a261d0aa6"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3296,7 +3298,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "server_fn"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"actix-ws",
|
||||
@@ -3321,6 +3323,7 @@ dependencies = [
|
||||
"reqwest",
|
||||
"rkyv",
|
||||
"rmp-serde",
|
||||
"rustc_version",
|
||||
"rustversion",
|
||||
"send_wrapper",
|
||||
"serde",
|
||||
@@ -3359,10 +3362,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "server_fn_macro"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"const_format",
|
||||
"convert_case 0.6.0",
|
||||
"convert_case 0.8.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
@@ -3372,9 +3375,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "server_fn_macro_default"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"server_fn_macro 0.8.0-rc1",
|
||||
"server_fn_macro 0.8.2",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
@@ -3568,7 +3571,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tachys"
|
||||
version = "0.2.0-rc1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"any_spawner",
|
||||
"async-trait",
|
||||
@@ -4015,7 +4018,16 @@ version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7"
|
||||
dependencies = [
|
||||
"typed-builder-macro",
|
||||
"typed-builder-macro 0.20.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-builder"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce63bcaf7e9806c206f7d7b9c1f38e0dce8bb165a80af0898161058b19248534"
|
||||
dependencies = [
|
||||
"typed-builder-macro 0.21.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4029,6 +4041,17 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-builder-macro"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60d8d828da2a3d759d3519cdf29a5bac49c77d039ad36d0782edadbf9cd5415b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.18.0"
|
||||
|
||||
43
Cargo.toml
43
Cargo.toml
@@ -40,41 +40,44 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
convert_case = "0.8"
|
||||
throw_error = { path = "./any_error/", version = "0.3.0" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.3.0-rc1" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.3.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
|
||||
either_of = { path = "./either_of/", version = "0.1.5" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.3.0" }
|
||||
itertools = "0.14.0"
|
||||
leptos = { path = "./leptos", version = "0.8.0-rc1" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.0-rc1" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.0-rc1" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-rc1" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-rc1" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.0-rc1" }
|
||||
leptos_router = { path = "./router", version = "0.8.0-rc1" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.0-rc1" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.0-rc1" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.0-rc1" }
|
||||
leptos = { path = "./leptos", version = "0.8.2" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.2" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.2" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.2" }
|
||||
leptos_router = { path = "./router", version = "0.8.2" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.2" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.2" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.2" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.0-rc1" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.0-rc1" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-rc1" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.0" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.0" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0" }
|
||||
rustversion = "1"
|
||||
serde_json = "1.0.0"
|
||||
server_fn = { path = "./server_fn", version = "0.8.0-rc1" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-rc1" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-rc1" }
|
||||
tachys = { path = "./tachys", version = "0.2.0-rc1" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.2" }
|
||||
tachys = { path = "./tachys", version = "0.2.0" }
|
||||
trybuild = "1"
|
||||
wasm-bindgen = { version = "0.2.100" }
|
||||
typed-builder = "0.21.0"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.100"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "any_spawner"
|
||||
version = "0.3.0-rc1"
|
||||
version = "0.3.0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -12,7 +12,7 @@ edition.workspace = true
|
||||
async-executor = { version = "1.13.1", optional = true }
|
||||
futures = "0.3.31"
|
||||
glib = { version = "0.20.6", optional = true }
|
||||
thiserror = "2.0"
|
||||
thiserror = { workspace = true }
|
||||
tokio = { version = "1.41", optional = true, default-features = false, features = [
|
||||
"rt",
|
||||
] }
|
||||
|
||||
@@ -472,9 +472,8 @@ fn handle_uninitialized_spawn(_fut: PinnedFuture<()>) {
|
||||
#[cfg(all(debug_assertions, not(feature = "tracing")))]
|
||||
{
|
||||
panic!(
|
||||
"At {}, tried to spawn a Future with Executor::spawn() before a \
|
||||
global executor was initialized.",
|
||||
caller
|
||||
"At {caller}, tried to spawn a Future with Executor::spawn() \
|
||||
before a global executor was initialized."
|
||||
);
|
||||
}
|
||||
// In release builds (without tracing), call the specific no-op function.
|
||||
@@ -503,9 +502,8 @@ fn handle_uninitialized_spawn_local(_fut: PinnedLocalFuture<()>) {
|
||||
#[cfg(all(debug_assertions, not(feature = "tracing")))]
|
||||
{
|
||||
panic!(
|
||||
"At {}, tried to spawn a Future with Executor::spawn_local() \
|
||||
before a global executor was initialized.",
|
||||
caller
|
||||
"At {caller}, tried to spawn a Future with \
|
||||
Executor::spawn_local() before a global executor was initialized."
|
||||
);
|
||||
}
|
||||
// In release builds (without tracing), call the specific no-op function (which usually panics).
|
||||
|
||||
@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new(
|
||||
let story = Resource::new_blocking(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::Router;
|
||||
use axum::{routing::get, Router};
|
||||
use hackernews_axum::{shell, App};
|
||||
use leptos::config::get_configuration;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
@@ -13,6 +13,15 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/favicon.ico",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "image/x-icon")],
|
||||
include_bytes!("../public/favicon.ico"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
|
||||
@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new(
|
||||
let story = Resource::new_blocking(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
|
||||
@@ -26,12 +26,17 @@ pub async fn file_and_error_handler(
|
||||
.map(|h| h.to_str().unwrap_or("none"))
|
||||
.unwrap_or("none")
|
||||
.to_string();
|
||||
let res = get_static_file(uri.clone(), accept_encoding).await.unwrap();
|
||||
let static_result = get_static_file(uri.clone(), accept_encoding).await;
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, "Not found.").into_response()
|
||||
match static_result {
|
||||
Ok(res) => {
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, "Not found.").into_response()
|
||||
}
|
||||
}
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::routing::get;
|
||||
pub use axum::Router;
|
||||
use hackernews_islands::*;
|
||||
pub use leptos::config::get_configuration;
|
||||
@@ -25,6 +26,7 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/favicon.ico", get(fallback::file_and_error_handler))
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
|
||||
@@ -47,7 +47,7 @@ pub fn Stories() -> impl IntoView {
|
||||
let stories = Resource::new(
|
||||
move || (page(), story_type()),
|
||||
move |(page, story_type)| async move {
|
||||
fetch_stories(category(&story_type), page).await.ok()
|
||||
fetch_stories(story_type, page).await.ok()
|
||||
},
|
||||
);
|
||||
let (pending, set_pending) = signal(false);
|
||||
|
||||
@@ -13,7 +13,7 @@ pub async fn fetch_story(
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new(
|
||||
let story = Resource::new_blocking(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
|
||||
@@ -7,7 +7,7 @@ use send_wrapper::SendWrapper;
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new(
|
||||
let story = Resource::new_blocking(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| {
|
||||
SendWrapper::new(async move {
|
||||
|
||||
@@ -10,7 +10,11 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"tracing",
|
||||
"islands",
|
||||
"islands-router",
|
||||
] }
|
||||
leptos_router = { path = "../../router" }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", features = [
|
||||
|
||||
@@ -945,9 +945,7 @@ pub fn CustomClientExample() -> impl IntoView {
|
||||
Item = Result<server_fn::Bytes, server_fn::Bytes>,
|
||||
> + Send
|
||||
+ 'static,
|
||||
impl Sink<Result<server_fn::Bytes, server_fn::Bytes>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
impl Sink<server_fn::Bytes> + Send + 'static,
|
||||
),
|
||||
E,
|
||||
>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Leptos Counter Example
|
||||
# Stores Example
|
||||
|
||||
This example creates a simple counter in a client side rendered app with Rust and WASM!
|
||||
This example shows how to use reactive stores, by building a client-side rendered TODO application.
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ site-pkg-dir = "pkg"
|
||||
# The tailwind input file.
|
||||
#
|
||||
# Optional, Activates the tailwind build
|
||||
tailwind-input-file = "style/tailwind.css"
|
||||
tailwind-input-file = "input.css"
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: {
|
||||
files: ["*.html", "./src/**/*.rs"],
|
||||
transform: {
|
||||
rs: (content) => content.replace(/(?:^|\s)class:/g, ' '),
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" />
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico" />
|
||||
<link data-trunk rel="tailwind-css" href="/style/tailwind.css" />
|
||||
<link data-trunk rel="tailwind-css" href="input.css" />
|
||||
<title>Leptos • Counter with Tailwind</title>
|
||||
</head>
|
||||
|
||||
|
||||
1
examples/tailwind_csr/input.css
Normal file
1
examples/tailwind_csr/input.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,13 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: {
|
||||
files: ["*.html", "./src/**/*.rs"],
|
||||
transform: {
|
||||
rs: (content) => content.replace(/(?:^|\s)class:/g, ' '),
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
74
examples/websocket/Cargo.toml
Normal file
74
examples/websocket/Cargo.toml
Normal file
@@ -0,0 +1,74 @@
|
||||
[package]
|
||||
name = "websocket"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
leptos = { path = "../../leptos", features = ["tracing"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
log = "0.4.22"
|
||||
simple_logger = "5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
thiserror = "2.0"
|
||||
wasm-bindgen = "0.2.100"
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["dep:axum", "dep:tokio", "leptos/ssr", "dep:leptos_axum"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tokio", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"], []]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "websocket"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "cargo make test-ui"
|
||||
end2end-dir = "e2e"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
21
examples/websocket/LICENSE
Normal file
21
examples/websocket/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Greg Johnston
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
12
examples/websocket/Makefile.toml
Normal file
12
examples/websocket/Makefile.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
CLIENT_PROCESS_NAME = "websocket"
|
||||
|
||||
[tasks.test-ui]
|
||||
cwd = "./e2e"
|
||||
command = "cargo"
|
||||
args = ["make", "test-ui", "${@}"]
|
||||
19
examples/websocket/README.md
Normal file
19
examples/websocket/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Leptos WebSocket
|
||||
|
||||
This example creates a basic WebSocket echo app.
|
||||
|
||||
## Getting Started
|
||||
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
## E2E Testing
|
||||
|
||||
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
|
||||
|
||||
## Rendering
|
||||
|
||||
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `cargo leptos watch` to run this example.
|
||||
18
examples/websocket/e2e/Cargo.toml
Normal file
18
examples/websocket/e2e/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "websocket_e2e"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1.81"
|
||||
cucumber = "0.21.1"
|
||||
fantoccini = "0.21.1"
|
||||
pretty_assertions = "1.4"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.39", features = ["macros", "rt-multi-thread", "time"] }
|
||||
url = "2.5"
|
||||
|
||||
[[test]]
|
||||
name = "app_suite"
|
||||
harness = false # Allow Cucumber to print output instead of libtest
|
||||
20
examples/websocket/e2e/Makefile.toml
Normal file
20
examples/websocket/e2e/Makefile.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.test]
|
||||
env = { RUN_AUTOMATICALLY = false }
|
||||
condition = { env_true = ["RUN_AUTOMATICALLY"] }
|
||||
|
||||
[tasks.ci]
|
||||
|
||||
[tasks.test-ui]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--test",
|
||||
"app_suite",
|
||||
"--",
|
||||
"--retry",
|
||||
"2",
|
||||
"--fail-fast",
|
||||
"${@}",
|
||||
]
|
||||
34
examples/websocket/e2e/README.md
Normal file
34
examples/websocket/e2e/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# E2E Testing
|
||||
|
||||
This example demonstrates e2e testing with Rust using executable requirements.
|
||||
|
||||
## Testing Stack
|
||||
|
||||
| | Role | Description |
|
||||
|---|---|---|
|
||||
| [Cucumber](https://github.com/cucumber-rs/cucumber/tree/main) | Test Runner | Run [Gherkin](https://cucumber.io/docs/gherkin/reference/) specifications as Rust tests |
|
||||
| [Fantoccini](https://github.com/jonhoo/fantoccini/tree/main) | Browser Client | Interact with web pages through WebDriver |
|
||||
| [Cargo Leptos ](https://github.com/leptos-rs/cargo-leptos) | Build Tool | Compile example and start the server and end-2-end tests |
|
||||
| [chromedriver](https://chromedriver.chromium.org/downloads) | WebDriver | Provide WebDriver for Chrome
|
||||
|
||||
## Testing Organization
|
||||
|
||||
Testing is organized around what a user can do and see/not see. Test scenarios are grouped by the **user action** and the **object** of that action. This makes it easier to locate and reason about requirements.
|
||||
|
||||
Here is a brief overview of how things fit together.
|
||||
|
||||
```bash
|
||||
features
|
||||
└── {action}_{object}.feature # Specify test scenarios
|
||||
tests
|
||||
├── fixtures
|
||||
│ ├── action.rs # Perform a user action (click, type, etc.)
|
||||
│ ├── check.rs # Assert what a user can see/not see
|
||||
│ ├── find.rs # Query page elements
|
||||
│ ├── mod.rs
|
||||
│ └── world
|
||||
│ ├── action_steps.rs # Map Gherkin steps to user actions
|
||||
│ ├── check_steps.rs # Map Gherkin steps to user expectations
|
||||
│ └── mod.rs
|
||||
└── app_suite.rs # Test main
|
||||
```
|
||||
10
examples/websocket/e2e/features/echo_client_error.feature
Normal file
10
examples/websocket/e2e/features/echo_client_error.feature
Normal file
@@ -0,0 +1,10 @@
|
||||
@echo_client_error
|
||||
Feature: Echo Client Error
|
||||
|
||||
Background:
|
||||
Given I see the app
|
||||
|
||||
@echo_client_error-see-fifth-input-error
|
||||
Scenario: Should see the client error
|
||||
Given I add a text as abcde
|
||||
Then I see the label of the input is Error(ServerFnErrorWrapper(Registration("Error generated from client")))
|
||||
10
examples/websocket/e2e/features/echo_server_error.feature
Normal file
10
examples/websocket/e2e/features/echo_server_error.feature
Normal file
@@ -0,0 +1,10 @@
|
||||
@echo_server_error
|
||||
Feature: Echo Server Error
|
||||
|
||||
Background:
|
||||
Given I see the app
|
||||
|
||||
@echo_server_error-see-third-input-error
|
||||
Scenario: Should see the server error
|
||||
Given I add a text as abc
|
||||
Then I see the label of the input is Error(ServerFnErrorWrapper(Registration("Error generated from server")))
|
||||
17
examples/websocket/e2e/features/echo_text.feature
Normal file
17
examples/websocket/e2e/features/echo_text.feature
Normal file
@@ -0,0 +1,17 @@
|
||||
@echo_text
|
||||
Feature: Echo Text
|
||||
|
||||
Background:
|
||||
Given I see the app
|
||||
|
||||
@echo_text-see-first-input
|
||||
Scenario: Should see the label
|
||||
Given I add a text as a
|
||||
Then I see the label of the input is A
|
||||
|
||||
@add_text-see-second-input
|
||||
Scenario: Should see the label
|
||||
Given I add a text as ab
|
||||
Then I see the label of the input is AB
|
||||
|
||||
|
||||
7
examples/websocket/e2e/features/open_app.feature
Normal file
7
examples/websocket/e2e/features/open_app.feature
Normal file
@@ -0,0 +1,7 @@
|
||||
@open_app
|
||||
Feature: Open App
|
||||
|
||||
@open_app-title
|
||||
Scenario: Should see the home page title
|
||||
When I open the app
|
||||
Then I see the page title is Simple Echo WebSocket Communication
|
||||
14
examples/websocket/e2e/tests/app_suite.rs
Normal file
14
examples/websocket/e2e/tests/app_suite.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
mod fixtures;
|
||||
|
||||
use anyhow::Result;
|
||||
use cucumber::World;
|
||||
use fixtures::world::AppWorld;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
AppWorld::cucumber()
|
||||
.fail_on_skipped()
|
||||
.run_and_exit("./features")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
18
examples/websocket/e2e/tests/fixtures/action.rs
vendored
Normal file
18
examples/websocket/e2e/tests/fixtures/action.rs
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
use super::{find, world::HOST};
|
||||
use anyhow::Result;
|
||||
use fantoccini::Client;
|
||||
use std::result::Result::Ok;
|
||||
|
||||
pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
|
||||
let url = format!("{}{}", HOST, path);
|
||||
client.goto(&url).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fill_input(client: &Client, text: &str) -> Result<()> {
|
||||
let textbox = find::input(client).await;
|
||||
textbox.send_keys(text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
22
examples/websocket/e2e/tests/fixtures/check.rs
vendored
Normal file
22
examples/websocket/e2e/tests/fixtures/check.rs
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
use anyhow::{Ok, Result};
|
||||
use fantoccini::{Client, Locator};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
pub async fn text_on_element(
|
||||
client: &Client,
|
||||
selector: &str,
|
||||
expected_text: &str,
|
||||
) -> Result<()> {
|
||||
let element = client
|
||||
.wait()
|
||||
.for_element(Locator::Css(selector))
|
||||
.await
|
||||
.unwrap_or_else(|_| {
|
||||
panic!("Element not found by Css selector `{}`", selector)
|
||||
});
|
||||
|
||||
let actual = element.text().await?;
|
||||
assert_eq!(&actual, expected_text);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
11
examples/websocket/e2e/tests/fixtures/find.rs
vendored
Normal file
11
examples/websocket/e2e/tests/fixtures/find.rs
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
use fantoccini::{elements::Element, Client, Locator};
|
||||
|
||||
pub async fn input(client: &Client) -> Element {
|
||||
let textbox = client
|
||||
.wait()
|
||||
.for_element(Locator::Css("input"))
|
||||
.await
|
||||
.expect("websocket textbox not found");
|
||||
|
||||
textbox
|
||||
}
|
||||
4
examples/websocket/e2e/tests/fixtures/mod.rs
vendored
Normal file
4
examples/websocket/e2e/tests/fixtures/mod.rs
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod action;
|
||||
pub mod check;
|
||||
pub mod find;
|
||||
pub mod world;
|
||||
20
examples/websocket/e2e/tests/fixtures/world/action_steps.rs
vendored
Normal file
20
examples/websocket/e2e/tests/fixtures/world/action_steps.rs
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
use crate::fixtures::{action, world::AppWorld};
|
||||
use anyhow::{Ok, Result};
|
||||
use cucumber::{given, when};
|
||||
|
||||
#[given("I see the app")]
|
||||
#[when("I open the app")]
|
||||
async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::goto_path(client, "").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(regex = "^I add a text as (.*)$")]
|
||||
async fn i_add_a_text(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::fill_input(client, text.as_str()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
28
examples/websocket/e2e/tests/fixtures/world/check_steps.rs
vendored
Normal file
28
examples/websocket/e2e/tests/fixtures/world/check_steps.rs
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
use crate::fixtures::{check, world::AppWorld};
|
||||
use anyhow::{Ok, Result};
|
||||
use cucumber::then;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[then(regex = "^I see the page title is (.*)$")]
|
||||
async fn i_see_the_page_title_is(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::text_on_element(client, "h1", &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = "^I see the label of the input is (.*)$")]
|
||||
async fn i_see_the_label_of_the_input_is(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
let client = &world.client;
|
||||
check::text_on_element(client, "p", &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
39
examples/websocket/e2e/tests/fixtures/world/mod.rs
vendored
Normal file
39
examples/websocket/e2e/tests/fixtures/world/mod.rs
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
pub mod action_steps;
|
||||
pub mod check_steps;
|
||||
|
||||
use anyhow::Result;
|
||||
use cucumber::World;
|
||||
use fantoccini::{
|
||||
error::NewSessionError, wd::Capabilities, Client, ClientBuilder,
|
||||
};
|
||||
|
||||
pub const HOST: &str = "http://127.0.0.1:3000";
|
||||
|
||||
#[derive(Debug, World)]
|
||||
#[world(init = Self::new)]
|
||||
pub struct AppWorld {
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
impl AppWorld {
|
||||
async fn new() -> Result<Self, anyhow::Error> {
|
||||
let webdriver_client = build_client().await?;
|
||||
|
||||
Ok(Self {
|
||||
client: webdriver_client,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_client() -> Result<Client, NewSessionError> {
|
||||
let mut cap = Capabilities::new();
|
||||
let arg = serde_json::from_str("{\"args\": [\"-headless\"]}").unwrap();
|
||||
cap.insert("goog:chromeOptions".to_string(), arg);
|
||||
|
||||
let client = ClientBuilder::native()
|
||||
.capabilities(cap)
|
||||
.connect("http://localhost:4444")
|
||||
.await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
BIN
examples/websocket/public/favicon.ico
Normal file
BIN
examples/websocket/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
9
examples/websocket/src/lib.rs
Normal file
9
examples/websocket/src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod websocket;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::websocket::App;
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount::hydrate_body(App);
|
||||
}
|
||||
44
examples/websocket/src/main.rs
Normal file
44
examples/websocket/src/main.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::Router;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use websocket::websocket::{shell, App};
|
||||
|
||||
simple_logger::init_with_level(log::Level::Error)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
})
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
println!("listening on http://{}", &addr);
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
use leptos::mount::mount_to_body;
|
||||
use websocket::websocket::App;
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(App);
|
||||
}
|
||||
123
examples/websocket/src/websocket.rs
Normal file
123
examples/websocket/src/websocket.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use leptos::{prelude::*, task::spawn_local};
|
||||
use server_fn::{codec::JsonEncoding, BoxedStream, ServerFnError, Websocket};
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<AutoReload options=options.clone() />
|
||||
<HydrationScripts options />
|
||||
<link rel="shortcut icon" type="image/ico" href="/favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<App />
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
|
||||
// The websocket protocol can be used on any server function that accepts and returns a [`BoxedStream`]
|
||||
// with items that can be encoded by the input and output encoding generics.
|
||||
//
|
||||
// In this case, the input and output encodings are [`Json`] and [`Json`], respectively which requires
|
||||
// the items to implement [`Serialize`] and [`Deserialize`].
|
||||
#[server(protocol = Websocket<JsonEncoding, JsonEncoding>)]
|
||||
async fn echo_websocket(
|
||||
input: BoxedStream<String, ServerFnError>,
|
||||
) -> Result<BoxedStream<String, ServerFnError>, ServerFnError> {
|
||||
use futures::{channel::mpsc, SinkExt, StreamExt};
|
||||
let mut input = input; // FIXME :-) server fn fields should pass mut through to destructure
|
||||
|
||||
// create a channel of outgoing websocket messages
|
||||
// we'll return rx, so sending a message to tx will send a message to the client via the websocket
|
||||
let (mut tx, rx) = mpsc::channel(1);
|
||||
|
||||
// spawn a task to listen to the input stream of messages coming in over the websocket
|
||||
tokio::spawn(async move {
|
||||
let mut x = 0;
|
||||
while let Some(msg) = input.next().await {
|
||||
// do some work on each message, and then send our responses
|
||||
x += 1;
|
||||
println!("In server: {} {:?}", x, msg);
|
||||
if x % 3 == 0 {
|
||||
let _ = tx
|
||||
.send(Err(ServerFnError::Registration(
|
||||
"Error generated from server".to_string(),
|
||||
)))
|
||||
.await;
|
||||
} else {
|
||||
let _ = tx.send(msg.map(|msg| msg.to_ascii_uppercase())).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(rx.into())
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
let (mut tx, rx) = mpsc::channel(1);
|
||||
let latest = RwSignal::new(Ok("".into()));
|
||||
|
||||
// we'll only listen for websocket messages on the client
|
||||
if cfg!(feature = "hydrate") {
|
||||
spawn_local(async move {
|
||||
match echo_websocket(rx.into()).await {
|
||||
Ok(mut messages) => {
|
||||
while let Some(msg) = messages.next().await {
|
||||
leptos::logging::log!("{:?}", msg);
|
||||
latest.set(msg);
|
||||
}
|
||||
}
|
||||
Err(e) => leptos::logging::warn!("{e}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut x = 0;
|
||||
view! {
|
||||
<h1>Simple Echo WebSocket Communication</h1>
|
||||
<input
|
||||
type="text"
|
||||
on:input:target=move |ev| {
|
||||
x += 1;
|
||||
let msg = ev.target().value();
|
||||
leptos::logging::log!("In client: {} {:?}", x, msg);
|
||||
if x % 5 == 0 {
|
||||
let _ = tx
|
||||
.try_send(
|
||||
Err(
|
||||
ServerFnError::Registration(
|
||||
"Error generated from client".to_string(),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
let _ = tx.try_send(Ok(msg));
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<ErrorBoundary fallback=|errors| {
|
||||
view! {
|
||||
<p>
|
||||
{move || {
|
||||
errors
|
||||
.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| format!("{e:?}"))
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
}}
|
||||
</p>
|
||||
}
|
||||
}>
|
||||
<p>{latest}</p>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
0
examples/websocket/style.css
Normal file
0
examples/websocket/style.css
Normal file
@@ -14,7 +14,7 @@ throw_error = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
futures = "0.3.31"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
wasm-bindgen = { version = "0.2.100", optional = true }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
js-sys = { version = "0.3.74", optional = true }
|
||||
once_cell = "1.20"
|
||||
pin-project-lite = "0.2.15"
|
||||
|
||||
@@ -1230,7 +1230,7 @@ fn static_path(options: &LeptosOptions, path: &str) -> String {
|
||||
// If the path ends with a trailing slash, we generate the path
|
||||
// as a directory with a index.html file inside.
|
||||
if path != "/" && path.ends_with("/") {
|
||||
static_file_path(options, &format!("{}index", path))
|
||||
static_file_path(options, &format!("{path}index"))
|
||||
} else {
|
||||
static_file_path(options, path)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Axum integrations for the Leptos web framework."
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
|
||||
@@ -1543,7 +1543,7 @@ fn static_path(options: &LeptosOptions, path: &str) -> String {
|
||||
// If the path ends with a trailing slash, we generate the path
|
||||
// as a directory with a index.html file inside.
|
||||
if path != "/" && path.ends_with("/") {
|
||||
static_file_path(options, &format!("{}index", path))
|
||||
static_file_path(options, &format!("{path}index"))
|
||||
} else {
|
||||
static_file_path(options, path)
|
||||
}
|
||||
|
||||
@@ -39,10 +39,10 @@ tachys = { workspace = true, features = [
|
||||
"reactive_stores",
|
||||
"oco",
|
||||
] }
|
||||
thiserror = "2.0"
|
||||
thiserror = { workspace = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
typed-builder = "0.20.0"
|
||||
typed-builder-macro = "0.20.0"
|
||||
typed-builder = { workspace = true }
|
||||
typed-builder-macro = "0.21.0"
|
||||
serde = "1.0"
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
server_fn = { workspace = true, features = ["form-redirects", "browser"] }
|
||||
@@ -52,7 +52,7 @@ web-sys = { version = "0.3.72", features = [
|
||||
"ShadowRootMode",
|
||||
] }
|
||||
wasm-bindgen = { workspace = true }
|
||||
serde_qs = "0.13.0"
|
||||
serde_qs = "0.14.0"
|
||||
slotmap = "1.0"
|
||||
futures = "0.3.31"
|
||||
send_wrapper = "0.6.0"
|
||||
@@ -99,6 +99,7 @@ trace-component-props = [
|
||||
"leptos_dom/trace-component-props",
|
||||
]
|
||||
delegation = ["tachys/delegation"]
|
||||
islands-router = ["tachys/mark_branches"]
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4.1"
|
||||
@@ -107,7 +108,7 @@ rustc_version = "0.4.1"
|
||||
# https://github.com/rust-lang/cargo/issues/4423
|
||||
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
|
||||
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
|
||||
# This is automatic as long as the leptos crate is depended upon,
|
||||
# This is automatic as long as the leptos crate is depended upon,
|
||||
# downstream usage should never manually enable this feature.
|
||||
[target.'cfg(erase_components)'.dependencies]
|
||||
leptos_macro = { workspace = true, features = ["__internal_erase_components"] }
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
use crate::{children::TypedChildren, IntoView};
|
||||
use futures::{channel::oneshot, future::join_all};
|
||||
use hydration_context::{SerializedDataId, SharedContext};
|
||||
use leptos_macro::component;
|
||||
use reactive_graph::{
|
||||
computed::ArcMemo,
|
||||
effect::RenderEffect,
|
||||
owner::{provide_context, Owner},
|
||||
owner::{provide_context, ArcStoredValue, Owner},
|
||||
signal::ArcRwSignal,
|
||||
traits::{Get, Update, With, WithUntracked},
|
||||
traits::{Get, Update, With, WithUntracked, WriteValue},
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
use std::{collections::VecDeque, fmt::Debug, mem, sync::Arc};
|
||||
use tachys::{
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
hydration::Cursor,
|
||||
reactive_graph::OwnedView,
|
||||
ssr::StreamBuilder,
|
||||
ssr::{StreamBuilder, StreamChunk},
|
||||
view::{
|
||||
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
|
||||
RenderHtml,
|
||||
@@ -96,10 +97,12 @@ where
|
||||
let hook = hook as Arc<dyn ErrorHook>;
|
||||
|
||||
let _guard = throw_error::set_error_hook(Arc::clone(&hook));
|
||||
let suspended_children = ErrorBoundarySuspendedChildren::default();
|
||||
|
||||
let owner = Owner::new();
|
||||
let children = owner.with(|| {
|
||||
provide_context(Arc::clone(&hook));
|
||||
provide_context(suspended_children.clone());
|
||||
children.into_inner()()
|
||||
});
|
||||
|
||||
@@ -111,11 +114,15 @@ where
|
||||
children,
|
||||
errors,
|
||||
fallback,
|
||||
suspended_children,
|
||||
},
|
||||
owner,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) type ErrorBoundarySuspendedChildren =
|
||||
ArcStoredValue<Vec<oneshot::Receiver<()>>>;
|
||||
|
||||
struct ErrorBoundaryView<Chil, FalFn> {
|
||||
hook: Arc<dyn ErrorHook>,
|
||||
boundary_id: SerializedDataId,
|
||||
@@ -123,6 +130,7 @@ struct ErrorBoundaryView<Chil, FalFn> {
|
||||
children: Chil,
|
||||
fallback: FalFn,
|
||||
errors: ArcRwSignal<Errors>,
|
||||
suspended_children: ErrorBoundarySuspendedChildren,
|
||||
}
|
||||
|
||||
struct ErrorBoundaryViewState<Chil, Fal> {
|
||||
@@ -257,6 +265,7 @@ where
|
||||
children,
|
||||
fallback,
|
||||
errors,
|
||||
suspended_children,
|
||||
} = self;
|
||||
ErrorBoundaryView {
|
||||
hook,
|
||||
@@ -265,6 +274,7 @@ where
|
||||
children: children.add_any_attr(attr.into_cloneable_owned()),
|
||||
fallback,
|
||||
errors,
|
||||
suspended_children,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,6 +302,7 @@ where
|
||||
children,
|
||||
fallback,
|
||||
errors,
|
||||
suspended_children,
|
||||
..
|
||||
} = self;
|
||||
ErrorBoundaryView {
|
||||
@@ -301,6 +312,7 @@ where
|
||||
children: children.resolve().await,
|
||||
fallback,
|
||||
errors,
|
||||
suspended_children,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +361,8 @@ where
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
let _hook = throw_error::set_error_hook(self.hook);
|
||||
let _hook = throw_error::set_error_hook(Arc::clone(&self.hook));
|
||||
|
||||
// first, attempt to serialize the children to HTML, then check for errors
|
||||
let mut new_buf = StreamBuilder::new(buf.clone_id());
|
||||
let mut new_pos = *position;
|
||||
@@ -361,20 +374,76 @@ where
|
||||
extra_attrs.clone(),
|
||||
);
|
||||
|
||||
// any thrown errors would've been caught here
|
||||
if self.errors.with_untracked(|map| map.is_empty()) {
|
||||
buf.append(new_buf);
|
||||
let suspense_children =
|
||||
mem::take(&mut *self.suspended_children.write_value());
|
||||
|
||||
// not waiting for any suspended children: just render
|
||||
if suspense_children.is_empty() {
|
||||
// any thrown errors would've been caught here
|
||||
if self.errors.with_untracked(|map| map.is_empty()) {
|
||||
buf.append(new_buf);
|
||||
} else {
|
||||
// otherwise, serialize the fallback instead
|
||||
let mut fallback = String::with_capacity(Fal::MIN_LENGTH);
|
||||
(self.fallback)(self.errors).to_html_with_buf(
|
||||
&mut fallback,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
buf.push_sync(&fallback);
|
||||
}
|
||||
} else {
|
||||
// otherwise, serialize the fallback instead
|
||||
let mut fallback = String::with_capacity(Fal::MIN_LENGTH);
|
||||
(self.fallback)(self.errors).to_html_with_buf(
|
||||
&mut fallback,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
buf.push_sync(&fallback);
|
||||
let mut position = *position;
|
||||
// if we're waiting for suspended children, we'll first wait for them to load
|
||||
// in this implementation, an ErrorBoundary that *contains* Suspense essentially acts
|
||||
// like a Suspense: it will wait for (all top-level) child Suspense to load before rendering anything
|
||||
let mut view_buf = StreamBuilder::new(new_buf.clone_id());
|
||||
view_buf.next_id();
|
||||
let hook = Arc::clone(&self.hook);
|
||||
view_buf.push_async(async move {
|
||||
let _hook = throw_error::set_error_hook(Arc::clone(&hook));
|
||||
let _ = join_all(suspense_children).await;
|
||||
|
||||
let mut my_chunks = VecDeque::new();
|
||||
for chunk in new_buf.take_chunks() {
|
||||
match chunk {
|
||||
StreamChunk::Sync(data) => {
|
||||
my_chunks.push_back(StreamChunk::Sync(data))
|
||||
}
|
||||
StreamChunk::Async { chunks } => {
|
||||
let chunks = chunks.await;
|
||||
my_chunks.extend(chunks);
|
||||
}
|
||||
StreamChunk::OutOfOrder { chunks } => {
|
||||
let chunks = chunks.await;
|
||||
my_chunks.push_back(StreamChunk::OutOfOrder {
|
||||
chunks: Box::pin(async move { chunks }),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.errors.with_untracked(|map| map.is_empty()) {
|
||||
// if no errors, just go ahead with the stream
|
||||
my_chunks
|
||||
} else {
|
||||
// otherwise, serialize the fallback instead
|
||||
let mut fallback = String::with_capacity(Fal::MIN_LENGTH);
|
||||
(self.fallback)(self.errors).to_html_with_buf(
|
||||
&mut fallback,
|
||||
&mut position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
my_chunks.clear();
|
||||
my_chunks.push_back(StreamChunk::Sync(fallback));
|
||||
my_chunks
|
||||
}
|
||||
});
|
||||
buf.append(view_buf);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
((root, pkg_path, output_name, wasm_output_name) => {
|
||||
let MOST_RECENT_CHILDREN_CB;
|
||||
let MOST_RECENT_CHILDREN_CB = [];
|
||||
|
||||
function idle(c) {
|
||||
if ("requestIdleCallback" in window) {
|
||||
@@ -22,12 +22,18 @@
|
||||
traverse(child, children);
|
||||
}
|
||||
} else {
|
||||
if(tag === 'leptos-children') {
|
||||
MOST_RECENT_CHILDREN_CB = node.$$on_hydrate;
|
||||
if (tag === 'leptos-children') {
|
||||
MOST_RECENT_CHILDREN_CB.push(node.$$on_hydrate);
|
||||
for(const child of node.children) {
|
||||
traverse(child);
|
||||
};
|
||||
// un-set the "most recent children"
|
||||
MOST_RECENT_CHILDREN_CB.pop();
|
||||
} else {
|
||||
for(const child of node.children) {
|
||||
traverse(child);
|
||||
};
|
||||
}
|
||||
for(const child of node.children) {
|
||||
traverse(child);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,8 +43,9 @@
|
||||
function hydrateIsland(el, id, mod) {
|
||||
const islandFn = mod[id];
|
||||
if (islandFn) {
|
||||
if (MOST_RECENT_CHILDREN_CB) {
|
||||
MOST_RECENT_CHILDREN_CB();
|
||||
const children_cb = MOST_RECENT_CHILDREN_CB[MOST_RECENT_CHILDREN_CB.length-1];
|
||||
if (children_cb) {
|
||||
children_cb();
|
||||
}
|
||||
islandFn(el);
|
||||
} else {
|
||||
|
||||
@@ -17,6 +17,10 @@ window.addEventListener("popstate", async (ev) => {
|
||||
});
|
||||
|
||||
window.addEventListener("submit", async (ev) => {
|
||||
if (ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
const req = submitToReq(ev);
|
||||
if(!req) {
|
||||
return;
|
||||
@@ -194,6 +198,15 @@ function diffRange(oldDocument, oldRoot, newDocument, newRoot, oldEnd, newEnd) {
|
||||
else if (oldNode.nodeType === Node.TEXT_NODE) {
|
||||
oldNode.textContent = newNode.textContent;
|
||||
}
|
||||
// islands should not be diffed on the client, because we do not want to overwrite client-side state
|
||||
// but their children should be diffed still, because they could contain new server content
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE && oldNode.tagName === "LEPTOS-ISLAND") {
|
||||
// TODO: diff the leptos-children
|
||||
|
||||
// skip over leptos-island otherwise
|
||||
oldDocWalker.nextSibling();
|
||||
newDocWalker.nextSibling();
|
||||
}
|
||||
// if it's an element, replace if it's a different tag, or update attributes
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
|
||||
diffElement(oldNode, newNode);
|
||||
|
||||
@@ -107,21 +107,19 @@ pub fn HydrationScripts(
|
||||
.unwrap_or_default();
|
||||
|
||||
let root = root.unwrap_or_default();
|
||||
use_context::<IslandsRouterNavigation>().is_none().then(|| {
|
||||
view! {
|
||||
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
|
||||
<link
|
||||
rel="preload"
|
||||
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
|
||||
r#as="fetch"
|
||||
r#type="application/wasm"
|
||||
crossorigin=nonce.clone().unwrap_or_default()
|
||||
/>
|
||||
<script type="module" nonce=nonce>
|
||||
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
|
||||
</script>
|
||||
}
|
||||
})
|
||||
view! {
|
||||
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
|
||||
<link
|
||||
rel="preload"
|
||||
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
|
||||
r#as="fetch"
|
||||
r#type="application/wasm"
|
||||
crossorigin=nonce.clone().unwrap_or_default()
|
||||
/>
|
||||
<script type="module" nonce=nonce>
|
||||
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
/// If this is provided via context, it means that you are using the islands router and
|
||||
|
||||
@@ -7,7 +7,7 @@ ws.onmessage = (ev) => {
|
||||
let found = false;
|
||||
document.querySelectorAll("link").forEach((link) => {
|
||||
if (link.getAttribute('href').includes(msg.css)) {
|
||||
let newHref = '/' + msg.css + '?version=' + new Date().getMilliseconds();
|
||||
let newHref = '/' + msg.css + '?version=' + Date.now();
|
||||
link.setAttribute('href', newHref);
|
||||
found = true;
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ pub mod prelude {
|
||||
pub use crate::{
|
||||
callback::*, children::*, component::*, control_flow::*, error::*,
|
||||
form::*, hydration::*, into_view::*, mount::*, suspense::*,
|
||||
text_prop::*,
|
||||
};
|
||||
pub use leptos_config::*;
|
||||
pub use leptos_dom::helpers::*;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::{
|
||||
children::{TypedChildren, ViewFnOnce},
|
||||
error::ErrorBoundarySuspendedChildren,
|
||||
IntoView,
|
||||
};
|
||||
use futures::{select, FutureExt};
|
||||
use futures::{channel::oneshot, select, FutureExt};
|
||||
use hydration_context::SerializedDataId;
|
||||
use leptos_macro::component;
|
||||
use reactive_graph::{
|
||||
@@ -13,7 +14,7 @@ use reactive_graph::{
|
||||
effect::RenderEffect,
|
||||
owner::{provide_context, use_context, Owner},
|
||||
signal::ArcRwSignal,
|
||||
traits::{Dispose, Get, Read, Track, With},
|
||||
traits::{Dispose, Get, Read, Track, With, WriteValue},
|
||||
};
|
||||
use slotmap::{DefaultKey, SlotMap};
|
||||
use std::sync::Arc;
|
||||
@@ -99,6 +100,8 @@ pub fn Suspense<Chil>(
|
||||
where
|
||||
Chil: IntoView + Send + 'static,
|
||||
{
|
||||
let error_boundary_parent = use_context::<ErrorBoundarySuspendedChildren>();
|
||||
|
||||
let owner = Owner::new();
|
||||
owner.with(|| {
|
||||
let (starts_local, id) = {
|
||||
@@ -129,6 +132,7 @@ where
|
||||
none_pending,
|
||||
fallback,
|
||||
children,
|
||||
error_boundary_parent,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -150,6 +154,7 @@ pub(crate) struct SuspenseBoundary<const TRANSITION: bool, Fal, Chil> {
|
||||
pub none_pending: ArcMemo<bool>,
|
||||
pub fallback: Fal,
|
||||
pub children: Chil,
|
||||
pub error_boundary_parent: Option<ErrorBoundarySuspendedChildren>,
|
||||
}
|
||||
|
||||
impl<const TRANSITION: bool, Fal, Chil> Render
|
||||
@@ -228,12 +233,14 @@ where
|
||||
none_pending,
|
||||
fallback,
|
||||
children,
|
||||
error_boundary_parent,
|
||||
} = self;
|
||||
SuspenseBoundary {
|
||||
id,
|
||||
none_pending,
|
||||
fallback,
|
||||
children: children.add_any_attr(attr),
|
||||
error_boundary_parent,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,6 +295,13 @@ where
|
||||
let suspense_context = use_context::<SuspenseContext>().unwrap();
|
||||
let owner = Owner::current().unwrap();
|
||||
|
||||
let mut notify_error_boundary =
|
||||
self.error_boundary_parent.map(|children| {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
children.write_value().push(rx);
|
||||
tx
|
||||
});
|
||||
|
||||
// we need to wait for one of two things: either
|
||||
// 1. all tasks are finished loading, or
|
||||
// 2. we read from a local resource, meaning this Suspense can never resolve on the server
|
||||
@@ -318,6 +332,9 @@ where
|
||||
// dropped, so it doesn't matter if we manage to send this.
|
||||
_ = tx.send(());
|
||||
}
|
||||
if let Some(tx) = notify_error_boundary.take() {
|
||||
_ = tx.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,6 +430,11 @@ where
|
||||
extra_attrs,
|
||||
);
|
||||
} else {
|
||||
// calling this will walk over the tree, removing all event listeners
|
||||
// and other single-threaded values from the view tree. this needs to be
|
||||
// done because the fallback can be shifted to another thread in push_async below.
|
||||
self.fallback.dry_resolve();
|
||||
|
||||
buf.push_async({
|
||||
let mut position = *position;
|
||||
async move {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
children::{TypedChildren, ViewFnOnce},
|
||||
error::ErrorBoundarySuspendedChildren,
|
||||
suspense_component::SuspenseBoundary,
|
||||
IntoView,
|
||||
};
|
||||
@@ -7,7 +8,7 @@ use leptos_macro::component;
|
||||
use reactive_graph::{
|
||||
computed::{suspense::SuspenseContext, ArcMemo},
|
||||
effect::Effect,
|
||||
owner::{provide_context, Owner},
|
||||
owner::{provide_context, use_context, Owner},
|
||||
signal::ArcRwSignal,
|
||||
traits::{Get, Set, Track, With},
|
||||
wrappers::write::SignalSetter,
|
||||
@@ -85,6 +86,8 @@ pub fn Transition<Chil>(
|
||||
where
|
||||
Chil: IntoView + Send + 'static,
|
||||
{
|
||||
let error_boundary_parent = use_context::<ErrorBoundarySuspendedChildren>();
|
||||
|
||||
let owner = Owner::new();
|
||||
owner.with(|| {
|
||||
let (starts_local, id) = {
|
||||
@@ -123,6 +126,7 @@ where
|
||||
none_pending,
|
||||
fallback,
|
||||
children,
|
||||
error_boundary_parent,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ config = { version = "0.15.8", default-features = false, features = [
|
||||
] }
|
||||
regex = "1.11"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
thiserror = "2.0"
|
||||
typed-builder = "0.20.0"
|
||||
thiserror = { workspace = true }
|
||||
typed-builder = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.43", features = ["rt", "macros"] }
|
||||
|
||||
@@ -25,7 +25,7 @@ syn = { version = "2.0", features = ["full"] }
|
||||
rstml = "0.12.0"
|
||||
leptos_hot_reload = { workspace = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
convert_case = "0.7"
|
||||
convert_case = { workspace = true }
|
||||
uuid = { version = "1.11", features = ["v4"] }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
|
||||
|
||||
@@ -359,23 +359,11 @@ impl ToTokens for Model {
|
||||
let component = if is_island {
|
||||
let hydrate_fn_name = hydrate_fn_name.as_ref().unwrap();
|
||||
quote! {
|
||||
{
|
||||
if ::leptos::context::use_context::<::leptos::reactive::owner::IsHydrating>()
|
||||
.map(|h| h.0)
|
||||
.unwrap_or(false) {
|
||||
::leptos::either::Either::Left(
|
||||
#component
|
||||
)
|
||||
} else {
|
||||
::leptos::either::Either::Right(
|
||||
::leptos::tachys::html::islands::Island::new(
|
||||
stringify!(#hydrate_fn_name),
|
||||
#component
|
||||
)
|
||||
#island_serialized_props
|
||||
)
|
||||
}
|
||||
}
|
||||
::leptos::tachys::html::islands::Island::new(
|
||||
stringify!(#hydrate_fn_name),
|
||||
#component
|
||||
)
|
||||
#island_serialized_props
|
||||
}
|
||||
} else {
|
||||
component
|
||||
@@ -645,7 +633,9 @@ impl Parse for DummyModel {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let mut attrs = input.call(Attribute::parse_outer)?;
|
||||
// Drop unknown attributes like #[deprecated]
|
||||
drain_filter(&mut attrs, |attr| !attr.path().is_ident("doc"));
|
||||
drain_filter(&mut attrs, |attr| {
|
||||
!(attr.path().is_ident("doc") || attr.path().is_ident("allow"))
|
||||
});
|
||||
|
||||
let vis: Visibility = input.parse()?;
|
||||
let mut sig: Signature = input.parse()?;
|
||||
@@ -939,6 +929,10 @@ impl UnknownAttrs {
|
||||
}
|
||||
}
|
||||
|
||||
if attr.path().is_ident("allow") {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((attr.into_token_stream(), attr.span()))
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
@@ -361,7 +361,7 @@ fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(all(debug_assertions, feature = "nightly", rustc_nightly))] {
|
||||
Some(leptos_hot_reload::span_to_stable_id(
|
||||
site.source_file().path(),
|
||||
site.file(),
|
||||
site.start().line()
|
||||
))
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_server"
|
||||
# TODO revert to { workspace = true } before 0.8.0 release
|
||||
# this is a hack because I missing bumping the hydration_context version number before publishing
|
||||
version = "0.8.0-rc1"
|
||||
version = { workspace = true }
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -28,7 +26,7 @@ send_wrapper = "0.6"
|
||||
# serialization formats
|
||||
serde = { version = "1.0" }
|
||||
js-sys = { version = "0.3.74", optional = true }
|
||||
wasm-bindgen = { version = "0.2.100", optional = true }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -300,6 +300,34 @@ impl<T> LocalResource<T> {
|
||||
pub fn refetch(&self) {
|
||||
self.refetch.try_update(|n| *n += 1);
|
||||
}
|
||||
|
||||
/// Synchronously, reactively reads the current value of the resource and applies the function
|
||||
/// `f` to its value if it is `Some(_)`.
|
||||
#[track_caller]
|
||||
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.data.try_with(|n| n.as_ref().map(f))?
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> LocalResource<Result<T, E>>
|
||||
where
|
||||
T: 'static,
|
||||
E: Clone + 'static,
|
||||
{
|
||||
/// Applies the given function when a resource that returns `Result<T, E>`
|
||||
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
|
||||
/// calls over the `Option<Result<_, _>>` returned by the resource.
|
||||
///
|
||||
/// This is useful when used with features like server functions, in conjunction
|
||||
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
|
||||
/// left to handle the `None` and `Err(_)` states.
|
||||
#[track_caller]
|
||||
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
|
||||
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoFuture for LocalResource<T>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
|
||||
@@ -242,23 +242,22 @@ impl ServerMetaContextOutput {
|
||||
let head_loc = first_chunk
|
||||
.find("</head>")
|
||||
.expect("you are using leptos_meta without a </head> tag");
|
||||
let marker_loc =
|
||||
first_chunk.find("<!--HEAD-->").unwrap_or_else(|| {
|
||||
let marker_loc = first_chunk
|
||||
.find("<!--HEAD-->")
|
||||
.map(|pos| pos + "<!--HEAD-->".len())
|
||||
.unwrap_or_else(|| {
|
||||
first_chunk.find("</head>").unwrap_or(head_loc)
|
||||
});
|
||||
let (before_marker, after_marker) =
|
||||
first_chunk.split_at_mut(marker_loc);
|
||||
let (before_head_close, after_head) =
|
||||
after_marker.split_at_mut(head_loc - marker_loc);
|
||||
buf.push_str(before_marker);
|
||||
buf.push_str(&meta_buf);
|
||||
if let Some(title) = title {
|
||||
buf.push_str("<title>");
|
||||
buf.push_str(&title);
|
||||
buf.push_str("</title>");
|
||||
}
|
||||
buf.push_str(before_head_close);
|
||||
buf.push_str(&meta_buf);
|
||||
buf.push_str(after_head);
|
||||
buf.push_str(after_marker);
|
||||
buf
|
||||
};
|
||||
|
||||
@@ -446,7 +445,7 @@ where
|
||||
tracing::warn!("{}", msg);
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!("{}", msg);
|
||||
eprintln!("{msg}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ use leptos::{
|
||||
/// Injects an [`HTMLLinkElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
|
||||
/// head that loads a stylesheet from the URL given by the `href` property.
|
||||
///
|
||||
/// Note that this does *not* work with the `cargo-leptos` `hash-files` feature: if you are using file
|
||||
/// hashing, you should use [`HashedStylesheet`](crate::HashedStylesheet).
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_meta::*;
|
||||
|
||||
@@ -10,7 +10,7 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0"
|
||||
thiserror = "2.0"
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.2.0-rc1"
|
||||
version = "0.2.2"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -18,7 +18,7 @@ pin-project-lite = "0.2.15"
|
||||
rustc-hash = "2.0"
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
slotmap = "1.0"
|
||||
thiserror = "2.0"
|
||||
thiserror = { workspace = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
guardian = "1.2"
|
||||
async-lock = "3.4.0"
|
||||
|
||||
@@ -560,7 +560,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
};
|
||||
let initial_value = SendOption::new_local(initial_value);
|
||||
let (this, _) = spawn_derived!(
|
||||
crate::spawn_local_scoped,
|
||||
crate::spawn_local,
|
||||
initial_value,
|
||||
fun,
|
||||
true,
|
||||
@@ -595,7 +595,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
async move { SendOption::new_local(Some(fut.await)) }
|
||||
};
|
||||
let (this, _) = spawn_derived!(
|
||||
crate::spawn_local_scoped,
|
||||
crate::spawn_local,
|
||||
initial,
|
||||
fun,
|
||||
false,
|
||||
|
||||
@@ -369,7 +369,7 @@ mod inner {
|
||||
const MSG: &str = "ImmediateEffect recursed more than once.";
|
||||
match effect.defined_at() {
|
||||
Some(defined_at) => {
|
||||
log_warning(format_args!("{MSG} Defined at: {}", defined_at));
|
||||
log_warning(format_args!("{MSG} Defined at: {defined_at}"));
|
||||
}
|
||||
None => {
|
||||
log_warning(format_args!("{MSG}"));
|
||||
|
||||
@@ -121,7 +121,7 @@ pub fn log_warning(text: Arguments) {
|
||||
not(all(target_arch = "wasm32", target_os = "unknown"))
|
||||
))]
|
||||
{
|
||||
eprintln!("{}", text);
|
||||
eprintln!("{text}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,15 @@ pub fn spawn(task: impl Future<Output = ()> + Send + 'static) {
|
||||
any_spawner::Executor::spawn_local(task);
|
||||
}
|
||||
|
||||
/// Calls [`Executor::spawn_local`](any_spawner::Executor::spawn_local), but ensures that the task also runs in the current arena, if
|
||||
/// multithreaded arena sandboxing is enabled.
|
||||
pub fn spawn_local(task: impl Future<Output = ()> + 'static) {
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
let task = owner::Sandboxed::new(task);
|
||||
|
||||
any_spawner::Executor::spawn_local(task);
|
||||
}
|
||||
|
||||
/// Calls [`Executor::spawn_local`](any_spawner::Executor), but ensures that the task runs under the current reactive [`Owner`](crate::owner::Owner) and observer.
|
||||
///
|
||||
/// Does not cancel the task if the owner is cleaned up.
|
||||
|
||||
@@ -236,11 +236,11 @@ pub fn provide_context<T: Send + Sync + 'static>(value: T) {
|
||||
///
|
||||
/// Effect::new(move |_| {
|
||||
/// // each use_context clones the value
|
||||
/// let value =
|
||||
/// use_context::<String>().expect("could not find i32 in context");
|
||||
/// let value = use_context::<String>()
|
||||
/// .expect("could not find String in context");
|
||||
/// assert_eq!(value, "foo");
|
||||
/// let value2 =
|
||||
/// use_context::<String>().expect("could not find i32 in context");
|
||||
/// let value2 = use_context::<String>()
|
||||
/// .expect("could not find String in context");
|
||||
/// assert_eq!(value2, "foo");
|
||||
/// });
|
||||
/// });
|
||||
@@ -284,11 +284,11 @@ pub fn use_context<T: Clone + 'static>() -> Option<T> {
|
||||
///
|
||||
/// Effect::new(move |_| {
|
||||
/// // each use_context clones the value
|
||||
/// let value =
|
||||
/// use_context::<String>().expect("could not find i32 in context");
|
||||
/// let value = use_context::<String>()
|
||||
/// .expect("could not find String in context");
|
||||
/// assert_eq!(value, "foo");
|
||||
/// let value2 =
|
||||
/// use_context::<String>().expect("could not find i32 in context");
|
||||
/// let value2 = use_context::<String>()
|
||||
/// .expect("could not find String in context");
|
||||
/// assert_eq!(value2, "foo");
|
||||
/// });
|
||||
/// });
|
||||
|
||||
@@ -181,10 +181,10 @@ impl<T: Debug> Debug for SendOption<T> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.inner {
|
||||
Inner::Threadsafe(value) => {
|
||||
write!(f, "SendOption::Threadsafe({:?})", value)
|
||||
write!(f, "SendOption::Threadsafe({value:?})")
|
||||
}
|
||||
Inner::Local(value) => {
|
||||
write!(f, "SendOption::Local({:?})", value)
|
||||
write!(f, "SendOption::Local({value:?})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ use std::{
|
||||
/// to more complex data structures. Instead, it allows you to provide a signal-like API for wrapped types
|
||||
/// without exposing the original type directly to users.
|
||||
pub struct ArcMappedSignal<T> {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
try_read_untracked: Arc<
|
||||
@@ -44,7 +44,7 @@ pub struct ArcMappedSignal<T> {
|
||||
impl<T> Clone for ArcMappedSignal<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: self.defined_at,
|
||||
try_read_untracked: self.try_read_untracked.clone(),
|
||||
try_write: self.try_write.clone(),
|
||||
@@ -67,7 +67,7 @@ impl<T> ArcMappedSignal<T> {
|
||||
U: Send + Sync + 'static,
|
||||
{
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
try_read_untracked: {
|
||||
let this = inner.clone();
|
||||
@@ -110,7 +110,7 @@ impl<T> ArcMappedSignal<T> {
|
||||
impl<T> Debug for ArcMappedSignal<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut partial = f.debug_struct("ArcMappedSignal");
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
partial.field("defined_at", &self.defined_at);
|
||||
partial.finish()
|
||||
}
|
||||
@@ -118,11 +118,11 @@ impl<T> Debug for ArcMappedSignal<T> {
|
||||
|
||||
impl<T> DefinedAt for ArcMappedSignal<T> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
|
||||
{
|
||||
None
|
||||
}
|
||||
@@ -228,7 +228,7 @@ where
|
||||
/// to more complex data structures. Instead, it allows you to provide a signal-like API for wrapped types
|
||||
/// without exposing the original type directly to users.
|
||||
pub struct MappedSignal<T, S = SyncStorage> {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
inner: StoredValue<ArcMappedSignal<T>, S>,
|
||||
}
|
||||
@@ -246,7 +246,7 @@ impl<T> MappedSignal<T> {
|
||||
U: Send + Sync + 'static,
|
||||
{
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
inner: {
|
||||
let this = ArcRwSignal::from(inner);
|
||||
@@ -269,7 +269,7 @@ impl<T> Clone for MappedSignal<T> {
|
||||
impl<T> Debug for MappedSignal<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut partial = f.debug_struct("MappedSignal");
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
partial.field("defined_at", &self.defined_at);
|
||||
partial.finish()
|
||||
}
|
||||
@@ -277,11 +277,11 @@ impl<T> Debug for MappedSignal<T> {
|
||||
|
||||
impl<T> DefinedAt for MappedSignal<T> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
|
||||
{
|
||||
None
|
||||
}
|
||||
@@ -352,7 +352,7 @@ where
|
||||
#[track_caller]
|
||||
fn from(value: ArcMappedSignal<T>) -> Self {
|
||||
MappedSignal {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
inner: StoredValue::new(value),
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ where
|
||||
#[track_caller]
|
||||
fn from(value: ArcWriteSignal<T>) -> Self {
|
||||
WriteSignal {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
inner: ArenaItem::new_with_storage(value),
|
||||
}
|
||||
@@ -132,7 +132,7 @@ where
|
||||
#[track_caller]
|
||||
fn from_local(value: ArcWriteSignal<T>) -> Self {
|
||||
WriteSignal {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
inner: ArenaItem::new_with_storage(value),
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ pub mod read {
|
||||
},
|
||||
signal::{
|
||||
guards::{Mapped, Plain, ReadGuard},
|
||||
ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal,
|
||||
ArcMappedSignal, ArcReadSignal, ArcRwSignal, MappedSignal,
|
||||
ReadSignal, RwSignal,
|
||||
},
|
||||
traits::{
|
||||
DefinedAt, Dispose, Get, Read, ReadUntracked, ReadValue, Track,
|
||||
@@ -807,6 +808,16 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<MappedSignal<T>> for Signal<T>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: MappedSignal<T>) -> Self {
|
||||
Self::derive(move || value.get())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RwSignal<T, LocalStorage>> for Signal<T, LocalStorage>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -839,6 +850,16 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ArcMappedSignal<T>> for Signal<T>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: ArcMappedSignal<T>) -> Self {
|
||||
MappedSignal::from(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ArcRwSignal<T>> for Signal<T, LocalStorage>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
@@ -1133,7 +1154,7 @@ pub mod read {
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[deprecated(
|
||||
since = "0.7.0-rc1",
|
||||
since = "0.7.0-rc3",
|
||||
note = "`MaybeSignal<T>` is deprecated in favour of `Signal<T>` which \
|
||||
is `Copy`, now has a more efficient From<T> implementation \
|
||||
and other benefits in 0.7."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_stores"
|
||||
version = "0.2.0-rc1"
|
||||
version = "0.2.2"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -17,6 +17,8 @@ paste = "1.0"
|
||||
reactive_graph = { workspace = true }
|
||||
rustc-hash = "2.0"
|
||||
reactive_stores_macro = { workspace = true }
|
||||
dashmap = "6.1"
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
@@ -28,14 +28,14 @@ where
|
||||
{
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
path: StorePath,
|
||||
trigger: StoreFieldTrigger,
|
||||
path: Arc<dyn Fn() -> StorePath + Send + Sync>,
|
||||
get_trigger: Arc<dyn Fn(StorePath) -> StoreFieldTrigger + Send + Sync>,
|
||||
read: Arc<dyn Fn() -> Option<StoreFieldReader<T>> + Send + Sync>,
|
||||
pub(crate) write:
|
||||
Arc<dyn Fn() -> Option<StoreFieldWriter<T>> + Send + Sync>,
|
||||
keys: Arc<dyn Fn() -> Option<KeyMap> + Send + Sync>,
|
||||
track_field: Arc<dyn Fn() + Send + Sync>,
|
||||
notify: Arc<dyn Fn() + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<T> Debug for ArcField<T>
|
||||
@@ -46,9 +46,7 @@ where
|
||||
let mut f = f.debug_struct("ArcField");
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
let f = f.field("defined_at", &self.defined_at);
|
||||
f.field("path", &self.path)
|
||||
.field("trigger", &self.trigger)
|
||||
.finish()
|
||||
f.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +104,7 @@ impl<T> StoreField for ArcField<T> {
|
||||
}
|
||||
|
||||
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
|
||||
self.path.clone()
|
||||
(self.path)()
|
||||
}
|
||||
|
||||
fn reader(&self) -> Option<Self::Reader> {
|
||||
@@ -132,13 +130,13 @@ where
|
||||
ArcField {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
path: value.path().into_iter().collect(),
|
||||
trigger: value.get_trigger(value.path().into_iter().collect()),
|
||||
path: Arc::new(move || value.path().into_iter().collect()),
|
||||
get_trigger: Arc::new(move |path| value.get_trigger(path)),
|
||||
read: Arc::new(move || value.reader().map(StoreFieldReader::new)),
|
||||
write: Arc::new(move || value.writer().map(StoreFieldWriter::new)),
|
||||
keys: Arc::new(move || value.keys()),
|
||||
track_field: Arc::new(move || value.track_field()),
|
||||
notify: Arc::new(move || value.notify()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,8 +150,10 @@ where
|
||||
ArcField {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
path: value.path().into_iter().collect(),
|
||||
trigger: value.get_trigger(value.path().into_iter().collect()),
|
||||
path: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -174,6 +174,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.track_field()
|
||||
}),
|
||||
notify: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.notify()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,8 +194,10 @@ where
|
||||
ArcField {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
path: value.path().into_iter().collect(),
|
||||
trigger: value.get_trigger(value.path().into_iter().collect()),
|
||||
path: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -212,6 +218,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.track_field()
|
||||
}),
|
||||
notify: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.notify()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,8 +237,10 @@ where
|
||||
ArcField {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
path: value.path().into_iter().collect(),
|
||||
trigger: value.get_trigger(value.path().into_iter().collect()),
|
||||
path: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -249,6 +261,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.track_field()
|
||||
}),
|
||||
notify: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.notify()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,8 +281,10 @@ where
|
||||
ArcField {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
path: value.path().into_iter().collect(),
|
||||
trigger: value.get_trigger(value.path().into_iter().collect()),
|
||||
path: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -287,6 +305,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.track_field()
|
||||
}),
|
||||
notify: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.notify()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,8 +329,10 @@ where
|
||||
ArcField {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
path: value.path().into_iter().collect(),
|
||||
trigger: value.get_trigger(value.path().into_iter().collect()),
|
||||
path: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.path().into_iter().collect()
|
||||
}),
|
||||
get_trigger: Arc::new({
|
||||
let value = value.clone();
|
||||
move |path| value.get_trigger(path)
|
||||
@@ -329,6 +353,10 @@ where
|
||||
let value = value.clone();
|
||||
move || value.track_field()
|
||||
}),
|
||||
notify: Arc::new({
|
||||
let value = value.clone();
|
||||
move || value.notify()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,12 +367,12 @@ impl<T> Clone for ArcField<T> {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: self.defined_at,
|
||||
path: self.path.clone(),
|
||||
trigger: self.trigger.clone(),
|
||||
get_trigger: Arc::clone(&self.get_trigger),
|
||||
read: Arc::clone(&self.read),
|
||||
write: Arc::clone(&self.write),
|
||||
keys: Arc::clone(&self.keys),
|
||||
track_field: Arc::clone(&self.track_field),
|
||||
notify: Arc::clone(&self.notify),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -364,7 +392,7 @@ impl<T> DefinedAt for ArcField<T> {
|
||||
|
||||
impl<T> Notify for ArcField<T> {
|
||||
fn notify(&self) {
|
||||
self.trigger.this.notify();
|
||||
(self.notify)()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ where
|
||||
#[track_caller]
|
||||
fn deref_field(self) -> DerefedField<Self> {
|
||||
DerefedField {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
inner: self,
|
||||
}
|
||||
@@ -51,7 +51,7 @@ where
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct DerefedField<S> {
|
||||
inner: S,
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
|
||||
@@ -92,11 +92,11 @@ where
|
||||
<S::Value as Deref>::Target: Sized + 'static,
|
||||
{
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
|
||||
{
|
||||
None
|
||||
}
|
||||
|
||||
@@ -56,12 +56,12 @@
|
||||
//!
|
||||
//! # if false { // don't run effect in doctests
|
||||
//! Effect::new(move |_| {
|
||||
//! // you can access individual store withs field a getter
|
||||
//! // you can access individual store fields with a getter
|
||||
//! println!("todos: {:?}", &*store.todos().read());
|
||||
//! });
|
||||
//! # }
|
||||
//!
|
||||
//! // won't notify the effect that listen to `todos`
|
||||
//! // won't notify the effect that listens to `todos`
|
||||
//! store.todos().write().push(Todo {
|
||||
//! label: "Test".to_string(),
|
||||
//! completed: false,
|
||||
@@ -69,7 +69,7 @@
|
||||
//! ```
|
||||
//! ### Generated traits
|
||||
//! The [`Store`](macro@Store) macro generates traits for each `struct` to which it is applied. When working
|
||||
//! within a single file more module, this is not an issue. However, when working with multiple modules
|
||||
//! within a single file or module, this is not an issue. However, when working with multiple modules
|
||||
//! or files, one needs to `use` the generated traits. The general pattern is that for each `struct`
|
||||
//! named `Foo`, the macro generates a trait named `FooStoreFields`. For example:
|
||||
//! ```rust
|
||||
@@ -239,7 +239,6 @@
|
||||
//! 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::{
|
||||
@@ -255,7 +254,6 @@ pub use reactive_stores_macro::{Patch, Store};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{
|
||||
any::Any,
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
hash::Hash,
|
||||
ops::DerefMut,
|
||||
@@ -345,7 +343,7 @@ where
|
||||
|
||||
Self {
|
||||
spare_keys: Vec::new(),
|
||||
current_key: 0,
|
||||
current_key: keys.len().saturating_sub(1),
|
||||
keys,
|
||||
}
|
||||
}
|
||||
@@ -408,9 +406,25 @@ 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>>>,
|
||||
>;
|
||||
|
||||
/// A map of the keys for a keyed subfield.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct KeyMap(Arc<RwLock<HashMap<StorePath, Box<dyn Any + Send + Sync>>>>);
|
||||
#[derive(Clone)]
|
||||
pub struct KeyMap(HashMap<StorePath, Box<dyn Any + Send + Sync>>);
|
||||
|
||||
impl Default for KeyMap {
|
||||
fn default() -> Self {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
return Self(Default::default());
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
return Self(send_wrapper::SendWrapper::new(Default::default()));
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyMap {
|
||||
fn with_field_keys<K, T>(
|
||||
@@ -422,26 +436,25 @@ impl KeyMap {
|
||||
where
|
||||
K: Debug + Hash + PartialEq + Eq + Send + Sync + 'static,
|
||||
{
|
||||
// this incredibly defensive mechanism takes the guard twice
|
||||
// on initialization. unfortunately, this is because `initialize`, on
|
||||
// a nested keyed field can, when being initialized), can in fact try
|
||||
// to take the lock again, as we try to insert the keys of the parent
|
||||
// while inserting the keys on this child.
|
||||
//
|
||||
// see here https://github.com/leptos-rs/leptos/issues/3086
|
||||
let mut guard = self.0.write().or_poisoned();
|
||||
if guard.contains_key(&path) {
|
||||
let entry = guard.get_mut(&path)?;
|
||||
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
|
||||
Some(fun(entry))
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut entry = self
|
||||
.0
|
||||
.entry(path)
|
||||
.or_insert_with(|| Box::new(FieldKeys::new(initialize())));
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let entry = if !self.0.borrow().contains_key(&path) {
|
||||
Some(Box::new(FieldKeys::new(initialize())))
|
||||
} else {
|
||||
drop(guard);
|
||||
let keys = Box::new(FieldKeys::new(initialize()));
|
||||
let mut guard = self.0.write().or_poisoned();
|
||||
let entry = guard.entry(path).or_insert(keys);
|
||||
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
|
||||
Some(fun(entry))
|
||||
}
|
||||
None
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let mut map = self.0.borrow_mut();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let entry = map.entry(path).or_insert_with(|| entry.unwrap());
|
||||
|
||||
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
|
||||
Some(fun(entry))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,6 +608,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> PartialEq for Store<T, S> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Eq for Store<T, S> {}
|
||||
|
||||
impl<T> Store<T, LocalStorage>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -746,7 +767,7 @@ where
|
||||
{
|
||||
fn from(value: ArcStore<T>) -> Self {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: value.defined_at,
|
||||
inner: ArenaItem::new_with_storage(value),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_stores_macro"
|
||||
version = "0.2.0-rc1"
|
||||
version = "0.2.2"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -13,7 +13,7 @@ edition.workspace = true
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
convert_case = "0.7"
|
||||
convert_case = { workspace = true }
|
||||
proc-macro-error2 = "2.0"
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
|
||||
@@ -79,7 +79,7 @@ impl Parse for Model {
|
||||
|
||||
#[derive(Clone)]
|
||||
enum SubfieldMode {
|
||||
Keyed(ExprClosure, Box<Type>),
|
||||
Keyed(Box<ExprClosure>, Box<Type>),
|
||||
Skip,
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ impl Parse for SubfieldMode {
|
||||
let ty: Type = input.parse()?;
|
||||
let _eq: Token![=] = input.parse()?;
|
||||
let closure: ExprClosure = input.parse()?;
|
||||
Ok(SubfieldMode::Keyed(closure, Box::new(ty)))
|
||||
Ok(SubfieldMode::Keyed(Box::new(closure), Box::new(ty)))
|
||||
} else if mode == "skip" {
|
||||
Ok(SubfieldMode::Skip)
|
||||
} else {
|
||||
@@ -403,7 +403,7 @@ fn variant_to_tokens(
|
||||
let field_ident = field.ident.as_ref().unwrap();
|
||||
let field_ty = &field.ty;
|
||||
let combined_ident = Ident::new(
|
||||
&format!("{}_{}", ident, field_ident),
|
||||
&format!("{ident}_{field_ident}"),
|
||||
field_ident.span(),
|
||||
);
|
||||
|
||||
@@ -481,7 +481,7 @@ fn variant_to_tokens(
|
||||
let field_ident = idx;
|
||||
let field_ty = &field.ty;
|
||||
let combined_ident = Ident::new(
|
||||
&format!("{}_{}", ident, field_ident),
|
||||
&format!("{ident}_{field_ident}"),
|
||||
ident.span(),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -24,7 +24,7 @@ wasm-bindgen = { workspace = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
once_cell = "1.20"
|
||||
send_wrapper = "0.6.0"
|
||||
thiserror = "2.0"
|
||||
thiserror = { workspace = true }
|
||||
percent-encoding = { version = "2.3", optional = true }
|
||||
gloo-net = "0.6.0"
|
||||
|
||||
|
||||
@@ -418,7 +418,7 @@ impl RenderHtml for MatchedRoute {
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
if mark_branches {
|
||||
if mark_branches && escape {
|
||||
buf.open_branch(&self.0);
|
||||
}
|
||||
self.1.to_html_with_buf(
|
||||
@@ -428,8 +428,11 @@ impl RenderHtml for MatchedRoute {
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
if mark_branches {
|
||||
if mark_branches && escape {
|
||||
buf.close_branch(&self.0);
|
||||
if *position == Position::NextChildAfterText {
|
||||
*position = Position::NextChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,7 +446,7 @@ impl RenderHtml for MatchedRoute {
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
if mark_branches {
|
||||
if mark_branches && escape {
|
||||
buf.open_branch(&self.0);
|
||||
}
|
||||
self.1.to_html_async_with_buf::<OUT_OF_ORDER>(
|
||||
@@ -453,8 +456,11 @@ impl RenderHtml for MatchedRoute {
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
if mark_branches {
|
||||
if mark_branches && escape {
|
||||
buf.close_branch(&self.0);
|
||||
if *position == Position::NextChildAfterText {
|
||||
*position = Position::NextChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router_macro"
|
||||
version = "0.8.0-rc1"
|
||||
version = "0.8.2"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -14,13 +14,13 @@ throw_error = { workspace = true }
|
||||
server_fn_macro_default = { workspace = true }
|
||||
# used for hashing paths in #[server] macro
|
||||
const_format = "0.2.33"
|
||||
const-str = "0.5.7"
|
||||
const-str = "0.6.2"
|
||||
xxhash-rust = { version = "0.8.12", features = ["const_xxh64"] }
|
||||
rustversion = { workspace = true}
|
||||
rustversion = { workspace = true }
|
||||
# used across multiple features
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
send_wrapper = { version = "0.6.0", features = ["futures"], optional = true }
|
||||
thiserror = "2.0"
|
||||
thiserror = { workspace = true }
|
||||
|
||||
# registration system
|
||||
inventory = { version = "0.3.15", optional = true }
|
||||
@@ -40,7 +40,7 @@ tower = { version = "0.5.1", optional = true }
|
||||
tower-layer = { version = "0.3.3", optional = true }
|
||||
|
||||
## input encodings
|
||||
serde_qs = { version = "0.13.0" }
|
||||
serde_qs = { version = "0.14.0" }
|
||||
multer = { version = "3.1", optional = true }
|
||||
|
||||
## output encodings
|
||||
@@ -61,7 +61,7 @@ base64 = { version = "0.22.1" }
|
||||
# client
|
||||
gloo-net = { version = "0.6.0", optional = true }
|
||||
js-sys = { version = "0.3.74", optional = true }
|
||||
wasm-bindgen = { version = "0.2.100", optional = true }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.50", optional = true }
|
||||
wasm-streams = { version = "0.4.2", optional = true }
|
||||
web-sys = { version = "0.3.72", optional = true, features = [
|
||||
@@ -82,6 +82,9 @@ url = "2"
|
||||
pin-project-lite = "0.2.15"
|
||||
tokio = { version = "1.43.0", features = ["rt"], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4.1"
|
||||
|
||||
[dev-dependencies]
|
||||
trybuild = { workspace = true }
|
||||
|
||||
@@ -238,4 +241,7 @@ skip_feature_sets = [
|
||||
max_combination_size = 2
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(leptos_debuginfo)',
|
||||
'cfg(rustc_nightly)',
|
||||
] }
|
||||
|
||||
8
server_fn/build.rs
Normal file
8
server_fn/build.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use rustc_version::{version_meta, Channel};
|
||||
|
||||
fn main() {
|
||||
// Set cfg flags depending on release channel
|
||||
if matches!(version_meta().unwrap().channel, Channel::Nightly) {
|
||||
println!("cargo:rustc-cfg=rustc_nightly");
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ pub trait Client<Error, InputStreamError = Error, OutputStreamError = Error> {
|
||||
Output = Result<
|
||||
(
|
||||
impl Stream<Item = Result<Bytes, Bytes>> + Send + 'static,
|
||||
impl Sink<Result<Bytes, Bytes>> + Send + 'static,
|
||||
impl Sink<Bytes> + Send + 'static,
|
||||
),
|
||||
Error,
|
||||
>,
|
||||
@@ -62,8 +62,8 @@ pub mod browser {
|
||||
response::browser::BrowserResponse,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures::{Sink, SinkExt, StreamExt, TryStreamExt};
|
||||
use gloo_net::websocket::{events::CloseEvent, Message, WebSocketError};
|
||||
use futures::{Sink, SinkExt, StreamExt};
|
||||
use gloo_net::websocket::{Message, WebSocketError};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::future::Future;
|
||||
|
||||
@@ -115,7 +115,7 @@ pub mod browser {
|
||||
impl futures::Stream<Item = Result<Bytes, Bytes>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
impl futures::Sink<Result<Bytes, Bytes>> + Send + 'static,
|
||||
impl futures::Sink<Bytes> + Send + 'static,
|
||||
),
|
||||
Error,
|
||||
>,
|
||||
@@ -131,18 +131,19 @@ pub mod browser {
|
||||
})?;
|
||||
let (sink, stream) = websocket.split();
|
||||
|
||||
let stream = stream
|
||||
.map_err(|err| {
|
||||
web_sys::console::error_1(&err.to_string().into());
|
||||
OutputStreamError::from_server_fn_error(
|
||||
ServerFnErrorErr::Request(err.to_string()),
|
||||
)
|
||||
.ser()
|
||||
})
|
||||
.map_ok(move |msg| match msg {
|
||||
let stream = stream.map(|message| match message {
|
||||
Ok(message) => Ok(match message {
|
||||
Message::Text(text) => Bytes::from(text),
|
||||
Message::Bytes(bytes) => Bytes::from(bytes),
|
||||
});
|
||||
}),
|
||||
Err(err) => {
|
||||
web_sys::console::error_1(&err.to_string().into());
|
||||
Err(OutputStreamError::from_server_fn_error(
|
||||
ServerFnErrorErr::Request(err.to_string()),
|
||||
)
|
||||
.ser())
|
||||
}
|
||||
});
|
||||
let stream = SendWrapper::new(stream);
|
||||
|
||||
struct SendWrapperSink<S> {
|
||||
@@ -195,26 +196,11 @@ pub mod browser {
|
||||
}
|
||||
}
|
||||
|
||||
let sink =
|
||||
sink.with(|message: Result<Bytes, Bytes>| async move {
|
||||
match message {
|
||||
Ok(message) => Ok(Message::Bytes(message.into())),
|
||||
Err(err) => {
|
||||
let err = InputStreamError::de(err);
|
||||
web_sys::console::error_1(
|
||||
&js_sys::JsString::from(err.to_string()),
|
||||
);
|
||||
const CLOSE_CODE_ERROR: u16 = 1011;
|
||||
Err(WebSocketError::ConnectionClose(
|
||||
CloseEvent {
|
||||
code: CLOSE_CODE_ERROR,
|
||||
reason: err.to_string(),
|
||||
was_clean: true,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
});
|
||||
let sink = sink.with(|message: Bytes| async move {
|
||||
Ok::<Message, WebSocketError>(Message::Bytes(
|
||||
message.into(),
|
||||
))
|
||||
});
|
||||
let sink = SendWrapperSink::new(Box::pin(sink));
|
||||
|
||||
Ok((stream, sink))
|
||||
@@ -243,13 +229,19 @@ pub mod reqwest {
|
||||
/// Implements [`Client`] for a request made by [`reqwest`].
|
||||
pub struct ReqwestClient;
|
||||
|
||||
impl<E: FromServerFnError + Send + 'static> Client<E> for ReqwestClient {
|
||||
impl<
|
||||
Error: FromServerFnError,
|
||||
InputStreamError: FromServerFnError,
|
||||
OutputStreamError: FromServerFnError,
|
||||
> Client<Error, InputStreamError, OutputStreamError> for ReqwestClient
|
||||
{
|
||||
type Request = Request;
|
||||
type Response = Response;
|
||||
|
||||
fn send(
|
||||
req: Self::Request,
|
||||
) -> impl Future<Output = Result<Self::Response, E>> + Send {
|
||||
) -> impl Future<Output = Result<Self::Response, Error>> + Send
|
||||
{
|
||||
CLIENT.execute(req).map_err(|e| {
|
||||
ServerFnErrorErr::Request(e.to_string()).into_app_error()
|
||||
})
|
||||
@@ -259,26 +251,24 @@ pub mod reqwest {
|
||||
path: &str,
|
||||
) -> Result<
|
||||
(
|
||||
impl futures::Stream<Item = Result<bytes::Bytes, Bytes>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
impl futures::Sink<Result<bytes::Bytes, Bytes>> + Send + 'static,
|
||||
impl futures::Stream<Item = Result<Bytes, Bytes>> + Send + 'static,
|
||||
impl futures::Sink<Bytes> + Send + 'static,
|
||||
),
|
||||
E,
|
||||
Error,
|
||||
> {
|
||||
let mut websocket_server_url = get_server_url().to_string();
|
||||
if let Some(postfix) = websocket_server_url.strip_prefix("http://")
|
||||
{
|
||||
websocket_server_url = format!("ws://{}", postfix);
|
||||
websocket_server_url = format!("ws://{postfix}");
|
||||
} else if let Some(postfix) =
|
||||
websocket_server_url.strip_prefix("https://")
|
||||
{
|
||||
websocket_server_url = format!("wss://{}", postfix);
|
||||
websocket_server_url = format!("wss://{postfix}");
|
||||
}
|
||||
let url = format!("{}{}", websocket_server_url, path);
|
||||
let url = format!("{websocket_server_url}{path}");
|
||||
let (ws_stream, _) =
|
||||
tokio_tungstenite::connect_async(url).await.map_err(|e| {
|
||||
E::from_server_fn_error(ServerFnErrorErr::Request(
|
||||
Error::from_server_fn_error(ServerFnErrorErr::Request(
|
||||
e.to_string(),
|
||||
))
|
||||
})?;
|
||||
@@ -288,25 +278,18 @@ pub mod reqwest {
|
||||
Ok((
|
||||
read.map(|msg| match msg {
|
||||
Ok(msg) => Ok(msg.into_data()),
|
||||
Err(e) => Err(E::from_server_fn_error(
|
||||
Err(e) => Err(OutputStreamError::from_server_fn_error(
|
||||
ServerFnErrorErr::Request(e.to_string()),
|
||||
)
|
||||
.ser()),
|
||||
}),
|
||||
write.with(|msg: Result<Bytes, Bytes>| async move {
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
Ok(tokio_tungstenite::tungstenite::Message::Binary(
|
||||
msg,
|
||||
))
|
||||
}
|
||||
Err(err) => {
|
||||
let err = E::de(err);
|
||||
Err(tokio_tungstenite::tungstenite::Error::Io(
|
||||
std::io::Error::other(err.to_string()),
|
||||
))
|
||||
}
|
||||
}
|
||||
write.with(|msg: Bytes| async move {
|
||||
Ok::<
|
||||
tokio_tungstenite::tungstenite::Message,
|
||||
tokio_tungstenite::tungstenite::Error,
|
||||
>(
|
||||
tokio_tungstenite::tungstenite::Message::Binary(msg)
|
||||
)
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
ContentType, IntoRes, ServerFnError,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures::{Stream, StreamExt};
|
||||
use futures::{Stream, StreamExt, TryStreamExt};
|
||||
use http::Method;
|
||||
use std::{fmt::Debug, pin::Pin};
|
||||
|
||||
@@ -35,7 +35,8 @@ impl Encoding for Streaming {
|
||||
impl<E, T, Request> IntoReq<Streaming, Request, E> for T
|
||||
where
|
||||
Request: ClientReq<E>,
|
||||
T: Stream<Item = Bytes> + Send + Sync + 'static,
|
||||
T: Stream<Item = Bytes> + Send + 'static,
|
||||
E: FromServerFnError,
|
||||
{
|
||||
fn into_req(self, path: &str, accepts: &str) -> Result<Request, E> {
|
||||
Request::try_new_post_streaming(
|
||||
@@ -50,11 +51,12 @@ where
|
||||
impl<E, T, Request> FromReq<Streaming, Request, E> for T
|
||||
where
|
||||
Request: Req<E> + Send + 'static,
|
||||
T: From<ByteStream> + 'static,
|
||||
T: From<ByteStream<E>> + 'static,
|
||||
E: FromServerFnError,
|
||||
{
|
||||
async fn from_req(req: Request) -> Result<Self, E> {
|
||||
let data = req.try_into_stream()?;
|
||||
let s = ByteStream::new(data);
|
||||
let s = ByteStream::new(data.map_err(|e| E::de(e)));
|
||||
Ok(s.into())
|
||||
}
|
||||
}
|
||||
@@ -71,37 +73,36 @@ where
|
||||
/// end before the output will begin.
|
||||
///
|
||||
/// Streaming requests are only allowed over HTTP2 or HTTP3.
|
||||
pub struct ByteStream(Pin<Box<dyn Stream<Item = Result<Bytes, Bytes>> + Send>>);
|
||||
pub struct ByteStream<E = ServerFnError>(
|
||||
Pin<Box<dyn Stream<Item = Result<Bytes, E>> + Send>>,
|
||||
);
|
||||
|
||||
impl ByteStream {
|
||||
impl<E> ByteStream<E> {
|
||||
/// Consumes the wrapper, returning a stream of bytes.
|
||||
pub fn into_inner(self) -> impl Stream<Item = Result<Bytes, Bytes>> + Send {
|
||||
pub fn into_inner(self) -> impl Stream<Item = Result<Bytes, E>> + Send {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for ByteStream {
|
||||
impl<E> Debug for ByteStream<E> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("ByteStream").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ByteStream {
|
||||
impl<E> ByteStream<E> {
|
||||
/// Creates a new `ByteStream` from the given stream.
|
||||
pub fn new<T, E>(
|
||||
pub fn new<T>(
|
||||
value: impl Stream<Item = Result<T, E>> + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
T: Into<Bytes>,
|
||||
E: Into<Bytes>,
|
||||
{
|
||||
Self(Box::pin(
|
||||
value.map(|value| value.map(Into::into).map_err(Into::into)),
|
||||
))
|
||||
Self(Box::pin(value.map(|value| value.map(Into::into))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> From<S> for ByteStream
|
||||
impl<E, S, T> From<S> for ByteStream<E>
|
||||
where
|
||||
S: Stream<Item = T> + Send + 'static,
|
||||
T: Into<Bytes>,
|
||||
@@ -111,23 +112,27 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, Response> IntoRes<Streaming, Response, E> for ByteStream
|
||||
impl<E, Response> IntoRes<Streaming, Response, E> for ByteStream<E>
|
||||
where
|
||||
Response: TryRes<E>,
|
||||
E: 'static,
|
||||
E: FromServerFnError,
|
||||
{
|
||||
async fn into_res(self) -> Result<Response, E> {
|
||||
Response::try_from_stream(Streaming::CONTENT_TYPE, self.into_inner())
|
||||
Response::try_from_stream(
|
||||
Streaming::CONTENT_TYPE,
|
||||
self.into_inner().map_err(|e| e.ser()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, Response> FromRes<Streaming, Response, E> for ByteStream
|
||||
impl<E, Response> FromRes<Streaming, Response, E> for ByteStream<E>
|
||||
where
|
||||
Response: ClientRes<E> + Send,
|
||||
E: FromServerFnError,
|
||||
{
|
||||
async fn from_res(res: Response) -> Result<Self, E> {
|
||||
let stream = res.try_into_stream()?;
|
||||
Ok(ByteStream(Box::pin(stream)))
|
||||
Ok(ByteStream::new(stream.map_err(|e| E::de(e))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,14 +174,14 @@ pub struct TextStream<E = ServerFnError>(
|
||||
Pin<Box<dyn Stream<Item = Result<String, E>> + Send>>,
|
||||
);
|
||||
|
||||
impl<E: FromServerFnError> Debug for TextStream<E> {
|
||||
impl<E> Debug for TextStream<E> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("TextStream").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: FromServerFnError> TextStream<E> {
|
||||
/// Creates a new `ByteStream` from the given stream.
|
||||
impl<E> TextStream<E> {
|
||||
/// Creates a new `TextStream` from the given stream.
|
||||
pub fn new(
|
||||
value: impl Stream<Item = Result<String, E>> + Send + 'static,
|
||||
) -> Self {
|
||||
@@ -184,7 +189,7 @@ impl<E: FromServerFnError> TextStream<E> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: FromServerFnError> TextStream<E> {
|
||||
impl<E> TextStream<E> {
|
||||
/// Consumes the wrapper, returning a stream of text.
|
||||
pub fn into_inner(self) -> impl Stream<Item = Result<String, E>> + Send {
|
||||
self.0
|
||||
@@ -195,7 +200,6 @@ impl<E, S, T> From<S> for TextStream<E>
|
||||
where
|
||||
S: Stream<Item = T> + Send + 'static,
|
||||
T: Into<String>,
|
||||
E: FromServerFnError,
|
||||
{
|
||||
fn from(value: S) -> Self {
|
||||
Self(Box::pin(value.map(|data| Ok(data.into()))))
|
||||
|
||||
@@ -311,7 +311,7 @@ where
|
||||
|
||||
fn decode(bytes: Bytes) -> Result<ServerFnError<CustErr>, Self::Error> {
|
||||
let data = String::from_utf8(bytes.to_vec())
|
||||
.map_err(|err| format!("UTF-8 conversion error: {}", err))?;
|
||||
.map_err(|err| format!("UTF-8 conversion error: {err}"))?;
|
||||
|
||||
data.split_once('|')
|
||||
.ok_or_else(|| {
|
||||
@@ -561,9 +561,7 @@ impl<E: FromServerFnError> FromStr for ServerFnErrorWrapper<E> {
|
||||
}
|
||||
|
||||
/// A trait for types that can be returned from a server function.
|
||||
pub trait FromServerFnError:
|
||||
std::fmt::Debug + Sized + Display + 'static
|
||||
{
|
||||
pub trait FromServerFnError: std::fmt::Debug + Sized + 'static {
|
||||
/// The encoding strategy used to serialize and deserialize this error type. Must implement the [`Encodes`](server_fn::Encodes) trait for references to the error type.
|
||||
type Encoder: Encodes<Self> + Decodes<Self>;
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ use base64::{engine::general_purpose::STANDARD_NO_PAD, DecodeError, Engine};
|
||||
// re-exported to make it possible to implement a custom Client without adding a separate
|
||||
// dependency on `bytes`
|
||||
pub use bytes::Bytes;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use client::Client;
|
||||
use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
|
||||
#[doc(hidden)]
|
||||
@@ -635,15 +636,19 @@ where
|
||||
{
|
||||
let (request_bytes, response_stream, response) =
|
||||
request.try_into_websocket().await?;
|
||||
let input = request_bytes.map(|request_bytes| match request_bytes {
|
||||
Ok(request_bytes) => {
|
||||
InputEncoding::decode(request_bytes).map_err(|e| {
|
||||
InputStreamError::from_server_fn_error(
|
||||
ServerFnErrorErr::Deserialization(e.to_string()),
|
||||
)
|
||||
})
|
||||
let input = request_bytes.map(|request_bytes| {
|
||||
let request_bytes = request_bytes
|
||||
.map(|bytes| deserialize_result::<InputStreamError>(bytes))
|
||||
.unwrap_or_else(Err);
|
||||
match request_bytes {
|
||||
Ok(request_bytes) => InputEncoding::decode(request_bytes)
|
||||
.map_err(|e| {
|
||||
InputStreamError::from_server_fn_error(
|
||||
ServerFnErrorErr::Deserialization(e.to_string()),
|
||||
)
|
||||
}),
|
||||
Err(err) => Err(InputStreamError::de(err)),
|
||||
}
|
||||
Err(err) => Err(InputStreamError::de(err)),
|
||||
});
|
||||
let boxed = Box::pin(input)
|
||||
as Pin<
|
||||
@@ -656,14 +661,17 @@ where
|
||||
|
||||
let output = server_fn(input.into()).await?;
|
||||
|
||||
let output = output.stream.map(|output| match output {
|
||||
Ok(output) => OutputEncoding::encode(&output).map_err(|e| {
|
||||
OutputStreamError::from_server_fn_error(
|
||||
ServerFnErrorErr::Serialization(e.to_string()),
|
||||
)
|
||||
.ser()
|
||||
}),
|
||||
Err(err) => Err(err.ser()),
|
||||
let output = output.stream.map(|output| {
|
||||
let result = match output {
|
||||
Ok(output) => OutputEncoding::encode(&output).map_err(|e| {
|
||||
OutputStreamError::from_server_fn_error(
|
||||
ServerFnErrorErr::Serialization(e.to_string()),
|
||||
)
|
||||
.ser()
|
||||
}),
|
||||
Err(err) => Err(err.ser()),
|
||||
};
|
||||
serialize_result(result)
|
||||
});
|
||||
|
||||
Server::spawn(async move {
|
||||
@@ -695,37 +703,42 @@ where
|
||||
pin_mut!(input);
|
||||
pin_mut!(sink);
|
||||
while let Some(input) = input.stream.next().await {
|
||||
if sink
|
||||
.send(
|
||||
input
|
||||
.and_then(|input| {
|
||||
InputEncoding::encode(&input).map_err(|e| {
|
||||
InputStreamError::from_server_fn_error(
|
||||
ServerFnErrorErr::Serialization(
|
||||
e.to_string(),
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.ser()),
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
let result = match input {
|
||||
Ok(input) => {
|
||||
InputEncoding::encode(&input).map_err(|e| {
|
||||
InputStreamError::from_server_fn_error(
|
||||
ServerFnErrorErr::Serialization(
|
||||
e.to_string(),
|
||||
),
|
||||
)
|
||||
.ser()
|
||||
})
|
||||
}
|
||||
Err(err) => Err(err.ser()),
|
||||
};
|
||||
let result = serialize_result(result);
|
||||
if sink.send(result).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Return the output stream
|
||||
let stream = stream.map(|request_bytes| match request_bytes {
|
||||
Ok(request_bytes) => OutputEncoding::decode(request_bytes)
|
||||
.map_err(|e| {
|
||||
OutputStreamError::from_server_fn_error(
|
||||
ServerFnErrorErr::Deserialization(e.to_string()),
|
||||
)
|
||||
}),
|
||||
Err(err) => Err(OutputStreamError::de(err)),
|
||||
let stream = stream.map(|request_bytes| {
|
||||
let request_bytes = request_bytes
|
||||
.map(|bytes| deserialize_result::<OutputStreamError>(bytes))
|
||||
.unwrap_or_else(Err);
|
||||
match request_bytes {
|
||||
Ok(request_bytes) => OutputEncoding::decode(request_bytes)
|
||||
.map_err(|e| {
|
||||
OutputStreamError::from_server_fn_error(
|
||||
ServerFnErrorErr::Deserialization(
|
||||
e.to_string(),
|
||||
),
|
||||
)
|
||||
}),
|
||||
Err(err) => Err(OutputStreamError::de(err)),
|
||||
}
|
||||
});
|
||||
let boxed = Box::pin(stream)
|
||||
as Pin<
|
||||
@@ -740,6 +753,51 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// Serializes a Result<Bytes, Bytes> into a single Bytes instance.
|
||||
// Format: [tag: u8][content: Bytes]
|
||||
// - Tag 0: Ok variant
|
||||
// - Tag 1: Err variant
|
||||
fn serialize_result(result: Result<Bytes, Bytes>) -> Bytes {
|
||||
match result {
|
||||
Ok(bytes) => {
|
||||
let mut buf = BytesMut::with_capacity(1 + bytes.len());
|
||||
buf.put_u8(0); // Tag for Ok variant
|
||||
buf.extend_from_slice(&bytes);
|
||||
buf.freeze()
|
||||
}
|
||||
Err(bytes) => {
|
||||
let mut buf = BytesMut::with_capacity(1 + bytes.len());
|
||||
buf.put_u8(1); // Tag for Err variant
|
||||
buf.extend_from_slice(&bytes);
|
||||
buf.freeze()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deserializes a Bytes instance back into a Result<Bytes, Bytes>.
|
||||
fn deserialize_result<E: FromServerFnError>(
|
||||
bytes: Bytes,
|
||||
) -> Result<Bytes, Bytes> {
|
||||
if bytes.is_empty() {
|
||||
return Err(E::from_server_fn_error(
|
||||
ServerFnErrorErr::Deserialization("Data is empty".into()),
|
||||
)
|
||||
.ser());
|
||||
}
|
||||
|
||||
let tag = bytes[0];
|
||||
let content = bytes.slice(1..);
|
||||
|
||||
match tag {
|
||||
0 => Ok(content),
|
||||
1 => Err(content),
|
||||
_ => Err(E::from_server_fn_error(ServerFnErrorErr::Deserialization(
|
||||
"Invalid data tag".into(),
|
||||
))
|
||||
.ser()), // Invalid tag
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode format type
|
||||
pub enum Format {
|
||||
/// Binary representation
|
||||
@@ -1218,3 +1276,45 @@ pub mod mock {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::codec::JsonEncoding;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum TestError {
|
||||
ServerFnError(ServerFnErrorErr),
|
||||
}
|
||||
|
||||
impl FromServerFnError for TestError {
|
||||
type Encoder = JsonEncoding;
|
||||
|
||||
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
|
||||
Self::ServerFnError(value)
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_result_serialization() {
|
||||
// Test Ok variant
|
||||
let ok_result: Result<Bytes, Bytes> =
|
||||
Ok(Bytes::from_static(b"success data"));
|
||||
let serialized = serialize_result(ok_result);
|
||||
let deserialized = deserialize_result::<TestError>(serialized);
|
||||
assert!(deserialized.is_ok());
|
||||
assert_eq!(deserialized.unwrap(), Bytes::from_static(b"success data"));
|
||||
|
||||
// Test Err variant
|
||||
let err_result: Result<Bytes, Bytes> =
|
||||
Err(Bytes::from_static(b"error details"));
|
||||
let serialized = serialize_result(err_result);
|
||||
let deserialized = deserialize_result::<TestError>(serialized);
|
||||
assert!(deserialized.is_err());
|
||||
assert_eq!(
|
||||
deserialized.unwrap_err(),
|
||||
Bytes::from_static(b"error details")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ where
|
||||
) -> Result<
|
||||
(
|
||||
impl Stream<Item = Result<Bytes, Bytes>> + Send + 'static,
|
||||
impl futures::Sink<Result<Bytes, Bytes>> + Send + 'static,
|
||||
impl futures::Sink<Bytes> + Send + 'static,
|
||||
Self::WebsocketResponse,
|
||||
),
|
||||
Error,
|
||||
@@ -133,7 +133,7 @@ where
|
||||
let (mut response_stream_tx, response_stream_rx) =
|
||||
futures::channel::mpsc::channel(2048);
|
||||
let (response_sink_tx, mut response_sink_rx) =
|
||||
futures::channel::mpsc::channel::<Result<Bytes, Bytes>>(2048);
|
||||
futures::channel::mpsc::channel::<Bytes>(2048);
|
||||
|
||||
actix_web::rt::spawn(async move {
|
||||
loop {
|
||||
@@ -142,16 +142,9 @@ where
|
||||
let Some(incoming) = incoming else {
|
||||
break;
|
||||
};
|
||||
match incoming {
|
||||
Ok(message) => {
|
||||
if let Err(err) = session.binary(message).await {
|
||||
if let Err(err) = session.binary(incoming).await {
|
||||
_ = response_stream_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())).ser()));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
_ = response_stream_tx.start_send(Err(err));
|
||||
}
|
||||
}
|
||||
},
|
||||
outgoing = msg_stream.next().fuse() => {
|
||||
let Some(outgoing) = outgoing else {
|
||||
@@ -172,6 +165,9 @@ where
|
||||
Ok(Message::Text(text)) => {
|
||||
_ = response_stream_tx.start_send(Ok(text.into_bytes()));
|
||||
}
|
||||
Ok(Message::Close(_)) => {
|
||||
break;
|
||||
}
|
||||
Ok(_other) => {
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@@ -79,7 +79,7 @@ where
|
||||
) -> Result<
|
||||
(
|
||||
impl Stream<Item = Result<Bytes, Bytes>> + Send + 'static,
|
||||
impl Sink<Result<Bytes, Bytes>> + Send + 'static,
|
||||
impl Sink<Bytes> + Send + 'static,
|
||||
Self::WebsocketResponse,
|
||||
),
|
||||
Error,
|
||||
@@ -91,7 +91,7 @@ where
|
||||
futures::stream::Once<
|
||||
std::future::Ready<Result<Bytes, Bytes>>,
|
||||
>,
|
||||
futures::sink::Drain<Result<Bytes, Bytes>>,
|
||||
futures::sink::Drain<Bytes>,
|
||||
Self::WebsocketResponse,
|
||||
),
|
||||
Error,
|
||||
@@ -117,9 +117,9 @@ where
|
||||
))
|
||||
})?;
|
||||
let (mut outgoing_tx, outgoing_rx) =
|
||||
futures::channel::mpsc::channel(2048);
|
||||
let (incoming_tx, mut incoming_rx) =
|
||||
futures::channel::mpsc::channel::<Result<Bytes, Bytes>>(2048);
|
||||
let (incoming_tx, mut incoming_rx) =
|
||||
futures::channel::mpsc::channel::<Bytes>(2048);
|
||||
let response = upgrade
|
||||
.on_failed_upgrade({
|
||||
let mut outgoing_tx = outgoing_tx.clone();
|
||||
@@ -134,18 +134,11 @@ where
|
||||
let Some(incoming) = incoming else {
|
||||
break;
|
||||
};
|
||||
match incoming {
|
||||
Ok(message) => {
|
||||
if let Err(err) = session.send(Message::Binary(message)).await {
|
||||
_ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())).ser()));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
_ = outgoing_tx.start_send(Err(err));
|
||||
}
|
||||
if let Err(err) = session.send(Message::Binary(incoming)).await {
|
||||
_ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Request(err.to_string())).ser()));
|
||||
}
|
||||
},
|
||||
outgoing = session.recv().fuse() => {
|
||||
outgoing = session.recv().fuse() => {
|
||||
let Some(outgoing) = outgoing else {
|
||||
break;
|
||||
};
|
||||
@@ -159,6 +152,11 @@ where
|
||||
Ok(Message::Text(text)) => {
|
||||
_ = outgoing_tx.start_send(Ok(Bytes::from(text)));
|
||||
}
|
||||
Ok(Message::Ping(bytes)) => {
|
||||
if session.send(Message::Pong(bytes)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_other) => {}
|
||||
Err(e) => {
|
||||
_ = outgoing_tx.start_send(Err(InputStreamError::from_server_fn_error(ServerFnErrorErr::Response(e.to_string())).ser()));
|
||||
|
||||
@@ -79,7 +79,7 @@ where
|
||||
) -> Result<
|
||||
(
|
||||
impl Stream<Item = Result<Bytes, Bytes>> + Send + 'static,
|
||||
impl Sink<Result<Bytes, Bytes>> + Send + 'static,
|
||||
impl Sink<Bytes> + Send + 'static,
|
||||
Self::WebsocketResponse,
|
||||
),
|
||||
Error,
|
||||
@@ -87,7 +87,7 @@ where
|
||||
Err::<
|
||||
(
|
||||
futures::stream::Once<std::future::Ready<Result<Bytes, Bytes>>>,
|
||||
futures::sink::Drain<Result<Bytes, Bytes>>,
|
||||
futures::sink::Drain<Bytes>,
|
||||
Self::WebsocketResponse,
|
||||
),
|
||||
_,
|
||||
|
||||
@@ -360,7 +360,7 @@ where
|
||||
Output = Result<
|
||||
(
|
||||
impl Stream<Item = Result<Bytes, Bytes>> + Send + 'static,
|
||||
impl Sink<Result<Bytes, Bytes>> + Send + 'static,
|
||||
impl Sink<Bytes> + Send + 'static,
|
||||
Self::WebsocketResponse,
|
||||
),
|
||||
Error,
|
||||
@@ -415,7 +415,7 @@ where
|
||||
) -> Result<
|
||||
(
|
||||
impl Stream<Item = Result<Bytes, Bytes>> + Send + 'static,
|
||||
impl Sink<Result<Bytes, Bytes>> + Send + 'static,
|
||||
impl Sink<Bytes> + Send + 'static,
|
||||
Self::WebsocketResponse,
|
||||
),
|
||||
Error,
|
||||
@@ -424,7 +424,7 @@ where
|
||||
Err::<
|
||||
(
|
||||
futures::stream::Once<std::future::Ready<Result<Bytes, Bytes>>>,
|
||||
futures::sink::Drain<Result<Bytes, Bytes>>,
|
||||
futures::sink::Drain<Bytes>,
|
||||
Self::WebsocketResponse,
|
||||
),
|
||||
_,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user