Compare commits

...

113 Commits

Author SHA1 Message Date
Greg Johnston
3d733b54b1 missed one 2025-02-13 14:39:19 -05:00
Greg Johnston
8284f303d5 pause AsyncDerived (resources) as well 2025-02-13 14:22:14 -05:00
Greg Johnston
1dab980b02 fix: don't re-run memos if the effects that read them are paused 2025-02-13 14:22:14 -05:00
autofix-ci[bot]
4612125e5e [autofix.ci] apply automated fixes 2025-02-13 01:06:28 +00:00
Greg Johnston
53853ab105 feat: allow pausing and resuming effects 2025-02-12 19:56:11 -05:00
Greg Johnston
7be6a9da86 v0.7.7 2025-02-11 20:29:31 -05:00
Greg Johnston
c51e07b5a4 v0.7.6 (#3586) 2025-02-11 20:23:57 -05:00
jasper
b17a4c92c7 fix: allow non static lifetimes in component macro (#3571) 2025-02-10 20:51:15 -05:00
Alexis Fontaine
03f9c6cb6d fix: add <fieldset> attributes (#3581) 2025-02-10 15:19:00 -08:00
Greg Johnston
9e8b8886da feat: add :capture flag for events to handle them during capture phase (closes #3457) (#3575) 2025-02-10 08:26:00 -05:00
Danik Vitek
6a6b3dee15 fix(reactive_stores_macro): store attribute signature error message (#3567) 2025-02-09 14:09:43 -05:00
Alexis Fontaine
5d71913523 Handle erase_components on Either<A, B> (#3572) 2025-02-09 10:50:20 -08:00
mahdi739
706617ab0a fix: panic at fewer places in stores (#3551) 2025-02-07 14:45:12 -05:00
mahdi739
cd64bb9d67 fix: return empty set of keys instead of panicking when the KeyedSubfield is disposed (#3550) 2025-02-07 14:43:33 -05:00
Greg Johnston
f881c1877d fix: do not hold lock on arena when dispatching Action (#3561) 2025-02-07 14:26:58 -05:00
Greg Johnston
8783f6a478 fix: don't use InertElement for style: etc. (closes #3554) (#3558) 2025-02-06 21:26:15 -05:00
Greg Johnston
2add714c65 chore(ci): cargo install --locked for cargo-leptos installation (#3559) 2025-02-06 21:22:37 -05:00
Alexis Fontaine
88b1b2d882 feat: implement Attribute for Either<A, B> (#3556) 2025-02-06 21:21:53 -05:00
zakstucke
9d3a743d33 Fix ci (#3557) 2025-02-06 16:17:31 -08:00
Greg Johnston
c6de7c714e fix: emit syntax errors in components rather than swallowing them (closes #3535) (#3538) 2025-02-01 15:29:40 -05:00
Greg Johnston
6154199850 fix: attribute type erasure nightly (closes #3536) (#3537) 2025-02-01 11:32:32 -05:00
Ivan Radiček
32be3a023a feat: implement PatchField for Option<_> (#3528) 2025-02-01 09:40:43 -05:00
Greg Johnston
d9043e4f34 feat: impl From<ArcField<T>> for Field<T> (#3533)
* feat: impl `From<ArcField<T>>` for `Field<T>`

* [autofix.ci] apply automated fixes

---------

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

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-31 12:33:55 -05:00
Greg Johnston
1dcc5838f7 v0.7.5 2025-01-30 21:42:39 -05:00
Georg Vienna
1ff373dbc2 feat: allow raw identifiers in Params derive macro (#3525) 2025-01-29 19:39:23 -05:00
Greg Johnston
c35c42c6e3 fix: correctly notify descendants and ancestors of store fields (closes #3523) (#3524) 2025-01-29 19:28:26 -05:00
Geoffrey Garrett
d4cbba7c63 feat(callback): implement matches method for Callback and UnsyncCallback (#3520)
* feat(callback): implement matches method for Callback and UnsyncCallback

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-27 20:37:11 -05:00
stefnotch
9cc8ee3c5a fix: memo recomputation (closes #3252) (#3495) 2025-01-27 20:19:16 -05:00
SleeplessOne1917
f0c5ffe55f impl IntoAttributeValue for TextProp (#3517)
* impl IntoAttributeValue for TextProp

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-26 14:16:58 -05:00
benwis
586c330995 Fix formatting 2025-01-26 09:46:19 -08:00
zakstucke
72f960a026 AddAnyAttr working with erase_components (#3518)
* AddAnyAttr working with erase_components

* CI fixes
2025-01-26 09:43:32 -08:00
Danik Vitek
b62ae56094 feat (either_of): Extent API; Implement other iterator methods; Update deps (#3478)
* Implement other iterator methods. Update deps

* Formatting

* Update Cargo.lock

* [autofix.ci] apply automated fixes

* Formatting

* Move `Either` declaration into the `tuples` macro

* Comment out non-MSRV-compliant methods

* [autofix.ci] apply automated fixes

* Formatting

* Implement mapping functions

* Fix clippy warnings

* Impl `Error`; Impl `From<Result<A, B>> for Either<B, A>`

* Fix `Error` impl

* Move `Error` impl under `#[cfg(not(feature="no_std"))] until MSRV >= 1.81

* [autofix.ci] apply automated fixes

* Make `From<Result>` compliant with `EitherOr`. Add `impl EitherOr for Either`

* fix: use fully-qualified name

* fix: `EitherOf` test

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-24 21:15:29 -08:00
Greg Johnston
9ccefbbd8c chore: update workspace dependency versions to latest (#3506) 2025-01-24 21:07:20 -08:00
Danik Vitek
d1108826cc docs: Fix README.md & Add MSRV badge (#3480)
* Fix README.md

* Add MSRV badge

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-24 21:06:19 -08:00
starmaker
d6c4cd2a81 issue-3467 - bumping codee version to support rkyv 8 (#3504)
* issue-3467 - bumping codee version to support rkyv 8

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-24 20:29:55 -08:00
Sam Jude
f8acdf9168 hexagonal architecture (#3342)
Co-authored-by: Sam <@>
2025-01-24 20:29:07 -08:00
dcsturman
09bbae2a72 Enhanced docs for reactive_stores (#3508)
Added docs on shadow traits, Option, Enum, Vec, and Box usage with Store.
2025-01-24 20:23:13 -08:00
Greg Johnston
ac3352724b fix: including node_ref after {..} on arbitrary components (#3503) 2025-01-18 09:18:05 -05:00
Greg Johnston
3413825638 chore(ci): add CI for leptos_0.8 branch (#3500) 2025-01-17 16:04:36 -05:00
Greg Johnston
22b2d8ec84 feat: #[lazy] macros to support lazy loading and code splitting (#3477) 2025-01-17 15:15:12 -05:00
Greg Johnston
1c3e013a63 chore: restore reactivity warning at top level of components (closes #3354) (#3499) 2025-01-17 14:06:39 -05:00
Greg Johnston
9d4b1bc4b7 fix: correctly handle ErrorBoundary through reactive views (closes #3487) (#3492) 2025-01-17 13:22:06 -05:00
Greg Johnston
f303ff9706 feat: implement unboxing support for recursive store nodes (closes #3491) (#3493) 2025-01-17 13:21:40 -05:00
Georg Vienna
9fd2a75da4 feat: either_or combinator (#3417) 2025-01-17 13:12:18 -05:00
Michael Scofield
429d1b1262 feat: support for custom patch function on stores (#3449) 2025-01-17 13:10:44 -05:00
Greg Johnston
035586e5d8 chore: work around wasm-bindgen breakage (#3498) 2025-01-17 12:16:05 -05:00
Greg Johnston
fdd5671fe1 v0.7.4 2025-01-15 19:38:44 -05:00
Greg Johnston
293149eeb2 fix: include missing nonces on streaming script tags and on leptos_meta components (closes #3482) (#3485) 2025-01-13 22:06:14 -05:00
Greg Johnston
a9ce608433 fix: do not use stale values in AsyncDerived if it is mutated before running (#3475) 2025-01-13 21:32:08 -05:00
Thomas Versteeg
62196ff638 docs: warn about callbacks outside the ownership tree (#3442) 2025-01-10 15:23:08 -05:00
WorldSEnder
db9cabb365 chore: use cfg! macro instead of #[cfg] attribute (#3446)
this unifies the code path. The alternative will *move* arguments passed
to the debug_warn! macro only when compiling without debug_assertions.
This version will instead always use additional format arguments by reference.
2025-01-10 15:22:41 -05:00
Greg Johnston
5293afddb7 chore: fix tracing warning mess (#3473) 2025-01-10 14:27:03 -05:00
林玮 (Jade Lin)
eeb01e2d52 feat: add [Arc]LocalResource::refetch method (#3394) 2025-01-10 13:57:37 -05:00
Geoffrey Garrett
7d9cd43c0e feat: impl AddAnyAttr trait for Static<V> (#3464)
* Implement AddAnyAttr trait for Static<V>

Closes #3463

* [autofix.ci] apply automated fixes

---------

Co-authored-by: geoffreygarrett <geoffrey@apollo.xyz>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-01-10 12:54:01 -05:00
Greg Johnston
723685c370 fix: do not overwrite SuspenseContext in adjacent Transition components (closes #3465) (#3471) 2025-01-10 12:52:28 -05:00
Michael Kadziela
6d19e1a921 chore: derive Clone for RouteChildren (#3462)
This change allows users to clone RouteChildren and call .into_inner() on it to get an owned version of <Children>, and then pass on the original RouteChildren to an underlying Routes, or ParentRoute component.
This helps users snapshot all possible routes in an application.
2025-01-10 12:03:52 -05:00
Greg Johnston
165911b2e6 fix: allow multiple overlapping notifications of AsyncDerived/LocalResource (closes #3454) (#3455) 2025-01-06 22:53:15 -05:00
starmaker
575186349c Rkyv feature should propagate properly (#3448)
I suppose enabling `rkyv` feature on `leptos` crate should enable it on all dependencies.
2025-01-06 12:09:26 -08:00
Greg Johnston
10db2f83eb v0.7.3 2025-01-04 08:55:44 -05:00
Greg Johnston
357f3be393 fix: erase_components with AttributeInterceptor (#3435) 2025-01-04 08:46:17 -05:00
Soso
a1f5d6f79f feat: impl IntoClass for Cow<'_, str> (#3420)
I also thought about making the implementation more generic, for example over
T: AsRef<str>, but that would change the associated types and be a breaking change
it would also conflict with the `Option` impl due to the orphan rules.

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

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

* fix: doctests on for_loop

---------

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

* [autofix.ci] apply automated fixes

---------

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

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2024-12-05 21:08:05 -05:00
172 changed files with 6979 additions and 1662 deletions

View File

@@ -13,6 +13,7 @@ concurrency:
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
DEBIAN_FRONTEND: noninteractive
jobs:
autofix:
runs-on: ubuntu-latest
@@ -21,6 +22,10 @@ jobs:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: {toolchain: nightly, components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
- name: Install Glib
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- name: Install jq
run: sudo apt-get install jq
- run: |

View File

@@ -4,10 +4,12 @@ on:
branches:
- main
- leptos_0.6
- leptos_0.8
pull_request:
branches:
- main
- leptos_0.6
- leptos_0.8
jobs:
get-example-changed:
uses: ./.github/workflows/get-example-changed.yml

View File

@@ -4,10 +4,12 @@ on:
branches:
- main
- leptos_0.6
- leptos_0.8
pull_request:
branches:
- main
- leptos_0.6
- leptos_0.8
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml

View File

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

View File

@@ -4,10 +4,12 @@ on:
branches:
- main
- leptos_0.6
- leptos_0.8
pull_request:
branches:
- main
- leptos_0.6
- leptos_0.8
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml

View File

@@ -14,6 +14,7 @@ on:
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
DEBIAN_FRONTEND: noninteractive
jobs:
test:
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
@@ -33,6 +34,10 @@ jobs:
echo "Disk space after cleanup:"
df -h
# Setup environment
- name: Install Glib
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
@@ -50,7 +55,7 @@ jobs:
- name: Install wasm-bindgen
run: cargo binstall wasm-bindgen-cli --no-confirm
- name: Install cargo-leptos
run: cargo binstall cargo-leptos --no-confirm
run: cargo binstall cargo-leptos --locked --no-confirm
- name: Install Trunk
uses: jetli/trunk-action@v0.5.0
with:

786
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -5,6 +5,7 @@
[![crates.io](https://img.shields.io/crates/v/leptos.svg)](https://crates.io/crates/leptos)
[![docs.rs](https://docs.rs/leptos/badge.svg)](https://docs.rs/leptos)
![Crates.io MSRV](https://img.shields.io/crates/msrv/leptos)
[![Discord](https://img.shields.io/discord/1031524867910148188?color=%237289DA&label=discord)](https://discord.gg/YdRAhS7eQB)
[![Matrix](https://img.shields.io/badge/Matrix-leptos-grey?logo=matrix&labelColor=white&logoColor=black)](https://matrix.to/#/#leptos:matrix.org)
@@ -167,14 +168,14 @@ Yew is the most-used library for Rust web UI development, but there are several
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is.
- **Server integration:** Yew was created in an era in which browser-rendered single-page apps (SPAs) were the dominant paradigm. While Leptos supports client-side rendering, it also focuses on integrating with the server side of your application via server functions and multiple modes of serving HTML, including out-of-order streaming.
- ### How is this different from Dioxus?
### How is this different from Dioxus?
Like Leptos, Dioxus is a framework for building UIs using web technologies. However, there are significant differences in approach and features.
- **VDOM vs. fine-grained:** While Dioxus has a performant virtual DOM (VDOM), it still uses coarse-grained/component-scoped reactivity: changing a stateful value reruns the component function and diffs the old UI against the new one. Leptos components use a different mental model, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
- **Web vs. desktop priorities:** Dioxus uses Leptos server functions in its fullstack mode, but does not have the same `<Suspense>`-based support for things like streaming HTML rendering, or share the same focus on holistic web performance. Leptos tends to prioritize holistic web performance (streaming HTML rendering, smaller WASM binary sizes, etc.), whereas Dioxus has an unparalleled experience when building desktop apps, because your application logic runs as a native Rust binary.
- ### How is this different from Sycamore?
### How is this different from Sycamore?
Sycamore and Leptos are both heavily influenced by SolidJS. At this point, Leptos has a larger community and ecosystem and is more actively developed. Other differences:

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "either_of"
version = "0.1.1"
version = "0.1.5"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -10,4 +10,9 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
pin-project-lite = "0.2.15"
pin-project-lite = "0.2.16"
paste = "1.0.15"
[features]
default = ["no_std"]
no_std = []

View File

@@ -1,140 +1,758 @@
#![no_std]
#![cfg_attr(feature = "no_std", no_std)]
#![forbid(unsafe_code)]
//! Utilities for working with enumerated types that contain one of `2..n` other types.
use core::{
cmp::Ordering,
fmt::Display,
future::Future,
iter::{Product, Sum},
pin::Pin,
task::{Context, Poll},
};
use paste::paste;
use pin_project_lite::pin_project;
#[derive(Debug, Clone, Copy)]
pub enum Either<A, B> {
Left(A),
Right(B),
}
impl<Item, A, B> Iterator for Either<A, B>
where
A: Iterator<Item = Item>,
B: Iterator<Item = Item>,
{
type Item = Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
Either::Left(i) => i.next(),
Either::Right(i) => i.next(),
}
}
}
pin_project! {
#[project = EitherFutureProj]
pub enum EitherFuture<A, B> {
Left { #[pin] inner: A },
Right { #[pin] inner: B },
}
}
impl<A, B> Future for EitherFuture<A, B>
where
A: Future,
B: Future,
{
type Output = Either<A::Output, B::Output>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this {
EitherFutureProj::Left { inner } => match inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(inner) => Poll::Ready(Either::Left(inner)),
},
EitherFutureProj::Right { inner } => match inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(inner) => Poll::Ready(Either::Right(inner)),
},
}
}
}
#[cfg(not(feature = "no_std"))]
use std::error::Error; // TODO: replace with core::error::Error once MSRV is >= 1.81.0
macro_rules! tuples {
($name:ident + $fut_name:ident + $fut_proj:ident => $($ty:ident),*) => {
($name:ident + $fut_name:ident + $fut_proj:ident {
$($ty:ident => ($($rest_variant:ident),*) + <$($mapped_ty:ident),+>),+$(,)?
}) => {
tuples!($name + $fut_name + $fut_proj {
$($ty($ty) => ($($rest_variant),*) + <$($mapped_ty),+>),+
});
};
($name:ident + $fut_name:ident + $fut_proj:ident {
$($variant:ident($ty:ident) => ($($rest_variant:ident),*) + <$($mapped_ty:ident),+>),+$(,)?
}) => {
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum $name<$($ty,)*> {
$($ty ($ty),)*
pub enum $name<$($ty),+> {
$($variant ($ty),)+
}
impl<$($ty,)*> Display for $name<$($ty,)*>
impl<$($ty),+> $name<$($ty),+> {
paste! {
#[allow(clippy::too_many_arguments)]
pub fn map<$([<F $ty>]),+, $([<$ty 1>]),+>(self, $([<$variant:lower>]: [<F $ty>]),+) -> $name<$([<$ty 1>]),+>
where
$([<F $ty>]: FnOnce($ty) -> [<$ty 1>],)+
{
match self {
$($name::$variant(inner) => $name::$variant([<$variant:lower>](inner)),)+
}
}
$(
pub fn [<map_ $variant:lower>]<Fun, [<$ty 1>]>(self, f: Fun) -> $name<$($mapped_ty),+>
where
Fun: FnOnce($ty) -> [<$ty 1>],
{
match self {
$name::$variant(inner) => $name::$variant(f(inner)),
$($name::$rest_variant(inner) => $name::$rest_variant(inner),)*
}
}
pub fn [<inspect_ $variant:lower>]<Fun, [<$ty 1>]>(self, f: Fun) -> Self
where
Fun: FnOnce(&$ty),
{
if let $name::$variant(inner) = &self {
f(inner);
}
self
}
pub fn [<is_ $variant:lower>](&self) -> bool {
matches!(self, $name::$variant(_))
}
pub fn [<as_ $variant:lower>](&self) -> Option<&$ty> {
match self {
$name::$variant(inner) => Some(inner),
_ => None,
}
}
pub fn [<as_ $variant:lower _mut>](&mut self) -> Option<&mut $ty> {
match self {
$name::$variant(inner) => Some(inner),
_ => None,
}
}
pub fn [<unwrap_ $variant:lower>](self) -> $ty {
match self {
$name::$variant(inner) => inner,
_ => panic!(concat!(
"called `unwrap_", stringify!([<$variant:lower>]), "()` on a non-`", stringify!($variant), "` variant of `", stringify!($name), "`"
)),
}
}
pub fn [<into_ $variant:lower>](self) -> Result<$ty, Self> {
match self {
$name::$variant(inner) => Ok(inner),
_ => Err(self),
}
}
)+
}
}
impl<$($ty),+> Display for $name<$($ty),+>
where
$($ty: Display,)*
$($ty: Display,)+
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
$($name::$ty(this) => this.fmt(f),)*
$($name::$variant(this) => this.fmt(f),)+
}
}
}
impl<Item, $($ty,)*> Iterator for $name<$($ty,)*>
#[cfg(not(feature = "no_std"))]
impl<$($ty),+> Error for $name<$($ty),+>
where
$($ty: Iterator<Item = Item>,)*
$($ty: Error,)+
{
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
$($name::$variant(this) => this.source(),)+
}
}
}
impl<Item, $($ty),+> Iterator for $name<$($ty),+>
where
$($ty: Iterator<Item = Item>,)+
{
type Item = Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
$($name::$ty(i) => i.next(),)*
$($name::$variant(i) => i.next(),)+
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
match self {
$($name::$variant(i) => i.size_hint(),)+
}
}
fn count(self) -> usize
where
Self: Sized,
{
match self {
$($name::$variant(i) => i.count(),)+
}
}
fn last(self) -> Option<Self::Item>
where
Self: Sized,
{
match self {
$($name::$variant(i) => i.last(),)+
}
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
match self {
$($name::$variant(i) => i.nth(n),)+
}
}
fn for_each<Fun>(self, f: Fun)
where
Self: Sized,
Fun: FnMut(Self::Item),
{
match self {
$($name::$variant(i) => i.for_each(f),)+
}
}
fn collect<Col: FromIterator<Self::Item>>(self) -> Col
where
Self: Sized,
{
match self {
$($name::$variant(i) => i.collect(),)+
}
}
fn partition<Col, Fun>(self, f: Fun) -> (Col, Col)
where
Self: Sized,
Col: Default + Extend<Self::Item>,
Fun: FnMut(&Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.partition(f),)+
}
}
fn fold<Acc, Fun>(self, init: Acc, f: Fun) -> Acc
where
Self: Sized,
Fun: FnMut(Acc, Self::Item) -> Acc,
{
match self {
$($name::$variant(i) => i.fold(init, f),)+
}
}
fn reduce<Fun>(self, f: Fun) -> Option<Self::Item>
where
Self: Sized,
Fun: FnMut(Self::Item, Self::Item) -> Self::Item,
{
match self {
$($name::$variant(i) => i.reduce(f),)+
}
}
fn all<Fun>(&mut self, f: Fun) -> bool
where
Self: Sized,
Fun: FnMut(Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.all(f),)+
}
}
fn any<Fun>(&mut self, f: Fun) -> bool
where
Self: Sized,
Fun: FnMut(Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.any(f),)+
}
}
fn find<Pre>(&mut self, predicate: Pre) -> Option<Self::Item>
where
Self: Sized,
Pre: FnMut(&Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.find(predicate),)+
}
}
fn find_map<Out, Fun>(&mut self, f: Fun) -> Option<Out>
where
Self: Sized,
Fun: FnMut(Self::Item) -> Option<Out>,
{
match self {
$($name::$variant(i) => i.find_map(f),)+
}
}
fn position<Pre>(&mut self, predicate: Pre) -> Option<usize>
where
Self: Sized,
Pre: FnMut(Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.position(predicate),)+
}
}
fn max(self) -> Option<Self::Item>
where
Self: Sized,
Self::Item: Ord,
{
match self {
$($name::$variant(i) => i.max(),)+
}
}
fn min(self) -> Option<Self::Item>
where
Self: Sized,
Self::Item: Ord,
{
match self {
$($name::$variant(i) => i.min(),)+
}
}
fn max_by_key<Key: Ord, Fun>(self, f: Fun) -> Option<Self::Item>
where
Self: Sized,
Fun: FnMut(&Self::Item) -> Key,
{
match self {
$($name::$variant(i) => i.max_by_key(f),)+
}
}
fn max_by<Cmp>(self, compare: Cmp) -> Option<Self::Item>
where
Self: Sized,
Cmp: FnMut(&Self::Item, &Self::Item) -> Ordering,
{
match self {
$($name::$variant(i) => i.max_by(compare),)+
}
}
fn min_by_key<Key: Ord, Fun>(self, f: Fun) -> Option<Self::Item>
where
Self: Sized,
Fun: FnMut(&Self::Item) -> Key,
{
match self {
$($name::$variant(i) => i.min_by_key(f),)+
}
}
fn min_by<Cmp>(self, compare: Cmp) -> Option<Self::Item>
where
Self: Sized,
Cmp: FnMut(&Self::Item, &Self::Item) -> Ordering,
{
match self {
$($name::$variant(i) => i.min_by(compare),)+
}
}
fn sum<Out>(self) -> Out
where
Self: Sized,
Out: Sum<Self::Item>,
{
match self {
$($name::$variant(i) => i.sum(),)+
}
}
fn product<Out>(self) -> Out
where
Self: Sized,
Out: Product<Self::Item>,
{
match self {
$($name::$variant(i) => i.product(),)+
}
}
fn cmp<Other>(self, other: Other) -> Ordering
where
Other: IntoIterator<Item = Self::Item>,
Self::Item: Ord,
Self: Sized,
{
match self {
$($name::$variant(i) => i.cmp(other),)+
}
}
fn partial_cmp<Other>(self, other: Other) -> Option<Ordering>
where
Other: IntoIterator,
Self::Item: PartialOrd<Other::Item>,
Self: Sized,
{
match self {
$($name::$variant(i) => i.partial_cmp(other),)+
}
}
// TODO: uncomment once MSRV is >= 1.82.0
// fn is_sorted(self) -> bool
// where
// Self: Sized,
// Self::Item: PartialOrd,
// {
// match self {
// $($name::$variant(i) => i.is_sorted(),)+
// }
// }
//
// fn is_sorted_by<Cmp>(self, compare: Cmp) -> bool
// where
// Self: Sized,
// Cmp: FnMut(&Self::Item, &Self::Item) -> bool,
// {
// match self {
// $($name::$variant(i) => i.is_sorted_by(compare),)+
// }
// }
//
// fn is_sorted_by_key<Fun, Key>(self, f: Fun) -> bool
// where
// Self: Sized,
// Fun: FnMut(Self::Item) -> Key,
// Key: PartialOrd,
// {
// match self {
// $($name::$variant(i) => i.is_sorted_by_key(f),)+
// }
// }
}
impl<Item, $($ty),+> ExactSizeIterator for $name<$($ty),+>
where
$($ty: ExactSizeIterator<Item = Item>,)+
{
fn len(&self) -> usize {
match self {
$($name::$variant(i) => i.len(),)+
}
}
}
impl<Item, $($ty),+> DoubleEndedIterator for $name<$($ty),+>
where
$($ty: DoubleEndedIterator<Item = Item>,)+
{
fn next_back(&mut self) -> Option<Self::Item> {
match self {
$($name::$variant(i) => i.next_back(),)+
}
}
fn nth_back(&mut self, n: usize) -> Option<Self::Item> {
match self {
$($name::$variant(i) => i.nth_back(n),)+
}
}
fn rfind<Pre>(&mut self, predicate: Pre) -> Option<Self::Item>
where
Pre: FnMut(&Self::Item) -> bool,
{
match self {
$($name::$variant(i) => i.rfind(predicate),)+
}
}
}
pin_project! {
#[project = $fut_proj]
pub enum $fut_name<$($ty,)*> {
$($ty { #[pin] inner: $ty },)*
pub enum $fut_name<$($ty),+> {
$($variant { #[pin] inner: $ty },)+
}
}
impl<$($ty,)*> Future for $fut_name<$($ty,)*>
impl<$($ty),+> Future for $fut_name<$($ty),+>
where
$($ty: Future,)*
$($ty: Future,)+
{
type Output = $name<$($ty::Output,)*>;
type Output = $name<$($ty::Output),+>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this {
$($fut_proj::$ty { inner } => match inner.poll(cx) {
$($fut_proj::$variant { inner } => match inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(inner) => Poll::Ready($name::$ty(inner)),
},)*
Poll::Ready(inner) => Poll::Ready($name::$variant(inner)),
},)+
}
}
}
}
}
tuples!(EitherOf3 + EitherOf3Future + EitherOf3FutureProj => A, B, C);
tuples!(EitherOf4 + EitherOf4Future + EitherOf4FutureProj => A, B, C, D);
tuples!(EitherOf5 + EitherOf5Future + EitherOf5FutureProj => A, B, C, D, E);
tuples!(EitherOf6 + EitherOf6Future + EitherOf6FutureProj => A, B, C, D, E, F);
tuples!(EitherOf7 + EitherOf7Future + EitherOf7FutureProj => A, B, C, D, E, F, G);
tuples!(EitherOf8 + EitherOf8Future + EitherOf8FutureProj => A, B, C, D, E, F, G, H);
tuples!(EitherOf9 + EitherOf9Future + EitherOf9FutureProj => A, B, C, D, E, F, G, H, I);
tuples!(EitherOf10 + EitherOf10Future + EitherOf10FutureProj => A, B, C, D, E, F, G, H, I, J);
tuples!(EitherOf11 + EitherOf11Future + EitherOf11FutureProj => A, B, C, D, E, F, G, H, I, J, K);
tuples!(EitherOf12 + EitherOf12Future + EitherOf12FutureProj => A, B, C, D, E, F, G, H, I, J, K, L);
tuples!(EitherOf13 + EitherOf13Future + EitherOf13FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M);
tuples!(EitherOf14 + EitherOf14Future + EitherOf14FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N);
tuples!(EitherOf15 + EitherOf15Future + EitherOf15FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
tuples!(EitherOf16 + EitherOf16Future + EitherOf16FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
tuples!(Either + EitherFuture + EitherFutureProj {
Left(A) => (Right) + <A1, B>,
Right(B) => (Left) + <A, B1>,
});
/// Matches over the first expression and returns an either ([`Either`], [`EitherOf3`], ... [`EitherOf6`])
impl<A, B> Either<A, B> {
pub fn swap(self) -> Either<B, A> {
match self {
Either::Left(a) => Either::Right(a),
Either::Right(b) => Either::Left(b),
}
}
}
impl<A, B> From<Result<A, B>> for Either<A, B> {
fn from(value: Result<A, B>) -> Self {
match value {
Ok(left) => Either::Left(left),
Err(right) => Either::Right(right),
}
}
}
pub trait EitherOr {
type Left;
type Right;
fn either_or<FA, A, FB, B>(self, a: FA, b: FB) -> Either<A, B>
where
FA: FnOnce(Self::Left) -> A,
FB: FnOnce(Self::Right) -> B;
}
impl EitherOr for bool {
type Left = ();
type Right = ();
fn either_or<FA, A, FB, B>(self, a: FA, b: FB) -> Either<A, B>
where
FA: FnOnce(Self::Left) -> A,
FB: FnOnce(Self::Right) -> B,
{
if self {
Either::Left(a(()))
} else {
Either::Right(b(()))
}
}
}
impl<T> EitherOr for Option<T> {
type Left = T;
type Right = ();
fn either_or<FA, A, FB, B>(self, a: FA, b: FB) -> Either<A, B>
where
FA: FnOnce(Self::Left) -> A,
FB: FnOnce(Self::Right) -> B,
{
match self {
Some(t) => Either::Left(a(t)),
None => Either::Right(b(())),
}
}
}
impl<T, E> EitherOr for Result<T, E> {
type Left = T;
type Right = E;
fn either_or<FA, A, FB, B>(self, a: FA, b: FB) -> Either<A, B>
where
FA: FnOnce(Self::Left) -> A,
FB: FnOnce(Self::Right) -> B,
{
match self {
Ok(t) => Either::Left(a(t)),
Err(err) => Either::Right(b(err)),
}
}
}
impl<A, B> EitherOr for Either<A, B> {
type Left = A;
type Right = B;
#[inline]
fn either_or<FA, A1, FB, B1>(self, a: FA, b: FB) -> Either<A1, B1>
where
FA: FnOnce(<Self as EitherOr>::Left) -> A1,
FB: FnOnce(<Self as EitherOr>::Right) -> B1,
{
self.map(a, b)
}
}
#[test]
fn test_either_or() {
let right = false.either_or(|_| 'a', |_| 12);
assert!(matches!(right, Either::Right(12)));
let left = true.either_or(|_| 'a', |_| 12);
assert!(matches!(left, Either::Left('a')));
let left = Some(12).either_or(|a| a, |_| 'a');
assert!(matches!(left, Either::Left(12)));
let right = None.either_or(|a: i32| a, |_| 'a');
assert!(matches!(right, Either::Right('a')));
let result: Result<_, ()> = Ok(1.2f32);
let left = result.either_or(|a| a * 2f32, |b| b);
assert!(matches!(left, Either::Left(2.4f32)));
let result: Result<i32, _> = Err("12");
let right = result.either_or(|a| a, |b| b.chars().next());
assert!(matches!(right, Either::Right(Some('1'))));
let either = Either::<i32, char>::Left(12);
let left = either.either_or(|a| a, |b| b);
assert!(matches!(left, Either::Left(12)));
let either = Either::<i32, char>::Right('a');
let right = either.either_or(|a| a, |b| b);
assert!(matches!(right, Either::Right('a')));
}
tuples!(EitherOf3 + EitherOf3Future + EitherOf3FutureProj {
A => (B, C) + <A1, B, C>,
B => (A, C) + <A, B1, C>,
C => (A, B) + <A, B, C1>,
});
tuples!(EitherOf4 + EitherOf4Future + EitherOf4FutureProj {
A => (B, C, D) + <A1, B, C, D>,
B => (A, C, D) + <A, B1, C, D>,
C => (A, B, D) + <A, B, C1, D>,
D => (A, B, C) + <A, B, C, D1>,
});
tuples!(EitherOf5 + EitherOf5Future + EitherOf5FutureProj {
A => (B, C, D, E) + <A1, B, C, D, E>,
B => (A, C, D, E) + <A, B1, C, D, E>,
C => (A, B, D, E) + <A, B, C1, D, E>,
D => (A, B, C, E) + <A, B, C, D1, E>,
E => (A, B, C, D) + <A, B, C, D, E1>,
});
tuples!(EitherOf6 + EitherOf6Future + EitherOf6FutureProj {
A => (B, C, D, E, F) + <A1, B, C, D, E, F>,
B => (A, C, D, E, F) + <A, B1, C, D, E, F>,
C => (A, B, D, E, F) + <A, B, C1, D, E, F>,
D => (A, B, C, E, F) + <A, B, C, D1, E, F>,
E => (A, B, C, D, F) + <A, B, C, D, E1, F>,
F => (A, B, C, D, E) + <A, B, C, D, E, F1>,
});
tuples!(EitherOf7 + EitherOf7Future + EitherOf7FutureProj {
A => (B, C, D, E, F, G) + <A1, B, C, D, E, F, G>,
B => (A, C, D, E, F, G) + <A, B1, C, D, E, F, G>,
C => (A, B, D, E, F, G) + <A, B, C1, D, E, F, G>,
D => (A, B, C, E, F, G) + <A, B, C, D1, E, F, G>,
E => (A, B, C, D, F, G) + <A, B, C, D, E1, F, G>,
F => (A, B, C, D, E, G) + <A, B, C, D, E, F1, G>,
G => (A, B, C, D, E, F) + <A, B, C, D, E, F, G1>,
});
tuples!(EitherOf8 + EitherOf8Future + EitherOf8FutureProj {
A => (B, C, D, E, F, G, H) + <A1, B, C, D, E, F, G, H>,
B => (A, C, D, E, F, G, H) + <A, B1, C, D, E, F, G, H>,
C => (A, B, D, E, F, G, H) + <A, B, C1, D, E, F, G, H>,
D => (A, B, C, E, F, G, H) + <A, B, C, D1, E, F, G, H>,
E => (A, B, C, D, F, G, H) + <A, B, C, D, E1, F, G, H>,
F => (A, B, C, D, E, G, H) + <A, B, C, D, E, F1, G, H>,
G => (A, B, C, D, E, F, H) + <A, B, C, D, E, F, G1, H>,
H => (A, B, C, D, E, F, G) + <A, B, C, D, E, F, G, H1>,
});
tuples!(EitherOf9 + EitherOf9Future + EitherOf9FutureProj {
A => (B, C, D, E, F, G, H, I) + <A1, B, C, D, E, F, G, H, I>,
B => (A, C, D, E, F, G, H, I) + <A, B1, C, D, E, F, G, H, I>,
C => (A, B, D, E, F, G, H, I) + <A, B, C1, D, E, F, G, H, I>,
D => (A, B, C, E, F, G, H, I) + <A, B, C, D1, E, F, G, H, I>,
E => (A, B, C, D, F, G, H, I) + <A, B, C, D, E1, F, G, H, I>,
F => (A, B, C, D, E, G, H, I) + <A, B, C, D, E, F1, G, H, I>,
G => (A, B, C, D, E, F, H, I) + <A, B, C, D, E, F, G1, H, I>,
H => (A, B, C, D, E, F, G, I) + <A, B, C, D, E, F, G, H1, I>,
I => (A, B, C, D, E, F, G, H) + <A, B, C, D, E, F, G, H, I1>,
});
tuples!(EitherOf10 + EitherOf10Future + EitherOf10FutureProj {
A => (B, C, D, E, F, G, H, I, J) + <A1, B, C, D, E, F, G, H, I, J>,
B => (A, C, D, E, F, G, H, I, J) + <A, B1, C, D, E, F, G, H, I, J>,
C => (A, B, D, E, F, G, H, I, J) + <A, B, C1, D, E, F, G, H, I, J>,
D => (A, B, C, E, F, G, H, I, J) + <A, B, C, D1, E, F, G, H, I, J>,
E => (A, B, C, D, F, G, H, I, J) + <A, B, C, D, E1, F, G, H, I, J>,
F => (A, B, C, D, E, G, H, I, J) + <A, B, C, D, E, F1, G, H, I, J>,
G => (A, B, C, D, E, F, H, I, J) + <A, B, C, D, E, F, G1, H, I, J>,
H => (A, B, C, D, E, F, G, I, J) + <A, B, C, D, E, F, G, H1, I, J>,
I => (A, B, C, D, E, F, G, H, J) + <A, B, C, D, E, F, G, H, I1, J>,
J => (A, B, C, D, E, F, G, H, I) + <A, B, C, D, E, F, G, H, I, J1>,
});
tuples!(EitherOf11 + EitherOf11Future + EitherOf11FutureProj {
A => (B, C, D, E, F, G, H, I, J, K) + <A1, B, C, D, E, F, G, H, I, J, K>,
B => (A, C, D, E, F, G, H, I, J, K) + <A, B1, C, D, E, F, G, H, I, J, K>,
C => (A, B, D, E, F, G, H, I, J, K) + <A, B, C1, D, E, F, G, H, I, J, K>,
D => (A, B, C, E, F, G, H, I, J, K) + <A, B, C, D1, E, F, G, H, I, J, K>,
E => (A, B, C, D, F, G, H, I, J, K) + <A, B, C, D, E1, F, G, H, I, J, K>,
F => (A, B, C, D, E, G, H, I, J, K) + <A, B, C, D, E, F1, G, H, I, J, K>,
G => (A, B, C, D, E, F, H, I, J, K) + <A, B, C, D, E, F, G1, H, I, J, K>,
H => (A, B, C, D, E, F, G, I, J, K) + <A, B, C, D, E, F, G, H1, I, J, K>,
I => (A, B, C, D, E, F, G, H, J, K) + <A, B, C, D, E, F, G, H, I1, J, K>,
J => (A, B, C, D, E, F, G, H, I, K) + <A, B, C, D, E, F, G, H, I, J1, K>,
K => (A, B, C, D, E, F, G, H, I, J) + <A, B, C, D, E, F, G, H, I, J, K1>,
});
tuples!(EitherOf12 + EitherOf12Future + EitherOf12FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L) + <A1, B, C, D, E, F, G, H, I, J, K, L>,
B => (A, C, D, E, F, G, H, I, J, K, L) + <A, B1, C, D, E, F, G, H, I, J, K, L>,
C => (A, B, D, E, F, G, H, I, J, K, L) + <A, B, C1, D, E, F, G, H, I, J, K, L>,
D => (A, B, C, E, F, G, H, I, J, K, L) + <A, B, C, D1, E, F, G, H, I, J, K, L>,
E => (A, B, C, D, F, G, H, I, J, K, L) + <A, B, C, D, E1, F, G, H, I, J, K, L>,
F => (A, B, C, D, E, G, H, I, J, K, L) + <A, B, C, D, E, F1, G, H, I, J, K, L>,
G => (A, B, C, D, E, F, H, I, J, K, L) + <A, B, C, D, E, F, G1, H, I, J, K, L>,
H => (A, B, C, D, E, F, G, I, J, K, L) + <A, B, C, D, E, F, G, H1, I, J, K, L>,
I => (A, B, C, D, E, F, G, H, J, K, L) + <A, B, C, D, E, F, G, H, I1, J, K, L>,
J => (A, B, C, D, E, F, G, H, I, K, L) + <A, B, C, D, E, F, G, H, I, J1, K, L>,
K => (A, B, C, D, E, F, G, H, I, J, L) + <A, B, C, D, E, F, G, H, I, J, K1, L>,
L => (A, B, C, D, E, F, G, H, I, J, K) + <A, B, C, D, E, F, G, H, I, J, K, L1>,
});
tuples!(EitherOf13 + EitherOf13Future + EitherOf13FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L, M) + <A1, B, C, D, E, F, G, H, I, J, K, L, M>,
B => (A, C, D, E, F, G, H, I, J, K, L, M) + <A, B1, C, D, E, F, G, H, I, J, K, L, M>,
C => (A, B, D, E, F, G, H, I, J, K, L, M) + <A, B, C1, D, E, F, G, H, I, J, K, L, M>,
D => (A, B, C, E, F, G, H, I, J, K, L, M) + <A, B, C, D1, E, F, G, H, I, J, K, L, M>,
E => (A, B, C, D, F, G, H, I, J, K, L, M) + <A, B, C, D, E1, F, G, H, I, J, K, L, M>,
F => (A, B, C, D, E, G, H, I, J, K, L, M) + <A, B, C, D, E, F1, G, H, I, J, K, L, M>,
G => (A, B, C, D, E, F, H, I, J, K, L, M) + <A, B, C, D, E, F, G1, H, I, J, K, L, M>,
H => (A, B, C, D, E, F, G, I, J, K, L, M) + <A, B, C, D, E, F, G, H1, I, J, K, L, M>,
I => (A, B, C, D, E, F, G, H, J, K, L, M) + <A, B, C, D, E, F, G, H, I1, J, K, L, M>,
J => (A, B, C, D, E, F, G, H, I, K, L, M) + <A, B, C, D, E, F, G, H, I, J1, K, L, M>,
K => (A, B, C, D, E, F, G, H, I, J, L, M) + <A, B, C, D, E, F, G, H, I, J, K1, L, M>,
L => (A, B, C, D, E, F, G, H, I, J, K, M) + <A, B, C, D, E, F, G, H, I, J, K, L1, M>,
M => (A, B, C, D, E, F, G, H, I, J, K, L) + <A, B, C, D, E, F, G, H, I, J, K, L, M1>,
});
tuples!(EitherOf14 + EitherOf14Future + EitherOf14FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L, M, N) + <A1, B, C, D, E, F, G, H, I, J, K, L, M, N>,
B => (A, C, D, E, F, G, H, I, J, K, L, M, N) + <A, B1, C, D, E, F, G, H, I, J, K, L, M, N>,
C => (A, B, D, E, F, G, H, I, J, K, L, M, N) + <A, B, C1, D, E, F, G, H, I, J, K, L, M, N>,
D => (A, B, C, E, F, G, H, I, J, K, L, M, N) + <A, B, C, D1, E, F, G, H, I, J, K, L, M, N>,
E => (A, B, C, D, F, G, H, I, J, K, L, M, N) + <A, B, C, D, E1, F, G, H, I, J, K, L, M, N>,
F => (A, B, C, D, E, G, H, I, J, K, L, M, N) + <A, B, C, D, E, F1, G, H, I, J, K, L, M, N>,
G => (A, B, C, D, E, F, H, I, J, K, L, M, N) + <A, B, C, D, E, F, G1, H, I, J, K, L, M, N>,
H => (A, B, C, D, E, F, G, I, J, K, L, M, N) + <A, B, C, D, E, F, G, H1, I, J, K, L, M, N>,
I => (A, B, C, D, E, F, G, H, J, K, L, M, N) + <A, B, C, D, E, F, G, H, I1, J, K, L, M, N>,
J => (A, B, C, D, E, F, G, H, I, K, L, M, N) + <A, B, C, D, E, F, G, H, I, J1, K, L, M, N>,
K => (A, B, C, D, E, F, G, H, I, J, L, M, N) + <A, B, C, D, E, F, G, H, I, J, K1, L, M, N>,
L => (A, B, C, D, E, F, G, H, I, J, K, M, N) + <A, B, C, D, E, F, G, H, I, J, K, L1, M, N>,
M => (A, B, C, D, E, F, G, H, I, J, K, L, N) + <A, B, C, D, E, F, G, H, I, J, K, L, M1, N>,
N => (A, B, C, D, E, F, G, H, I, J, K, L, M) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N1>,
});
tuples!(EitherOf15 + EitherOf15Future + EitherOf15FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L, M, N, O) + <A1, B, C, D, E, F, G, H, I, J, K, L, M, N, O>,
B => (A, C, D, E, F, G, H, I, J, K, L, M, N, O) + <A, B1, C, D, E, F, G, H, I, J, K, L, M, N, O>,
C => (A, B, D, E, F, G, H, I, J, K, L, M, N, O) + <A, B, C1, D, E, F, G, H, I, J, K, L, M, N, O>,
D => (A, B, C, E, F, G, H, I, J, K, L, M, N, O) + <A, B, C, D1, E, F, G, H, I, J, K, L, M, N, O>,
E => (A, B, C, D, F, G, H, I, J, K, L, M, N, O) + <A, B, C, D, E1, F, G, H, I, J, K, L, M, N, O>,
F => (A, B, C, D, E, G, H, I, J, K, L, M, N, O) + <A, B, C, D, E, F1, G, H, I, J, K, L, M, N, O>,
G => (A, B, C, D, E, F, H, I, J, K, L, M, N, O) + <A, B, C, D, E, F, G1, H, I, J, K, L, M, N, O>,
H => (A, B, C, D, E, F, G, I, J, K, L, M, N, O) + <A, B, C, D, E, F, G, H1, I, J, K, L, M, N, O>,
I => (A, B, C, D, E, F, G, H, J, K, L, M, N, O) + <A, B, C, D, E, F, G, H, I1, J, K, L, M, N, O>,
J => (A, B, C, D, E, F, G, H, I, K, L, M, N, O) + <A, B, C, D, E, F, G, H, I, J1, K, L, M, N, O>,
K => (A, B, C, D, E, F, G, H, I, J, L, M, N, O) + <A, B, C, D, E, F, G, H, I, J, K1, L, M, N, O>,
L => (A, B, C, D, E, F, G, H, I, J, K, M, N, O) + <A, B, C, D, E, F, G, H, I, J, K, L1, M, N, O>,
M => (A, B, C, D, E, F, G, H, I, J, K, L, N, O) + <A, B, C, D, E, F, G, H, I, J, K, L, M1, N, O>,
N => (A, B, C, D, E, F, G, H, I, J, K, L, M, O) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N1, O>,
O => (A, B, C, D, E, F, G, H, I, J, K, L, M, N) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N, O1>,
});
tuples!(EitherOf16 + EitherOf16Future + EitherOf16FutureProj {
A => (B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) + <A1, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P>,
B => (A, C, D, E, F, G, H, I, J, K, L, M, N, O, P) + <A, B1, C, D, E, F, G, H, I, J, K, L, M, N, O, P>,
C => (A, B, D, E, F, G, H, I, J, K, L, M, N, O, P) + <A, B, C1, D, E, F, G, H, I, J, K, L, M, N, O, P>,
D => (A, B, C, E, F, G, H, I, J, K, L, M, N, O, P) + <A, B, C, D1, E, F, G, H, I, J, K, L, M, N, O, P>,
E => (A, B, C, D, F, G, H, I, J, K, L, M, N, O, P) + <A, B, C, D, E1, F, G, H, I, J, K, L, M, N, O, P>,
F => (A, B, C, D, E, G, H, I, J, K, L, M, N, O, P) + <A, B, C, D, E, F1, G, H, I, J, K, L, M, N, O, P>,
G => (A, B, C, D, E, F, H, I, J, K, L, M, N, O, P) + <A, B, C, D, E, F, G1, H, I, J, K, L, M, N, O, P>,
H => (A, B, C, D, E, F, G, I, J, K, L, M, N, O, P) + <A, B, C, D, E, F, G, H1, I, J, K, L, M, N, O, P>,
I => (A, B, C, D, E, F, G, H, J, K, L, M, N, O, P) + <A, B, C, D, E, F, G, H, I1, J, K, L, M, N, O, P>,
J => (A, B, C, D, E, F, G, H, I, K, L, M, N, O, P) + <A, B, C, D, E, F, G, H, I, J1, K, L, M, N, O, P>,
K => (A, B, C, D, E, F, G, H, I, J, L, M, N, O, P) + <A, B, C, D, E, F, G, H, I, J, K1, L, M, N, O, P>,
L => (A, B, C, D, E, F, G, H, I, J, K, M, N, O, P) + <A, B, C, D, E, F, G, H, I, J, K, L1, M, N, O, P>,
M => (A, B, C, D, E, F, G, H, I, J, K, L, N, O, P) + <A, B, C, D, E, F, G, H, I, J, K, L, M1, N, O, P>,
N => (A, B, C, D, E, F, G, H, I, J, K, L, M, O, P) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N1, O, P>,
O => (A, B, C, D, E, F, G, H, I, J, K, L, M, N, P) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N, O1, P>,
P => (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) + <A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P1>,
});
/// Matches over the first expression and returns an either ([`Either`], [`EitherOf3`], ... [`EitherOf8`])
/// composed of the values returned by the match arms.
///
/// The pattern syntax is exactly the same as found in a match arm.
@@ -197,40 +815,93 @@ macro_rules! either {
$e_pattern => $crate::EitherOf6::E($e_expression),
$f_pattern => $crate::EitherOf6::F($f_expression),
}
};
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr, $g_pattern:pat => $g_expression:expr,) => {
match $match {
$a_pattern => $crate::EitherOf7::A($a_expression),
$b_pattern => $crate::EitherOf7::B($b_expression),
$c_pattern => $crate::EitherOf7::C($c_expression),
$d_pattern => $crate::EitherOf7::D($d_expression),
$e_pattern => $crate::EitherOf7::E($e_expression),
$f_pattern => $crate::EitherOf7::F($f_expression),
$g_pattern => $crate::EitherOf7::G($g_expression),
}
};
($match:expr, $a_pattern:pat => $a_expression:expr, $b_pattern:pat => $b_expression:expr, $c_pattern:pat => $c_expression:expr, $d_pattern:pat => $d_expression:expr, $e_pattern:pat => $e_expression:expr, $f_pattern:pat => $f_expression:expr, $g_pattern:pat => $g_expression:expr, $h_pattern:pat => $h_expression:expr,) => {
match $match {
$a_pattern => $crate::EitherOf8::A($a_expression),
$b_pattern => $crate::EitherOf8::B($b_expression),
$c_pattern => $crate::EitherOf8::C($c_expression),
$d_pattern => $crate::EitherOf8::D($d_expression),
$e_pattern => $crate::EitherOf8::E($e_expression),
$f_pattern => $crate::EitherOf8::F($f_expression),
$g_pattern => $crate::EitherOf8::G($g_expression),
$h_pattern => $crate::EitherOf8::H($h_expression),
}
}; // if you need more eithers feel free to open a PR ;-)
}
// compile time test
#[test]
fn either_macro() {
let _: Either<&str, f64> = either!(12,
12 => "12",
_ => 0.0,
);
let _: EitherOf3<&str, f64, i32> = either!(12,
12 => "12",
13 => 0.0,
_ => 12,
);
let _: EitherOf4<&str, f64, char, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
_ => 12,
);
let _: EitherOf5<&str, f64, char, f32, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
_ => 12,
);
let _: EitherOf6<&str, f64, char, f32, u8, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
_ => 12,
);
#[cfg(test)]
mod tests {
use super::*;
// compile time test
#[test]
fn either_macro() {
let _: Either<&str, f64> = either!(12,
12 => "12",
_ => 0.0,
);
let _: EitherOf3<&str, f64, i32> = either!(12,
12 => "12",
13 => 0.0,
_ => 12,
);
let _: EitherOf4<&str, f64, char, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
_ => 12,
);
let _: EitherOf5<&str, f64, char, f32, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
_ => 12,
);
let _: EitherOf6<&str, f64, char, f32, u8, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
_ => 12,
);
let _: EitherOf7<&str, f64, char, f32, u8, i8, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
17 => 2i8,
_ => 12,
);
let _: EitherOf8<&str, f64, char, f32, u8, i8, u32, i32> = either!(12,
12 => "12",
13 => 0.0,
14 => ' ',
15 => 0.0f32,
16 => 24u8,
17 => 2i8,
18 => 42u32,
_ => 12,
);
}
#[test]
#[should_panic]
fn unwrap_wrong_either() {
Either::<i32, &str>::Left(0).unwrap_right();
}
}

View File

@@ -1227,4 +1227,4 @@ begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0
;return c.pop(),c.push(i),r.contains=c,{name:"YAML",case_insensitive:!0,
aliases:["yml"],contains:l}}});const Ke=te;for(const e of Object.keys(Pe)){
const n=e.replace("grmr_","").replace("_","-");Ke.registerLanguage(n,Pe[e])}
export{Ke as default};
export{Ke as defaultMod};

View File

@@ -13,13 +13,13 @@ mod csr {
extern "C" {
type HighlightOptions;
#[wasm_bindgen(catch, js_namespace = default, js_name = highlight)]
#[wasm_bindgen(catch, js_namespace = defaultMod, js_name = highlight)]
fn highlight_lang(
code: String,
options: Object,
) -> Result<Object, JsValue>;
#[wasm_bindgen(js_namespace = default, js_name = highlightAll)]
#[wasm_bindgen(js_namespace = defaultMod, js_name = highlightAll)]
pub fn highlight_all();
}

View File

@@ -1,5 +1,6 @@
[tasks.install-cargo-leptos]
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
args = ["--locked"]
[tasks.cargo-leptos-e2e]
command = "cargo"

View File

@@ -1,6 +1,6 @@
[package]
name = "hydration_context"
version = "0.2.0"
version = "0.2.1"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -25,3 +25,6 @@ browser = ["dep:wasm-bindgen", "dep:js-sys"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -18,7 +18,7 @@ hydration_context = { workspace = true }
leptos = { workspace = true, features = ["nonce", "ssr"] }
leptos_integration_utils = { workspace = true }
leptos_macro = { workspace = true, features = ["actix"] }
leptos_meta = { workspace = true }
leptos_meta = { workspace = true, features = ["nonce"] }
leptos_router = { workspace = true, features = ["ssr"] }
server_fn = { workspace = true, features = ["actix"] }
serde_json = "1.0"

View File

@@ -19,7 +19,7 @@ futures = "0.3.31"
leptos = { workspace = true, features = ["nonce", "ssr"] }
server_fn = { workspace = true, features = ["axum-no-default"] }
leptos_macro = { workspace = true, features = ["axum"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true, features = ["ssr", "nonce"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
once_cell = "1"

View File

@@ -16,7 +16,6 @@
//! - `default`: supports running in a typical native Tokio/Axum environment
//! - `wasm`: with `default-features = false`, supports running in a JS Fetch-based
//! environment
//! - `islands`: activates Leptos [islands mode](https://leptos-rs.github.io/leptos/islands.html)
//!
//! ### Important Note
//! Prior to 0.5, using `default-features = false` on `leptos_axum` simply did nothing. Now, it actively
@@ -1977,6 +1976,67 @@ where
.map_err(|e| ServerFnError::ServerError(format!("{e:?}")))
}
/// A reasonable handler for serving static files (like JS/WASM/CSS) and 404 errors.
///
/// This is provided as a convenience, but is a fairly simple function. If you need to adapt it,
/// simply reuse the source code of this function in your own application.
#[cfg(feature = "default")]
pub fn file_and_error_handler_with_context<S, IV>(
additional_context: impl Fn() + 'static + Clone + Send,
shell: fn(LeptosOptions) -> IV,
) -> impl Fn(
Uri,
State<S>,
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
IV: IntoView + 'static,
S: Send + Sync + Clone + 'static,
LeptosOptions: FromRef<S>,
{
move |uri: Uri, State(state): State<S>, req: Request<Body>| {
Box::pin({
let additional_context = additional_context.clone();
async move {
let options = LeptosOptions::from_ref(&state);
let res =
get_static_file(uri, &options.site_root, req.headers());
let res = res.await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else {
let mut res = handle_response_inner(
move || {
additional_context();
provide_context(state.clone());
},
move || shell(options),
req,
|app, chunks| {
Box::pin(async move {
let app = app
.to_html_stream_in_order()
.collect::<String>()
.await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
},
)
.await;
*res.status_mut() = StatusCode::NOT_FOUND;
res
}
}
})
}
}
/// A reasonable handler for serving static files (like JS/WASM/CSS) and 404 errors.
///
/// This is provided as a convenience, but is a fairly simple function. If you need to adapt it,
@@ -1994,40 +2054,10 @@ pub fn file_and_error_handler<S, IV>(
+ 'static
where
IV: IntoView + 'static,
S: Send + 'static,
S: Send + Sync + Clone + 'static,
LeptosOptions: FromRef<S>,
{
move |uri: Uri, State(options): State<S>, req: Request<Body>| {
Box::pin(async move {
let options = LeptosOptions::from_ref(&options);
let res = get_static_file(uri, &options.site_root, req.headers());
let res = res.await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else {
let mut res = handle_response_inner(
|| {},
move || shell(options),
req,
|app, chunks| {
Box::pin(async move {
let app = app
.to_html_stream_in_order()
.collect::<String>()
.await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
},
)
.await;
*res.status_mut() = StatusCode::NOT_FOUND;
res
}
})
}
file_and_error_handler_with_context(move || (), shell)
}
#[cfg(feature = "default")]

View File

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

View File

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

View File

@@ -80,6 +80,16 @@ impl<In, Out> UnsyncCallback<In, Out> {
{
Self(StoredValue::new_local(Rc::new(f)))
}
/// Returns `true` if both callbacks wrap the same underlying function pointer.
#[inline]
pub fn matches(&self, other: &Self) -> bool {
self.0.with_value(|self_value| {
other
.0
.with_value(|other_value| Rc::ptr_eq(self_value, other_value))
})
}
}
impl<In: 'static, Out: 'static> Callable<In, Out> for UnsyncCallback<In, Out> {
@@ -212,6 +222,19 @@ impl<In: 'static, Out: 'static> Callback<In, Out> {
{
Self(StoredValue::new(Arc::new(fun)))
}
/// Returns `true` if both callbacks wrap the same underlying function pointer.
#[inline]
pub fn matches(&self, other: &Self) -> bool {
self.0
.try_with_value(|self_value| {
other.0.try_with_value(|other_value| {
Arc::ptr_eq(self_value, other_value)
})
})
.flatten()
.unwrap_or(false)
}
}
#[cfg(test)]
@@ -246,4 +269,32 @@ mod tests {
let _callback: UnsyncCallback<(i32, String), String> =
(|num, s| format!("{num} {s}")).into();
}
#[test]
fn callback_matches_same() {
let callback1 = Callback::new(|x: i32| x * 2);
let callback2 = callback1.clone();
assert!(callback1.matches(&callback2));
}
#[test]
fn callback_matches_different() {
let callback1 = Callback::new(|x: i32| x * 2);
let callback2 = Callback::new(|x: i32| x + 1);
assert!(!callback1.matches(&callback2));
}
#[test]
fn unsync_callback_matches_same() {
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
let callback2 = callback1.clone();
assert!(callback1.matches(&callback2));
}
#[test]
fn unsync_callback_matches_different() {
let callback1 = UnsyncCallback::new(|x: i32| x * 2);
let callback2 = UnsyncCallback::new(|x: i32| x + 1);
assert!(!callback1.matches(&callback2));
}
}

View File

@@ -85,7 +85,7 @@ type BoxedChildrenFn = Box<dyn Fn() -> AnyView + Send>;
/// )
/// }
pub trait ToChildren<F> {
/// Convert the provided type to (generally a closure) to Self (generally a "children" type,
/// Convert the provided type (generally a closure) to Self (generally a "children" type,
/// e.g., [Children]). See the implementations to see exactly which input types are supported
/// and which "children" type they are converted to.
fn to_children(f: F) -> Self;
@@ -285,6 +285,13 @@ impl<T> Debug for TypedChildrenFn<T> {
}
}
impl<T> Clone for TypedChildrenFn<T> {
// Manual implementation to avoid the `T: Clone` bound.
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> TypedChildrenFn<T> {
/// Extracts the inner `children` function.
pub fn into_inner(self) -> Arc<dyn Fn() -> View<T> + Send + Sync> {

View File

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

View File

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

View File

@@ -192,6 +192,9 @@ pub mod callback;
/// Types that can be passed as the `children` prop of a component.
pub mod children;
/// Wrapper for intercepting component attributes.
pub mod attribute_interceptor;
#[doc(hidden)]
/// Traits used to implement component constructors.
pub mod component;

View File

@@ -51,6 +51,13 @@ use tachys::html::attribute::AttributeValue;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Nonce(pub(crate) Arc<str>);
impl Nonce {
/// Returns a reference to the inner reference-counted string slice representing the nonce.
pub fn as_inner(&self) -> &Arc<str> {
&self.0
}
}
impl Deref for Nonce {
type Target = str;

View File

@@ -16,6 +16,7 @@ use reactive_graph::{
traits::{Dispose, Get, Read, Track, With},
};
use slotmap::{DefaultKey, SlotMap};
use std::sync::Arc;
use tachys::{
either::Either,
html::attribute::Attribute,
@@ -132,6 +133,18 @@ where
})
}
fn nonce_or_not() -> Option<Arc<str>> {
#[cfg(feature = "nonce")]
{
use crate::nonce::Nonce;
use_context::<Nonce>().map(|n| n.0)
}
#[cfg(not(feature = "nonce"))]
{
None
}
}
pub(crate) struct SuspenseBoundary<const TRANSITION: bool, Fal, Chil> {
pub id: SerializedDataId,
pub none_pending: ArcMemo<bool>,
@@ -379,7 +392,12 @@ where
&mut fallback_position,
mark_branches,
);
buf.push_async_out_of_order(fut, position, mark_branches);
buf.push_async_out_of_order_with_nonce(
fut,
position,
mark_branches,
nonce_or_not(),
);
} else {
buf.push_async({
let mut position = *position;

View File

@@ -1,5 +1,6 @@
use oco_ref::Oco;
use std::sync::Arc;
use tachys::prelude::IntoAttributeValue;
/// Describes a value that is either a static or a reactive string, i.e.,
/// a [`String`], a [`&str`], or a reactive `Fn() -> String`.
@@ -73,3 +74,11 @@ impl Default for TextProp {
Self(Arc::new(|| Oco::Borrowed("")))
}
}
impl IntoAttributeValue for TextProp {
type Output = Oco<'static, str>;
fn into_attribute_value(self) -> Self::Output {
self.get()
}
}

View File

@@ -85,41 +85,44 @@ pub fn Transition<Chil>(
where
Chil: IntoView + Send + 'static,
{
let (starts_local, id) = {
Owner::current_shared_context()
.map(|sc| {
let id = sc.next_id();
(sc.get_incomplete_chunk(&id), id)
})
.unwrap_or_else(|| (false, Default::default()))
};
let fallback = fallback.run();
let children = children.into_inner()();
let tasks = ArcRwSignal::new(SlotMap::<DefaultKey, ()>::new());
provide_context(SuspenseContext {
tasks: tasks.clone(),
});
let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
tasks.track();
if prev.is_none() && starts_local {
false
} else {
tasks.with(SlotMap::is_empty)
}
});
if let Some(set_pending) = set_pending {
Effect::new_isomorphic({
let none_pending = none_pending.clone();
move |_| {
set_pending.set(!none_pending.get());
let owner = Owner::new();
owner.with(|| {
let (starts_local, id) = {
Owner::current_shared_context()
.map(|sc| {
let id = sc.next_id();
(sc.get_incomplete_chunk(&id), id)
})
.unwrap_or_else(|| (false, Default::default()))
};
let fallback = fallback.run();
let children = children.into_inner()();
let tasks = ArcRwSignal::new(SlotMap::<DefaultKey, ()>::new());
provide_context(SuspenseContext {
tasks: tasks.clone(),
});
let none_pending = ArcMemo::new(move |prev: Option<&bool>| {
tasks.track();
if prev.is_none() && starts_local {
false
} else {
tasks.with(SlotMap::is_empty)
}
});
}
if let Some(set_pending) = set_pending {
Effect::new_isomorphic({
let none_pending = none_pending.clone();
move |_| {
set_pending.set(!none_pending.get());
}
});
}
OwnedView::new(SuspenseBoundary::<true, _, _> {
id,
none_pending,
fallback,
children,
OwnedView::new(SuspenseBoundary::<true, _, _> {
id,
none_pending,
fallback,
children,
})
})
}

View File

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

View File

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

View File

@@ -37,3 +37,6 @@ rustdoc-args = ["--generate-link-to-definition"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -131,6 +131,10 @@ impl AnimationFrameRequestHandle {
/// Runs the given function between the next repaint using
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
@@ -159,6 +163,10 @@ fn closure_once(cb: impl FnOnce() + 'static) -> JsValue {
/// Runs the given function between the next repaint using
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame),
/// returning a cancelable handle.
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_animation_frame_with_handle(
@@ -197,6 +205,10 @@ impl IdleCallbackHandle {
/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_idle_callback(cb: impl Fn() + 'static) {
@@ -206,6 +218,10 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback),
/// returning a cancelable handle.
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_idle_callback_with_handle(
@@ -239,6 +255,8 @@ pub fn request_idle_callback_with_handle(
/// to perform final cleanup or other just-before-rendering tasks.
///
/// [MDN queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask)
///
/// <div class="warning">The task is called outside of the ownership tree, this means that if you want to access for example the context you need to reestablish the owner.</div>
pub fn queue_microtask(task: impl FnOnce() + 'static) {
use js_sys::{Function, Reflect};
@@ -265,6 +283,10 @@ impl TimeoutHandle {
/// Executes the given function after the given duration of time has passed.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
@@ -275,6 +297,10 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
/// Executes the given function after the given duration of time has passed, returning a cancelable handle.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
@@ -331,6 +357,10 @@ pub fn set_timeout_with_handle(
/// }
/// }
/// ```
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
pub fn debounce<T: 'static>(
delay: Duration,
mut cb: impl FnMut(T) + 'static,
@@ -398,6 +428,10 @@ impl IntervalHandle {
/// Repeatedly calls the given function, with a delay of the given duration between calls.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
@@ -409,6 +443,10 @@ pub fn set_interval(cb: impl Fn() + 'static, duration: Duration) {
/// Repeatedly calls the given function, with a delay of the given duration between calls,
/// returning a cancelable handle.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
@@ -451,6 +489,10 @@ pub fn set_interval_with_handle(
/// Adds an event listener to the `Window`, typed as a generic `Event`,
/// returning a cancelable handle.
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(event_name = %event_name))
@@ -519,6 +561,10 @@ pub fn window_event_listener_untyped(
/// on_cleanup(move || handle.remove());
/// }
/// ```
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
pub fn window_event_listener<E: EventDescriptor + 'static>(
event: E,
cb: impl Fn(E::EventType) + 'static,

View File

@@ -29,14 +29,9 @@ macro_rules! error {
macro_rules! debug_warn {
($($x:tt)*) => {
{
#[cfg(debug_assertions)]
{
if cfg!(debug_assertions) {
$crate::warn!($($x)*)
}
#[cfg(not(debug_assertions))]
{
($($x)*)
}
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.7.0"
version = { workspace = true }
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -34,6 +34,7 @@ log = "0.4.22"
typed-builder = "0.20.0"
trybuild = "1.0"
leptos = { path = "../leptos" }
leptos_router = { path = "../router", features = ["ssr"] }
server_fn = { path = "../server_fn", features = ["cbor"] }
insta = "1.41"
serde = "1.0"
@@ -45,6 +46,7 @@ ssr = ["server_fn_macro/ssr", "leptos/ssr"]
nightly = ["server_fn_macro/nightly"]
tracing = ["dep:tracing"]
islands = []
trace-components = []
trace-component-props = []
actix = ["server_fn_macro/actix"]
axum = ["server_fn_macro/axum"]
@@ -83,4 +85,7 @@ skip_feature_sets = [
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(erase_components)'] }
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(leptos_debuginfo)',
'cfg(erase_components)',
] }

View File

@@ -144,8 +144,6 @@ impl ToTokens for Model {
let (impl_generics, generics, where_clause) =
body.sig.generics.split_for_impl();
let lifetimes = body.sig.generics.lifetimes();
let props_name = format_ident!("{name}Props");
let props_builder_name = format_ident!("{name}PropsBuilder");
let props_serialized_name = format_ident!("{name}PropsSerialized");
@@ -201,13 +199,35 @@ impl ToTokens for Model {
) = {
#[cfg(feature = "tracing")]
{
/* TODO for 0.8: fix this
*
* The problem is that cargo now warns about an expected "tracing" cfg if
* you don't have a "tracing" feature in your actual crate
*
* However, until https://github.com/tokio-rs/tracing/pull/1819 is merged
* (?), you can't provide an alternate path for `tracing` (for example,
* ::leptos::tracing), which means that if you're going to use the macro
* you *must* have `tracing` in your Cargo.toml.
*
* Including the feature-check here causes cargo warnings on
* previously-working projects.
*
* Removing the feature-check here breaks any project that uses leptos with
* the tracing feature turned on, but without a tracing dependency in its
* Cargo.toml.
* /
*/
let instrument = cfg!(feature = "trace-components").then(|| quote! {
#[cfg_attr(
feature = "tracing",
::leptos::tracing::instrument(level = "info", name = #trace_name, skip_all)
)]
});
(
quote! {
#[allow(clippy::let_with_type_underscore)]
#[cfg_attr(
feature = "tracing",
::leptos::tracing::instrument(level = "info", name = #trace_name, skip_all)
)]
#instrument
},
quote! {
let __span = ::leptos::tracing::Span::current();
@@ -260,8 +280,12 @@ impl ToTokens for Model {
let body_name = unmodified_fn_name_from_fn_name(&body_name);
let body_expr = if is_island {
quote! {
::leptos::reactive::owner::Owner::with_hydration(move || {
#body_name(#prop_names)
::leptos::reactive::owner::Owner::new().with(|| {
::leptos::reactive::owner::Owner::with_hydration(move || {
::leptos::tachys::reactive_graph::OwnedView::new({
#body_name(#prop_names)
})
})
})
}
} else {
@@ -275,7 +299,7 @@ impl ToTokens for Model {
} else if cfg!(erase_components) {
quote! {
::leptos::prelude::IntoAny::into_any(
::leptos::prelude::untrack(
::leptos::reactive::graph::untrack_with_diagnostics(
move || {
#tracing_guard_expr
#tracing_props_expr
@@ -286,7 +310,7 @@ impl ToTokens for Model {
}
} else {
quote! {
::leptos::prelude::untrack(
::leptos::reactive::graph::untrack_with_diagnostics(
move || {
#tracing_guard_expr
#tracing_props_expr
@@ -301,8 +325,8 @@ impl ToTokens for Model {
let hydrate_fn_name = hydrate_fn_name.as_ref().unwrap();
quote! {
{
if ::leptos::reactive::owner::Owner::current_shared_context()
.map(|sc| sc.get_is_hydrating())
if ::leptos::context::use_context::<::leptos::reactive::owner::IsHydrating>()
.map(|h| h.0)
.unwrap_or(false) {
::leptos::either::Either::Left(
#component
@@ -339,9 +363,23 @@ impl ToTokens for Model {
let children = Box::new(|| {
let sc = ::leptos::reactive::owner::Owner::current_shared_context().unwrap();
let prev = sc.get_is_hydrating();
let value = ::leptos::reactive::owner::Owner::with_no_hydration(||
::leptos::tachys::html::islands::IslandChildren::new(children()).into_any()
);
let owner = ::leptos::reactive::owner::Owner::new();
let value = owner.clone().with(|| {
::leptos::reactive::owner::Owner::with_no_hydration(move || {
::leptos::tachys::reactive_graph::OwnedView::new({
::leptos::tachys::html::islands::IslandChildren::new_with_on_hydrate(
children(),
{
let owner = owner.clone();
move || {
owner.set()
}
}
)
}).into_any()
})
});
sc.set_is_hydrating(prev);
value
});
@@ -424,20 +462,21 @@ impl ToTokens for Model {
};
let children = if is_island_with_children {
quote! {
.children({Box::new(|| {
.children({
let owner = leptos::reactive::owner::Owner::current();
Box::new(move || {
use leptos::tachys::view::any_view::IntoAny;
::leptos::tachys::html::islands::IslandChildren::new(
// TODO owner restoration for context
()
::leptos::tachys::html::islands::IslandChildren::new_with_on_hydrate(
(),
{
let owner = owner.clone();
move || {
if let Some(owner) = &owner {
owner.set()
}
}
}
).into_any()})})
//.children(children)
/*.children(Box::new(|| {
use leptos::tachys::view::any_view::IntoAny;
::leptos::tachys::html::islands::IslandChildren::new(
// TODO owner restoration for context
()
).into_any()
}))*/
}
} else {
quote! {}
@@ -529,7 +568,7 @@ impl ToTokens for Model {
#tracing_instrument_attr
#vis fn #name #impl_generics (
#props_arg
) #ret #(+ #lifetimes)*
) #ret
#where_clause
{
#body
@@ -655,14 +694,44 @@ impl Prop {
abort!(e.span(), e.to_string());
});
let name = if let Pat::Ident(i) = *typed.pat {
i
} else {
abort!(
typed.pat,
"only `prop: bool` style types are allowed within the \
`#[component]` macro"
);
let name = match *typed.pat {
Pat::Ident(i) => {
if let Some(name) = &prop_opts.name {
PatIdent {
attrs: vec![],
by_ref: None,
mutability: None,
ident: Ident::new(name, i.span()),
subpat: None,
}
} else {
i
}
}
Pat::Struct(_) | Pat::Tuple(_) | Pat::TupleStruct(_) => {
if let Some(name) = &prop_opts.name {
PatIdent {
attrs: vec![],
by_ref: None,
mutability: None,
ident: Ident::new(name, typed.pat.span()),
subpat: None,
}
} else {
abort!(
typed.pat,
"destructured props must be given a name e.g. \
#[prop(name = \"data\")]"
);
}
}
_ => {
abort!(
typed.pat,
"only `prop: bool` style types are allowed within the \
`#[component]` macro"
);
}
};
Self {
@@ -865,6 +934,7 @@ struct PropOpt {
default: Option<syn::Expr>,
into: bool,
attrs: bool,
name: Option<String>,
}
struct TypedBuilderOpts {

32
leptos_macro/src/lazy.rs Normal file
View File

@@ -0,0 +1,32 @@
use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use proc_macro2::Ident;
use proc_macro_error2::abort;
use quote::quote;
use syn::{spanned::Spanned, ItemFn};
pub fn lazy_impl(
_args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
let fun = syn::parse::<ItemFn>(s).unwrap_or_else(|e| {
abort!(e.span(), "`lazy` can only be used on a function")
});
if fun.sig.asyncness.is_none() {
abort!(
fun.sig.asyncness.span(),
"`lazy` can only be used on an async function"
)
}
let converted_name = Ident::new(
&fun.sig.ident.to_string().to_case(Case::Snake),
fun.sig.ident.span(),
);
quote! {
#[cfg_attr(feature = "split", wasm_split::wasm_split(#converted_name))]
#fun
}
.into()
}

View File

@@ -23,6 +23,7 @@ mod params;
mod view;
use crate::component::unmodified_fn_name_from_fn_name;
mod component;
mod lazy;
mod memo;
mod slice;
mod slot;
@@ -676,17 +677,21 @@ fn component_macro(
#[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)]
#unexpanded
}
} else if let Ok(mut dummy) = dummy {
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
quote! {
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)]
#dummy
}
} else {
quote! {}
}
.into()
match dummy {
Ok(mut dummy) => {
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
quote! {
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments, clippy::needless_lifetimes)]
#dummy
}
}
Err(e) => {
proc_macro_error2::abort!(e.span(), e);
}
}
}.into()
}
/// Annotates a struct so that it can be used with your Component as a `slot`.
@@ -1002,3 +1007,17 @@ pub fn slice(input: TokenStream) -> TokenStream {
pub fn memo(input: TokenStream) -> TokenStream {
memo::memo_impl(input)
}
/// The `#[lazy]` macro marks an `async` function as a function that can be lazy-loaded from a
/// separate (WebAssembly) binary.
///
/// The first time the function is called, calling the function will first load that other binary,
/// then call the function. On subsequent call it will be called immediately, but still return
/// asynchronously to maintain the same API.
///
/// All parameters and output types should be concrete types, with no generics.
#[proc_macro_attribute]
#[proc_macro_error]
pub fn lazy(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
lazy::lazy_impl(args, s)
}

View File

@@ -13,7 +13,13 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
.named
.iter()
.map(|field| {
let field_name_string = &field.ident.as_ref().expect("expected named struct fields").to_string();
let field_name_string = &field
.ident
.as_ref()
.expect("expected named struct fields")
.to_string()
.trim_start_matches("r#")
.to_owned();
let ident = &field.ident;
let ty = &field.ty;
let span = field.span();

View File

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

View File

@@ -154,7 +154,12 @@ fn is_inert_element(orig_node: &Node<impl CustomNode>) -> bool {
Some(value) => {
matches!(&value.value, KVAttributeValue::Expr(expr) if {
if let Expr::Lit(lit) = expr {
matches!(&lit.lit, Lit::Str(_))
let key = attr.key.to_string();
if key.starts_with("style:") || key.starts_with("prop:") || key.starts_with("on:") || key.starts_with("use:") || key.starts_with("bind") {
false
} else {
matches!(&lit.lit, Lit::Str(_))
}
} else {
false
}
@@ -1129,6 +1134,11 @@ pub(crate) fn attribute_absolute(
::leptos::tachys::html::attribute::custom::custom_attribute(#name, #value)
}
}
else if name == "node_ref" {
quote! {
::leptos::tachys::html::node_ref::#key(#value)
}
}
else {
quote! {
::leptos::tachys::html::attribute::#key(#value)
@@ -1169,8 +1179,7 @@ pub(crate) fn event_type_and_handler(
) -> (TokenStream, TokenStream, TokenStream) {
let handler = attribute_value(node, false);
let (event_type, is_custom, is_force_undelegated, is_targeted) =
parse_event_name(name);
let (event_type, is_custom, options) = parse_event_name(name);
let event_name_ident = match &node.key {
NodeName::Punctuated(parts) => {
@@ -1188,11 +1197,17 @@ pub(crate) fn event_type_and_handler(
}
_ => unreachable!(),
};
let capture_ident = match &node.key {
NodeName::Punctuated(parts) => {
parts.iter().find(|part| part.to_string() == "capture")
}
_ => unreachable!(),
};
let on = match &node.key {
NodeName::Punctuated(parts) => &parts[0],
_ => unreachable!(),
};
let on = if is_targeted {
let on = if options.targeted {
Ident::new("on_target", on.span()).to_token_stream()
} else {
on.to_token_stream()
@@ -1205,15 +1220,29 @@ pub(crate) fn event_type_and_handler(
event_type
};
let event_type = if is_force_undelegated {
let event_type = quote! {
::leptos::tachys::html::event::#event_type
};
let event_type = if options.captured {
let capture = if let Some(capture) = capture_ident {
quote! { #capture }
} else {
quote! { capture }
};
quote! { ::leptos::tachys::html::event::#capture(#event_type) }
} else {
event_type
};
let event_type = if options.undelegated {
let undelegated = if let Some(undelegated) = undelegated_ident {
quote! { #undelegated }
} else {
quote! { undelegated }
};
quote! { ::leptos::tachys::html::event::#undelegated(::leptos::tachys::html::event::#event_type) }
quote! { ::leptos::tachys::html::event::#undelegated(#event_type) }
} else {
quote! { ::leptos::tachys::html::event::#event_type }
event_type
};
(on, event_type, handler)
@@ -1419,13 +1448,22 @@ fn is_ambiguous_element(tag: &str) -> bool {
tag == "a" || tag == "script" || tag == "title"
}
fn parse_event(event_name: &str) -> (String, bool, bool) {
let is_undelegated = event_name.contains(":undelegated");
let is_targeted = event_name.contains(":target");
fn parse_event(event_name: &str) -> (String, EventNameOptions) {
let undelegated = event_name.contains(":undelegated");
let targeted = event_name.contains(":target");
let captured = event_name.contains(":capture");
let event_name = event_name
.replace(":undelegated", "")
.replace(":target", "");
(event_name, is_undelegated, is_targeted)
.replace(":target", "")
.replace(":capture", "");
(
event_name,
EventNameOptions {
undelegated,
targeted,
captured,
},
)
}
/// Escapes Rust keywords that are also HTML attribute names
@@ -1617,8 +1655,17 @@ const TYPED_EVENTS: [&str; 126] = [
const CUSTOM_EVENT: &str = "Custom";
pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool, bool) {
let (name, is_force_undelegated, is_targeted) = parse_event(name);
#[derive(Debug)]
pub(crate) struct EventNameOptions {
undelegated: bool,
targeted: bool,
captured: bool,
}
pub(crate) fn parse_event_name(
name: &str,
) -> (TokenStream, bool, EventNameOptions) {
let (name, options) = parse_event(name);
let (event_type, is_custom) = TYPED_EVENTS
.binary_search(&name.as_str())
@@ -1634,7 +1681,7 @@ pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool, bool) {
} else {
event_type
};
(event_type, is_custom, is_force_undelegated, is_targeted)
(event_type, is_custom, options)
}
fn convert_to_snake_case(name: String) -> String {
@@ -1651,7 +1698,7 @@ pub(crate) fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
.path
.segments
.iter()
.last()
.next_back()
.map(|segment| segment.ident.clone())
.expect("element needs to have a name"),
NodeName::Block(_) => {

View File

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

View File

@@ -0,0 +1,28 @@
use leptos::prelude::*;
use leptos_router::params::Params;
#[derive(PartialEq, Debug, Params)]
struct UserInfo {
user_id: Option<String>,
email: Option<String>,
r#type: Option<i32>,
not_found: Option<i32>,
}
#[test]
fn params_test() {
let mut map = leptos_router::params::ParamsMap::new();
map.insert("user_id", "12".to_owned());
map.insert("email", "em@il".to_owned());
map.insert("type", "12".to_owned());
let user_info = UserInfo::from_map(&map).unwrap();
assert_eq!(
UserInfo {
email: Some("em@il".to_owned()),
user_id: Some("12".to_owned()),
r#type: Some(12),
not_found: None,
},
user_info
);
}

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ edition.workspace = true
[dependencies]
base64 = "0.22.1"
codee = { version = "0.2.0", features = ["json_serde"] }
codee = { version = "0.3.0", features = ["json_serde"] }
hydration_context = { workspace = true }
reactive_graph = { workspace = true, features = ["hydration"] }
server_fn = { workspace = true }
@@ -44,3 +44,6 @@ denylist = ["tracing"]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

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

View File

@@ -8,8 +8,11 @@ use reactive_graph::{
ToAnySource, ToAnySubscriber,
},
owner::use_context,
signal::guards::{AsyncPlain, ReadGuard},
traits::{DefinedAt, IsDisposed, ReadUntracked},
signal::{
guards::{AsyncPlain, ReadGuard},
ArcRwSignal, RwSignal,
},
traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Update, Write},
};
use send_wrapper::SendWrapper;
use std::{
@@ -20,7 +23,8 @@ use std::{
/// A reference-counted resource that only loads its data locally on the client.
pub struct ArcLocalResource<T> {
data: ArcAsyncDerived<SendWrapper<T>>,
#[cfg(debug_assertions)]
refetch: ArcRwSignal<usize>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -28,7 +32,8 @@ impl<T> Clone for ArcLocalResource<T> {
fn clone(&self) -> Self {
Self {
data: self.data.clone(),
#[cfg(debug_assertions)]
refetch: self.refetch.clone(),
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
}
}
@@ -65,15 +70,27 @@ impl<T> ArcLocalResource<T> {
}
};
let fetcher = SendWrapper::new(fetcher);
Self {
data: ArcAsyncDerived::new(move || {
let refetch = ArcRwSignal::new(0);
let data = {
let refetch = refetch.clone();
ArcAsyncDerived::new(move || {
refetch.track();
let fut = fetcher();
SendWrapper::new(async move { SendWrapper::new(fut.await) })
}),
#[cfg(debug_assertions)]
})
};
Self {
data,
refetch,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
/// Re-runs the async function.
pub fn refetch(&self) {
*self.refetch.write() += 1;
}
}
impl<T> IntoFuture for ArcLocalResource<T>
@@ -104,11 +121,11 @@ where
impl<T> DefinedAt for ArcLocalResource<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -200,7 +217,8 @@ impl<T> Subscriber for ArcLocalResource<T> {
/// A resource that only loads its data locally on the client.
pub struct LocalResource<T> {
data: AsyncDerived<SendWrapper<T>>,
#[cfg(debug_assertions)]
refetch: RwSignal<usize>,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -242,6 +260,7 @@ impl<T> LocalResource<T> {
}
}
};
let refetch = RwSignal::new(0);
Self {
data: if cfg!(feature = "ssr") {
@@ -249,14 +268,21 @@ impl<T> LocalResource<T> {
} else {
let fetcher = SendWrapper::new(fetcher);
AsyncDerived::new(move || {
refetch.track();
let fut = fetcher();
SendWrapper::new(async move { SendWrapper::new(fut.await) })
})
},
#[cfg(debug_assertions)]
refetch,
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
}
}
/// Re-runs the async function.
pub fn refetch(&self) {
self.refetch.try_update(|n| *n += 1);
}
}
impl<T> IntoFuture for LocalResource<T>
@@ -287,11 +313,11 @@ where
impl<T> DefinedAt for LocalResource<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -398,7 +424,8 @@ impl<T: 'static> From<ArcLocalResource<T>> for LocalResource<T> {
fn from(arc: ArcLocalResource<T>) -> Self {
Self {
data: arc.data.into(),
#[cfg(debug_assertions)]
refetch: arc.refetch.into(),
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: arc.defined_at,
}
}
@@ -408,7 +435,8 @@ impl<T: 'static> From<LocalResource<T>> for ArcLocalResource<T> {
fn from(local: LocalResource<T>) -> Self {
Self {
data: local.data.into(),
#[cfg(debug_assertions)]
refetch: local.refetch.into(),
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: local.defined_at,
}
}

View File

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

View File

@@ -60,7 +60,7 @@ pub struct ArcOnceResource<T, Ser = JsonSerdeCodec> {
suspenses: Arc<RwLock<Vec<SuspenseContext>>>,
loading: Arc<AtomicBool>,
ser: PhantomData<fn() -> Ser>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -73,7 +73,7 @@ impl<T, Ser> Clone for ArcOnceResource<T, Ser> {
suspenses: self.suspenses.clone(),
loading: self.loading.clone(),
ser: self.ser,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
}
}
@@ -140,7 +140,7 @@ where
wakers,
suspenses,
ser: PhantomData,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
};
@@ -183,11 +183,11 @@ impl<T, Ser> ArcOnceResource<T, Ser> {
impl<T, Ser> DefinedAt for ArcOnceResource<T, Ser> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
@@ -272,7 +272,7 @@ where
#[track_caller]
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
let _guard = SpecialNonReactiveZone::enter();
let waker = cx.waker();
self.source.track();
@@ -491,7 +491,7 @@ where
#[derive(Debug)]
pub struct OnceResource<T, Ser = JsonSerdeCodec> {
inner: ArenaItem<ArcOnceResource<T, Ser>>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -524,13 +524,13 @@ where
fut: impl Future<Output = T> + Send + 'static,
blocking: bool,
) -> Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
let defined_at = Location::caller();
Self {
inner: ArenaItem::new(ArcOnceResource::new_with_options(
fut, blocking,
)),
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at,
}
}
@@ -551,11 +551,11 @@ where
impl<T, Ser> DefinedAt for OnceResource<T, Ser> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.7.0"
version = "0.7.7"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -26,9 +26,13 @@ features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
default = []
ssr = []
tracing = ["dep:tracing"]
nonce = ["leptos/nonce"]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

@@ -37,20 +37,17 @@
//! }
//! ```
//! # Feature Flags
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
//! - `stable` By default, Leptos requires `nightly` Rust, which is what allows the ergonomics
//! of calling signals as functions. Enable this feature to support `stable` Rust.
//! - `tracing` Adds integration with the `tracing` crate.
//!
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in.
//! **Important Note:** If youre using server-side rendering, you should enable `ssr`.
use futures::{Stream, StreamExt};
use leptos::{
attr::NextAttribute,
component,
logging::debug_warn,
oco::Oco,
reactive::owner::{provide_context, use_context},
tachys::{
dom::document,
@@ -570,3 +567,25 @@ impl RenderHtml for MetaTagsView {
) -> Self::State {
}
}
pub(crate) trait OrDefaultNonce {
fn or_default_nonce(self) -> Option<Oco<'static, str>>;
}
impl OrDefaultNonce for Option<Oco<'static, str>> {
fn or_default_nonce(self) -> Option<Oco<'static, str>> {
#[cfg(feature = "nonce")]
{
use leptos::nonce::use_nonce;
match self {
Some(nonce) => Some(nonce),
None => use_nonce().map(|n| Arc::clone(n.as_inner()).into()),
}
}
#[cfg(not(feature = "nonce"))]
{
self
}
}
}

View File

@@ -1,4 +1,4 @@
use crate::register;
use crate::{register, OrDefaultNonce};
use leptos::{
component, oco::Oco, prelude::*, tachys::html::element::script, IntoView,
};
@@ -74,7 +74,7 @@ pub fn Script(
.fetchpriority(fetchpriority)
.integrity(integrity)
.nomodule(nomodule)
.nonce(nonce)
.nonce(nonce.or_default_nonce())
.referrerpolicy(referrerpolicy)
.src(src)
.r#type(type_)

View File

@@ -1,4 +1,4 @@
use crate::register;
use crate::{register, OrDefaultNonce};
use leptos::{
component, oco::Oco, prelude::*, tachys::html::element::style, IntoView,
};
@@ -48,7 +48,7 @@ pub fn Style(
style()
.id(id)
.media(media)
.nonce(nonce)
.nonce(nonce.or_default_nonce())
.title(title)
.blocking(blocking)
.child(children.map(|c| c())),

View File

@@ -0,0 +1,14 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

View File

@@ -0,0 +1,111 @@
[package]
name = "leptos-hexagonal-design"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
leptos = { version = "0.7.0" }
leptos_router = { version = "0.7.0" }
axum = { version = "0.7", optional = true }
console_error_panic_hook = "0.1"
leptos_axum = { version = "0.7.0", optional = true }
leptos_meta = { version = "0.7.0" }
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
wasm-bindgen = "=0.2.99"
thiserror = "1"
tracing = { version = "0.1", optional = true }
http = "1"
mockall = "0.13.1"
cfg-if = "1.0.0"
serde = "1.0.215"
pin-project-lite = "0.2.15"
[features]
config_1 = []
hydrate = ["leptos/hydrate"]
ssr = [
"dep:axum",
"dep:tokio",
"dep:tower",
"dep:tower-http",
"dep:leptos_axum",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:tracing",
]
# Defines a size-optimized profile for the WASM bundle in release mode
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "leptos-hexagonal-design"
# 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/main.scss"
# Assets source dir. All files found here will be copied and synchronized to site-root.
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
#
# Optional. Env: LEPTOS_ASSETS_DIR.
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.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# 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
# The profile to use for the lib target when compiling for release
#
# Optional. Defaults to "release".
lib-profile-release = "wasm-release"

View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
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 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.
For more information, please refer to <https://unlicense.org>

View File

@@ -0,0 +1,142 @@
### Leptos Hexagonal Design
This Blog Post / Github Repository is about applying principles of hexagonal design
- Isolating Business Logic from Sub Domains
- Decoupling design to improve flexibility and testablity
- Applying the principles hierachically so that sub domains which talk to external services also implement also implement hexagonal architecture
There are specific constraints that guide our design decisions
- Server Functions Can't be Generic
- Boxed Traits Objects Have overhead, so we only want to use as much generic code as possible avoid Trait Objects
The way this works is we define the functionality of our program in the main domain (i.e the business problem and processes our app is trying to solve / proceduralize). We then create sub domains and external services, although they are represented the same. External services are usually the end nodes of your app's architectural graph. Our main application builds it's service layout using configuration flags.
```rust
pub fn config() -> MainAppHandlerAlias {
cfg_if::cfg_if! {
if #[cfg(feature="open_ai_wrapper")] {
fn server_handler_config_1() -> MainAppHandler<
AuthService<PostgresDb, Redis>,
AiMessageGen<PostgresDb,OpenAiWrapper>,
> {
MainAppHandler::new_with_postgres_and_redis_open_ai()
}
server_handler_config_1()
} else {
fn server_handler_config_2() -> MainAppHandler<
AuthService<MySql, MemCache>,
OtherAiMessageGen<MySql,HuggingFaceWrapper>,
> {
MainAppHandler::new_with_my_sql_memcache_hugging_face()
}
server_handler_config_2()
}
}
}
```
And we pass in our handler which implements a trait
```rust
pub trait HandlerServerFn {
pub fn server_fn_1_inner(&self);
}
impl<S,S2> HandlerServerFn for MainAppHandler<S:SubDomain1Trait,S2:SubDomain2Trait> {
pub fn server_fn_1_inner(&self) {
// do thing
}
}
```
in our main fn we produce our applications service graph and pass it to our leptos router.
```rust
main () {
let leptos_options = conf.leptos_options;
let routes = generate_route_list(crate::app::App);
// our feature flag based config function.
let handler = config();
let handler_c = handler.clone();
// we implement FromRef<ServerState> for LeptosOptions
let server_state = ServerState {
handler,
leptos_options: leptos_options.clone(),
};
let app = Router::new()
.leptos_routes_with_context(
&server_state,
routes,
// We pass in the MainAppHandler struct as context so we can fetch it anywhere context is available on the server.
// This includes in middleware we define on server functions (see middleware.rs)
move || provide_context(handler_c.clone()),
{
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
},
)
.fallback(leptos_axum::file_and_error_handler::<
ServerState<HandlerStructAlias>,
_,
>(shell))
.with_state(server_state);
}
```
and then in our server functions
```rust
#[server]
pub async fn server_fn_1() -> Result<(),ServerFnError> {
// we type alias every variation of our services we plan on configuring. The alternative is using Box<dyn Trait> which isn't bad - just slower.
Ok(expect_context::<MainAppHandlerAlias>().server_fn_1_inner())
}
```
And then we can mock and service trait in any combination like so
```rust
#[tokio::test]
pub async fn test_subdomain_1_with_mocks() -> Result<(), Box<dyn Error>> {
let mut mock_external_service_1 = MockExternalServiceTrait1::new();
mock_external_service_1
.expect_external_service_1_method()
.returning(|| {
println!("Mock external service 1");
Ok(ExternalService1Data)
});
let mut mock_external_service_2 = MockExternalServiceTrait2::new();
mock_external_service_2
.expect_external_service_2_method()
.returning(|| {
println!("Mock external service 2");
Ok(ExternalService2Data)
});
let real_subdomain_1_with_mock_externals = SubDomainStruct1 {
external_service_1: mock_external_service_1,
external_service_2: mock_external_service_2,
};
let data = real_subdomain_1_with_mock_externals
.sub_domain_1_method()
.await?;
assert_eq!(data, SubDomain1Data);
Ok(())
}
```
Check out the code in the repository for a working example.
Run the tests with
` cargo test --features ssr `
and otherwise run
` cargo leptos serve `
and navigate to `127.0.0.1:3000`
here's a picture
![alt text](leptos_hexagonal_architecture.png)

View File

@@ -0,0 +1,167 @@
{
"name": "end2end",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "end2end",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.44.1",
"@types/node": "^20.12.12",
"typescript": "^5.4.5"
}
},
"node_modules/@playwright/test": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.44.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@types/node": {
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.44.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true,
"license": "MIT"
}
},
"dependencies": {
"@playwright/test": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
"requires": {
"playwright": "1.44.1"
}
},
"@types/node": {
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"
}
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"playwright": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
"playwright-core": "1.44.1"
}
},
"playwright-core": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true
},
"typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true
},
"undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
}
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "end2end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.44.1",
"@types/node": "^20.12.12",
"typescript": "^5.4.5"
}
}

View File

@@ -0,0 +1,105 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices, defineConfig } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
});

View File

@@ -0,0 +1,9 @@
import { test, expect } from "@playwright/test";
test("homepage has title and heading text", async ({ page }) => {
await page.goto("http://localhost:3000/");
await expect(page).toHaveTitle("Welcome to Leptos");
await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
});

View File

@@ -0,0 +1,109 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,87 @@
use leptos::prelude::*;
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
use leptos_router::{
components::{Route, Router, Routes},
StaticSegment,
};
#[cfg(feature = "ssr")]
use super::{server_types::HandlerStructAlias, traits::HandlerTrait};
use crate::ui_types::*;
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/>
<MetaTags/>
</head>
<body>
<App/>
</body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
view! {
// injects a stylesheet into the document <head>
// id=leptos means cargo-leptos will hot-reload this stylesheet
<Stylesheet id="leptos" href="/pkg/leptos-hexagonal-design.css"/>
// sets the document title
<Title text="Welcome to Leptos"/>
// content for this welcome page
<Router>
<main>
<Routes fallback=|| "Page not found.".into_view()>
<Route path=StaticSegment("") view=HomePage/>
</Routes>
</main>
</Router>
}
}
/// Renders the home page of your application.
#[component]
fn HomePage() -> impl IntoView {
let server_fn_1 = ServerAction::<ServerFunction1>::new();
let server_fn_2 = ServerAction::<ServerFunction2>::new();
let server_fn_3 = ServerAction::<ServerFunction3>::new();
Effect::new(move |_| {
server_fn_1.dispatch(ServerFunction1 {});
server_fn_2.dispatch(ServerFunction2 {});
server_fn_3.dispatch(ServerFunction3 {});
});
}
#[server]
#[middleware(crate::middleware::SubDomain1Layer)]
pub async fn server_function_1() -> Result<UiMappingFromDomainData, ServerFnError> {
Ok(expect_context::<HandlerStructAlias>()
.server_fn_1()
.await?
.into())
}
#[server]
pub async fn server_function_2() -> Result<UiMappingFromDomainData2, ServerFnError> {
Ok(expect_context::<HandlerStructAlias>()
.server_fn_2()
.await?
.into())
}
#[server]
pub async fn server_function_3() -> Result<UiMappingFromDomainData3, ServerFnError> {
Ok(expect_context::<HandlerStructAlias>()
.server_fn_3()
.await?
.into())
}

View File

@@ -0,0 +1,23 @@
use super::server_types::*;
pub fn config() -> HandlerStructAlias {
cfg_if::cfg_if! {
if #[cfg(feature="config_1")] {
fn server_handler_config_1() -> HandlerStruct<
SubDomainStruct1<ExternalService1_1, ExternalService2_1>,
SubDomainStruct2<ExternalService1_1>,
> {
HandlerStruct::default()
}
server_handler_config_1()
} else {
fn server_handler_config_2() -> HandlerStruct<
SubDomainStruct1<ExternalService1_2, ExternalService2_2>,
SubDomainStruct2<ExternalService1_2>,
> {
HandlerStruct::new()
}
server_handler_config_2()
}
}
}

View File

@@ -0,0 +1,150 @@
pub mod app;
pub mod ui_types;
#[cfg(feature = "ssr")]
pub mod config;
#[cfg(feature = "ssr")]
pub mod middleware;
#[cfg(feature = "ssr")]
pub mod server_types;
#[cfg(feature = "ssr")]
pub mod trait_impl;
#[cfg(feature = "ssr")]
pub mod traits;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::app::*;
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(App);
}
#[cfg(test)]
pub mod tests {
use super::server_types::*;
use super::traits::*;
use std::error::Error;
#[tokio::test]
pub async fn test_subdomain_1_with_mocks() -> Result<(), Box<dyn Error>> {
let mut mock_external_service_1 = MockExternalServiceTrait1::new();
mock_external_service_1
.expect_external_service_1_method()
.returning(|| {
println!("Mock external service 1");
Ok(ExternalService1Data)
});
let mut mock_external_service_2 = MockExternalServiceTrait2::new();
mock_external_service_2
.expect_external_service_2_method()
.returning(|| {
println!("Mock external service 2");
Ok(ExternalService2Data)
});
let real_subdomain_1_with_mock_externals = SubDomainStruct1 {
external_service_1: mock_external_service_1,
external_service_2: mock_external_service_2,
};
let data = real_subdomain_1_with_mock_externals
.sub_domain_1_method()
.await?;
assert_eq!(data, SubDomain1Data);
Ok(())
}
#[tokio::test]
pub async fn test_subdomain_2_with_mocks() -> Result<(), Box<dyn Error>> {
let mut mock_external_service_1 = MockExternalServiceTrait1::new();
mock_external_service_1
.expect_external_service_1_method()
.returning(|| {
println!("Mock external service 1 AGAIN");
Ok(ExternalService1Data)
});
let real_subdomain_2_with_mock_externals = SubDomainStruct2 {
external_service_1: mock_external_service_1,
};
let data = real_subdomain_2_with_mock_externals
.sub_domain_2_method()
.await?;
assert_eq!(data, SubDomain2Data);
Ok(())
}
#[tokio::test]
pub async fn test_handler_with_mocks() -> Result<(), Box<dyn Error>> {
let mut mock_subdomain_1_trait = MockSubDomainTrait1::new();
mock_subdomain_1_trait
.expect_sub_domain_1_method()
.returning(|| {
println!("Mock Subdomain 1");
Ok(SubDomain1Data)
});
let mut mock_subdomain_2_trait = MockSubDomainTrait2::new();
mock_subdomain_2_trait
.expect_sub_domain_2_method()
.returning(|| {
println!("Mock Subdomain 2");
Ok(SubDomain2Data)
});
let real_handler_with_mock_subdomains = HandlerStruct {
sub_domain_1: mock_subdomain_1_trait,
sub_domain_2: mock_subdomain_2_trait,
};
let data = real_handler_with_mock_subdomains.server_fn_1().await?;
assert_eq!(data, DomainData);
let data = real_handler_with_mock_subdomains.server_fn_2().await?;
assert_eq!(data, DomainData2);
let data = real_handler_with_mock_subdomains.server_fn_3().await?;
assert_eq!(data, DomainData3);
Ok(())
}
fn mock_subdomain_1() -> SubDomainStruct1<MockExternalServiceTrait1, MockExternalServiceTrait2>
{
let mut mock_external_service_1 = MockExternalServiceTrait1::new();
mock_external_service_1
.expect_external_service_1_method()
.returning(|| {
println!("Mock external service 1");
Ok(ExternalService1Data)
});
let mut mock_external_service_2 = MockExternalServiceTrait2::new();
mock_external_service_2
.expect_external_service_2_method()
.returning(|| {
println!("Mock external service 2");
Ok(ExternalService2Data)
});
let real_subdomain_1_with_mock_externals = SubDomainStruct1 {
external_service_1: mock_external_service_1,
external_service_2: mock_external_service_2,
};
real_subdomain_1_with_mock_externals
}
#[tokio::test]
pub async fn test_handler_with_mock_and_real_mix() -> Result<(), Box<dyn Error>> {
let sub_domain_1 = mock_subdomain_1();
let mut mock_subdomain_2_trait = MockSubDomainTrait2::new();
mock_subdomain_2_trait
.expect_sub_domain_2_method()
.returning(|| {
println!("Mock Subdomain 2");
Ok(SubDomain2Data)
});
let real_handler = HandlerStruct {
sub_domain_1,
sub_domain_2: mock_subdomain_2_trait,
};
let data = real_handler.server_fn_1().await?;
assert_eq!(data, DomainData);
let data = real_handler.server_fn_2().await?;
assert_eq!(data, DomainData2);
let data = real_handler.server_fn_3().await?;
assert_eq!(data, DomainData3);
Ok(())
}
}

View File

@@ -0,0 +1,52 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use leptos::logging::log;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use leptos_hexagonal_design::{
app::*,
config::config,
server_types::{HandlerStructAlias, ServerState},
};
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
let routes = generate_route_list(App);
let handler = config();
let handler_c = handler.clone();
let server_state = ServerState {
handler,
leptos_options: leptos_options.clone(),
};
let app = Router::new()
.leptos_routes_with_context(
&server_state,
routes,
move || provide_context(handler_c.clone()),
{
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
},
)
.fallback(leptos_axum::file_and_error_handler::<
ServerState<HandlerStructAlias>,
_,
>(shell))
.with_state(server_state);
log!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for pure client-side testing
// see lib.rs for hydration function instead
}

View File

@@ -0,0 +1,84 @@
use axum::{
body::Body,
http::{Request, Response},
};
use leptos::prelude::expect_context;
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use tower::{Layer, Service};
use crate::{
server_types::{HandlerStructAlias, ServerState},
traits::SubDomainTrait1,
};
use pin_project_lite::pin_project;
#[derive(Clone)]
pub struct SubDomain1Layer;
impl<S> Layer<S> for SubDomain1Layer {
type Service = SubDomain1MiddleWare<S>;
fn layer(&self, inner: S) -> Self::Service {
SubDomain1MiddleWare { inner }
}
}
pub struct SubDomain1MiddleWare<S> {
inner: S,
}
impl<S, ReqBody> Service<Request<ReqBody>> for SubDomain1MiddleWare<S>
where
S: Service<Request<ReqBody>, Response = Response<Body>>,
S::Error: std::fmt::Debug,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = SubDomain1Future<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let req_fut = self.inner.call(req);
SubDomain1Future { req_fut }
}
}
pin_project! {
pub struct SubDomain1Future<F> {
#[pin]
req_fut: F,
}
}
impl<F, Err> Future for SubDomain1Future<F>
where
F: Future<Output = Result<Response<Body>, Err>>,
{
type Output = Result<Response<Body>, Err>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let subdomain_1 = expect_context::<ServerState<HandlerStructAlias>>()
.handler
.sub_domain_1;
let mut subdomain_1_fut = subdomain_1.sub_domain_1_method();
match Pin::as_mut(&mut subdomain_1_fut).poll(cx) {
Poll::Ready(Ok(_)) => {
println!("Middleware for Subdomain 1 Passed, calling request...");
this.req_fut.poll(cx)
}
Poll::Ready(Err(_)) => Poll::Ready(Ok(Response::builder()
.status(http::StatusCode::FORBIDDEN)
.body(Body::from("Access denied"))
.unwrap())),
Poll::Pending => Poll::Pending,
}
}
}

View File

@@ -0,0 +1,102 @@
use super::traits::*;
use leptos::config::LeptosOptions;
use thiserror::Error;
#[derive(Clone)]
pub struct ServerState<Handler: HandlerTrait> {
pub handler: Handler,
pub leptos_options: LeptosOptions,
}
#[cfg(feature = "config_1")]
pub type HandlerStructAlias = HandlerStruct<
SubDomainStruct1<ExternalService1_1, ExternalService2_1>,
SubDomainStruct2<ExternalService1_1>,
>;
#[cfg(not(feature = "config_1"))]
pub type HandlerStructAlias = HandlerStruct<
SubDomainStruct1<ExternalService1_2, ExternalService2_2>,
SubDomainStruct2<ExternalService1_2>,
>;
#[derive(Clone, Default)]
pub struct HandlerStruct<SubDomain1: SubDomainTrait1, SubDomain2: SubDomainTrait2> {
pub sub_domain_1: SubDomain1,
pub sub_domain_2: SubDomain2,
}
#[derive(Clone, Default)]
pub struct SubDomainStruct1<
ExternalService1: ExternalServiceTrait1,
ExternalService2: ExternalServiceTrait2,
> {
pub external_service_1: ExternalService1,
pub external_service_2: ExternalService2,
}
#[derive(Clone, Default)]
pub struct SubDomainStruct2<ExternalService1: ExternalServiceTrait1> {
pub external_service_1: ExternalService1,
}
#[derive(Clone, Default)]
pub struct ExternalService1_1;
#[derive(Clone, Default)]
pub struct ExternalService1_2;
#[derive(Clone, Default)]
pub struct ExternalService2_1;
#[derive(Clone, Default)]
pub struct ExternalService2_2;
#[derive(Clone, Default)]
pub struct ExternalService1;
#[derive(Clone, PartialEq, Debug)]
pub struct DomainData;
#[derive(Clone, PartialEq, Debug)]
pub struct DomainData2;
#[derive(Clone, PartialEq, Debug)]
pub struct DomainData3;
#[derive(Clone, PartialEq, Debug)]
pub struct SubDomain1Data;
#[derive(Clone, PartialEq, Debug)]
pub struct SubDomain2Data;
#[derive(Clone)]
pub struct ExternalService1Data;
#[derive(Clone)]
pub struct ExternalService2Data;
#[derive(Clone, Error, Debug)]
pub enum DomainError {
#[error("Underlying Subdomain 1 Error")]
SubDomain1Error(#[from] SubDomain1Error),
#[error("Underlying Subdomain 2 Error")]
SubDomain2Error(#[from] SubDomain2Error),
}
#[derive(Clone, Error, Debug)]
pub enum SubDomain1Error {
#[error("Sub Domain 1 Error")]
SubDomain1Error,
#[error("Underlying Service 1")]
ExternalService1Error(#[from] ExternalService1Error),
#[error("Underlying Service 2")]
ExternalService2Error(#[from] ExternalService2Error),
}
#[derive(Clone, Error, Debug)]
pub enum SubDomain2Error {
#[error("Sub Domain 2 Error")]
SubDomain2Error,
#[error("Underlying Service 1")]
ExternalService1Error(#[from] ExternalService1Error),
}
#[derive(Clone, Error, Debug)]
pub enum ExternalService1Error {
#[error("Service 1 Error")]
Error,
}
#[derive(Clone, Error, Debug)]
pub enum ExternalService2Error {
#[error("Service 2 Error")]
Error,
}

View File

@@ -0,0 +1,149 @@
use crate::ui_types::*;
use super::server_types::*;
use super::traits::*;
use axum::async_trait;
use axum::extract::FromRef;
use leptos::config::LeptosOptions;
// So we can pass our server state as state into our leptos router.
impl<Handler: HandlerTrait + Clone> FromRef<ServerState<Handler>> for LeptosOptions {
fn from_ref(input: &ServerState<Handler>) -> Self {
input.leptos_options.clone()
}
}
#[async_trait]
impl<SubDomain1, SubDomain2> HandlerTrait for HandlerStruct<SubDomain1, SubDomain2>
where
SubDomain1: SubDomainTrait1 + Send + Sync,
SubDomain2: SubDomainTrait2 + Send + Sync,
{
async fn server_fn_1(&self) -> Result<DomainData, DomainError> {
Ok(self.sub_domain_1.sub_domain_1_method().await?.into())
}
async fn server_fn_2(&self) -> Result<DomainData2, DomainError> {
Ok(self.sub_domain_2.sub_domain_2_method().await?.into())
}
async fn server_fn_3(&self) -> Result<DomainData3, DomainError> {
Ok((
self.sub_domain_1.sub_domain_1_method().await?,
self.sub_domain_2.sub_domain_2_method().await?,
)
.into())
}
}
#[async_trait]
impl<ExternalService1, ExternalService2> SubDomainTrait1
for SubDomainStruct1<ExternalService1, ExternalService2>
where
ExternalService1: ExternalServiceTrait1 + Send + Sync,
ExternalService2: ExternalServiceTrait2 + Send + Sync,
{
async fn sub_domain_1_method(&self) -> Result<SubDomain1Data, SubDomain1Error> {
Ok((
self.external_service_1.external_service_1_method().await?,
self.external_service_2.external_service_2_method().await?,
)
.into())
}
}
#[async_trait]
impl<ExternalService1> SubDomainTrait2 for SubDomainStruct2<ExternalService1>
where
ExternalService1: ExternalServiceTrait1 + Send + Sync,
{
async fn sub_domain_2_method(&self) -> Result<SubDomain2Data, SubDomain2Error> {
Ok(self
.external_service_1
.external_service_1_method()
.await?
.into())
}
}
#[async_trait]
impl ExternalServiceTrait1 for ExternalService1_1 {
async fn external_service_1_method(
&self,
) -> Result<ExternalService1Data, ExternalService1Error> {
println!("External Service 1 From External Service 1_1");
Ok(ExternalService1Data)
}
}
#[async_trait]
impl ExternalServiceTrait1 for ExternalService1_2 {
async fn external_service_1_method(
&self,
) -> Result<ExternalService1Data, ExternalService1Error> {
println!("External Service 1 From External Service 1_2");
Ok(ExternalService1Data)
}
}
#[async_trait]
impl ExternalServiceTrait2 for ExternalService2_1 {
async fn external_service_2_method(
&self,
) -> Result<ExternalService2Data, ExternalService2Error> {
println!("External Service 2 From External Service 2_1");
Ok(ExternalService2Data)
}
}
#[async_trait]
impl ExternalServiceTrait2 for ExternalService2_2 {
async fn external_service_2_method(
&self,
) -> Result<ExternalService2Data, ExternalService2Error> {
println!("External Service 2 From External Service 2_2");
Ok(ExternalService2Data)
}
}
// Sub Domain mapping
impl From<(ExternalService1Data, ExternalService2Data)> for SubDomain1Data {
fn from(_: (ExternalService1Data, ExternalService2Data)) -> Self {
Self
}
}
impl From<ExternalService1Data> for SubDomain2Data {
fn from(_: ExternalService1Data) -> Self {
Self
}
}
// Domain Mapping
impl From<SubDomain1Data> for DomainData {
fn from(_: SubDomain1Data) -> Self {
Self
}
}
impl From<SubDomain2Data> for DomainData2 {
fn from(_: SubDomain2Data) -> Self {
Self
}
}
impl From<(SubDomain1Data, SubDomain2Data)> for DomainData3 {
fn from(_: (SubDomain1Data, SubDomain2Data)) -> Self {
Self
}
}
// Ui Mapping
impl From<DomainData> for UiMappingFromDomainData {
fn from(_: DomainData) -> Self {
Self
}
}
impl From<DomainData2> for UiMappingFromDomainData2 {
fn from(_: DomainData2) -> Self {
Self
}
}
impl From<DomainData3> for UiMappingFromDomainData3 {
fn from(_: DomainData3) -> Self {
Self
}
}

View File

@@ -0,0 +1,42 @@
use super::server_types::*;
use axum::async_trait;
use mockall::automock;
pub trait New {
fn new() -> Self;
}
#[automock]
#[async_trait]
pub trait HandlerTrait {
async fn server_fn_1(&self) -> Result<DomainData, DomainError>;
async fn server_fn_2(&self) -> Result<DomainData2, DomainError>;
async fn server_fn_3(&self) -> Result<DomainData3, DomainError>;
}
#[automock]
#[async_trait]
pub trait SubDomainTrait1 {
async fn sub_domain_1_method(&self) -> Result<SubDomain1Data, SubDomain1Error>;
}
#[automock]
#[async_trait]
pub trait SubDomainTrait2 {
async fn sub_domain_2_method(&self) -> Result<SubDomain2Data, SubDomain2Error>;
}
#[automock]
#[async_trait]
pub trait ExternalServiceTrait1 {
async fn external_service_1_method(
&self,
) -> Result<ExternalService1Data, ExternalService1Error>;
}
#[automock]
#[async_trait]
pub trait ExternalServiceTrait2 {
async fn external_service_2_method(
&self,
) -> Result<ExternalService2Data, ExternalService2Error>;
}

View File

@@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct UiMappingFromDomainData;
#[derive(Serialize, Deserialize)]
pub struct UiMappingFromDomainData2;
#[derive(Serialize, Deserialize)]
pub struct UiMappingFromDomainData3;

View File

@@ -0,0 +1,4 @@
body {
font-family: sans-serif;
text-align: center;
}

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.1.0"
version = "0.1.7"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -25,7 +25,7 @@ async-lock = "3.4.0"
send_wrapper = { version = "0.6.0", features = ["futures"] }
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
web-sys = "0.3.72"
web-sys = { version = "0.3.72", features = ["console"] }
[dev-dependencies]
tokio = { version = "1.41", features = ["rt-multi-thread", "macros"] }
@@ -47,3 +47,6 @@ rustdoc-args = ["--cfg", "docsrs"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }

View File

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

View File

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

View File

@@ -15,7 +15,6 @@ use crate::{
};
pub use arc_memo::*;
pub use async_derived::*;
pub(crate) use inner::MemoInner;
pub use memo::*;
pub use selector::*;

View File

@@ -12,11 +12,10 @@ use crate::{
traits::{DefinedAt, Get, IsDisposed, ReadUntracked},
};
use core::fmt::Debug;
use or_poisoned::OrPoisoned;
use std::{
hash::Hash,
panic::Location,
sync::{Arc, RwLock, Weak},
sync::{Arc, Weak},
};
/// An efficient derived reactive value based on other reactive values.
@@ -93,9 +92,9 @@ pub struct ArcMemo<T, S = SyncStorage>
where
S: Storage<T>,
{
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: Arc<RwLock<MemoInner<T, S>>>,
inner: Arc<MemoInner<T, S>>,
}
impl<T: 'static> ArcMemo<T, SyncStorage>
@@ -161,10 +160,10 @@ where
Weak::clone(weak) as Weak<dyn Subscriber + Send + Sync>,
);
RwLock::new(MemoInner::new(Arc::new(fun), subscriber))
MemoInner::new(Arc::new(fun), subscriber)
});
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner,
}
@@ -177,7 +176,7 @@ where
{
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
inner: Arc::clone(&self.inner),
}
@@ -190,11 +189,11 @@ where
{
#[inline(always)]
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -272,7 +271,7 @@ where
AnySource(
Arc::as_ptr(&self.inner) as usize,
Arc::downgrade(&self.inner) as Weak<dyn Source + Send + Sync>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
self.defined_at,
)
}
@@ -312,15 +311,11 @@ where
S: Storage<T>,
{
fn add_source(&self, source: AnySource) {
self.inner.write().or_poisoned().sources.insert(source);
self.inner.add_source(source);
}
fn clear_sources(&self, subscriber: &AnySubscriber) {
self.inner
.write()
.or_poisoned()
.sources
.clear_sources(subscriber);
self.inner.clear_sources(subscriber);
}
}
@@ -328,15 +323,15 @@ impl<T: 'static, S> ReadUntracked for ArcMemo<T, S>
where
S: Storage<T>,
{
type Value = ReadGuard<T, Mapped<Plain<MemoInner<T, S>>, T>>;
type Value = ReadGuard<T, Mapped<Plain<Option<S::Wrapped>>, T>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
self.update_if_necessary();
Mapped::try_new(Arc::clone(&self.inner), |t| {
Mapped::try_new(Arc::clone(&self.inner.value), |t| {
// safe to unwrap here because update_if_necessary
// guarantees the value is Some
t.value.as_ref().unwrap().as_borrowed()
t.as_ref().unwrap().as_borrowed()
})
.map(ReadGuard::new)
}

View File

@@ -107,7 +107,7 @@ use std::{
/// - [`IntoFuture`](std::future::Future) allows you to create a [`Future`] that resolves
/// when this resource is done loading.
pub struct ArcAsyncDerived<T> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
pub(crate) defined_at: &'static Location<'static>,
// the current state of this signal
pub(crate) value: Arc<AsyncRwLock<Option<T>>>,
@@ -184,7 +184,7 @@ impl<T> BlockingLock<T> for AsyncRwLock<T> {
impl<T> Clone for ArcAsyncDerived<T> {
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
value: Arc::clone(&self.value),
wakers: Arc::clone(&self.wakers),
@@ -197,7 +197,7 @@ impl<T> Clone for ArcAsyncDerived<T> {
impl<T> Debug for ArcAsyncDerived<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut f = f.debug_struct("ArcAsyncDerived");
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
f.field("defined_at", &self.defined_at);
f.finish_non_exhaustive()
}
@@ -206,11 +206,11 @@ impl<T> Debug for ArcAsyncDerived<T> {
impl<T> DefinedAt for ArcAsyncDerived<T> {
#[inline(always)]
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -241,7 +241,7 @@ macro_rules! spawn_derived {
let wakers = Arc::new(RwLock::new(Vec::new()));
let this = ArcAsyncDerived {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
value: Arc::clone(&value),
wakers,
@@ -272,7 +272,10 @@ macro_rules! spawn_derived {
// so that the correct value is set synchronously
let initial = initial_fut.as_mut().now_or_never();
match initial {
None => (false, Some(initial_fut)),
None => {
inner.write().or_poisoned().notifier.notify();
(false, Some(initial_fut))
}
Some(orig_value) => {
let mut guard = this.inner.write().or_poisoned();
@@ -296,10 +299,6 @@ macro_rules! spawn_derived {
if was_ready {
first_run.take();
}
// begin loading eagerly but asynchronously, if not already loaded
if !was_ready {
any_subscriber.mark_dirty();
}
if let Some(source) = $source {
any_subscriber.with_observer(|| source.track());
@@ -312,8 +311,20 @@ macro_rules! spawn_derived {
let wakers = Arc::downgrade(&this.wakers);
let loading = Arc::downgrade(&this.loading);
let fut = async move {
// if the AsyncDerived has *already* been marked dirty (i.e., one of its
// sources has changed after creation), we should throw out the Future
// we already created, because its values might be stale
let already_dirty = inner.upgrade()
.as_ref()
.and_then(|inner| inner.read().ok())
.map(|inner| inner.state == AsyncDerivedState::Dirty)
.unwrap_or(false);
if already_dirty {
initial_fut.take();
}
while rx.next().await.is_some() {
let update_if_necessary = if $should_track {
let update_if_necessary = !owner.paused() && if $should_track {
any_subscriber
.with_observer(|| any_subscriber.update_if_necessary())
} else {
@@ -413,7 +424,10 @@ impl<T: 'static> ArcAsyncDerived<T> {
) {
loading.store(false, Ordering::Relaxed);
inner.write().or_poisoned().state = AsyncDerivedState::Notifying;
let prev_state = mem::replace(
&mut inner.write().or_poisoned().state,
AsyncDerivedState::Notifying,
);
if let Some(ready_tx) = ready_tx {
// if it's an Err, that just means the Receiver was dropped
@@ -433,7 +447,10 @@ impl<T: 'static> ArcAsyncDerived<T> {
waker.wake();
}
inner.write().or_poisoned().state = AsyncDerivedState::Clean;
// if this was marked dirty before notifications began, this means it
// had been notified while loading; marking it clean will cause it not to
// run on the next tick of the async loop, so here it should be left dirty
inner.write().or_poisoned().state = prev_state;
}
}
@@ -632,7 +649,7 @@ impl<T: 'static> ToAnySource for ArcAsyncDerived<T> {
AnySource(
Arc::as_ptr(&self.inner) as usize,
Arc::downgrade(&self.inner) as Weak<dyn Source + Send + Sync>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
self.defined_at,
)
}

View File

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

View File

@@ -15,10 +15,15 @@ pub struct MemoInner<T, S>
where
S: Storage<T>,
{
pub(crate) value: Option<S::Wrapped>,
/// Must always be aquired *after* the reactivity lock
pub(crate) value: Arc<RwLock<Option<S::Wrapped>>>,
#[allow(clippy::type_complexity)]
pub(crate) fun: Arc<dyn Fn(Option<T>) -> (T, bool) + Send + Sync>,
pub(crate) owner: Owner,
pub(crate) reactivity: RwLock<MemoInnerReactivity>,
}
pub(crate) struct MemoInnerReactivity {
pub(crate) state: ReactiveNodeState,
pub(crate) sources: SourceSet,
pub(crate) subscribers: SubscriberSet,
@@ -44,38 +49,44 @@ where
any_subscriber: AnySubscriber,
) -> Self {
Self {
value: None,
value: Arc::new(RwLock::new(None)),
fun,
owner: Owner::new(),
state: ReactiveNodeState::Dirty,
sources: Default::default(),
subscribers: SubscriberSet::new(),
any_subscriber,
reactivity: RwLock::new(MemoInnerReactivity {
state: ReactiveNodeState::Dirty,
sources: Default::default(),
subscribers: SubscriberSet::new(),
any_subscriber,
}),
}
}
}
impl<T: 'static, S> ReactiveNode for RwLock<MemoInner<T, S>>
impl<T: 'static, S> ReactiveNode for MemoInner<T, S>
where
S: Storage<T>,
{
fn mark_dirty(&self) {
self.write().or_poisoned().state = ReactiveNodeState::Dirty;
self.reactivity.write().or_poisoned().state = ReactiveNodeState::Dirty;
self.mark_subscribers_check();
}
fn mark_check(&self) {
{
let mut lock = self.write().or_poisoned();
lock.state = ReactiveNodeState::Check;
let mut lock = self.reactivity.write().or_poisoned();
if lock.state != ReactiveNodeState::Dirty {
lock.state = ReactiveNodeState::Check;
}
}
for sub in (&self.read().or_poisoned().subscribers).into_iter() {
for sub in
(&self.reactivity.read().or_poisoned().subscribers).into_iter()
{
sub.mark_check();
}
}
fn mark_subscribers_check(&self) {
let lock = self.read().or_poisoned();
let lock = self.reactivity.read().or_poisoned();
for sub in (&lock.subscribers).into_iter() {
sub.mark_check();
}
@@ -83,7 +94,7 @@ where
fn update_if_necessary(&self) -> bool {
let (state, sources) = {
let inner = self.read().or_poisoned();
let inner = self.reactivity.read().or_poisoned();
(inner.state, inner.sources.clone())
};
@@ -92,32 +103,37 @@ where
ReactiveNodeState::Dirty => true,
ReactiveNodeState::Check => (&sources).into_iter().any(|source| {
source.update_if_necessary()
|| self.read().or_poisoned().state
|| self.reactivity.read().or_poisoned().state
== ReactiveNodeState::Dirty
}),
};
if needs_update {
let (fun, value, owner) = {
let mut lock = self.write().or_poisoned();
(lock.fun.clone(), lock.value.take(), lock.owner.clone())
};
let fun = self.fun.clone();
let owner = self.owner.clone();
// No deadlock risk, because we only hold the value lock.
let value = self.value.write().or_poisoned().take();
let any_subscriber =
{ self.read().or_poisoned().any_subscriber.clone() };
{ self.reactivity.read().or_poisoned().any_subscriber.clone() };
any_subscriber.clear_sources(&any_subscriber);
let (new_value, changed) = owner.with_cleanup(|| {
any_subscriber
.with_observer(|| fun(value.map(StorageAccess::into_taken)))
});
let mut lock = self.write().or_poisoned();
lock.value = Some(S::wrap(new_value));
lock.state = ReactiveNodeState::Clean;
// Two locks are aquired, so order matters.
let mut reactivity_lock = self.reactivity.write().or_poisoned();
{
// Safety: Can block endlessly if the user is has a ReadGuard on the value
let mut value_lock = self.value.write().or_poisoned();
*value_lock = Some(S::wrap(new_value));
}
reactivity_lock.state = ReactiveNodeState::Clean;
if changed {
let subs = lock.subscribers.clone();
drop(lock);
let subs = reactivity_lock.subscribers.clone();
drop(reactivity_lock);
for sub in subs {
// don't trigger reruns of effects/memos
// basically: if one of the observers has triggered this memo to
@@ -126,49 +142,54 @@ where
sub.mark_dirty();
}
}
} else {
drop(reactivity_lock);
}
changed
} else {
if let Ok(mut lock) = self.try_write() {
lock.state = ReactiveNodeState::Clean;
}
let mut lock = self.reactivity.write().or_poisoned();
lock.state = ReactiveNodeState::Clean;
false
}
}
}
impl<T: 'static, S> Source for RwLock<MemoInner<T, S>>
impl<T: 'static, S> Source for MemoInner<T, S>
where
S: Storage<T>,
{
fn add_subscriber(&self, subscriber: AnySubscriber) {
if let Ok(mut lock) = self.try_write() {
lock.subscribers.subscribe(subscriber);
}
let mut lock = self.reactivity.write().or_poisoned();
lock.subscribers.subscribe(subscriber);
}
fn remove_subscriber(&self, subscriber: &AnySubscriber) {
self.write()
self.reactivity
.write()
.or_poisoned()
.subscribers
.unsubscribe(subscriber);
}
fn clear_subscribers(&self) {
self.write().or_poisoned().subscribers.take();
self.reactivity.write().or_poisoned().subscribers.take();
}
}
impl<T: 'static, S> Subscriber for RwLock<MemoInner<T, S>>
impl<T: 'static, S> Subscriber for MemoInner<T, S>
where
S: Storage<T>,
{
fn add_source(&self, source: AnySource) {
self.write().or_poisoned().sources.insert(source);
self.reactivity.write().or_poisoned().sources.insert(source);
}
fn clear_sources(&self, subscriber: &AnySubscriber) {
self.write().or_poisoned().sources.clear_sources(subscriber);
self.reactivity
.write()
.or_poisoned()
.sources
.clear_sources(subscriber);
}
}

View File

@@ -1,4 +1,4 @@
use super::{inner::MemoInner, ArcMemo};
use super::ArcMemo;
use crate::{
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
signal::{
@@ -100,7 +100,7 @@ pub struct Memo<T, S = SyncStorage>
where
S: Storage<T>,
{
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: ArenaItem<ArcMemo<T, S>, S>,
}
@@ -121,7 +121,7 @@ where
#[track_caller]
fn from(value: ArcMemo<T, SyncStorage>) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value),
}
@@ -135,7 +135,7 @@ where
#[track_caller]
fn from_local(value: ArcMemo<T, LocalStorage>) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value),
}
@@ -175,7 +175,7 @@ where
T: PartialEq,
{
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcMemo::new(fun)),
}
@@ -197,7 +197,7 @@ where
changed: fn(Option<&T>, Option<&T>) -> bool,
) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcMemo::new_with_compare(
fun, changed,
@@ -221,7 +221,7 @@ where
fun: impl Fn(Option<T>) -> (T, bool) + Send + Sync + 'static,
) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcMemo::new_owning(fun)),
}
@@ -276,11 +276,11 @@ where
S: Storage<T>,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -306,7 +306,8 @@ where
T: 'static,
S: Storage<ArcMemo<T, S>> + Storage<T>,
{
type Value = ReadGuard<T, Mapped<Plain<MemoInner<T, S>>, T>>;
type Value =
ReadGuard<T, Mapped<Plain<Option<<S as Storage<T>>::Wrapped>>, T>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
self.inner

View File

@@ -170,9 +170,10 @@ impl Effect<LocalStorage> {
async move {
while rx.next().await.is_some() {
if subscriber
.with_observer(|| subscriber.update_if_necessary())
|| first_run
if !owner.paused()
&& (subscriber.with_observer(|| {
subscriber.update_if_necessary()
}) || first_run)
{
first_run = false;
subscriber.clear_sources(&subscriber);
@@ -321,9 +322,10 @@ impl Effect<LocalStorage> {
async move {
while rx.next().await.is_some() {
if subscriber
.with_observer(|| subscriber.update_if_necessary())
|| first_run
if !owner.paused()
&& (subscriber.with_observer(|| {
subscriber.update_if_necessary()
}) || first_run)
{
subscriber.clear_sources(&subscriber);
@@ -388,9 +390,10 @@ impl Effect<SyncStorage> {
async move {
while rx.next().await.is_some() {
if subscriber
.with_observer(|| subscriber.update_if_necessary())
|| first_run
if !owner.paused()
&& (subscriber.with_observer(|| {
subscriber.update_if_necessary()
}) || first_run)
{
first_run = false;
subscriber.clear_sources(&subscriber);
@@ -434,9 +437,10 @@ impl Effect<SyncStorage> {
async move {
while rx.next().await.is_some() {
if subscriber
.with_observer(|| subscriber.update_if_necessary())
|| first_run
if !owner.paused()
&& (subscriber
.with_observer(|| subscriber.update_if_necessary())
|| first_run)
{
first_run = false;
subscriber.clear_sources(&subscriber);
@@ -487,9 +491,10 @@ impl Effect<SyncStorage> {
async move {
while rx.next().await.is_some() {
if subscriber
.with_observer(|| subscriber.update_if_necessary())
|| first_run
if !owner.paused()
&& (subscriber.with_observer(|| {
subscriber.update_if_necessary()
}) || first_run)
{
subscriber.clear_sources(&subscriber);

View File

@@ -91,9 +91,11 @@ where
async move {
while rx.next().await.is_some() {
if subscriber.with_observer(|| {
subscriber.update_if_necessary()
}) {
if !owner.paused()
&& subscriber.with_observer(|| {
subscriber.update_if_necessary()
})
{
subscriber.clear_sources(&subscriber);
let old_value = mem::take(
@@ -159,8 +161,10 @@ where
async move {
while rx.next().await.is_some() {
if subscriber
.with_observer(|| subscriber.update_if_necessary())
if !owner.paused()
&& subscriber.with_observer(|| {
subscriber.update_if_necessary()
})
{
subscriber.clear_sources(&subscriber);

View File

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

View File

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

View File

@@ -135,6 +135,13 @@ pub fn untrack<T>(fun: impl FnOnce() -> T) -> T {
fun()
}
#[doc(hidden)]
#[track_caller]
pub fn untrack_with_diagnostics<T>(fun: impl FnOnce() -> T) -> T {
let _prev = Observer::take();
fun()
}
/// Converts a [`Subscriber`] to a type-erased [`AnySubscriber`].
pub trait ToAnySubscriber {
/// Converts this type to its type-erased equivalent.

View File

@@ -130,6 +130,7 @@ impl Owner {
.and_then(|parent| parent.upgrade())
.map(|parent| parent.read().or_poisoned().arena.clone())
.unwrap_or_default(),
paused: false,
})),
#[cfg(feature = "hydration")]
shared_context,
@@ -163,6 +164,7 @@ impl Owner {
children: Default::default(),
#[cfg(feature = "sandboxed-arenas")]
arena: Default::default(),
paused: false,
})),
#[cfg(feature = "hydration")]
shared_context,
@@ -174,8 +176,10 @@ impl Owner {
/// Creates a new `Owner` that is the child of the current `Owner`, if any.
pub fn child(&self) -> Self {
let parent = Some(Arc::downgrade(&self.inner));
let mut inner = self.inner.write().or_poisoned();
#[cfg(feature = "sandboxed-arenas")]
let arena = self.inner.read().or_poisoned().arena.clone();
let arena = inner.arena.clone();
let paused = inner.paused;
let child = Self {
inner: Arc::new(RwLock::new(OwnerInner {
parent,
@@ -185,15 +189,12 @@ impl Owner {
children: Default::default(),
#[cfg(feature = "sandboxed-arenas")]
arena,
paused,
})),
#[cfg(feature = "hydration")]
shared_context: self.shared_context.clone(),
};
self.inner
.write()
.or_poisoned()
.children
.push(Arc::downgrade(&child.inner));
inner.children.push(Arc::downgrade(&child.inner));
child
}
@@ -292,6 +293,8 @@ impl Owner {
#[cfg(feature = "hydration")]
pub fn with_hydration<T>(fun: impl FnOnce() -> T + 'static) -> T {
fn inner<T>(fun: Box<dyn FnOnce() -> T>) -> T {
provide_context(IsHydrating(true));
let sc = OWNER.with_borrow(|o| {
o.as_ref()
.and_then(|current| current.shared_context.clone())
@@ -315,6 +318,8 @@ impl Owner {
#[cfg(feature = "hydration")]
pub fn with_no_hydration<T>(fun: impl FnOnce() -> T + 'static) -> T {
fn inner<T>(fun: Box<dyn FnOnce() -> T>) -> T {
provide_context(IsHydrating(false));
let sc = OWNER.with_borrow(|o| {
o.as_ref()
.and_then(|current| current.shared_context.clone())
@@ -333,8 +338,53 @@ impl Owner {
inner(Box::new(fun))
}
/// Pauses the execution of side effects for this owner, and any of its descendants.
///
/// If this owner is the owner for an [`Effect`](crate::effect::Effect) or [`RenderEffect`](crate::effect::RenderEffect), this effect will not run until [`Owner::resume`] is called. All children of this effects are also paused.
///
/// Any notifications will be ignored; effects that are notified will paused will not run when
/// resumed, until they are notified again by a source after being resumed.
pub fn pause(&self) {
let mut stack = Vec::with_capacity(16);
stack.push(Arc::downgrade(&self.inner));
while let Some(curr) = stack.pop() {
if let Some(curr) = curr.upgrade() {
let mut curr = curr.write().or_poisoned();
curr.paused = true;
stack.extend(curr.children.iter().map(Weak::clone));
}
}
}
/// Whether this owner has been paused by [`Owner::pause`].
pub fn paused(&self) -> bool {
self.inner.read().or_poisoned().paused
}
/// Resumes side effects that have been paused by [`Owner::pause`].
///
/// All children will also be resumed.
///
/// This will *not* cause side effects that were notified while paused to run, until they are
/// notified again by a source after being resumed.
pub fn resume(&self) {
let mut stack = Vec::with_capacity(16);
stack.push(Arc::downgrade(&self.inner));
while let Some(curr) = stack.pop() {
if let Some(curr) = curr.upgrade() {
let mut curr = curr.write().or_poisoned();
curr.paused = false;
stack.extend(curr.children.iter().map(Weak::clone));
}
}
}
}
#[doc(hidden)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct IsHydrating(pub bool);
/// Registers a function to be run the next time the current owner is cleaned up.
///
/// Because the ownership model is associated with reactive nodes, each "decision point" in an
@@ -355,6 +405,7 @@ pub(crate) struct OwnerInner {
pub children: Vec<Weak<RwLock<OwnerInner>>>,
#[cfg(feature = "sandboxed-arenas")]
arena: Arc<RwLock<ArenaMap>>,
paused: bool,
}
impl Debug for OwnerInner {

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,6 +54,10 @@ pub trait Storage<T>: Send + Sync + 'static {
/// Sets a new value for the stored value. If it has been disposed, returns `Some(T)`.
fn try_set(node: NodeId, value: T) -> Option<T>;
/// Takes an item from the arena if it exists and can be accessed from this thread.
/// If it cannot be casted, it will still be removed from the arena.
fn take(node: NodeId) -> Option<T>;
}
/// A form of [`Storage`] that stores the type as itself, with no wrapper.
@@ -100,6 +104,16 @@ where
}
})
}
fn take(node: NodeId) -> Option<T> {
Arena::with_mut(|arena| {
let m = arena.remove(node)?;
match m.downcast::<T>() {
Ok(inner) => Some(*inner),
Err(_) => None,
}
})
}
}
/// A form of [`Storage`] that stores the type with a wrapper that makes it `Send + Sync`, but only
@@ -148,4 +162,14 @@ where
}
})
}
fn take(node: NodeId) -> Option<T> {
Arena::with_mut(|arena| {
let m = arena.remove(node)?;
match m.downcast::<SendWrapper<T>>() {
Ok(inner) => Some(inner.take()),
Err(_) => None,
}
})
}
}

View File

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

View File

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

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